import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import { RawIssue } from '../helpers/issues';
import throwGlobalError from '../app/utils/throwGlobalError';
+import getCoverageStatus from '../components/SourceViewer/helpers/getCoverageStatus';
export interface IssueResponse {
components?: Array<{ key: string; name: string }>;
}): Promise<string[]> {
return getJSON('/api/issues/authors', data).then(r => r.authors, throwGlobalError);
}
+
+export function getIssueFlowSnippets(issueKey: string): Promise<T.Dict<T.SnippetsByComponent>> {
+ return getJSON('/api/sources/issue_snippets', { issueKey }).then(result => {
+ Object.keys(result).forEach(k => {
+ if (result[k].sources) {
+ result[k].sources = result[k].sources.reduce(
+ (lineMap: T.Dict<T.SourceLine>, line: T.SourceLine) => {
+ line.coverageStatus = getCoverageStatus(line);
+ lineMap[line.line] = line;
+ return lineMap;
+ },
+ {}
+ );
+ }
+ });
+ return result;
+ }, throwGlobalError);
+}
}
.component-name-favorite {
- position: relative;
- top: -1px;
margin-left: 4px;
- padding: 2px 0;
+ padding: 0;
}
font-size: var(--smallFontSize);
}
+.nudged-up {
+ margin-top: -1px;
+}
+
.spacer-left {
margin-left: 8px !important;
}
export type EditionKey = 'community' | 'developer' | 'enterprise' | 'datacenter';
+ export type ExpandDirection = 'up' | 'down';
+
export interface Extension {
key: string;
name: string;
export interface FlowLocation {
component: string;
componentName?: string;
+ index?: number;
msg?: string;
textRange: TextRange;
}
export type IssueType = 'BUG' | 'VULNERABILITY' | 'CODE_SMELL' | 'SECURITY_HOTSPOT';
+ export interface IssuesByLine {
+ [key: number]: Issue[];
+ }
export interface Language {
key: string;
name: string;
index?: number;
line: number;
startLine?: number;
+ text?: string;
to: number;
}
+ export interface LineMap {
+ [line: number]: SourceLine;
+ }
+
export interface LoggedInUser extends CurrentUser {
avatar?: string;
email?: string;
type: 'SHORT';
}
+ export interface SnippetGroup extends SnippetsByComponent {
+ locations: T.FlowLocation[];
+ }
+ export interface SnippetsByComponent {
+ component: SourceViewerFile;
+ sources: { [line: number]: SourceLine };
+ }
+
export interface SourceLine {
code?: string;
conditions?: number;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { selectFlow } from '../actions';
-it('should select flow and enable locations navigator', () => {
- expect(selectFlow(5)()).toEqual({
- locationsNavigator: true,
- selectedFlowIndex: 5,
- selectedLocationIndex: 0
+import { selectFlow, selectLocation } from '../actions';
+import { mockIssue } from '../../../helpers/testMocks';
+
+describe('selectFlow', () => {
+ it('should select flow and enable locations navigator', () => {
+ expect(selectFlow(5)()).toEqual({
+ locationsNavigator: true,
+ selectedFlowIndex: 5,
+ selectedLocationIndex: 0
+ });
+ });
+});
+
+describe('selectLocation', () => {
+ it('should select location and enable locations navigator', () => {
+ expect(selectLocation(5)({ openIssue: mockIssue() })).toEqual({
+ locationsNavigator: true,
+ selectedLocationIndex: 5
+ });
+ });
+
+ it('should deselect location when clicked again', () => {
+ expect(selectLocation(5)({ openIssue: mockIssue(), selectedLocationIndex: 5 })).toEqual({
+ locationsNavigator: false,
+ selectedLocationIndex: undefined
+ });
+ });
+
+ it('should ignore if no open issue', () => {
+ expect(selectLocation(5)({ openIssue: undefined })).toBeNull();
});
});
return { locationsNavigator: false };
}
-export function selectLocation(nextIndex: number | undefined) {
- return (state: State) => {
+export function selectLocation(nextIndex: number) {
+ return (state: Pick<State, 'selectedLocationIndex' | 'openIssue'>) => {
const { selectedLocationIndex: index, openIssue } = state;
if (openIssue) {
- if (!state.locationsNavigator) {
- if (nextIndex !== undefined) {
- return { locationsNavigator: true, selectedLocationIndex: nextIndex };
- }
- } else if (index !== undefined) {
+ if (index === nextIndex) {
// disable locations when selecting (clicking) the same location
return {
- locationsNavigator: nextIndex !== index,
- selectedLocationIndex: nextIndex
+ locationsNavigator: false,
+ selectedLocationIndex: undefined
};
+ } else {
+ return { locationsNavigator: true, selectedLocationIndex: nextIndex };
}
}
return null;
};
}
-export function selectNextLocation(state: State) {
+export function selectNextLocation(
+ state: Pick<State, 'selectedFlowIndex' | 'selectedLocationIndex' | 'openIssue'>
+) {
const { selectedFlowIndex, selectedLocationIndex: index, openIssue } = state;
if (openIssue) {
const locations =
import Helmet from 'react-helmet';
import { keyBy, omit, without } from 'lodash';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
-import ComponentBreadcrumbs from './ComponentBreadcrumbs';
import IssuesList from './IssuesList';
import IssuesSourceViewer from './IssuesSourceViewer';
import MyIssuesFilter from './MyIssuesFilter';
);
};
- selectLocation = (index?: number) => {
+ selectLocation = (index: number) => {
this.setState(actions.selectLocation(index));
};
);
}
+ renderHeader({
+ openIssue,
+ paging,
+ selectedIndex
+ }: {
+ openIssue: T.Issue | undefined;
+ paging: T.Paging | undefined;
+ selectedIndex: number | undefined;
+ }) {
+ return openIssue ? (
+ <A11ySkipTarget anchor="issues_main" />
+ ) : (
+ <div className="layout-page-header-panel layout-page-main-header issues-main-header">
+ <div className="layout-page-header-panel-inner layout-page-main-header-inner">
+ <div className="layout-page-main-inner">
+ <A11ySkipTarget anchor="issues_main" />
+
+ {this.renderBulkChange(openIssue)}
+ <PageActions
+ canSetHome={Boolean(
+ !this.props.organization &&
+ !this.props.component &&
+ (!isSonarCloud() || this.props.myIssues)
+ )}
+ effortTotal={this.state.effortTotal}
+ onReload={this.handleReload}
+ paging={paging}
+ selectedIndex={selectedIndex}
+ />
+ </div>
+ </div>
+ </div>
+ );
+ }
+
renderPage() {
- const { checkAll, loading, openIssue, paging } = this.state;
+ const { checkAll, issues, loading, openIssue, paging } = this.state;
return (
<div className="layout-page-main-inner">
{openIssue ? (
<IssuesSourceViewer
branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+ issues={issues}
loadIssues={this.fetchIssuesForComponent}
locationsNavigator={this.state.locationsNavigator}
onIssueChange={this.handleIssueChange}
}
render() {
- const { component } = this.props;
const { openIssue, paging } = this.state;
const selectedIndex = this.getSelectedIndex();
return (
{this.renderSide(openIssue)}
<div className="layout-page-main">
- <div className="layout-page-header-panel layout-page-main-header issues-main-header">
- <div className="layout-page-header-panel-inner layout-page-main-header-inner">
- <div className="layout-page-main-inner">
- <A11ySkipTarget anchor="issues_main" />
-
- {this.renderBulkChange(openIssue)}
- {openIssue ? (
- <div className="pull-left width-60">
- <ComponentBreadcrumbs
- component={component}
- issue={openIssue}
- organization={this.props.organization}
- selectedFlowIndex={this.state.selectedFlowIndex}
- selectedLocationIndex={this.state.selectedLocationIndex}
- />
- </div>
- ) : (
- <PageActions
- canSetHome={Boolean(
- !this.props.organization &&
- !this.props.component &&
- (!isSonarCloud() || this.props.myIssues)
- )}
- effortTotal={this.state.effortTotal}
- onReload={this.handleReload}
- paging={paging}
- selectedIndex={selectedIndex}
- />
- )}
- </div>
- </div>
- </div>
+ {this.renderHeader({ openIssue, paging, selectedIndex })}
{this.renderPage()}
</div>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import { uniq } from 'lodash';
import { getLocations, getSelectedLocation } from '../utils';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { scrollToElement } from '../../../helpers/scrolling';
+import CrossComponentSourceViewer from '../crossComponentSourceViewer/CrossComponentSourceViewer';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
interface Props {
branchLike: T.BranchLike | undefined;
+ issues: T.Issue[];
loadIssues: (component: string, from: number, to: number) => Promise<T.Issue[]>;
locationsNavigator: boolean;
onIssueChange: (issue: T.Issue) => void;
render() {
const { openIssue, selectedFlowIndex, selectedLocationIndex } = this.props;
- const locations = getLocations(openIssue, selectedFlowIndex);
+ const locations = getLocations(openIssue, selectedFlowIndex).map((loc, index) => {
+ loc.index = index;
+ return loc;
+ });
const selectedLocation = getSelectedLocation(
openIssue,
selectedFlowIndex,
selectedLocationIndex
);
- const component = selectedLocation ? selectedLocation.component : openIssue.component;
-
- // if location is selected, show (and load) code around it
- // otherwise show code around the open issue
- const aroundLine = selectedLocation
- ? selectedLocation.textRange.startLine
- : openIssue.textRange && openIssue.textRange.endLine;
-
- // replace locations in another file with `undefined` to keep the same location indexes
- const highlightedLocations = locations.map(location =>
- location.component === component ? location : undefined
- );
-
const highlightedLocationMessage =
this.props.locationsNavigator && selectedLocationIndex !== undefined
? selectedLocation && { index: selectedLocationIndex, text: selectedLocation.msg }
: undefined;
- const allMessagesEmpty = locations !== undefined && locations.every(location => !location.msg);
-
- // do not load issues when open another file for a location
- const loadIssues =
- component === openIssue.component ? this.props.loadIssues : () => Promise.resolve([]);
- const selectedIssue = component === openIssue.component ? openIssue.key : undefined;
-
- return (
- <div ref={node => (this.node = node)}>
- <SourceViewer
- aroundLine={aroundLine}
- branchLike={this.props.branchLike}
- component={component}
- displayAllIssues={true}
- displayLocationMarkers={!allMessagesEmpty}
- highlightedLocationMessage={highlightedLocationMessage}
- highlightedLocations={highlightedLocations}
- loadIssues={loadIssues}
- onIssueChange={this.props.onIssueChange}
- onIssueSelect={this.props.onIssueSelect}
- onLoaded={this.handleLoaded}
- onLocationSelect={this.props.onLocationSelect}
- scroll={this.handleScroll}
- selectedIssue={selectedIssue}
- />
- </div>
- );
+ if (locations.length > 1) {
+ const components = uniq(locations.map(l => l.component));
+ return (
+ <div ref={node => (this.node = node)}>
+ <CrossComponentSourceViewer
+ branchLike={this.props.branchLike}
+ components={components}
+ highlightedLocationMessage={highlightedLocationMessage}
+ issue={openIssue}
+ issues={this.props.issues}
+ locations={locations}
+ onIssueChange={this.props.onIssueChange}
+ onLoaded={this.handleLoaded}
+ onLocationSelect={this.props.onLocationSelect}
+ scroll={this.handleScroll}
+ selectedFlowIndex={selectedFlowIndex}
+ />
+ </div>
+ );
+ } else {
+ // if location is selected, show (and load) code around it
+ // otherwise show code around the open issue
+ const aroundLine = selectedLocation
+ ? selectedLocation.textRange.startLine
+ : openIssue.textRange && openIssue.textRange.endLine;
+
+ const component = selectedLocation ? selectedLocation.component : openIssue.component;
+
+ const highlightedLocations = locations.filter(location => location.component === component);
+
+ // do not load issues when open another file for a location
+ const loadIssues =
+ component === openIssue.component ? this.props.loadIssues : () => Promise.resolve([]);
+ const selectedIssue = component === openIssue.component ? openIssue.key : undefined;
+
+ return (
+ <div ref={node => (this.node = node)}>
+ <SourceViewer
+ aroundLine={aroundLine}
+ branchLike={this.props.branchLike}
+ component={component}
+ displayAllIssues={true}
+ displayLocationMarkers={false}
+ highlightedLocationMessage={highlightedLocationMessage}
+ highlightedLocations={highlightedLocations}
+ loadIssues={loadIssues}
+ onIssueChange={this.props.onIssueChange}
+ onIssueSelect={this.props.onIssueSelect}
+ onLoaded={this.handleLoaded}
+ onLocationSelect={this.props.onLocationSelect}
+ scroll={this.handleScroll}
+ selectedIssue={selectedIssue}
+ slimHeader={true}
+ />
+ </div>
+ );
+ }
}
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import {
+ createSnippets,
+ expandSnippet,
+ inSnippet,
+ EXPAND_BY_LINES,
+ LINES_BELOW_LAST,
+ MERGE_DISTANCE
+} from './utils';
+import { getSources } from '../../../api/components';
+import ExpandSnippetIcon from '../../../components/icons-components/ExpandSnippetIcon';
+import Line from '../../../components/SourceViewer/components/Line';
+import SourceViewerHeaderSlim from '../../../components/SourceViewer/SourceViewerHeaderSlim';
+import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus';
+import { symbolsByLine, locationsByLine } from '../../../components/SourceViewer/helpers/indexing';
+import { getSecondaryIssueLocationsForLine } from '../../../components/SourceViewer/helpers/issueLocations';
+import {
+ optimizeLocationMessage,
+ optimizeHighlightedSymbols,
+ optimizeSelectedIssue
+} from '../../../components/SourceViewer/helpers/lines';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+ branchLike: T.BranchLike | undefined;
+ highlightedLocationMessage: { index: number; text: string | undefined } | undefined;
+ issue: T.Issue;
+ issuePopup?: { issue: string; name: string };
+ issuesByLine: T.IssuesByLine;
+ last: boolean;
+ locations: T.FlowLocation[];
+ onIssueChange: (issue: T.Issue) => void;
+ onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void;
+ onLocationSelect: (index: number) => void;
+ renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ scroll?: (element: HTMLElement) => void;
+ snippetGroup: T.SnippetGroup;
+}
+
+interface State {
+ additionalLines: { [line: number]: T.SourceLine };
+ highlightedSymbols: string[];
+ loading: boolean;
+ openIssuesByLine: T.Dict<boolean>;
+ snippets: T.SourceLine[][];
+}
+
+export default class ComponentSourceSnippetViewer extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ additionalLines: {},
+ highlightedSymbols: [],
+ loading: false,
+ openIssuesByLine: {},
+ snippets: []
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.createSnippetsFromProps();
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ createSnippetsFromProps() {
+ const mainLocation: T.FlowLocation = {
+ component: this.props.issue.component,
+ textRange: this.props.issue.textRange || {
+ endLine: 0,
+ endOffset: 0,
+ startLine: 0,
+ startOffset: 0
+ }
+ };
+ const snippets = createSnippets(
+ this.props.snippetGroup.locations.concat(mainLocation),
+ this.props.snippetGroup.sources,
+ this.props.last
+ );
+ this.setState({ snippets });
+ }
+
+ expandBlock = (snippetIndex: number, direction: T.ExpandDirection) => {
+ const { snippets } = this.state;
+
+ const snippet = snippets[snippetIndex];
+
+ // Extend by EXPAND_BY_LINES and add buffer for merging snippets
+ const extension = EXPAND_BY_LINES + MERGE_DISTANCE - 1;
+
+ const range =
+ direction === 'up'
+ ? {
+ from: Math.max(1, snippet[0].line - extension),
+ to: snippet[0].line - 1
+ }
+ : {
+ from: snippet[snippet.length - 1].line + 1,
+ to: snippet[snippet.length - 1].line + extension
+ };
+
+ getSources({
+ key: this.props.snippetGroup.component.key,
+ ...range
+ })
+ .then(lines =>
+ lines.reduce((lineMap: T.Dict<T.SourceLine>, line) => {
+ line.coverageStatus = getCoverageStatus(line);
+ lineMap[line.line] = line;
+ return lineMap;
+ }, {})
+ )
+ .then(
+ newLinesMapped => {
+ if (this.mounted) {
+ this.setState(({ additionalLines, snippets }) => {
+ const combinedLines = { ...additionalLines, ...newLinesMapped };
+
+ return {
+ additionalLines: combinedLines,
+ snippets: expandSnippet({
+ direction,
+ lines: { ...combinedLines, ...this.props.snippetGroup.sources },
+ snippetIndex,
+ snippets
+ })
+ };
+ });
+ }
+ },
+ () => null
+ );
+ };
+
+ expandComponent = () => {
+ const { key } = this.props.snippetGroup.component;
+
+ this.setState({ loading: true });
+
+ getSources({ key }).then(
+ lines => {
+ if (this.mounted) {
+ this.setState({ loading: false, snippets: [lines] });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ };
+
+ handleOpenIssues = (line: T.SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true }
+ }));
+ };
+
+ handleCloseIssues = (line: T.SourceLine) => {
+ this.setState(state => ({
+ openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false }
+ }));
+ };
+
+ renderLine({
+ index,
+ issuesForLine,
+ issueLocations,
+ line,
+ snippet,
+ symbols,
+ verticalBuffer
+ }: {
+ index: number;
+ issuesForLine: T.Issue[];
+ issueLocations: T.LinearIssueLocation[];
+ line: T.SourceLine;
+ snippet: T.SourceLine[];
+ symbols: string[];
+ verticalBuffer: number;
+ }) {
+ const { openIssuesByLine } = this.state;
+
+ const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, this.props.locations);
+
+ const noop = () => {};
+
+ const isSinkLine = issuesForLine.some(i => i.key === this.props.issue.key);
+
+ return (
+ <Line
+ branchLike={undefined}
+ displayAllIssues={false}
+ displayCoverage={true}
+ displayDuplications={false}
+ displayIssues={!isSinkLine || issuesForLine.length > 1}
+ displayLocationMarkers={true}
+ duplications={[]}
+ duplicationsCount={0}
+ highlighted={false}
+ highlightedLocationMessage={optimizeLocationMessage(
+ this.props.highlightedLocationMessage,
+ secondaryIssueLocations
+ )}
+ highlightedSymbols={optimizeHighlightedSymbols(symbols, this.state.highlightedSymbols)}
+ issueLocations={issueLocations}
+ issuePopup={this.props.issuePopup}
+ issues={issuesForLine}
+ key={line.line}
+ last={false}
+ line={line}
+ linePopup={undefined}
+ loadDuplications={noop}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ onIssueSelect={noop}
+ onIssueUnselect={noop}
+ onIssuesClose={this.handleCloseIssues}
+ onIssuesOpen={this.handleOpenIssues}
+ onLinePopupToggle={noop}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={highlightedSymbols => this.setState({ highlightedSymbols })}
+ openIssues={openIssuesByLine[line.line]}
+ previousLine={index > 0 ? snippet[index - 1] : undefined}
+ renderDuplicationPopup={this.props.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ secondaryIssueLocations={secondaryIssueLocations}
+ selectedIssue={optimizeSelectedIssue(this.props.issue.key, issuesForLine)}
+ verticalBuffer={verticalBuffer}
+ />
+ );
+ }
+
+ renderSnippet({
+ snippet,
+ index,
+ issue,
+ issuesByLine = {},
+ locationsByLine,
+ last
+ }: {
+ snippet: T.SourceLine[];
+ index: number;
+ issue: T.Issue;
+ issuesByLine: T.IssuesByLine;
+ locationsByLine: { [line: number]: T.LinearIssueLocation[] };
+ last: boolean;
+ }) {
+ const { component } = this.props.snippetGroup;
+ const lastLine =
+ component.measures && component.measures.lines && parseInt(component.measures.lines, 10);
+
+ const symbols = symbolsByLine(snippet);
+
+ const expandBlock = (direction: T.ExpandDirection) => () => this.expandBlock(index, direction);
+
+ const bottomLine = snippet[snippet.length - 1].line;
+ const issueLine = issue.textRange ? issue.textRange.endLine : issue.line;
+ const lowestVisibleIssue = Math.max(
+ ...Object.keys(issuesByLine)
+ .map(k => parseInt(k, 10))
+ .filter(l => inSnippet(l, snippet) && (l === issueLine || this.state.openIssuesByLine[l]))
+ );
+ const verticalBuffer = last
+ ? Math.max(0, LINES_BELOW_LAST - (bottomLine - lowestVisibleIssue))
+ : 0;
+
+ return (
+ <div className="source-viewer-code snippet" key={index}>
+ {snippet[0].line > 1 && (
+ <button
+ aria-label={translate('source_viewer.expand_above')}
+ className="expand-block expand-block-above"
+ onClick={expandBlock('up')}
+ type="button">
+ <ExpandSnippetIcon />
+ </button>
+ )}
+ <table className="source-table">
+ <tbody>
+ {snippet.map((line, index) =>
+ this.renderLine({
+ index,
+ issuesForLine: issuesByLine[line.line] || [],
+ issueLocations: locationsByLine[line.line] || [],
+ line,
+ snippet,
+ symbols: symbols[line.line],
+ verticalBuffer: index === snippet.length - 1 ? verticalBuffer : 0
+ })
+ )}
+ </tbody>
+ </table>
+ {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
+ <button
+ aria-label={translate('source_viewer.expand_below')}
+ className="expand-block expand-block-below"
+ onClick={expandBlock('down')}
+ type="button">
+ <ExpandSnippetIcon />
+ </button>
+ )}
+ </div>
+ );
+ }
+
+ render() {
+ const { branchLike, issue, issuesByLine, last, snippetGroup } = this.props;
+ const { loading, snippets } = this.state;
+ const locations = locationsByLine([issue]);
+
+ const fullyShown =
+ snippets.length === 1 &&
+ snippetGroup.component.measures &&
+ snippets[0].length === parseInt(snippetGroup.component.measures.lines || '', 10);
+
+ return (
+ <div className="component-source-container">
+ <SourceViewerHeaderSlim
+ branchLike={branchLike}
+ expandable={!fullyShown}
+ loading={loading}
+ onExpand={this.expandComponent}
+ sourceViewerFile={snippetGroup.component}
+ />
+ {snippets.map((snippet, index) =>
+ this.renderSnippet({
+ snippet,
+ index,
+ issue,
+ issuesByLine: last ? issuesByLine : {},
+ locationsByLine: last && index === snippets.length - 1 ? locations : {},
+ last: last && index === snippets.length - 1
+ })
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { lazyLoad } from '../../../components/lazyLoad';
+
+const CrossComponentSourceViewer = lazyLoad(() =>
+ import(/* webpackPrefetch: true */ './CrossComponentSourceViewerWrapper')
+);
+
+export default CrossComponentSourceViewer;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import ComponentSourceSnippetViewer from './ComponentSourceSnippetViewer';
+import { groupLocationsByComponent } from './utils';
+import DeferredSpinner from '../../../components/common/DeferredSpinner';
+import { getIssueFlowSnippets } from '../../../api/issues';
+import { issuesByComponentAndLine } from '../../../components/SourceViewer/helpers/indexing';
+
+interface State {
+ components: T.Dict<T.SnippetsByComponent>;
+ issuePopup?: { issue: string; name: string };
+ loading: boolean;
+}
+
+interface Props {
+ branchLike: T.Branch | T.PullRequest | undefined;
+ highlightedLocationMessage?: { index: number; text: string | undefined };
+ issue: T.Issue;
+ issues: T.Issue[];
+ locations: T.FlowLocation[];
+ onIssueChange: (issue: T.Issue) => void;
+ onLoaded?: () => void;
+ onLocationSelect: (index: number) => void;
+ renderDuplicationPopup: (index: number, line: number) => JSX.Element;
+ scroll?: (element: HTMLElement) => void;
+ selectedFlowIndex: number | undefined;
+}
+
+export default class CrossComponentSourceViewerWrapper extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = {
+ components: {},
+ loading: true
+ };
+
+ componentDidMount() {
+ this.mounted = true;
+ this.fetchIssueFlowSnippets(this.props.issue.key);
+ }
+
+ componentWillReceiveProps(newProps: Props) {
+ if (newProps.issue.key !== this.props.issue.key) {
+ this.fetchIssueFlowSnippets(newProps.issue.key);
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ fetchIssueFlowSnippets(issueKey: string) {
+ this.setState({ loading: true });
+ getIssueFlowSnippets(issueKey).then(
+ components => {
+ if (this.mounted) {
+ this.setState({ components, issuePopup: undefined, loading: false });
+ if (this.props.onLoaded) {
+ this.props.onLoaded();
+ }
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ }
+ );
+ }
+
+ handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => {
+ this.setState((state: State) => {
+ const samePopup =
+ state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue;
+ if (open !== false && !samePopup) {
+ return { issuePopup: { issue, name: popupName } };
+ } else if (open !== true && samePopup) {
+ return { issuePopup: undefined };
+ }
+ return null;
+ });
+ };
+
+ render() {
+ const { components, loading } = this.state;
+
+ if (loading) {
+ return (
+ <div>
+ <DeferredSpinner />
+ </div>
+ );
+ }
+
+ const issuesByComponent = issuesByComponentAndLine(this.props.issues);
+ const locationsByComponent = groupLocationsByComponent(this.props.locations, components);
+
+ return (
+ <div>
+ {locationsByComponent.map((g, i) => (
+ <ComponentSourceSnippetViewer
+ branchLike={this.props.branchLike}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ issue={this.props.issue}
+ issuePopup={this.state.issuePopup}
+ issuesByLine={issuesByComponent[g.component.key] || {}}
+ key={this.props.issue.key + '-' + this.props.selectedFlowIndex + '-' + i}
+ last={i === locationsByComponent.length - 1}
+ locations={g.locations || []}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ renderDuplicationPopup={this.props.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ snippetGroup={g}
+ />
+ ))}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import ComponentSourceSnippetViewer from '../ComponentSourceSnippetViewer';
+import {
+ mockMainBranch,
+ mockIssue,
+ mockSourceViewerFile,
+ mockFlowLocation,
+ mockSnippetsByComponent
+} from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/components', () => {
+ const { mockSnippetsByComponent } = require.requireActual('../../../../helpers/testMocks');
+
+ return {
+ getSources: jest
+ .fn()
+ .mockResolvedValue(
+ Object.values(
+ mockSnippetsByComponent('a', [22, 23, 24, 25, 26, 27, 28, 29, 30, 31]).sources
+ )
+ )
+ };
+});
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('should expand block', async () => {
+ const snippetGroup: T.SnippetGroup = {
+ locations: [
+ mockFlowLocation({
+ component: 'a',
+ textRange: { startLine: 34, endLine: 34, startOffset: 0, endOffset: 0 }
+ }),
+ mockFlowLocation({
+ component: 'a',
+ textRange: { startLine: 54, endLine: 54, startOffset: 0, endOffset: 0 }
+ })
+ ],
+ ...mockSnippetsByComponent('a', [32, 33, 34, 35, 36, 52, 53, 54, 55, 56])
+ };
+
+ const wrapper = shallowRender({ snippetGroup });
+
+ wrapper.instance().expandBlock(0, 'up');
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('snippets')).toHaveLength(2);
+ expect(wrapper.state('snippets')[0]).toHaveLength(15);
+ expect(Object.keys(wrapper.state('additionalLines'))).toHaveLength(10);
+});
+
+function shallowRender(props: Partial<ComponentSourceSnippetViewer['props']> = {}) {
+ const snippetGroup: T.SnippetGroup = {
+ component: mockSourceViewerFile(),
+ locations: [],
+ sources: []
+ };
+ return shallow<ComponentSourceSnippetViewer>(
+ <ComponentSourceSnippetViewer
+ branchLike={mockMainBranch()}
+ highlightedLocationMessage={{ index: 0, text: '' }}
+ issue={mockIssue()}
+ issuesByLine={{}}
+ last={false}
+ locations={[]}
+ onIssueChange={jest.fn()}
+ onIssuePopupToggle={jest.fn()}
+ onLocationSelect={jest.fn()}
+ renderDuplicationPopup={jest.fn()}
+ scroll={jest.fn()}
+ snippetGroup={snippetGroup}
+ {...props}
+ />
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import { shallow } from 'enzyme';
+import CrossComponentSourceViewerWrapper from '../CrossComponentSourceViewerWrapper';
+import { mockIssue, mockSourceViewerFile } from '../../../../helpers/testMocks';
+import { waitAndUpdate } from '../../../../helpers/testUtils';
+
+jest.mock('../../../../api/issues', () => {
+ const { mockSourceViewerFile } = require.requireActual('../../../../helpers/testMocks');
+ return {
+ getIssueFlowSnippets: jest.fn().mockResolvedValue([mockSourceViewerFile()])
+ };
+});
+
+it('should render correctly', () => {
+ expect(shallowRender()).toMatchSnapshot();
+});
+
+it('Should fetch data', async () => {
+ const wrapper = shallowRender();
+ wrapper.instance().fetchIssueFlowSnippets('124');
+ await waitAndUpdate(wrapper);
+
+ expect(wrapper.state('components')).toEqual([mockSourceViewerFile()]);
+});
+
+it('should handle issue popup', () => {
+ const wrapper = shallowRender();
+ // open
+ wrapper.instance().handleIssuePopupToggle('1', 'popup1');
+ expect(wrapper.state('issuePopup')).toEqual({ issue: '1', name: 'popup1' });
+
+ // close
+ wrapper.instance().handleIssuePopupToggle('1', 'popup1');
+ expect(wrapper.state('issuePopup')).toBeUndefined();
+});
+
+function shallowRender(props: Partial<CrossComponentSourceViewerWrapper['props']> = {}) {
+ return shallow<CrossComponentSourceViewerWrapper>(
+ <CrossComponentSourceViewerWrapper
+ branchLike={undefined}
+ highlightedLocationMessage={undefined}
+ issue={mockIssue(true)}
+ issues={[]}
+ locations={[]}
+ onIssueChange={jest.fn()}
+ onLoaded={jest.fn()}
+ onLocationSelect={jest.fn()}
+ renderDuplicationPopup={jest.fn()}
+ scroll={jest.fn()}
+ selectedFlowIndex={0}
+ {...props}
+ />
+ );
+}
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div
+ className="component-source-container"
+>
+ <SourceViewerHeaderSlim
+ branchLike={
+ Object {
+ "analysisDate": "2018-01-01",
+ "isMain": true,
+ "name": "master",
+ }
+ }
+ expandable={true}
+ loading={false}
+ onExpand={[Function]}
+ sourceViewerFile={
+ Object {
+ "key": "foo",
+ "measures": Object {
+ "coverage": "85.2",
+ "duplicationDensity": "1.0",
+ "issues": "12",
+ "lines": "56",
+ },
+ "path": "foo/bar.ts",
+ "project": "my-project",
+ "projectName": "MyProject",
+ "q": "FIL",
+ "uuid": "foo-bar",
+ }
+ }
+ />
+</div>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render correctly 1`] = `
+<div>
+ <DeferredSpinner
+ timeout={100}
+ />
+</div>
+`;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { keyBy, range } from 'lodash';
+import { groupLocationsByComponent, createSnippets, expandSnippet } from '../utils';
+import {
+ mockFlowLocation,
+ mockSnippetsByComponent,
+ mockSourceLine
+} from '../../../../helpers/testMocks';
+
+describe('groupLocationsByComponent', () => {
+ it('should handle empty args', () => {
+ expect(groupLocationsByComponent([], {})).toEqual([]);
+ });
+
+ it('should group correctly', () => {
+ const results = groupLocationsByComponent(
+ [
+ mockFlowLocation({
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 16, startOffset: 2, endLine: 16, endOffset: 3 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 24, startOffset: 1, endLine: 24, endOffset: 2 }
+ })
+ ],
+ { 'main.js': mockSnippetsByComponent('main.js', [14, 15, 16, 17, 18, 22, 23, 24, 25, 26]) }
+ );
+
+ expect(results).toHaveLength(1);
+ });
+
+ it('should preserve step order when jumping between files', () => {
+ const results = groupLocationsByComponent(
+ [
+ mockFlowLocation({
+ component: 'A.js',
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ component: 'B.js',
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ component: 'A.js',
+ textRange: { startLine: 15, startOffset: 2, endLine: 15, endOffset: 3 }
+ })
+ ],
+ {
+ 'A.js': mockSnippetsByComponent('A.js', [13, 14, 15, 16, 17, 18]),
+ 'B.js': mockSnippetsByComponent('B.js', [14, 15, 16, 17, 18])
+ }
+ );
+
+ expect(results).toHaveLength(3);
+ expect(results[0].component.key).toBe('A.js');
+ expect(results[1].component.key).toBe('B.js');
+ expect(results[2].component.key).toBe('A.js');
+ expect(results[0].locations).toHaveLength(1);
+ expect(results[1].locations).toHaveLength(1);
+ expect(results[2].locations).toHaveLength(1);
+ });
+});
+
+describe('createSnippets', () => {
+ it('should merge snippets correctly', () => {
+ const results = createSnippets(
+ [
+ mockFlowLocation({
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 19, startOffset: 2, endLine: 19, endOffset: 3 }
+ })
+ ],
+ mockSnippetsByComponent('', [14, 15, 16, 17, 18, 19, 20, 21, 22]).sources,
+ false
+ );
+
+ expect(results).toHaveLength(1);
+ expect(results[0]).toHaveLength(8);
+ });
+
+ it('should merge snippets correctly, even when not in sequence', () => {
+ const results = createSnippets(
+ [
+ mockFlowLocation({
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 47, startOffset: 2, endLine: 47, endOffset: 3 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 14, startOffset: 2, endLine: 14, endOffset: 3 }
+ })
+ ],
+ mockSnippetsByComponent('', [12, 13, 14, 15, 16, 17, 18, 45, 46, 47, 48, 49]).sources,
+ false
+ );
+
+ expect(results).toHaveLength(2);
+ expect(results[0]).toHaveLength(7);
+ expect(results[1]).toHaveLength(5);
+ });
+
+ it('should merge three snippets together', () => {
+ const results = createSnippets(
+ [
+ mockFlowLocation({
+ textRange: { startLine: 16, startOffset: 10, endLine: 16, endOffset: 14 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 47, startOffset: 2, endLine: 47, endOffset: 3 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 22, startOffset: 2, endLine: 22, endOffset: 3 }
+ }),
+ mockFlowLocation({
+ textRange: { startLine: 18, startOffset: 2, endLine: 18, endOffset: 3 }
+ })
+ ],
+ mockSnippetsByComponent('', [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 45, 46, 47, 48, 49])
+ .sources,
+ false
+ );
+
+ expect(results).toHaveLength(2);
+ expect(results[0]).toHaveLength(11);
+ expect(results[1]).toHaveLength(5);
+ });
+});
+
+describe('expandSnippet', () => {
+ it('should add lines above', () => {
+ const lines = keyBy(range(4, 19).map(line => mockSourceLine({ line })), 'line');
+ const snippets = [[lines[14], lines[15], lines[16], lines[17], lines[18]]];
+
+ const result = expandSnippet({ direction: 'up', lines, snippetIndex: 0, snippets });
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toHaveLength(15);
+ expect(result[0].map(l => l.line)).toEqual(range(4, 19));
+ });
+
+ it('should add lines below', () => {
+ const lines = keyBy(range(4, 19).map(line => mockSourceLine({ line })), 'line');
+ const snippets = [[lines[4], lines[5], lines[6], lines[7], lines[8]]];
+
+ const result = expandSnippet({ direction: 'down', lines, snippetIndex: 0, snippets });
+
+ expect(result).toHaveLength(1);
+ expect(result[0].map(l => l.line)).toEqual(range(4, 19));
+ });
+
+ it('should merge snippets if necessary', () => {
+ const lines = keyBy(
+ range(4, 23)
+ .concat(range(38, 43))
+ .map(line => mockSourceLine({ line })),
+ 'line'
+ );
+ const snippets = [
+ [lines[4], lines[5], lines[6], lines[7], lines[8]],
+ [lines[38], lines[39], lines[40], lines[41], lines[42]],
+ [lines[17], lines[18], lines[19], lines[20], lines[21]]
+ ];
+
+ const result = expandSnippet({ direction: 'down', lines, snippetIndex: 0, snippets });
+
+ expect(result).toHaveLength(2);
+ expect(result[0].map(l => l.line)).toEqual(range(4, 22));
+ expect(result[1].map(l => l.line)).toEqual(range(38, 43));
+ });
+});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+const LINES_ABOVE = 2;
+const LINES_BELOW = 2;
+export const MERGE_DISTANCE = 4; // Merge if snippets are four lines away (separated by 3 lines) or fewer
+export const LINES_BELOW_LAST = 9;
+export const EXPAND_BY_LINES = 10;
+
+function unknownComponent(key: string): T.SnippetsByComponent {
+ return {
+ component: {
+ key,
+ measures: {},
+ path: '',
+ project: '',
+ projectName: '',
+ q: 'FIL',
+ uuid: ''
+ },
+ sources: []
+ };
+}
+
+function collision([startA, endA]: number[], [startB, endB]: number[]) {
+ return !(startA > endB + MERGE_DISTANCE || endA < startB - MERGE_DISTANCE);
+}
+
+export function createSnippets(
+ locations: T.FlowLocation[],
+ componentLines: T.LineMap = {},
+ last: boolean
+): T.SourceLine[][] {
+ return rangesToSnippets(
+ // For each location's range (2 above and 2 below), and then compare with other ranges
+ // to merge snippets that collide.
+ locations.reduce((snippets: Array<{ start: number; end: number }>, loc, index) => {
+ const startIndex = Math.max(1, loc.textRange.startLine - LINES_ABOVE);
+ const endIndex =
+ loc.textRange.endLine +
+ (last && index === locations.length - 1 ? LINES_BELOW_LAST : LINES_BELOW);
+
+ let firstCollision: { start: number; end: number } | undefined;
+
+ // Remove ranges that collide into the first collision
+ snippets = snippets.filter(snippet => {
+ if (collision([snippet.start, snippet.end], [startIndex, endIndex])) {
+ let keep = false;
+ // Check if we've already collided
+ if (!firstCollision) {
+ firstCollision = snippet;
+ keep = true;
+ }
+ // Merge with first collision:
+ firstCollision.start = Math.min(startIndex, snippet.start, firstCollision.start);
+ firstCollision.end = Math.max(endIndex, snippet.end, firstCollision.end);
+
+ // remove the range if it was not the first collision
+ return keep;
+ }
+ return true;
+ });
+
+ if (firstCollision === undefined) {
+ snippets.push({
+ start: startIndex,
+ end: endIndex
+ });
+ }
+
+ return snippets;
+ }, []),
+ componentLines
+ );
+}
+
+function rangesToSnippets(
+ ranges: Array<{ start: number; end: number }>,
+ componentLines: T.LineMap
+) {
+ return ranges
+ .map(range => {
+ const lines = [];
+ for (let i = range.start; i <= range.end; i++) {
+ if (componentLines[i]) {
+ lines.push(componentLines[i]);
+ }
+ }
+ return lines;
+ })
+ .filter(snippet => snippet.length > 0);
+}
+
+export function groupLocationsByComponent(
+ locations: T.FlowLocation[],
+ components: { [key: string]: T.SnippetsByComponent }
+) {
+ let currentComponent = '';
+ let currentGroup: T.SnippetGroup;
+ const groups: T.SnippetGroup[] = [];
+
+ locations.forEach((loc, index) => {
+ if (loc.component !== currentComponent) {
+ currentGroup = {
+ ...(components[loc.component] || unknownComponent(loc.component)),
+ locations: []
+ };
+ groups.push(currentGroup);
+ currentComponent = loc.component;
+ }
+ loc.index = index;
+ currentGroup.locations.push(loc);
+ });
+
+ return groups;
+}
+
+export function expandSnippet({
+ direction,
+ lines,
+ snippetIndex,
+ snippets
+}: {
+ direction: T.ExpandDirection;
+ lines: T.LineMap;
+ snippetIndex: number;
+ snippets: T.SourceLine[][];
+}) {
+ const snippetToExpand = snippets[snippetIndex];
+
+ const snippetToExpandRange = {
+ start: Math.max(0, snippetToExpand[0].line - (direction === 'up' ? EXPAND_BY_LINES : 0)),
+ end:
+ snippetToExpand[snippetToExpand.length - 1].line +
+ (direction === 'down' ? EXPAND_BY_LINES : 0)
+ };
+
+ const ranges: Array<{ start: number; end: number }> = [];
+
+ snippets.forEach((snippet, index: number) => {
+ const snippetRange = {
+ start: snippet[0].line,
+ end: snippet[snippet.length - 1].line
+ };
+
+ if (index === snippetIndex) {
+ // keep expanded snippet
+ ranges.push(snippetToExpandRange);
+ } else if (
+ collision(
+ [snippetRange.start, snippetRange.end],
+ [snippetToExpandRange.start, snippetToExpandRange.end]
+ )
+ ) {
+ // Merge with expanded snippet
+ snippetToExpandRange.start = Math.min(snippetRange.start, snippetToExpandRange.start);
+ snippetToExpandRange.end = Math.max(snippetRange.end, snippetToExpandRange.end);
+ } else {
+ // No collision, jsut keep the snippet
+ ranges.push(snippetRange);
+ }
+ });
+
+ return rangesToSnippets(ranges, lines);
+}
+
+export function inSnippet(line: number, snippet: T.SourceLine[]) {
+ return line >= snippet[0].line && line <= snippet[snippet.length - 1].line;
+}
border-color: rgba(209, 133, 130, 0.6);
}
+.component-source-container {
+ border: 1px solid var(--gray80);
+}
+
+.component-source-container + .component-source-container {
+ margin-top: var(--gridSize);
+}
+
+.component-source-container-header {
+ background-color: var(--gray94);
+ padding: var(--gridSize);
+}
+
+.snippet {
+ margin: var(--gridSize);
+ border: 1px solid var(--gray80);
+ overflow-x: auto;
+}
+
+.snippet > .expand-block {
+ box-sizing: border-box;
+ color: var(--secondFontColor);
+ height: 20px;
+ width: 100%;
+ padding: calc(var(--gridSize) / 4);
+ border: 0;
+ text-align: left;
+ cursor: pointer;
+}
+.snippet > .expand-block:hover {
+ color: var(--darkBlue);
+}
+.snippet > .expand-block-above {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAAXNSR0IArs4c6QAAADdJREFUCB1dzMEKADAIAlBd1v9/bcc2YgRjHh8qq2qTxCQzsX4wM6y30RARF3sy0Es1SIK7Y64OpCES1W69JS4AAAAASUVORK5CYII=');
+}
+.snippet > .expand-block-below {
+ background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4wQQBjQEQVd5jwAAADhJREFUCNddyTEKADEMA8GVA/7/Z+PGwUp1cGTaYe/tv5lxrLWoKj6SiMzkjZDEG7JtANt0N+ccLrB/KZxXTt7fAAAAAElFTkSuQmCC');
+}
+
.issues-my-issues-filter {
margin-bottom: 24px;
text-align: center;
import * as React from 'react';
import * as classNames from 'classnames';
import { intersection, uniqBy } from 'lodash';
-import SourceViewerHeader from './SourceViewerHeader';
import SourceViewerCode from './SourceViewerCode';
+import SourceViewerHeader from './SourceViewerHeader';
+import SourceViewerHeaderSlim from './SourceViewerHeaderSlim';
import { SourceViewerContext } from './SourceViewerContext';
import DuplicationPopup from './components/DuplicationPopup';
import defaultLoadIssues from './helpers/loadIssues';
scroll?: (element: HTMLElement) => void;
selectedIssue?: string;
showMeasures?: boolean;
+ slimHeader?: boolean;
}
interface State {
);
}
+ renderHeader(branchLike: T.BranchLike | undefined, sourceViewerFile: T.SourceViewerFile) {
+ return this.props.slimHeader ? (
+ <SourceViewerHeaderSlim branchLike={branchLike} sourceViewerFile={sourceViewerFile} />
+ ) : (
+ <WorkspaceContext.Consumer>
+ {({ openComponent }) => (
+ <SourceViewerHeader
+ branchLike={this.props.branchLike}
+ issues={this.state.issues}
+ openComponent={openComponent}
+ showMeasures={this.props.showMeasures}
+ sourceViewerFile={sourceViewerFile}
+ />
+ )}
+ </WorkspaceContext.Consumer>
+ );
+ }
+
render() {
const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
return (
<SourceViewerContext.Provider value={{ branchLike: this.props.branchLike, file: component }}>
<div className={className} ref={node => (this.node = node)}>
- <WorkspaceContext.Consumer>
- {({ openComponent }) => (
- <SourceViewerHeader
- branchLike={this.props.branchLike}
- issues={this.state.issues}
- openComponent={openComponent}
- showMeasures={this.props.showMeasures}
- sourceViewerFile={component}
- />
- )}
- </WorkspaceContext.Consumer>
+ {this.renderHeader(this.props.branchLike, component)}
{sourceRemoved && (
<Alert className="spacer-top" variant="warning">
{translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { intersection } from 'lodash';
import Line from './components/Line';
-import { getLinearLocations } from './helpers/issueLocations';
+import { getSecondaryIssueLocationsForLine } from './helpers/issueLocations';
+import {
+ optimizeSelectedIssue,
+ optimizeLocationMessage,
+ optimizeHighlightedSymbols
+} from './helpers/lines';
import { translate } from '../../helpers/l10n';
import { Button } from '../ui/buttons';
return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY;
};
- getSecondaryIssueLocationsForLine = (line: T.SourceLine): T.LinearIssueLocation[] => {
- const { highlightedLocations } = this.props;
- if (!highlightedLocations) {
- return EMPTY_ARRAY;
- }
- return highlightedLocations.reduce((locations, location, index) => {
- const linearLocations: T.LinearIssueLocation[] = location
- ? getLinearLocations(location.textRange)
- .filter(l => l.line === line.line)
- .map(l => ({ ...l, startLine: location.textRange.startLine, index }))
- : [];
- return [...locations, ...linearLocations];
- }, []);
- };
-
renderLine = ({
line,
index,
displayDuplications: boolean;
displayIssues: boolean;
}) => {
- const { highlightedLocationMessage, selectedIssue, sources } = this.props;
+ const { highlightedLocationMessage, highlightedLocations, selectedIssue, sources } = this.props;
- const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line);
+ const secondaryIssueLocations = getSecondaryIssueLocationsForLine(line, highlightedLocations);
const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0;
const issuesForLine = this.getIssuesForLine(line);
- // for the following properties pass null if the line for sure is not impacted
- const symbolsForLine = this.props.symbolsByLine[line.line] || [];
- const { highlightedSymbols } = this.props;
- let optimizedHighlightedSymbols: string[] | undefined = intersection(
- symbolsForLine,
- highlightedSymbols
- );
- if (!optimizedHighlightedSymbols.length) {
- optimizedHighlightedSymbols = undefined;
- }
-
- const optimizedSelectedIssue =
- selectedIssue !== undefined && issuesForLine.find(issue => issue.key === selectedIssue)
- ? selectedIssue
- : undefined;
-
- const optimizedSecondaryIssueLocations =
- secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY;
-
- const optimizedLocationMessage =
- highlightedLocationMessage != null &&
- optimizedSecondaryIssueLocations.some(
- location => location.index === highlightedLocationMessage.index
- )
- ? highlightedLocationMessage
- : undefined;
-
return (
<Line
branchLike={this.props.branchLike}
duplications={this.getDuplicationsForLine(line)}
duplicationsCount={duplicationsCount}
highlighted={line.line === this.props.highlightedLine}
- highlightedLocationMessage={optimizedLocationMessage}
- highlightedSymbols={optimizedHighlightedSymbols}
+ highlightedLocationMessage={optimizeLocationMessage(
+ highlightedLocationMessage,
+ secondaryIssueLocations
+ )}
+ highlightedSymbols={optimizeHighlightedSymbols(
+ this.props.symbolsByLine[line.line],
+ this.props.highlightedSymbols
+ )}
issueLocations={this.getIssueLocationsForLine(line)}
issuePopup={this.props.issuePopup}
issues={issuesForLine}
previousLine={index > 0 ? sources[index - 1] : undefined}
renderDuplicationPopup={this.props.renderDuplicationPopup}
scroll={this.props.scroll}
- secondaryIssueLocations={optimizedSecondaryIssueLocations}
- selectedIssue={optimizedSelectedIssue}
+ secondaryIssueLocations={secondaryIssueLocations}
+ selectedIssue={optimizeSelectedIssue(selectedIssue, issuesForLine)}
/>
);
};
</a>
</div>
- {subProject != null && (
+ {subProject !== undefined && (
<div className="component-name-parent">
<QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
</div>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.source-viewer-header-slim {
+ padding: 4px 10px 4px;
+ border-bottom: 1px solid var(--gray80);
+ background-color: var(--barBackgroundColor);
+ align-items: center;
+ min-height: 25px;
+}
+
+.source-viewer-header-slim-actions {
+ margin-left: calc(3 * var(--gridSize));
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import DeferredSpinner from '../common/DeferredSpinner';
+import Favorite from '../controls/Favorite';
+import ExpandSnippetIcon from '../icons-components/ExpandSnippetIcon';
+import QualifierIcon from '../icons-components/QualifierIcon';
+import { ButtonIcon } from '../ui/buttons';
+import { getPathUrlAsString, getBranchLikeUrl } from '../../helpers/urls';
+import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
+import { isMainBranch } from '../../helpers/branches';
+import './SourceViewerHeaderSlim.css';
+
+interface Props {
+ branchLike: T.BranchLike | undefined;
+ expandable?: boolean;
+ loading?: boolean;
+ onExpand?: () => void;
+ sourceViewerFile: T.SourceViewerFile;
+}
+
+export default function SourceViewerHeaderSlim({
+ branchLike,
+ expandable,
+ loading,
+ onExpand,
+ sourceViewerFile
+}: Props) {
+ const { key, path, project, projectName, q, subProject, subProjectName } = sourceViewerFile;
+
+ return (
+ <div className="source-viewer-header-slim display-flex-row display-flex-space-between">
+ <div className="display-flex-row flex-1">
+ <div>
+ <a
+ className="link-with-icon"
+ href={getPathUrlAsString(getBranchLikeUrl(project, branchLike))}>
+ <QualifierIcon qualifier="TRK" /> <span>{projectName}</span>
+ </a>
+ </div>
+
+ {subProject !== undefined && (
+ <div className="">
+ <QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span>
+ </div>
+ )}
+
+ <div className="spacer-left">
+ <QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span>
+ <span className="component-name-file">{fileFromPath(path)}</span>
+ </div>
+ {sourceViewerFile.canMarkAsFavorite && (!branchLike || isMainBranch(branchLike)) && (
+ <div className="nudged-up">
+ <Favorite
+ className="component-name-favorite"
+ component={key}
+ favorite={sourceViewerFile.fav || false}
+ qualifier={sourceViewerFile.q}
+ />
+ </div>
+ )}
+ </div>
+
+ {expandable && (
+ <DeferredSpinner className="little-spacer-right" loading={loading}>
+ <div className="source-viewer-header-slim-actions flex-0">
+ <ButtonIcon className="js-actions" onClick={onExpand}>
+ <ExpandSnippetIcon />
+ </ButtonIcon>
+ </div>
+ </DeferredSpinner>
+ )}
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+.source-line:hover .source-line-number,
+.source-line:hover .source-line-issues,
+.source-line:hover .source-line-coverage,
+.source-line:hover .source-line-duplications,
+.source-line:hover .source-line-duplications-extra,
+.source-line:hover .source-line-scm {
+ border-color: #e9e9e9;
+ background-color: #e9e9e9;
+}
+
+.source-line:hover .source-line-code {
+ background-color: #f5f5f5;
+}
+
+.source-line-highlighted .source-line-number,
+.source-line-highlighted:hover .source-line-number,
+.source-line-highlighted .source-line-issues,
+.source-line-highlighted:hover .source-line-issues,
+.source-line-highlighted .source-line-coverage,
+.source-line-highlighted:hover .source-line-coverage,
+.source-line-highlighted .source-line-duplications,
+.source-line-highlighted:hover .source-line-duplications,
+.source-line-highlighted .source-line-duplications-extra,
+.source-line-highlighted:hover .source-line-duplications-extra,
+.source-line-highlighted .source-line-scm,
+.source-line-highlighted:hover .source-line-scm {
+ border-color: #c4dfec !important;
+ background-color: #c4dfec;
+}
+
+.source-line-highlighted .source-line-code,
+.source-line-highlighted:hover .source-line-code {
+ background-color: #d9edf7;
+}
+
+.source-line-filtered .source-line-code {
+ background-color: var(--leakColor) !important;
+}
+
+.source-line-filtered.source-line-highlighted .source-line-code,
+.source-line-filtered.source-line-highlighted:hover .source-line-code {
+ background-color: #cdd9c4 !important;
+}
+
+.source-line-filtered:hover .source-line-code {
+ background-color: #f1e8cb !important;
+}
+
+.source-line-filtered.source-line-filtered-dark .source-line-code {
+ background-color: #f9ebb7 !important;
+}
+
+.source-line-filtered.source-line-filtered-dark:hover .source-line-code {
+ background-color: #eaddb2 !important;
+}
+
+.source-line-last .source-line-code {
+ padding-bottom: 160px;
+}
+
+.source-viewer pre {
+ height: 18px;
+ padding: 0;
+}
+
+.source-viewer pre,
+.source-line-number,
+.source-line-scm {
+ line-height: 18px;
+ font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+ font-size: var(--smallFontSize);
+}
+
+.source-line-code {
+ position: relative;
+ padding: 0 10px;
+}
+
+.source-line-code pre {
+ float: left;
+}
+
+.source-line-code .issue-list {
+ margin-left: -10px;
+ margin-right: -10px;
+}
+
+.source-line-code-inner {
+ min-height: 18px;
+}
+
+.source-line-code-inner:before,
+.source-line-code-inner:after {
+ display: table;
+ content: '';
+ line-height: 0;
+}
+
+.source-line-code-inner:after {
+ clear: both;
+}
+
+.source-line-code-issue {
+ display: inline-block;
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAGCAYAAAAPDoR2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1M0M2Rjk4M0M3QUYxMUUzODkzRUREMUM5OTNDMjY4QSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1M0M2Rjk4NEM3QUYxMUUzODkzRUREMUM5OTNDMjY4QSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjUzQzZGOTgxQzdBRjExRTM4OTNFREQxQzk5M0MyNjhBIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjUzQzZGOTgyQzdBRjExRTM4OTNFREQxQzk5M0MyNjhBIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+bcqJtQAAAEhJREFUeNpi+G+swwDGDAwgbAWlwZiJAQFCgfgwEIfDRaC67ID4NRDnQ2kQnwFZwgFqnANMAQOUYY9sF0wBiCGH5CBkrAgQYACuWi4sSGW8yAAAAABJRU5ErkJggg==);
+ background-repeat: repeat-x;
+ background-size: 4px;
+ background-position: bottom;
+}
+
+.source-meta {
+ position: relative;
+ vertical-align: top;
+ width: 1px;
+ background-clip: padding-box;
+ user-select: none;
+}
+
+.source-meta:focus {
+ outline: none;
+}
+
+.source-meta[role='button'] {
+ cursor: pointer;
+}
+
+.source-meta + .source-meta {
+ border-left: 1px solid var(--barBackgroundColor);
+}
+
+.source-line-number {
+ min-width: 18px;
+ padding: 0 10px;
+ background-color: var(--barBackgroundColor);
+ color: var(--secondFontColor);
+ text-align: right;
+}
+
+.source-line-number:before {
+ content: attr(data-line-number);
+}
+
+.source-line-issues {
+ position: relative;
+ padding: 0 2px;
+ background-color: var(--barBackgroundColor);
+ white-space: nowrap;
+}
+
+.source-line-with-issues {
+ padding-right: 4px;
+}
+
+.source-line-issues-counter {
+ position: absolute;
+ left: 17px;
+ line-height: 8px;
+ font-size: 8px;
+ z-index: 900;
+}
+
+.source-line-coverage {
+ background-color: var(--barBackgroundColor);
+}
+
+.source-line-duplications,
+.source-line-duplications-extra {
+ background-color: var(--barBackgroundColor);
+}
+
+.source-line-duplications-extra {
+ display: none;
+}
+
+.source-duplications-expanded .source-line-duplications {
+ display: none;
+}
+
+.source-duplications-expanded .source-line-duplications-extra {
+ display: table-cell;
+}
+
+.source-line-scm {
+ padding: 0 5px;
+ background-color: var(--barBackgroundColor);
+}
+
+.source-line-scm-inner {
+ max-width: 40px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ 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;
+}
+
+.source-line-covered {
+ background-color: var(--lineCoverageGreen) !important;
+}
+
+.source-line-uncovered {
+ background-color: var(--lineCoverageRed) !important;
+}
+
+.source-line-partially-covered {
+ background-color: var(--lineCoverageRed) !important;
+ background-image: repeating-linear-gradient(
+ 45deg,
+ rgba(255, 255, 255, 0.5) 4px,
+ transparent 4px,
+ transparent 8px,
+ rgba(255, 255, 255, 0.5) 8px,
+ rgba(255, 255, 255, 0.5) 12px,
+ transparent 12px,
+ transparent 16px,
+ rgba(255, 255, 255, 0.5) 16px,
+ rgba(255, 255, 255, 0.5) 20px
+ ) !important;
+}
+
+.source-line-duplicated {
+ background-color: #797979 !important;
+}
import LineDuplicationBlock from './LineDuplicationBlock';
import LineIssuesIndicator from './LineIssuesIndicator';
import LineCode from './LineCode';
+import './Line.css';
interface Props {
branchLike: T.BranchLike | undefined;
previousLine: T.SourceLine | undefined;
renderDuplicationPopup: (index: number, line: number) => JSX.Element;
scroll?: (element: HTMLElement) => void;
- secondaryIssueLocations: Array<{
- from: number;
- to: number;
- line: number;
- index: number;
- startLine: number;
- }>;
+ secondaryIssueLocations: T.LinearIssueLocation[];
selectedIssue: string | undefined;
+ verticalBuffer?: number;
}
+const LINE_HEIGHT = 18;
+
export default class Line extends React.PureComponent<Props> {
isPopupOpen = (name: string, index?: number) => {
const { line, linePopup } = this.props;
'source-line-filtered-dark':
displayCoverage &&
(line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'),
- 'source-line-last': this.props.last
+ 'source-line-last': this.props.last === true
});
+ const bottomPadding = this.props.verticalBuffer
+ ? this.props.verticalBuffer * LINE_HEIGHT
+ : undefined;
+
return (
<tr className={className} data-line-number={line.line}>
<LineNumber
previousLine={this.props.previousLine}
/>
- {this.props.displayIssues && !this.props.displayAllIssues && (
+ {this.props.displayIssues && !this.props.displayAllIssues ? (
<LineIssuesIndicator
issues={this.props.issues}
line={line}
onClick={this.handleIssuesIndicatorClick}
/>
+ ) : (
+ <td className="source-meta source-line-issues" />
)}
{this.props.displayDuplications && (
onIssueSelect={this.props.onIssueSelect}
onLocationSelect={this.props.onLocationSelect}
onSymbolClick={this.props.onSymbolClick}
+ padding={bottomPadding}
scroll={this.props.scroll}
secondaryIssueLocations={this.props.secondaryIssueLocations}
selectedIssue={this.props.selectedIssue}
onIssueSelect: (issueKey: string) => void;
onLocationSelect: ((index: number) => void) | undefined;
onSymbolClick: (symbols: Array<string>) => void;
+ padding?: number;
scroll?: (element: HTMLElement) => void;
- secondaryIssueLocations: Array<{
- from: number;
- to: number;
- line: number;
- index: number;
- startLine: number;
- }>;
+ secondaryIssueLocations: T.LinearIssueLocation[];
selectedIssue: string | undefined;
showIssues?: boolean;
}
this.attachEvents();
if (
this.props.highlightedLocationMessage &&
- prevProps.highlightedLocationMessage !== this.props.highlightedLocationMessage &&
+ (!prevProps.highlightedLocationMessage ||
+ prevProps.highlightedLocationMessage.index !==
+ this.props.highlightedLocationMessage.index) &&
this.activeMarkerNode &&
this.props.scroll
) {
issueLocations,
line,
onIssueSelect,
+ padding,
secondaryIssueLocations,
selectedIssue,
showIssues
token.markers.forEach(marker => {
const selected =
highlightedLocationMessage !== undefined && highlightedLocationMessage.index === marker;
- const message = selected ? highlightedLocationMessage!.text : undefined;
+ const loc = secondaryIssueLocations.find(loc => loc.index === marker);
+ const message = loc && loc.text;
renderedTokens.push(this.renderMarker(marker, message, selected, leadingMarker));
});
}
leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
});
+ const style = padding
+ ? {
+ paddingBottom: padding + 'px'
+ }
+ : undefined;
+
return (
- <td className={className} data-line-number={line.line}>
+ <td className={className} data-line-number={line.line} style={style}>
<div className="source-line-code-inner">
<pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre>
</div>
selectedIssue={selectedIssue}
/>
)}
+ {selectedIssue && !showIssues && (
+ <LineIssuesList
+ branchLike={this.props.branchLike}
+ issuePopup={this.props.issuePopup}
+ issues={issues.filter(i => i.key === selectedIssue)}
+ onIssueChange={this.props.onIssueChange}
+ onIssueClick={onIssueSelect}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ selectedIssue={selectedIssue}
+ />
+ )}
</td>
);
}
displayAllIssues={false}
displayCoverage={false}
displayDuplications={false}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayIssues={false}
displayLocationMarkers={false}
duplications={[]}
exports[`should render correctly 1`] = `
<tr
- className="source-line"
- data-line-number={5}
+ className="source-line source-line-filtered"
+ data-line-number={16}
>
<LineNumber
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
<LineSCM
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
+ <td
+ className="source-meta source-line-issues"
+ />
<LineCode
branchLike={
Object {
"title": "Foo Bar feature",
}
}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
issues={
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onIssueChange={[MockFunction]}
exports[`should render correctly for last, new, and highlighted lines 1`] = `
<tr
className="source-line source-line-highlighted source-line-filtered source-line-last"
- data-line-number={5}
+ data-line-number={16}
>
<LineNumber
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
+ "duplicated": false,
"isNew": true,
- "line": 5,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
<LineSCM
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
+ "duplicated": false,
"isNew": true,
- "line": 5,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
+ <td
+ className="source-meta source-line-issues"
+ />
<LineCode
branchLike={
Object {
"title": "Foo Bar feature",
}
}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
issues={
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
+ "duplicated": false,
"isNew": true,
- "line": 5,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onIssueChange={[MockFunction]}
exports[`should render correctly with coverage 1`] = `
<tr
- className="source-line"
- data-line-number={5}
+ className="source-line source-line-filtered"
+ data-line-number={16}
>
<LineNumber
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
<LineSCM
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
+ <td
+ className="source-meta source-line-issues"
+ />
<LineCoverage
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
/>
"title": "Foo Bar feature",
}
}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
issues={
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onIssueChange={[MockFunction]}
exports[`should render correctly with duplication information 1`] = `
<tr
- className="source-line"
- data-line-number={5}
+ className="source-line source-line-filtered"
+ data-line-number={16}
>
<LineNumber
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
<LineSCM
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
popupOpen={false}
/>
+ <td
+ className="source-meta source-line-issues"
+ />
<LineDuplications
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onClick={[MockFunction]}
key="0"
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
key="1"
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
key="2"
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
"title": "Foo Bar feature",
}
}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
issues={
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onIssueChange={[MockFunction]}
exports[`should render correctly with issues info 1`] = `
<tr
- className="source-line"
- data-line-number={5}
+ className="source-line source-line-filtered"
+ data-line-number={16}
>
<LineNumber
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
<LineSCM
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onPopupToggle={[MockFunction]}
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onClick={[Function]}
"title": "Foo Bar feature",
}
}
- displayIssueLocationsCount={false}
- displayIssueLocationsLink={false}
displayLocationMarkers={false}
issueLocations={Array []}
issues={
}
line={
Object {
- "code": "function fooBar() {",
+ "code": "<span class=\\"k\\">import</span> java.util.<span class=\\"sym-9 sym\\">ArrayList</span>;",
"coverageStatus": "covered",
"coveredConditions": 2,
- "line": 5,
+ "duplicated": false,
+ "isNew": true,
+ "line": 16,
+ "scmAuthor": "simon.brandhof@sonarsource.com",
+ "scmDate": "2018-12-11T10:48:39+0100",
+ "scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0",
}
}
onIssueChange={[MockFunction]}
return index;
}
+export function issuesByComponentAndLine(
+ issues: T.Issue[] = []
+): { [component: string]: { [line: number]: T.Issue[] } } {
+ return issues.reduce((mapping: { [component: string]: { [line: number]: T.Issue[] } }, issue) => {
+ mapping[issue.component] = mapping[issue.component] || {};
+ const line = issue.textRange ? issue.textRange.endLine : 0;
+ mapping[issue.component][line] = mapping[issue.component][line] || [];
+ mapping[issue.component][line].push(issue);
+ return mapping;
+ }, {});
+}
+
export function locationsByLine(issues: T.Issue[]) {
const index: { [line: number]: T.LinearIssueLocation[] } = {};
issues.forEach(issue => {
}
return locations;
}
+
+export function getSecondaryIssueLocationsForLine(
+ line: T.SourceLine,
+ highlightedLocations: (T.FlowLocation | undefined)[] | undefined
+): T.LinearIssueLocation[] {
+ if (!highlightedLocations) {
+ return [];
+ }
+ return highlightedLocations.reduce((locations, location) => {
+ const linearLocations: T.LinearIssueLocation[] = location
+ ? getLinearLocations(location.textRange)
+ .filter(l => l.line === line.line)
+ .map(l => ({
+ ...l,
+ startLine: location.textRange.startLine,
+ index: location.index,
+ text: location.msg
+ }))
+ : [];
+ return [...locations, ...linearLocations];
+ }, []);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import { intersection } from 'lodash';
+
+export function optimizeHighlightedSymbols(
+ symbolsForLine: string[] = [],
+ highlightedSymbols: string[] = []
+): string[] | undefined {
+ const symbols = intersection(symbolsForLine, highlightedSymbols);
+
+ return symbols.length ? symbols : undefined;
+}
+
+export function optimizeLocationMessage(
+ highlightedLocationMessage: { index: number; text: string | undefined } | undefined,
+ optimizedSecondaryIssueLocations: T.LinearIssueLocation[]
+) {
+ return highlightedLocationMessage != null &&
+ optimizedSecondaryIssueLocations.some(
+ location => location.index === highlightedLocationMessage.index
+ )
+ ? highlightedLocationMessage
+ : undefined;
+}
+
+export function optimizeSelectedIssue(selectedIssue: string | undefined, issuesForLine: T.Issue[]) {
+ return selectedIssue !== undefined && issuesForLine.find(issue => issue.key === selectedIssue)
+ ? selectedIssue
+ : undefined;
+}
border-collapse: collapse;
}
-.source-line:hover .source-line-number,
-.source-line:hover .source-line-issues,
-.source-line:hover .source-line-coverage,
-.source-line:hover .source-line-duplications,
-.source-line:hover .source-line-duplications-extra,
-.source-line:hover .source-line-scm {
- border-color: #e9e9e9;
- background-color: #e9e9e9;
-}
-
-.source-line:hover .source-line-code {
- background-color: #f5f5f5;
-}
-
-.source-line-highlighted .source-line-number,
-.source-line-highlighted:hover .source-line-number,
-.source-line-highlighted .source-line-issues,
-.source-line-highlighted:hover .source-line-issues,
-.source-line-highlighted .source-line-coverage,
-.source-line-highlighted:hover .source-line-coverage,
-.source-line-highlighted .source-line-duplications,
-.source-line-highlighted:hover .source-line-duplications,
-.source-line-highlighted .source-line-duplications-extra,
-.source-line-highlighted:hover .source-line-duplications-extra,
-.source-line-highlighted .source-line-scm,
-.source-line-highlighted:hover .source-line-scm {
- border-color: #c4dfec !important;
- background-color: #c4dfec;
-}
-
-.source-line-highlighted .source-line-code,
-.source-line-highlighted:hover .source-line-code {
- background-color: #d9edf7;
-}
-
-.source-line-filtered .source-line-code {
- background-color: var(--leakColor) !important;
-}
-
-.source-line-filtered.source-line-highlighted .source-line-code,
-.source-line-filtered.source-line-highlighted:hover .source-line-code {
- background-color: #cdd9c4 !important;
-}
-
-.source-line-filtered:hover .source-line-code {
- background-color: #f1e8cb !important;
-}
-
-.source-line-filtered.source-line-filtered-dark .source-line-code {
- background-color: #f9ebb7 !important;
-}
-
-.source-line-filtered.source-line-filtered-dark:hover .source-line-code {
- background-color: #eaddb2 !important;
-}
-
-.source-line-last .source-line-code {
- padding-bottom: 160px;
-}
-
-.source-viewer pre {
- height: 18px;
- padding: 0;
-}
-
-.source-viewer pre,
-.source-line-number,
-.source-line-scm {
- line-height: 18px;
- font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
- font-size: var(--smallFontSize);
-}
-
-.source-line-code {
- position: relative;
- padding: 0 10px;
-}
-
-.source-line-code pre {
- float: left;
-}
-
-.source-line-code .issue-list {
- margin-left: -10px;
- margin-right: -10px;
-}
-
-.source-line-code-inner:before,
-.source-line-code-inner:after {
- display: table;
- content: '';
- line-height: 0;
-}
-
-.source-line-code-inner:after {
- clear: both;
-}
-
-.source-line-code-issue {
- display: inline-block;
- background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAGCAYAAAAPDoR2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo1M0M2Rjk4M0M3QUYxMUUzODkzRUREMUM5OTNDMjY4QSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo1M0M2Rjk4NEM3QUYxMUUzODkzRUREMUM5OTNDMjY4QSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjUzQzZGOTgxQzdBRjExRTM4OTNFREQxQzk5M0MyNjhBIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjUzQzZGOTgyQzdBRjExRTM4OTNFREQxQzk5M0MyNjhBIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+bcqJtQAAAEhJREFUeNpi+G+swwDGDAwgbAWlwZiJAQFCgfgwEIfDRaC67ID4NRDnQ2kQnwFZwgFqnANMAQOUYY9sF0wBiCGH5CBkrAgQYACuWi4sSGW8yAAAAABJRU5ErkJggg==);
- background-repeat: repeat-x;
- background-size: 4px;
- background-position: bottom;
-}
-
-.source-meta {
- position: relative;
- vertical-align: top;
- width: 1px;
- background-clip: padding-box;
- user-select: none;
-}
-
-.source-meta:focus {
- outline: none;
-}
-
-.source-meta[role='button'] {
- cursor: pointer;
-}
-
-.source-meta + .source-meta {
- border-left: 1px solid var(--barBackgroundColor);
-}
-
-.source-line-number {
- min-width: 18px;
- padding: 0 10px;
- background-color: var(--barBackgroundColor);
- color: var(--secondFontColor);
- text-align: right;
-}
-
-.source-line-number:before {
- content: attr(data-line-number);
-}
-
-.source-line-issues {
- position: relative;
- padding: 0 2px;
- background-color: var(--barBackgroundColor);
- white-space: nowrap;
-}
-
-.source-line-with-issues {
- padding-right: 4px;
-}
-
-.source-line-issues-counter {
- position: absolute;
- left: 17px;
- line-height: 8px;
- font-size: 8px;
- z-index: 900;
-}
-
-.source-line-coverage {
- background-color: var(--barBackgroundColor);
-}
-
-.source-line-duplications,
-.source-line-duplications-extra {
- background-color: var(--barBackgroundColor);
-}
-
-.source-line-duplications-extra {
- display: none;
-}
-
-.source-duplications-expanded .source-line-duplications {
- display: none;
-}
-
-.source-duplications-expanded .source-line-duplications-extra {
- display: table-cell;
-}
-
-.source-line-scm {
- padding: 0 5px;
- background-color: var(--barBackgroundColor);
-}
-
-.source-line-scm-inner {
- max-width: 40px;
- overflow: hidden;
- text-overflow: ellipsis;
- 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;
-}
-
-.source-line-covered {
- background-color: var(--lineCoverageGreen) !important;
-}
-
-.source-line-uncovered {
- background-color: var(--lineCoverageRed) !important;
-}
-
-.source-line-partially-covered {
- background-color: var(--lineCoverageRed) !important;
- background-image: repeating-linear-gradient(
- 45deg,
- rgba(255, 255, 255, 0.5) 4px,
- transparent 4px,
- transparent 8px,
- rgba(255, 255, 255, 0.5) 8px,
- rgba(255, 255, 255, 0.5) 12px,
- transparent 12px,
- transparent 16px,
- rgba(255, 255, 255, 0.5) 16px,
- rgba(255, 255, 255, 0.5) 20px
- ) !important;
-}
-
-.source-line-duplicated {
- background-color: #797979 !important;
-}
-
.source-viewer-header {
position: relative;
padding: 2px 10px 4px;
}
.location-index.selected {
- background-color: #bc5e5e;
+ background-color: #8f3030;
}
.location-index.muted {
}
.location-index > .location-message {
+ display: none;
position: absolute;
bottom: calc(100% + 4px);
left: 0;
}
+.location-index:hover > .location-message {
+ display: block;
+}
+
.location-index > .location-message::after {
position: absolute;
bottom: -5px;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+import * as React from 'react';
+import Icon, { IconProps } from './Icon';
+
+export default function ExpandSnippetIcon({ className, fill = 'currentColor', size }: IconProps) {
+ return (
+ <Icon className={className} size={size}>
+ <g fill="none" fillRule="evenodd">
+ <path
+ d="M8 1v4H4"
+ stroke={fill}
+ strokeWidth="2"
+ transform="scale(-.83333 -.84583) rotate(45 7.66 -19.75)"
+ />
+ <path d="M3 5.78h10v1.7H3z" fill={fill} />
+ <path d="M7.17 2.4h1.66v5.07H7.17z" fill={fill} />
+ <g>
+ <path
+ d="M8.16 1.81V6.1H3.9"
+ stroke={fill}
+ strokeWidth="2"
+ transform="scale(.83333 .84583) rotate(45 -4.2 13.2)"
+ />
+ <path d="M13 10.01H3v-1.7h10z" fill={fill} />
+ <path d="M8.83 13.4H7.17V9.15h1.66z" fill={fill} />
+ </g>
+ </g>
+ </Icon>
+ );
+}
};
}
+export function mockSnippetsByComponent(
+ component = 'main.js',
+ lines: number[] = [16]
+): T.SnippetsByComponent {
+ const sources = lines.reduce((lines: { [key: number]: T.SourceLine }, line) => {
+ lines[line] = mockSourceLine({ line });
+ return lines;
+ }, {});
+ return {
+ component: mockSourceViewerFile({
+ key: component,
+ path: component
+ }),
+ sources
+ };
+}
+
+export function mockSourceLine(overrides: Partial<T.SourceLine> = {}): T.SourceLine {
+ return {
+ line: 16,
+ code: '<span class="k">import</span> java.util.<span class="sym-9 sym">ArrayList</span>;',
+ coverageStatus: 'covered',
+ coveredConditions: 2,
+ scmRevision: '80f564becc0c0a1c9abaa006eca83a4fd278c3f0',
+ scmAuthor: 'simon.brandhof@sonarsource.com',
+ scmDate: '2018-12-11T10:48:39+0100',
+ duplicated: false,
+ isNew: true,
+ ...overrides
+ };
+}
+
export function mockCurrentUser(overrides: Partial<T.CurrentUser> = {}): T.CurrentUser {
return {
isLoggedIn: false,
return createStore(reducer, state);
}
-export function mockSourceLine(overrides: Partial<T.SourceLine> = {}): T.SourceLine {
- return {
- code: 'function fooBar() {',
- coverageStatus: 'covered',
- coveredConditions: 2,
- line: 5,
- ...overrides
- };
-}
-
export function mockDocumentationEntry(
overrides: Partial<DocumentationEntry> = {}
): DocumentationEntry {
source_viewer.load_more_code=Load More Code
source_viewer.loading_more_code=Loading More Code...
+source_viewer.expand_above=Expand above
+source_viewer.expand_below=Expand below
#------------------------------------------------------------------------------
#