Also includes SONAR-11901: Add slim header for the issues pagetags/7.8
@@ -20,6 +20,7 @@ | |||
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 }>; | |||
@@ -166,3 +167,21 @@ export function searchIssueAuthors(data: { | |||
}): 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); | |||
} |
@@ -52,8 +52,6 @@ | |||
} | |||
.component-name-favorite { | |||
position: relative; | |||
top: -1px; | |||
margin-left: 4px; | |||
padding: 2px 0; | |||
padding: 0; | |||
} |
@@ -57,6 +57,10 @@ th.nowrap { | |||
font-size: var(--smallFontSize); | |||
} | |||
.nudged-up { | |||
margin-top: -1px; | |||
} | |||
.spacer-left { | |||
margin-left: 8px !important; | |||
} |
@@ -273,6 +273,8 @@ declare namespace T { | |||
export type EditionKey = 'community' | 'developer' | 'enterprise' | 'datacenter'; | |||
export type ExpandDirection = 'up' | 'down'; | |||
export interface Extension { | |||
key: string; | |||
name: string; | |||
@@ -286,6 +288,7 @@ declare namespace T { | |||
export interface FlowLocation { | |||
component: string; | |||
componentName?: string; | |||
index?: number; | |||
msg?: string; | |||
textRange: TextRange; | |||
} | |||
@@ -400,6 +403,9 @@ declare namespace T { | |||
export type IssueType = 'BUG' | 'VULNERABILITY' | 'CODE_SMELL' | 'SECURITY_HOTSPOT'; | |||
export interface IssuesByLine { | |||
[key: number]: Issue[]; | |||
} | |||
export interface Language { | |||
key: string; | |||
name: string; | |||
@@ -418,9 +424,14 @@ declare namespace T { | |||
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; | |||
@@ -794,6 +805,14 @@ declare namespace T { | |||
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; |
@@ -17,12 +17,36 @@ | |||
* 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(); | |||
}); | |||
}); |
@@ -43,27 +43,27 @@ export function disableLocationsNavigator() { | |||
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 = |
@@ -23,7 +23,6 @@ import * as key from 'keymaster'; | |||
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'; | |||
@@ -802,7 +801,7 @@ export class App extends React.PureComponent<Props, State> { | |||
); | |||
}; | |||
selectLocation = (index?: number) => { | |||
selectLocation = (index: number) => { | |||
this.setState(actions.selectLocation(index)); | |||
}; | |||
@@ -1036,13 +1035,49 @@ export class App extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
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} | |||
@@ -1071,7 +1106,6 @@ export class App extends React.PureComponent<Props, State> { | |||
} | |||
render() { | |||
const { component } = this.props; | |||
const { openIssue, paging } = this.state; | |||
const selectedIndex = this.getSelectedIndex(); | |||
return ( | |||
@@ -1082,38 +1116,7 @@ export class App extends React.PureComponent<Props, State> { | |||
{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> |
@@ -18,12 +18,15 @@ | |||
* 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; | |||
@@ -72,57 +75,77 @@ export default class IssuesSourceViewer extends React.PureComponent<Props> { | |||
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> | |||
); | |||
} | |||
} | |||
} |
@@ -0,0 +1,361 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
/* | |||
* 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; |
@@ -0,0 +1,138 @@ | |||
/* | |||
* 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> | |||
); | |||
} | |||
} |
@@ -0,0 +1,98 @@ | |||
/* | |||
* 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} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,73 @@ | |||
/* | |||
* 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} | |||
/> | |||
); | |||
} |
@@ -0,0 +1,36 @@ | |||
// 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> | |||
`; |
@@ -0,0 +1,9 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should render correctly 1`] = ` | |||
<div> | |||
<DeferredSpinner | |||
timeout={100} | |||
/> | |||
</div> | |||
`; |
@@ -0,0 +1,193 @@ | |||
/* | |||
* 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)); | |||
}); | |||
}); |
@@ -0,0 +1,185 @@ | |||
/* | |||
* 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; | |||
} |
@@ -225,6 +225,45 @@ | |||
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; |
@@ -20,8 +20,9 @@ | |||
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'; | |||
@@ -81,6 +82,7 @@ export interface Props { | |||
scroll?: (element: HTMLElement) => void; | |||
selectedIssue?: string; | |||
showMeasures?: boolean; | |||
slimHeader?: boolean; | |||
} | |||
interface State { | |||
@@ -667,6 +669,24 @@ export default class SourceViewerBase extends React.PureComponent<Props, 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; | |||
@@ -701,17 +721,7 @@ export default class SourceViewerBase extends React.PureComponent<Props, 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')} |
@@ -18,9 +18,13 @@ | |||
* 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'; | |||
@@ -88,21 +92,6 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
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, | |||
@@ -116,41 +105,14 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
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} | |||
@@ -162,8 +124,14 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
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} | |||
@@ -185,8 +153,8 @@ export default class SourceViewerCode extends React.PureComponent<Props> { | |||
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)} | |||
/> | |||
); | |||
}; |
@@ -97,7 +97,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props, State | |||
</a> | |||
</div> | |||
{subProject != null && ( | |||
{subProject !== undefined && ( | |||
<div className="component-name-parent"> | |||
<QualifierIcon qualifier="BRC" /> <span>{subProjectName}</span> | |||
</div> |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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)); | |||
} |
@@ -0,0 +1,92 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -0,0 +1,257 @@ | |||
/* | |||
* 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; | |||
} |
@@ -27,6 +27,7 @@ import LineDuplications from './LineDuplications'; | |||
import LineDuplicationBlock from './LineDuplicationBlock'; | |||
import LineIssuesIndicator from './LineIssuesIndicator'; | |||
import LineCode from './LineCode'; | |||
import './Line.css'; | |||
interface Props { | |||
branchLike: T.BranchLike | undefined; | |||
@@ -60,16 +61,13 @@ interface Props { | |||
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; | |||
@@ -103,9 +101,13 @@ export default class Line extends React.PureComponent<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 | |||
@@ -121,12 +123,14 @@ export default class Line extends React.PureComponent<Props> { | |||
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 && ( | |||
@@ -161,6 +165,7 @@ export default class Line extends React.PureComponent<Props> { | |||
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} |
@@ -43,14 +43,9 @@ interface Props { | |||
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; | |||
} | |||
@@ -94,7 +89,9 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
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 | |||
) { | |||
@@ -159,6 +156,7 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
issueLocations, | |||
line, | |||
onIssueSelect, | |||
padding, | |||
secondaryIssueLocations, | |||
selectedIssue, | |||
showIssues | |||
@@ -204,7 +202,8 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
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)); | |||
}); | |||
} | |||
@@ -218,8 +217,14 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
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> | |||
@@ -234,6 +239,17 @@ export default class LineCode extends React.PureComponent<Props, State> { | |||
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> | |||
); | |||
} |
@@ -92,8 +92,6 @@ function shallowRender(props: Partial<Line['props']> = {}) { | |||
displayAllIssues={false} | |||
displayCoverage={false} | |||
displayDuplications={false} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayIssues={false} | |||
displayLocationMarkers={false} | |||
duplications={[]} |
@@ -2,16 +2,21 @@ | |||
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]} | |||
@@ -20,15 +25,23 @@ exports[`should render correctly 1`] = ` | |||
<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 { | |||
@@ -40,8 +53,6 @@ exports[`should render correctly 1`] = ` | |||
"title": "Foo Bar feature", | |||
} | |||
} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayLocationMarkers={false} | |||
issueLocations={Array []} | |||
issues={ | |||
@@ -112,10 +123,15 @@ exports[`should render correctly 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", | |||
} | |||
} | |||
onIssueChange={[MockFunction]} | |||
@@ -133,16 +149,20 @@ exports[`should render correctly 1`] = ` | |||
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]} | |||
@@ -151,16 +171,23 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
<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 { | |||
@@ -172,8 +199,6 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
"title": "Foo Bar feature", | |||
} | |||
} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayLocationMarkers={false} | |||
issueLocations={Array []} | |||
issues={ | |||
@@ -244,11 +269,15 @@ exports[`should render correctly for last, new, and highlighted lines 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, | |||
"duplicated": false, | |||
"isNew": true, | |||
"line": 5, | |||
"line": 16, | |||
"scmAuthor": "simon.brandhof@sonarsource.com", | |||
"scmDate": "2018-12-11T10:48:39+0100", | |||
"scmRevision": "80f564becc0c0a1c9abaa006eca83a4fd278c3f0", | |||
} | |||
} | |||
onIssueChange={[MockFunction]} | |||
@@ -265,16 +294,21 @@ exports[`should render correctly for last, new, and highlighted lines 1`] = ` | |||
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]} | |||
@@ -283,22 +317,35 @@ exports[`should render correctly with coverage 1`] = ` | |||
<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", | |||
} | |||
} | |||
/> | |||
@@ -313,8 +360,6 @@ exports[`should render correctly with coverage 1`] = ` | |||
"title": "Foo Bar feature", | |||
} | |||
} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayLocationMarkers={false} | |||
issueLocations={Array []} | |||
issues={ | |||
@@ -385,10 +430,15 @@ exports[`should render correctly with coverage 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", | |||
} | |||
} | |||
onIssueChange={[MockFunction]} | |||
@@ -405,16 +455,21 @@ exports[`should render correctly with coverage 1`] = ` | |||
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]} | |||
@@ -423,22 +478,35 @@ exports[`should render correctly with duplication information 1`] = ` | |||
<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]} | |||
@@ -449,10 +517,15 @@ exports[`should render correctly with duplication information 1`] = ` | |||
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]} | |||
@@ -465,10 +538,15 @@ exports[`should render correctly with duplication information 1`] = ` | |||
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]} | |||
@@ -481,10 +559,15 @@ exports[`should render correctly with duplication information 1`] = ` | |||
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]} | |||
@@ -502,8 +585,6 @@ exports[`should render correctly with duplication information 1`] = ` | |||
"title": "Foo Bar feature", | |||
} | |||
} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayLocationMarkers={false} | |||
issueLocations={Array []} | |||
issues={ | |||
@@ -574,10 +655,15 @@ exports[`should render correctly with duplication information 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", | |||
} | |||
} | |||
onIssueChange={[MockFunction]} | |||
@@ -594,16 +680,21 @@ exports[`should render correctly with duplication information 1`] = ` | |||
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]} | |||
@@ -612,10 +703,15 @@ exports[`should render correctly with issues info 1`] = ` | |||
<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]} | |||
@@ -690,10 +786,15 @@ exports[`should render correctly with issues info 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", | |||
} | |||
} | |||
onClick={[Function]} | |||
@@ -709,8 +810,6 @@ exports[`should render correctly with issues info 1`] = ` | |||
"title": "Foo Bar feature", | |||
} | |||
} | |||
displayIssueLocationsCount={false} | |||
displayIssueLocationsLink={false} | |||
displayLocationMarkers={false} | |||
issueLocations={Array []} | |||
issues={ | |||
@@ -781,10 +880,15 @@ exports[`should render correctly with issues info 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", | |||
} | |||
} | |||
onIssueChange={[MockFunction]} |
@@ -33,6 +33,18 @@ export function issuesByLine(issues: T.Issue[]) { | |||
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 => { |
@@ -32,3 +32,25 @@ export function getLinearLocations(textRange: T.TextRange | undefined): T.Linear | |||
} | |||
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]; | |||
}, []); | |||
} |
@@ -0,0 +1,47 @@ | |||
/* | |||
* 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; | |||
} |
@@ -32,241 +32,6 @@ | |||
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; |
@@ -34,7 +34,7 @@ | |||
} | |||
.location-index.selected { | |||
background-color: #bc5e5e; | |||
background-color: #8f3030; | |||
} | |||
.location-index.muted { |
@@ -41,11 +41,16 @@ | |||
} | |||
.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; |
@@ -0,0 +1,48 @@ | |||
/* | |||
* 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> | |||
); | |||
} |
@@ -155,6 +155,38 @@ export function mockQualityGateStatusCondition( | |||
}; | |||
} | |||
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, | |||
@@ -472,16 +504,6 @@ export function mockStore(state: any = {}, reducer = (state: any) => state): Sto | |||
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 { |
@@ -2243,6 +2243,8 @@ source_viewer.tooltip.no_information_about_tests=There is no extra information a | |||
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 | |||
#------------------------------------------------------------------------------ | |||
# |