diff options
author | Stas Vilchik <stas-vilchik@users.noreply.github.com> | 2017-03-08 09:30:24 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-03-08 09:30:24 +0100 |
commit | dba5bc28b25fe68ab6965d5f43c6f2a8c706e42d (patch) | |
tree | c24768c167fe8745808d4e3c23d05387a5860c41 /server/sonar-web/src/main | |
parent | 8b65d7659da13196cf26e2dacf1137c17f3b7a9d (diff) | |
download | sonarqube-dba5bc28b25fe68ab6965d5f43c6f2a8c706e42d.tar.gz sonarqube-dba5bc28b25fe68ab6965d5f43c6f2a8c706e42d.zip |
Polish new source viewer (#1755)
Diffstat (limited to 'server/sonar-web/src/main')
71 files changed, 1701 insertions, 2230 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/components/App.js b/server/sonar-web/src/main/js/apps/code/components/App.js index 4208e8d409b..8e75483c1da 100644 --- a/server/sonar-web/src/main/js/apps/code/components/App.js +++ b/server/sonar-web/src/main/js/apps/code/components/App.js @@ -22,7 +22,7 @@ import React from 'react'; import { connect } from 'react-redux'; import Components from './Components'; import Breadcrumbs from './Breadcrumbs'; -import SourceViewer from './../../../components/SourceViewer/StandaloneSourceViewer'; +import SourceViewer from './../../../components/SourceViewer/SourceViewer'; import Search from './Search'; import ListFooter from '../../../components/controls/ListFooter'; import { retrieveComponentChildren, retrieveComponent, loadMoreChildren, parseError } from '../utils'; diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js index bbfc0ae32bf..6f52f65713e 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/ListView.js @@ -23,7 +23,7 @@ import moment from 'moment'; import ComponentsList from './ComponentsList'; import ListHeader from './ListHeader'; import Spinner from '../../components/Spinner'; -import SourceViewer from '../../../../components/SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../../../components/SourceViewer/SourceViewer'; import ListFooter from '../../../../components/controls/ListFooter'; export default class ListView extends React.Component { diff --git a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js index fb0bb744bd0..4c6e64c94e4 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/details/drilldown/TreeView.js @@ -22,7 +22,7 @@ import moment from 'moment'; import ComponentsList from './ComponentsList'; import ListHeader from './ListHeader'; import Spinner from '../../components/Spinner'; -import SourceViewer from '../../../../components/SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../../../components/SourceViewer/SourceViewer'; import ListFooter from '../../../../components/controls/ListFooter'; export default class TreeView extends React.Component { diff --git a/server/sonar-web/src/main/js/apps/component/components/App.js b/server/sonar-web/src/main/js/apps/component/components/App.js index 041625d8243..735e89a21d1 100644 --- a/server/sonar-web/src/main/js/apps/component/components/App.js +++ b/server/sonar-web/src/main/js/apps/component/components/App.js @@ -19,7 +19,7 @@ */ // @flow import React from 'react'; -import SourceViewer from '../../../components/SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../../components/SourceViewer/SourceViewer'; export default class App extends React.Component { props: { diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.js b/server/sonar-web/src/main/js/apps/overview/components/App.js index 91e636eb52a..c30c80f2630 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.js +++ b/server/sonar-web/src/main/js/apps/overview/components/App.js @@ -23,6 +23,7 @@ import shallowCompare from 'react-addons-shallow-compare'; import { withRouter } from 'react-router'; import OverviewApp from './OverviewApp'; import EmptyOverview from './EmptyOverview'; +import SourceViewer from '../../../components/SourceViewer/SourceViewer'; type Props = { component: { @@ -54,7 +55,6 @@ class App extends React.Component { const { component } = this.props; if (['FIL', 'UTS'].includes(component.qualifier)) { - const SourceViewer = require('../../../components/SourceViewer/StandaloneSourceViewer').default; return ( <div className="page"> <SourceViewer component={component.key}/> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js index 2c6e5b594a6..e7b999e3231 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js @@ -25,18 +25,19 @@ import { receiveIssues } from '../../store/issues/duck'; const mapStateToProps = null; -const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => { - if (component.canMarkAsFavorite) { - const favorites = []; - const notFavorites = []; - if (component.fav) { - favorites.push({ key: component.key }); - } else { - notFavorites.push({ key: component.key }); +const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => + dispatch => { + if (component.canMarkAsFavorite) { + const favorites = []; + const notFavorites = []; + if (component.fav) { + favorites.push({ key: component.key }); + } else { + notFavorites.push({ key: component.key }); + } + dispatch(receiveFavorites(favorites, notFavorites)); } - dispatch(receiveFavorites(favorites, notFavorites)); - } -}; + }; const onReceiveIssues = (issues: Array<*>) => dispatch => { dispatch(receiveIssues(issues)); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index 4a011e163c3..96e7641e525 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -24,12 +24,11 @@ import uniqBy from 'lodash/uniqBy'; import SourceViewerHeader from './SourceViewerHeader'; import SourceViewerCode from './SourceViewerCode'; import SourceViewerIssueLocations from './SourceViewerIssueLocations'; -import CoveragePopupView from '../source-viewer/popups/coverage-popup'; -import DuplicationPopupView from '../source-viewer/popups/duplication-popup'; -import LineActionsPopupView from '../source-viewer/popups/line-actions-popup'; -import SCMPopupView from '../source-viewer/popups/scm-popup'; -import MeasuresOverlay from '../source-viewer/measures-overlay'; -import Source from '../source-viewer/source'; +import CoveragePopupView from './popups/coverage-popup'; +import DuplicationPopupView from './popups/duplication-popup'; +import LineActionsPopupView from './popups/line-actions-popup'; +import SCMPopupView from './popups/scm-popup'; +import MeasuresOverlay from './views/measures-overlay'; import loadIssues from './helpers/loadIssues'; import getCoverageStatus from './helpers/getCoverageStatus'; import { @@ -47,7 +46,12 @@ import type { IndexedIssueLocationsByIssueAndLine, IndexedIssueLocationMessagesByIssueAndLine } from './helpers/indexing'; -import { getComponentForSourceViewer, getSources, getDuplications, getTests } from '../../api/components'; +import { + getComponentForSourceViewer, + getSources, + getDuplications, + getTests +} from '../../api/components'; import { translate } from '../../helpers/l10n'; import { scrollToElement } from '../../helpers/scrolling'; import type { SourceLine } from './types'; @@ -66,11 +70,11 @@ type Props = { loadIssues: (string, number, number) => Promise<*>, loadSources: (string, number, number) => Promise<*>, onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, - onIssueSelect: (string) => void, - onIssueUnselect: () => void, + onIssueSelect?: (string) => void, + onIssueUnselect?: () => void, onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void, onReceiveIssues: (issues: Array<*>) => void, - selectedIssue: string | null, + selectedIssue?: string }; type State = { @@ -99,6 +103,8 @@ type State = { locationsPanelHeight: number, notAccessible: boolean, notExist: boolean, + openIssuesByLine: { [number]: boolean }, + selectedIssue?: string, selectedIssueLocation: IndexedIssueLocation | null, sources?: Array<SourceLine>, symbolsByLine: { [number]: Array<string> } @@ -125,8 +131,6 @@ export default class SourceViewerBase extends React.Component { static defaultProps = { displayAllIssues: false, - onIssueSelect: () => { }, - onIssueUnselect: () => { }, loadComponent, loadIssues, loadSources @@ -150,7 +154,8 @@ export default class SourceViewerBase extends React.Component { locationsPanelHeight: this.getInitialLocationsPanelHeight(), notAccessible: false, notExist: false, - selectedIssue: props.defaultSelectedIssue || null, + openIssuesByLine: {}, + selectedIssue: props.selectedIssue, selectedIssueLocation: null, symbolsByLine: {} }; @@ -161,16 +166,27 @@ export default class SourceViewerBase extends React.Component { this.fetchComponent(); } + componentWillReceiveProps (nextProps: Props) { + if (nextProps.onIssueSelect != null && nextProps.selectedIssue !== this.props.selectedIssue) { + this.setState({ selectedIssue: nextProps.selectedIssue, selectedIssueLocation: null }); + } + } + componentDidUpdate (prevProps: Props, prevState: State) { if (prevProps.component !== this.props.component) { this.fetchComponent(); - } else if (this.props.aroundLine != null && prevProps.aroundLine !== this.props.aroundLine && - this.isLineOutsideOfRange(this.props.aroundLine)) { + } else if ( + this.props.aroundLine != null && + prevProps.aroundLine !== this.props.aroundLine && + this.isLineOutsideOfRange(this.props.aroundLine) + ) { this.fetchSources(); } - if (prevState.selectedIssueLocation !== this.state.selectedIssueLocation && - this.state.selectedIssueLocation != null) { + if ( + prevState.selectedIssueLocation !== this.state.selectedIssueLocation && + this.state.selectedIssueLocation != null + ) { this.scrollToLine(this.state.selectedIssueLocation.line); } } @@ -211,22 +227,25 @@ export default class SourceViewerBase extends React.Component { this.props.onReceiveIssues(issues); if (this.mounted) { const finalSources = sources.slice(0, LINES); - this.setState({ - component, - issues, - issuesByLine: issuesByLine(issues), - issueLocationsByLine: locationsByLine(issues), - issueSecondaryLocationsByIssueByLine: locationsByIssueAndLine(issues), - issueSecondaryLocationMessagesByIssueByLine: locationMessagesByIssueAndLine(issues), - loading: false, - hasSourcesAfter: sources.length > LINES, - sources: this.computeCoverageStatus(finalSources), - symbolsByLine: symbolsByLine(sources.slice(0, LINES)) - }, () => { - if (this.props.onLoaded) { - this.props.onLoaded(component, finalSources, issues); + this.setState( + { + component, + issues, + issuesByLine: issuesByLine(issues), + issueLocationsByLine: locationsByLine(issues), + issueSecondaryLocationsByIssueByLine: locationsByIssueAndLine(issues), + issueSecondaryLocationMessagesByIssueByLine: locationMessagesByIssueAndLine(issues), + loading: false, + hasSourcesAfter: sources.length > LINES, + sources: this.computeCoverageStatus(finalSources), + symbolsByLine: symbolsByLine(sources.slice(0, LINES)) + }, + () => { + if (this.props.onLoaded) { + this.props.onLoaded(component, finalSources, issues); + } } - }); + ); } }); }; @@ -262,15 +281,18 @@ export default class SourceViewerBase extends React.Component { this.loadSources().then(sources => { if (this.mounted) { const finalSources = sources.slice(0, LINES); - this.setState({ - sources: sources.slice(0, LINES), - hasSourcesAfter: sources.length > LINES - }, () => { - if (this.props.onLoaded) { - // $FlowFixMe - this.props.onLoaded(this.state.component, finalSources, this.state.issues); + this.setState( + { + sources: sources.slice(0, LINES), + hasSourcesAfter: sources.length > LINES + }, + () => { + if (this.props.onLoaded) { + // $FlowFixMe + this.props.onLoaded(this.state.component, finalSources, this.state.issues); + } } - }); + ); } }); } @@ -292,10 +314,9 @@ export default class SourceViewerBase extends React.Component { // request one additional line to define `hasSourcesAfter` const to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1; - return this.props.loadSources(this.props.component, from, to).then( - sources => resolve(sources), - onFailLoadSources - ); + return this.props + .loadSources(this.props.component, from, to) + .then(sources => resolve(sources), onFailLoadSources); }); } @@ -349,17 +370,20 @@ export default class SourceViewerBase extends React.Component { loadDuplications = (line: SourceLine, element: HTMLElement) => { getDuplications(this.props.component).then(r => { if (this.mounted) { - this.setState({ - displayDuplications: true, - duplications: r.duplications, - duplicationsByLine: duplicationsByLine(r.duplications), - duplicatedFiles: r.files - }, () => { - // immediately show dropdown popup if there is only one duplicated block - if (r.duplications.length === 1) { - this.handleDuplicationClick(0, line.line, element); + this.setState( + { + displayDuplications: true, + duplications: r.duplications, + duplicationsByLine: duplicationsByLine(r.duplications), + duplicatedFiles: r.files + }, + () => { + // immediately show dropdown popup if there is only one duplicated block + if (r.duplications.length === 1) { + this.handleDuplicationClick(0, line.line, element); + } } - }); + ); } }); }; @@ -394,9 +418,8 @@ export default class SourceViewerBase extends React.Component { }; showMeasures = () => { - const model = new Source(this.state.component); - const measuresOvervlay = new MeasuresOverlay({ model, large: true }); - measuresOvervlay.render(); + const measuresOverlay = new MeasuresOverlay({ component: this.state.component, large: true }); + measuresOverlay.render(); }; handleCoverageClick = (line: SourceLine, element: HTMLElement) => { @@ -416,14 +439,16 @@ export default class SourceViewerBase extends React.Component { const currentFile = b._ref === '1'; const shouldDisplayForCurrentFile = outOfBounds || foundOne; const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; - const isOk = (b._ref != null) && shouldDisplay; + const isOk = b._ref != null && shouldDisplay; if (b._ref === '1' && !outOfBounds) { foundOne = true; } return isOk; }); - const element = this.node.querySelector(`.source-line-duplications-extra[data-line-number="${line}"]`); + const element = this.node.querySelector( + `.source-line-duplications-extra[data-line-number="${line}"]` + ); if (element) { const popup = new DuplicationPopupView({ blocks, @@ -445,11 +470,11 @@ export default class SourceViewerBase extends React.Component { popup.render(); } - handleLineClick = (line: number, element: HTMLElement) => { + handleLineClick = (line: SourceLine, element: HTMLElement) => { this.setState(prevState => ({ - highlightedLine: prevState.highlightedLine === line ? null : line + highlightedLine: prevState.highlightedLine === line.line ? null : line })); - this.displayLinePopup(line, element); + this.displayLinePopup(line.line, element); }; handleSymbolClick = (symbol: string) => { @@ -479,6 +504,34 @@ export default class SourceViewerBase extends React.Component { this.storeLocationsPanelHeight(height); }; + handleIssueSelect = (issue: string) => { + if (this.props.onIssueSelect) { + this.props.onIssueSelect(issue); + } else { + this.setState({ selectedIssue: issue, selectedIssueLocation: null }); + } + }; + + handleIssueUnselect = () => { + if (this.props.onIssueUnselect) { + this.props.onIssueUnselect(); + } else { + this.setState({ selectedIssue: undefined, selectedIssueLocation: null }); + } + }; + + handleOpenIssues = (line: SourceLine) => { + this.setState(state => ({ + openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } + })); + }; + + handleCloseIssues = (line: SourceLine) => { + this.setState(state => ({ + openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false } + })); + }; + renderCode (sources: Array<SourceLine>) { const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; return ( @@ -496,7 +549,9 @@ export default class SourceViewerBase extends React.Component { issuesByLine={this.state.issuesByLine} issueLocationsByLine={this.state.issueLocationsByLine} issueSecondaryLocationsByIssueByLine={this.state.issueSecondaryLocationsByIssueByLine} - issueSecondaryLocationMessagesByIssueByLine={this.state.issueSecondaryLocationMessagesByIssueByLine} + issueSecondaryLocationMessagesByIssueByLine={ + this.state.issueSecondaryLocationMessagesByIssueByLine + } loadDuplications={this.loadDuplications} loadSourcesAfter={this.loadSourcesAfter} loadSourcesBefore={this.loadSourcesBefore} @@ -504,13 +559,16 @@ export default class SourceViewerBase extends React.Component { loadingSourcesBefore={this.state.loadingSourcesBefore} onCoverageClick={this.handleCoverageClick} onDuplicationClick={this.handleDuplicationClick} - onIssueSelect={this.props.onIssueSelect} - onIssueUnselect={this.props.onIssueUnselect} + onIssueSelect={this.handleIssueSelect} + onIssueUnselect={this.handleIssueUnselect} + onIssuesOpen={this.handleOpenIssues} + onIssuesClose={this.handleCloseIssues} onLineClick={this.handleLineClick} onSCMClick={this.handleSCMClick} - onSelectLocation={this.handleSelectIssueLocation} + onLocationSelect={this.handleSelectIssueLocation} onSymbolClick={this.handleSymbolClick} - selectedIssue={this.props.selectedIssue} + openIssuesByLine={this.state.openIssuesByLine} + selectedIssue={this.state.selectedIssue} selectedIssueLocation={this.state.selectedIssueLocation} sources={sources} symbolsByLine={this.state.symbolsByLine}/> @@ -526,7 +584,9 @@ export default class SourceViewerBase extends React.Component { if (this.state.notExist) { return ( - <div className="alert alert-warning spacer-top">{translate('component_viewer.no_component')}</div> + <div className="alert alert-warning spacer-top"> + {translate('component_viewer.no_component')} + </div> ); } @@ -534,11 +594,13 @@ export default class SourceViewerBase extends React.Component { return null; } - const className = classNames('source-viewer', { 'source-duplications-expanded': this.state.displayDuplications }); + const className = classNames('source-viewer', { + 'source-duplications-expanded': this.state.displayDuplications + }); - const selectedIssueObj = this.props.selectedIssue && this.state.issues != null ? - this.state.issues.find(issue => issue.key === this.props.selectedIssue) : - null; + const selectedIssueObj = this.state.selectedIssue && this.state.issues != null + ? this.state.issues.find(issue => issue.key === this.state.selectedIssue) + : null; return ( <div className={className} ref={node => this.node = node}> @@ -546,20 +608,19 @@ export default class SourceViewerBase extends React.Component { component={this.state.component} openNewWindow={this.openNewWindow} showMeasures={this.showMeasures}/> - {this.state.notAccessible && ( + {this.state.notAccessible && <div className="alert alert-warning spacer-top"> {translate('code_viewer.no_source_code_displayed_due_to_security')} - </div> - )} + </div>} {this.state.sources != null && this.renderCode(this.state.sources)} - {selectedIssueObj != null && selectedIssueObj.flows.length > 0 && ( + {selectedIssueObj != null && + selectedIssueObj.flows.length > 0 && <SourceViewerIssueLocations height={this.state.locationsPanelHeight} issue={selectedIssueObj} onResize={this.handleLocationsPanelResize} onSelectLocation={this.handleSelectIssueLocation} - selectedLocation={this.state.selectedIssueLocation}/> - )} + selectedLocation={this.state.selectedIssueLocation}/>} </div> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js index fb98c06ab15..d0b5eff25b0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js @@ -19,8 +19,7 @@ */ // @flow import React from 'react'; -import SourceViewerLine from './SourceViewerLine'; -import { TooltipsContainer } from '../mixins/tooltips-mixin'; +import Line from './components/Line'; import { translate } from '../../helpers/l10n'; import type { Duplication, SourceLine } from './types'; import type { Issue } from '../issue/types'; @@ -64,24 +63,19 @@ export default class SourceViewerCode extends React.PureComponent { onDuplicationClick: (number, number) => void, onIssueSelect: (string) => void, onIssueUnselect: () => void, - onLineClick: (number, HTMLElement) => void, + onIssuesOpen: (SourceLine) => void, + onIssuesClose: (SourceLine) => void, + onLineClick: (SourceLine, HTMLElement) => void, onSCMClick: (SourceLine, HTMLElement) => void, - onSelectLocation: (flowIndex: number, locationIndex: number) => void, + onLocationSelect: (flowIndex: number, locationIndex: number) => void, onSymbolClick: (string) => void, + openIssuesByLine: { [number]: boolean }, selectedIssue: string | null, selectedIssueLocation: IndexedIssueLocation | null, sources: Array<SourceLine>, symbolsByLine: { [number]: Array<string> } }; - isSCMChanged (s: SourceLine, p: null | SourceLine) { - let changed = true; - if (p != null && s.scmAuthor != null && p.scmAuthor != null) { - changed = (s.scmAuthor !== p.scmAuthor) || (s.scmDate !== p.scmDate); - } - return changed; - } - getDuplicationsForLine (line: SourceLine) { return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; } @@ -103,7 +97,8 @@ export default class SourceViewerCode extends React.PureComponent { } getSecondaryIssueLocationMessagesForLine (line: SourceLine, issueKey: string) { - return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] || EMPTY_ARRAY; + return this.props.issueSecondaryLocationMessagesByIssueByLine[issueKey][line.line] || + EMPTY_ARRAY; } renderLine = ( @@ -116,10 +111,12 @@ export default class SourceViewerCode extends React.PureComponent { ) => { const { filterLine, selectedIssue, sources } = this.props; const filtered = filterLine ? filterLine(line) : null; - const secondaryIssueLocations = selectedIssue ? - this.getSecondaryIssueLocationsForLine(line, selectedIssue) : EMPTY_ARRAY; - const secondaryIssueLocationMessages = selectedIssue ? - this.getSecondaryIssueLocationMessagesForLine(line, selectedIssue) : EMPTY_ARRAY; + const secondaryIssueLocations = selectedIssue + ? this.getSecondaryIssueLocationsForLine(line, selectedIssue) + : EMPTY_ARRAY; + const secondaryIssueLocationMessages = selectedIssue + ? this.getSecondaryIssueLocationMessagesForLine(line, selectedIssue) + : EMPTY_ARRAY; const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0; @@ -128,28 +125,32 @@ export default class SourceViewerCode extends React.PureComponent { // for the following properties pass null if the line for sure is not impacted const symbolsForLine = this.props.symbolsByLine[line.line] || []; const { highlightedSymbol } = this.props; - const optimizedHighlightedSymbol = highlightedSymbol != null && symbolsForLine.includes(highlightedSymbol) ? - highlightedSymbol : null; + const optimizedHighlightedSymbol = highlightedSymbol != null && + symbolsForLine.includes(highlightedSymbol) + ? highlightedSymbol + : null; - const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) ? - selectedIssue : null; + const optimizedSelectedIssue = selectedIssue != null && issuesForLine.includes(selectedIssue) + ? selectedIssue + : null; const { selectedIssueLocation } = this.props; - const optimizedSelectedIssueLocation = - selectedIssueLocation != null && - secondaryIssueLocations.some(location => + const optimizedSelectedIssueLocation = selectedIssueLocation != null && + secondaryIssueLocations.some( + location => location.flowIndex === selectedIssueLocation.flowIndex && location.locationIndex === selectedIssueLocation.locationIndex - ) ? selectedIssueLocation : null; + ) + ? selectedIssueLocation + : null; return ( - <SourceViewerLine + <Line displayAllIssues={this.props.displayAllIssues} displayCoverage={displayCoverage} displayDuplications={displayDuplications} displayFiltered={displayFiltered} displayIssues={displayIssues} - displaySCM={this.isSCMChanged(line, index > 0 ? sources[index - 1] : null)} duplications={this.getDuplicationsForLine(line)} duplicationsCount={duplicationsCount} filtered={filtered} @@ -165,9 +166,13 @@ export default class SourceViewerCode extends React.PureComponent { onDuplicationClick={this.props.onDuplicationClick} onIssueSelect={this.props.onIssueSelect} onIssueUnselect={this.props.onIssueUnselect} + onIssuesOpen={this.props.onIssuesOpen} + onIssuesClose={this.props.onIssuesClose} onSCMClick={this.props.onSCMClick} - onSelectLocation={this.props.onSelectLocation} + onLocationSelect={this.props.onLocationSelect} onSymbolClick={this.props.onSymbolClick} + openIssues={this.props.openIssuesByLine[line.line] || false} + previousLine={index > 0 ? sources[index - 1] : undefined} secondaryIssueLocations={secondaryIssueLocations} secondaryIssueLocationMessages={secondaryIssueLocationMessages} selectedIssue={optimizedSelectedIssue} @@ -187,48 +192,60 @@ export default class SourceViewerCode extends React.PureComponent { return ( <div> - {this.props.hasSourcesBefore && ( + {this.props.hasSourcesBefore && <div className="source-viewer-more-code"> - {this.props.loadingSourcesBefore ? ( - <div className="js-component-viewer-loading-before"> + {this.props.loadingSourcesBefore + ? <div className="js-component-viewer-loading-before"> <i className="spinner"/> - <span className="note spacer-left">{translate('source_viewer.loading_more_code')}</span> + <span className="note spacer-left"> + {translate('source_viewer.loading_more_code')} + </span> </div> - ) : ( - <button className="js-component-viewer-source-before" onClick={this.props.loadSourcesBefore}> + : <button + className="js-component-viewer-source-before" + onClick={this.props.loadSourcesBefore}> {translate('source_viewer.load_more_code')} - </button> + </button>} + </div>} + + <table className="source-table"> + <tbody> + {hasFileIssues && + this.renderLine( + ZERO_LINE, + -1, + hasCoverage, + hasDuplications, + displayFiltered, + hasIssues )} - </div> - )} - - <TooltipsContainer> - <table className="source-table"> - <tbody> - {hasFileIssues && ( - this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, displayFiltered, hasIssues) - )} - {sources.map((line, index) => ( - this.renderLine(line, index, hasCoverage, hasDuplications, displayFiltered, hasIssues) + {sources.map((line, index) => + this.renderLine( + line, + index, + hasCoverage, + hasDuplications, + displayFiltered, + hasIssues ))} - </tbody> - </table> - </TooltipsContainer> + </tbody> + </table> - {this.props.hasSourcesAfter && ( + {this.props.hasSourcesAfter && <div className="source-viewer-more-code"> - {this.props.loadingSourcesAfter ? ( - <div className="js-component-viewer-loading-after"> + {this.props.loadingSourcesAfter + ? <div className="js-component-viewer-loading-after"> <i className="spinner"/> - <span className="note spacer-left">{translate('source_viewer.loading_more_code')}</span> + <span className="note spacer-left"> + {translate('source_viewer.loading_more_code')} + </span> </div> - ) : ( - <button className="js-component-viewer-source-after" onClick={this.props.loadSourcesAfter}> + : <button + className="js-component-viewer-source-after" + onClick={this.props.loadSourcesAfter}> {translate('source_viewer.load_more_code')} - </button> - )} - </div> - )} + </button>} + </div>} </div> ); } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js index 14dedd85572..af05c46d954 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js @@ -22,13 +22,12 @@ import React from 'react'; import { Link } from 'react-router'; import QualifierIcon from '../shared/qualifier-icon'; import FavoriteContainer from '../controls/FavoriteContainer'; -import Workspace from '../workspace/main'; import { getProjectUrl, getIssuesUrl } from '../../helpers/urls'; import { collapsedDirFromPath, fileFromPath } from '../../helpers/path'; import { translate } from '../../helpers/l10n'; import { formatMeasure } from '../../helpers/measures'; -export default class SourceViewerHeader extends React.Component { +export default class SourceViewerHeader extends React.PureComponent { props: { component: { canMarkAsFavorite: boolean, @@ -64,11 +63,21 @@ export default class SourceViewerHeader extends React.Component { openInWorkspace = (e: SyntheticInputEvent) => { e.preventDefault(); const { key } = this.props.component; + const Workspace = require('../workspace/main').default; Workspace.openComponent({ key }); }; render () { - const { key, measures, path, project, projectName, q, subProject, subProjectName } = this.props.component; + const { + key, + measures, + path, + project, + projectName, + q, + subProject, + subProjectName + } = this.props.component; const isUnitTest = q === 'UTS'; // TODO check if source viewer is displayed inside workspace const workspace = false; @@ -85,13 +94,12 @@ export default class SourceViewerHeader extends React.Component { </Link> </div> - {subProject != null && ( + {subProject != null && <div className="component-name-parent"> <Link to={getProjectUrl(subProject)} className="link-with-icon"> <QualifierIcon qualifier="BRC"/> <span>{subProjectName}</span> </Link> - </div> - )} + </div>} <div className="component-name-path"> <QualifierIcon qualifier={q}/> @@ -99,17 +107,17 @@ export default class SourceViewerHeader extends React.Component { <span>{collapsedDirFromPath(path)}</span> <span className="component-name-file">{fileFromPath(path)}</span> - {this.props.component.canMarkAsFavorite && ( - <FavoriteContainer className="component-name-favorite" componentKey={key}/> - )} + {this.props.component.canMarkAsFavorite && + <FavoriteContainer className="component-name-favorite" componentKey={key}/>} </div> </div> </div> <div className="dropdown source-viewer-header-actions"> - <a className="js-actions icon-list dropdown-toggle" - data-toggle="dropdown" - title={translate('component_viewer.more_actions')}/> + <a + className="js-actions icon-list dropdown-toggle" + data-toggle="dropdown" + title={translate('component_viewer.more_actions')}/> <ul className="dropdown-menu dropdown-menu-right"> <li> <a className="js-measures" href="#" onClick={this.showMeasures}> @@ -121,63 +129,76 @@ export default class SourceViewerHeader extends React.Component { {translate('component_viewer.new_window')} </a> </li> - {!workspace && ( + {!workspace && <li> <a className="js-workspace" href="#" onClick={this.openInWorkspace}> {translate('component_viewer.open_in_workspace')} </a> - </li> - )} + </li>} <li> <a className="js-raw-source" href={rawSourcesLink} target="_blank"> {translate('component_viewer.show_raw_source')} </a> </li> </ul> - </div> + </div> <div className="source-viewer-header-measures"> - {isUnitTest && ( + {isUnitTest && <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.tests, 'SHORT_INT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.tests.name')}</span> - </div> - )} + <span className="source-viewer-header-measure-value"> + {formatMeasure(measures.tests, 'SHORT_INT')} + </span> + <span className="source-viewer-header-measure-label"> + {translate('metric.tests.name')} + </span> + </div>} - {!isUnitTest && ( + {!isUnitTest && <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.lines, 'SHORT_INT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.lines.name')}</span> - </div> - )} + <span className="source-viewer-header-measure-value"> + {formatMeasure(measures.lines, 'SHORT_INT')} + </span> + <span className="source-viewer-header-measure-label"> + {translate('metric.lines.name')} + </span> + </div>} <div className="source-viewer-header-measure"> <span className="source-viewer-header-measure-value"> - <Link to={getIssuesUrl({ resolved: 'false', componentKeys: key })} - className="source-viewer-header-external-link" target="_blank"> + <Link + to={getIssuesUrl({ resolved: 'false', componentKeys: key })} + className="source-viewer-header-external-link" + target="_blank"> {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0} {' '} <i className="icon-detach"/> </Link> </span> - <span className="source-viewer-header-measure-label">{translate('metric.violations.name')}</span> + <span className="source-viewer-header-measure-label"> + {translate('metric.violations.name')} + </span> </div> - {measures.coverage != null && ( + {measures.coverage != null && <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value">{formatMeasure(measures.coverage, 'PERCENT')}</span> - <span className="source-viewer-header-measure-label">{translate('metric.coverage.name')}</span> - </div> - )} + <span className="source-viewer-header-measure-value"> + {formatMeasure(measures.coverage, 'PERCENT')} + </span> + <span className="source-viewer-header-measure-label"> + {translate('metric.coverage.name')} + </span> + </div>} - {measures.duplicationDensity != null && ( + {measures.duplicationDensity != null && <div className="source-viewer-header-measure"> - <span className="source-viewer-header-measure-value"> - {formatMeasure(measures.duplicationDensity, 'PERCENT')} - </span> - <span className="source-viewer-header-measure-label">{translate('duplications')}</span> - </div> - )} + <span className="source-viewer-header-measure-value"> + {formatMeasure(measures.duplicationDensity, 'PERCENT')} + </span> + <span className="source-viewer-header-measure-label"> + {translate('duplications')} + </span> + </div>} </div> </div> ); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js index d4881347372..abe72568770 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssueLocations.js @@ -61,9 +61,6 @@ export default class SourceViewerIssueLocations extends React.Component { } componentWillReceiveProps (nextProps: Props) { - /* eslint-disable no-console */ - console.log('foo'); - if (nextProps.selectedLocation !== this.props.selectedLocation) { this.setState({ locationBlink: false }); } @@ -296,15 +293,10 @@ export default class SourceViewerIssueLocations extends React.Component { {flows.map( (flow, flowIndex) => flow.locations != null && - this - .reverseLocations(flow.locations) - .map((location, locationIndex) => - this.renderLocation( - location, - flowIndex, - locationIndex, - flow.locations || [] - )) + this.reverseLocations( + flow.locations + ).map((location, locationIndex) => + this.renderLocation(location, flowIndex, locationIndex, flow.locations || [])) )} </ul> <DraggableCore axis="y" onDrag={this.handleDrag} offsetParent={document.body}> diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js deleted file mode 100644 index 5a53275c7af..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerLine.js +++ /dev/null @@ -1,410 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -// @flow -import React from 'react'; -import classNames from 'classnames'; -import times from 'lodash/times'; -import ConnectedIssue from '../issue/ConnectedIssue'; -import SourceViewerIssuesIndicator from './SourceViewerIssuesIndicator'; -import { translate } from '../../helpers/l10n'; -import { splitByTokens, highlightSymbol, highlightIssueLocations, generateHTML } from './helpers/highlight'; -import type { SourceLine } from './types'; -import type { LinearIssueLocation, IndexedIssueLocation, IndexedIssueLocationMessage } from './helpers/indexing'; - -type Props = { - displayAllIssues: boolean, - displayCoverage: boolean, - displayDuplications: boolean, - displayFiltered: boolean, - displayIssues: boolean, - displaySCM: boolean, - duplications: Array<number>, - duplicationsCount: number, - filtered: boolean | null, - highlighted: boolean, - highlightedSymbol: string | null, - issueLocations: Array<LinearIssueLocation>, - issues: Array<string>, - line: SourceLine, - loadDuplications: (SourceLine, HTMLElement) => void, - onClick: (number, HTMLElement) => void, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueSelect: (string) => void, - onIssueUnselect: () => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSelectLocation: (flowIndex: number, locationIndex: number) => void, - onSymbolClick: (string) => void, - selectedIssue: string | null, - secondaryIssueLocations: Array<IndexedIssueLocation>, - // $FlowFixMe - secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>, - selectedIssueLocation: IndexedIssueLocation | null -}; - -type State = { - issuesOpen: boolean -}; - -export default class SourceViewerLine extends React.PureComponent { - codeNode: HTMLElement; - props: Props; - issueElements: { [string]: HTMLElement } = {}; - issueViews: { [string]: { destroy: () => void } } = {}; - state: State = { issuesOpen: false }; - symbols: NodeList<HTMLElement>; - - componentDidMount () { - this.attachEvents(); - } - - componentWillUpdate () { - this.detachEvents(); - } - - componentDidUpdate () { - this.attachEvents(); - } - - componentWillUnmount () { - this.detachEvents(); - } - - attachEvents () { - this.symbols = this.codeNode.querySelectorAll('.sym'); - for (const symbol of this.symbols) { - symbol.addEventListener('click', this.handleSymbolClick); - } - } - - detachEvents () { - if (this.symbols) { - for (const symbol of this.symbols) { - symbol.removeEventListener('click', this.handleSymbolClick); - } - } - } - - handleClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.onClick(this.props.line.line, e.target); - }; - - handleCoverageClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.onCoverageClick(this.props.line, e.target); - }; - - handleIssuesIndicatorClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.setState(prevState => { - // TODO not sure if side effects allowed here - if (!prevState.issuesOpen) { - const { issues } = this.props; - if (issues.length > 0) { - this.props.onIssueSelect(issues[0]); - } - } else { - this.props.onIssueUnselect(); - } - - return { issuesOpen: !prevState.issuesOpen }; - }); - } - - handleSCMClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.onSCMClick(this.props.line, e.target); - } - - handleSymbolClick = (e: Object) => { - e.preventDefault(); - const key = e.currentTarget.className.match(/sym-\d+/); - if (key && key[0]) { - this.props.onSymbolClick(key[0]); - } - }; - - handleIssueSelect = (issueKey: string) => { - this.props.onIssueSelect(issueKey); - }; - - renderLineNumber () { - const { line } = this.props; - return ( - <td className="source-meta source-line-number" - // don't display 0 - data-line-number={line.line ? line.line : undefined} - role={line.line ? 'button' : undefined} - tabIndex={line.line ? 0 : undefined} - onClick={line.line ? this.handleClick : undefined}/> - ); - } - - renderSCM () { - const { line } = this.props; - const clickable = !!line.line; - return ( - <td className="source-meta source-line-scm" - data-line-number={line.line} - role={clickable ? 'button' : undefined} - tabIndex={clickable ? 0 : undefined} - onClick={clickable ? this.handleSCMClick : undefined}> - {this.props.displaySCM && ( - <div className="source-line-scm-inner" data-author={line.scmAuthor}/> - )} - </td> - ); - } - - renderCoverage () { - const { line } = this.props; - const className = 'source-meta source-line-coverage' + - (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); - return ( - <td className={className} - data-line-number={line.line} - title={line.coverageStatus != null && translate('source_viewer.tooltip', line.coverageStatus)} - data-placement={line.coverageStatus != null && 'right'} - data-toggle={line.coverageStatus != null && 'tooltip'} - role={line.coverageStatus != null ? 'button' : undefined} - tabIndex={line.coverageStatus != null ? 0 : undefined} - onClick={line.coverageStatus != null && this.handleCoverageClick}> - <div className="source-line-bar"/> - </td> - ); - } - - renderDuplications () { - const { line } = this.props; - const className = classNames('source-meta', 'source-line-duplications', { - 'source-line-duplicated': line.duplicated - }); - - const handleDuplicationClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.loadDuplications(this.props.line, e.target); - }; - - return ( - <td className={className} - title={line.duplicated && translate('source_viewer.tooltip.duplicated_line')} - data-placement={line.duplicated && 'right'} - data-toggle={line.duplicated && 'tooltip'} - role="button" - tabIndex="0" - onClick={handleDuplicationClick}> - <div className="source-line-bar"/> - </td> - ); - } - - renderDuplicationsExtra () { - const { duplications, duplicationsCount } = this.props; - return times(duplicationsCount).map(index => this.renderDuplication(index, duplications.includes(index))); - } - - renderDuplication = (index: number, duplicated: boolean) => { - const className = classNames('source-meta', 'source-line-duplications-extra', { - 'source-line-duplicated': duplicated - }); - - const handleDuplicationClick = (e: SyntheticInputEvent) => { - e.preventDefault(); - this.props.onDuplicationClick(index, this.props.line.line); - }; - - return ( - <td key={index} - className={className} - data-line-number={this.props.line.line} - data-index={index} - title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined} - data-placement={duplicated ? 'right' : undefined} - data-toggle={duplicated ? 'tooltip' : undefined} - role={duplicated ? 'button' : undefined} - tabIndex={duplicated ? '0' : undefined} - onClick={duplicated ? handleDuplicationClick : undefined}> - <div className="source-line-bar"/> - </td> - ); - }; - - renderIssuesIndicator () { - const { issues } = this.props; - const hasIssues = issues.length > 0; - const className = classNames('source-meta', 'source-line-issues', { 'source-line-with-issues': hasIssues }); - const onClick = hasIssues ? this.handleIssuesIndicatorClick : undefined; - - return ( - <td className={className} - data-line-number={this.props.line.line} - role="button" - tabIndex="0" - onClick={onClick}> - {hasIssues && ( - <SourceViewerIssuesIndicator issues={issues}/> - )} - {issues.length > 1 && ( - <span className="source-line-issues-counter">{issues.length}</span> - )} - </td> - ); - } - - isSecondaryIssueLocationSelected (location: IndexedIssueLocation | IndexedIssueLocationMessage) { - const { selectedIssueLocation } = this.props; - if (selectedIssueLocation == null) { - return false; - } else { - return selectedIssueLocation.flowIndex === location.flowIndex && - selectedIssueLocation.locationIndex === location.locationIndex; - } - } - - handleLocationMessageClick (flowIndex: number, locationIndex: number, e: SyntheticInputEvent) { - e.preventDefault(); - this.props.onSelectLocation(flowIndex, locationIndex); - } - - renderSecondaryIssueLocationMessage = (location: IndexedIssueLocationMessage) => { - const className = classNames('source-viewer-issue-location', 'issue-location-message', { - 'selected': this.isSecondaryIssueLocationSelected(location) - }); - - const limitString = (str: string) => ( - str.length > 30 ? str.substr(0, 30) + '...' : str - ); - - return ( - <a - key={`${location.flowIndex}-${location.locationIndex}`} - href="#" - className={className} - title={location.msg} - onClick={e => this.handleLocationMessageClick(location.flowIndex, location.locationIndex, e)}> - {location.index && ( - <strong>{location.index}: </strong> - )} - {limitString(location.msg)} - </a> - ); - }; - - renderSecondaryIssueLocationMessages (locations: Array<IndexedIssueLocationMessage>) { - return ( - <div className="source-line-issue-locations"> - {locations.map(this.renderSecondaryIssueLocationMessage)} - </div> - ); - } - - renderCode () { - const { line, highlightedSymbol, issueLocations, issues, secondaryIssueLocations } = this.props; - const { secondaryIssueLocationMessages } = this.props; - const className = classNames('source-line-code', 'code', { 'has-issues': issues.length > 0 }); - - const code = line.code || ''; - let tokens = splitByTokens(code); - - if (highlightedSymbol) { - tokens = highlightSymbol(tokens, highlightedSymbol); - } - - if (issueLocations.length > 0) { - tokens = highlightIssueLocations(tokens, issueLocations); - } - - if (secondaryIssueLocations) { - const linearLocations = secondaryIssueLocations.map(location => ({ - from: location.from, - line: location.line, - to: location.to - })); - tokens = highlightIssueLocations(tokens, linearLocations, 'issue-location'); - const { selectedIssueLocation } = this.props; - if (selectedIssueLocation != null) { - const x = secondaryIssueLocations.find(location => this.isSecondaryIssueLocationSelected(location)); - if (x) { - tokens = highlightIssueLocations(tokens, [x], 'selected'); - } - } - } - - const finalCode = generateHTML(tokens); - - const showIssues = (this.state.issuesOpen || this.props.displayAllIssues) && issues.length > 0; - - return ( - <td className={className} data-line-number={line.line}> - <div className="source-line-code-inner"> - <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/> - {secondaryIssueLocationMessages != null && secondaryIssueLocationMessages.length > 0 && ( - this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages) - )} - </div> - {showIssues && ( - <div className="issue-list"> - {issues.map(issue => ( - <ConnectedIssue - key={issue} - issueKey={issue} - onClick={this.handleIssueSelect} - selected={this.props.selectedIssue === issue}/> - ))} - </div> - )} - </td> - ); - } - - render () { - const { line, duplicationsCount, filtered } = this.props; - const className = classNames('source-line', { - 'source-line-highlighted': this.props.highlighted, - 'source-line-shadowed': filtered === false, - 'source-line-filtered': filtered === true - }); - - return ( - <tr className={className} data-line-number={line.line}> - {this.renderLineNumber()} - - {this.renderSCM()} - - {this.props.displayCoverage && this.renderCoverage()} - - {this.props.displayDuplications && this.renderDuplications()} - - {duplicationsCount > 0 && this.renderDuplicationsExtra()} - - {this.props.displayIssues && !this.props.displayAllIssues && this.renderIssuesIndicator()} - - {this.props.displayFiltered && ( - <td className="source-meta source-line-filtered-container" data-line-number={line.line}> - <div className="source-line-bar"/> - </td> - )} - - {this.renderCode()} - </tr> - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js new file mode 100644 index 00000000000..c9da31d1cf8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js @@ -0,0 +1,152 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import times from 'lodash/times'; +import LineNumber from './LineNumber'; +import LineSCM from './LineSCM'; +import LineCoverage from './LineCoverage'; +import LineDuplications from './LineDuplications'; +import LineDuplicationBlock from './LineDuplicationBlock'; +import LineIssuesIndicatorContainer from './LineIssuesIndicatorContainer'; +import LineCode from './LineCode'; +import { TooltipsContainer } from '../../mixins/tooltips-mixin'; +import type { SourceLine } from '../types'; +import type { + LinearIssueLocation, + IndexedIssueLocation, + IndexedIssueLocationMessage +} from '../helpers/indexing'; + +type Props = { + displayAllIssues: boolean, + displayCoverage: boolean, + displayDuplications: boolean, + displayFiltered: boolean, + displayIssues: boolean, + duplications: Array<number>, + duplicationsCount: number, + filtered: boolean | null, + highlighted: boolean, + highlightedSymbol: string | null, + issueLocations: Array<LinearIssueLocation>, + issues: Array<string>, + line: SourceLine, + loadDuplications: (SourceLine, HTMLElement) => void, + onClick: (SourceLine, HTMLElement) => void, + onCoverageClick: (SourceLine, HTMLElement) => void, + onDuplicationClick: (number, number) => void, + onIssueSelect: (string) => void, + onIssueUnselect: () => void, + onIssuesOpen: (SourceLine) => void, + onIssuesClose: (SourceLine) => void, + onSCMClick: (SourceLine, HTMLElement) => void, + onLocationSelect: (flowIndex: number, locationIndex: number) => void, + onSymbolClick: (string) => void, + openIssues: boolean, + previousLine?: SourceLine, + selectedIssue: string | null, + secondaryIssueLocations: Array<IndexedIssueLocation>, + // $FlowFixMe + secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>, + selectedIssueLocation: IndexedIssueLocation | null +}; + +export default class Line extends React.PureComponent { + props: Props; + + handleIssuesIndicatorClick = () => { + if (this.props.openIssues) { + this.props.onIssuesClose(this.props.line); + this.props.onIssueUnselect(); + } else { + this.props.onIssuesOpen(this.props.line); + + const { issues } = this.props; + if (issues.length > 0) { + this.props.onIssueSelect(issues[0]); + } + } + }; + + render () { + const { line, duplications, duplicationsCount, filtered } = this.props; + const className = classNames('source-line', { + 'source-line-highlighted': this.props.highlighted, + 'source-line-shadowed': filtered === false, + 'source-line-filtered': filtered === true + }); + + return ( + <TooltipsContainer> + <tr className={className} data-line-number={line.line}> + <LineNumber line={line} onClick={this.props.onClick}/> + + <LineSCM + line={line} + onClick={this.props.onSCMClick} + previousLine={this.props.previousLine}/> + + {this.props.displayCoverage && + <LineCoverage line={line} onClick={this.props.onCoverageClick}/>} + + {this.props.displayDuplications && + <LineDuplications line={line} onClick={this.props.loadDuplications}/>} + + {times(duplicationsCount).map(index => ( + <LineDuplicationBlock + duplicated={duplications.includes(index)} + index={index} + key={index} + line={this.props.line} + onClick={this.props.onDuplicationClick}/> + ))} + + {this.props.displayIssues && + !this.props.displayAllIssues && + <LineIssuesIndicatorContainer + issueKeys={this.props.issues} + line={line} + onClick={this.handleIssuesIndicatorClick}/>} + + {this.props.displayFiltered && + <td className="source-meta source-line-filtered-container" data-line-number={line.line}> + <div className="source-line-bar"/> + </td>} + + <LineCode + highlightedSymbol={this.props.highlightedSymbol} + issueKeys={this.props.issues} + issueLocations={this.props.issueLocations} + line={line} + onIssueSelect={this.props.onIssueSelect} + onLocationSelect={this.props.onLocationSelect} + onSymbolClick={this.props.onSymbolClick} + secondaryIssueLocationMessages={this.props.secondaryIssueLocationMessages} + secondaryIssueLocations={this.props.secondaryIssueLocations} + selectedIssue={this.props.selectedIssue} + selectedIssueLocation={this.props.selectedIssueLocation} + showIssues={this.props.openIssues || this.props.displayAllIssues}/> + </tr> + </TooltipsContainer> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js new file mode 100644 index 00000000000..944922c0fe8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js @@ -0,0 +1,224 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import LineIssuesList from './LineIssuesList'; +import { + splitByTokens, + highlightSymbol, + highlightIssueLocations, + generateHTML +} from '../helpers/highlight'; +import type { Tokens } from '../helpers/highlight'; +import type { SourceLine } from '../types'; +import type { + LinearIssueLocation, + IndexedIssueLocation, + IndexedIssueLocationMessage +} from '../helpers/indexing'; + +type Props = { + highlightedSymbol: string | null, + issueKeys: Array<string>, + issueLocations: Array<LinearIssueLocation>, + line: SourceLine, + onIssueSelect: (issueKey: string) => void, + onLocationSelect: (flowIndex: number, locationIndex: number) => void, + onSymbolClick: (symbol: string) => void, + // $FlowFixMe + secondaryIssueLocations: Array<IndexedIssueLocation>, + secondaryIssueLocationMessages: Array<IndexedIssueLocationMessage>, + selectedIssue: string | null, + selectedIssueLocation: IndexedIssueLocation | null, + showIssues: boolean +}; + +type State = { + tokens: Tokens +}; + +export default class LineCode extends React.PureComponent { + codeNode: HTMLElement; + props: Props; + state: State; + symbols: NodeList<HTMLElement>; + + constructor (props: Props) { + super(props); + this.state = { + tokens: splitByTokens(props.line.code || '') + }; + } + + componentDidMount () { + this.attachEvents(); + } + + componentWillReceiveProps (nextProps: Props) { + if (nextProps.line.code !== this.props.line.code) { + this.setState({ + tokens: splitByTokens(nextProps.line.code || '') + }); + } + } + + componentWillUpdate () { + this.detachEvents(); + } + + componentDidUpdate () { + this.attachEvents(); + } + + componentWillUnmount () { + this.detachEvents(); + } + + attachEvents () { + this.symbols = this.codeNode.querySelectorAll('.sym'); + for (const symbol of this.symbols) { + symbol.addEventListener('click', this.handleSymbolClick); + } + } + + detachEvents () { + if (this.symbols) { + for (const symbol of this.symbols) { + symbol.removeEventListener('click', this.handleSymbolClick); + } + } + } + + handleSymbolClick = (e: Object) => { + e.preventDefault(); + const key = e.currentTarget.className.match(/sym-\d+/); + if (key && key[0]) { + this.props.onSymbolClick(key[0]); + } + }; + + handleLocationMessageClick = ( + e: SyntheticInputEvent, + flowIndex: number, + locationIndex: number + ) => { + e.preventDefault(); + this.props.onLocationSelect(flowIndex, locationIndex); + }; + + isSecondaryIssueLocationSelected (location: IndexedIssueLocation | IndexedIssueLocationMessage) { + const { selectedIssueLocation } = this.props; + if (selectedIssueLocation == null) { + return false; + } else { + return selectedIssueLocation.flowIndex === location.flowIndex && + selectedIssueLocation.locationIndex === location.locationIndex; + } + } + + renderSecondaryIssueLocationMessage = (location: IndexedIssueLocationMessage) => { + const className = classNames('source-viewer-issue-location', 'issue-location-message', { + selected: this.isSecondaryIssueLocationSelected(location) + }); + + const limitString = (str: string) => str.length > 30 ? str.substr(0, 30) + '...' : str; + + return ( + <a + key={`${location.flowIndex}-${location.locationIndex}`} + href="#" + className={className} + title={location.msg} + onClick={e => + this.handleLocationMessageClick(e, location.flowIndex, location.locationIndex)}> + {location.index && <strong>{location.index}: </strong>} + {limitString(location.msg)} + </a> + ); + }; + + renderSecondaryIssueLocationMessages (locations: Array<IndexedIssueLocationMessage>) { + return ( + <div className="source-line-issue-locations"> + {locations.map(this.renderSecondaryIssueLocationMessage)} + </div> + ); + } + + render () { + const { + highlightedSymbol, + issueKeys, + issueLocations, + line, + onIssueSelect, + secondaryIssueLocationMessages, + secondaryIssueLocations, + selectedIssue, + selectedIssueLocation, + showIssues + } = this.props; + + let tokens = [...this.state.tokens]; + + if (highlightedSymbol) { + tokens = highlightSymbol(tokens, highlightedSymbol); + } + + if (issueLocations.length > 0) { + tokens = highlightIssueLocations(tokens, issueLocations); + } + + if (secondaryIssueLocations) { + tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location'); + if (selectedIssueLocation != null) { + const x = secondaryIssueLocations.find(location => + this.isSecondaryIssueLocationSelected(location)); + if (x) { + tokens = highlightIssueLocations(tokens, [x], 'selected'); + } + } + } + + const finalCode = generateHTML(tokens); + + const className = classNames('source-line-code', 'code', { + 'has-issues': issueKeys.length > 0 + }); + + return ( + <td className={className} data-line-number={line.line}> + <div className="source-line-code-inner"> + <pre ref={node => this.codeNode = node} dangerouslySetInnerHTML={{ __html: finalCode }}/> + {secondaryIssueLocationMessages != null && + secondaryIssueLocationMessages.length > 0 && + this.renderSecondaryIssueLocationMessages(secondaryIssueLocationMessages)} + </div> + {showIssues && + issueKeys.length > 0 && + <LineIssuesList + issueKeys={issueKeys} + onIssueClick={onIssueSelect} + selectedIssue={selectedIssue}/>} + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js new file mode 100644 index 00000000000..55e7aad96cd --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import { translate } from '../../../helpers/l10n'; +import type { SourceLine } from '../types'; + +type Props = { + line: SourceLine, + onClick: (SourceLine, HTMLElement) => void +}; + +export default class LineCoverage extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(this.props.line, e.target); + }; + + render () { + const { line } = this.props; + const className = 'source-meta source-line-coverage' + + (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); + const title = line.coverageStatus != null + ? translate('source_viewer.tooltip', line.coverageStatus) + : undefined; + return ( + <td + className={className} + data-line-number={line.line} + title={title} + data-placement={line.coverageStatus != null ? 'right' : undefined} + data-toggle={line.coverageStatus != null ? 'tooltip' : undefined} + role={line.coverageStatus != null ? 'button' : undefined} + tabIndex={line.coverageStatus != null ? 0 : undefined} + onClick={line.coverageStatus != null ? this.handleClick : undefined}> + <div className="source-line-bar"/> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js new file mode 100644 index 00000000000..021be8e1e7c --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import { translate } from '../../../helpers/l10n'; +import type { SourceLine } from '../types'; + +type Props = { + duplicated: boolean, + index: number, + line: SourceLine, + onClick: (index: number, lineNumber: number) => void +}; + +export default class LineDuplicationBlock extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(this.props.index, this.props.line.line); + }; + + render () { + const { duplicated, index, line } = this.props; + const className = classNames('source-meta', 'source-line-duplications-extra', { + 'source-line-duplicated': duplicated + }); + + return ( + <td + key={index} + className={className} + data-line-number={line.line} + data-index={index} + title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined} + data-placement={duplicated ? 'right' : undefined} + data-toggle={duplicated ? 'tooltip' : undefined} + role={duplicated ? 'button' : undefined} + tabIndex={duplicated ? '0' : undefined} + onClick={duplicated ? this.handleClick : undefined}> + <div className="source-line-bar"/> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js new file mode 100644 index 00000000000..941227de0dc --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import { translate } from '../../../helpers/l10n'; +import type { SourceLine } from '../types'; + +type Props = { + line: SourceLine, + onClick: (SourceLine, HTMLElement) => void +}; + +export default class LineDuplications extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(this.props.line, e.target); + }; + + render () { + const { line } = this.props; + const className = classNames('source-meta', 'source-line-duplications', { + 'source-line-duplicated': line.duplicated + }); + const title = line.duplicated ? translate('source_viewer.tooltip.duplicated_line') : undefined; + + return ( + <td + className={className} + title={title} + data-placement={line.duplicated ? 'right' : undefined} + data-toggle={line.duplicated ? 'tooltip' : undefined} + role={line.duplicated ? 'button' : undefined} + tabIndex={line.duplicated ? 0 : undefined} + onClick={line.duplicated ? this.handleClick : undefined}> + <div className="source-line-bar"/> + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js new file mode 100644 index 00000000000..200174283cc --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import classNames from 'classnames'; +import SeverityIcon from '../../shared/severity-icon'; +import { sortBySeverity } from '../../../helpers/issues'; +import type { SourceLine } from '../types'; + +type Props = { + issues: Array<{ severity: string }>, + line: SourceLine, + onClick: () => void +}; + +export default class LineIssuesIndicator extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(); + }; + + render () { + const { issues, line } = this.props; + const hasIssues = issues.length > 0; + const className = classNames('source-meta', 'source-line-issues', { + 'source-line-with-issues': hasIssues + }); + const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null; + + return ( + <td + className={className} + data-line-number={line.line} + role={hasIssues ? 'button' : undefined} + tabIndex={hasIssues ? '0' : undefined} + onClick={hasIssues ? this.handleClick : undefined}> + {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity}/>} + {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>} + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js index d673bd44dd0..8d15af06288 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewer.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicatorContainer.js @@ -19,29 +19,11 @@ */ // @flow import { connect } from 'react-redux'; -import StandaloneSourceViewerBase from './StandaloneSourceViewerBase'; -import { receiveFavorites } from '../../store/favorites/duck'; -import { receiveIssues } from '../../store/issues/duck'; +import LineIssuesIndicator from './LineIssuesIndicator'; +import { getIssueByKey } from '../../../store/rootReducer'; -const mapStateToProps = null; +const mapStateToProps = (state, ownProps: { issueKeys: Array<string> }) => ({ + issues: ownProps.issueKeys.map(issueKey => getIssueByKey(state, issueKey)) +}); -const onReceiveComponent = (component: { key: string, canMarkAsFavorite: boolean, fav: boolean }) => dispatch => { - if (component.canMarkAsFavorite) { - const favorites = []; - const notFavorites = []; - if (component.fav) { - favorites.push({ key: component.key }); - } else { - notFavorites.push({ key: component.key }); - } - dispatch(receiveFavorites(favorites, notFavorites)); - } -}; - -const onReceiveIssues = (issues: Array<*>) => dispatch => { - dispatch(receiveIssues(issues)); -}; - -const mapDispatchToProps = { onReceiveComponent, onReceiveIssues }; - -export default connect(mapStateToProps, mapDispatchToProps)(StandaloneSourceViewerBase); +export default connect(mapStateToProps)(LineIssuesIndicator); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js index ea28e00b36f..0238b021891 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/StandaloneSourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js @@ -19,32 +19,30 @@ */ // @flow import React from 'react'; -import SourceViewerBase from './SourceViewerBase'; +import ConnectedIssue from '../../issue/ConnectedIssue'; -type State = { +type Props = { + issueKeys: Array<string>, + onIssueClick: (issueKey: string) => void, selectedIssue: string | null }; -export default class StandaloneSourceViewerBase extends React.Component { - state: State = { - selectedIssue: null - }; - - handleIssueSelect = (issue: string) => { - this.setState({ selectedIssue: issue }); - }; - - handleIssueUnselect = () => { - this.setState({ selectedIssue: null }); - }; +export default class LineIssuesList extends React.PureComponent { + props: Props; render () { + const { issueKeys, onIssueClick, selectedIssue } = this.props; + return ( - <SourceViewerBase - {...this.props} - onIssueSelect={this.handleIssueSelect} - onIssueUnselect={this.handleIssueUnselect} - selectedIssue={this.state.selectedIssue}/> + <div className="issue-list"> + {issueKeys.map(issueKey => ( + <ConnectedIssue + issueKey={issueKey} + key={issueKey} + onClick={onIssueClick} + selected={selectedIssue === issueKey}/> + ))} + </div> ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js index f6993949244..a477bfbad6b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerIssuesIndicator.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js @@ -19,26 +19,32 @@ */ // @flow import React from 'react'; -import { connect } from 'react-redux'; -import SeverityIcon from '../shared/severity-icon'; -import { getIssueByKey } from '../../store/rootReducer'; -import { sortBySeverity } from '../../helpers/issues'; +import type { SourceLine } from '../types'; -class SourceViewerIssuesIndicator extends React.Component { - props: { - issue: { severity: string } +type Props = { + line: SourceLine, + onClick: (SourceLine, HTMLElement) => void +}; + +export default class LineNumber extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(this.props.line, e.target); }; render () { + const { line } = this.props.line; + return ( - <SeverityIcon severity={this.props.issue.severity}/> + <td + className="source-meta source-line-number" + /* don't display 0 */ + data-line-number={line ? line : undefined} + role={line ? 'button' : undefined} + tabIndex={line ? 0 : undefined} + onClick={line ? this.handleClick : undefined}/> ); } } - -const mapStateToProps = (state, ownProps: { issues: Array<string> }) => { - const issues = ownProps.issues.map(issueKey => getIssueByKey(state, issueKey)); - return { issue: sortBySeverity(issues)[0] }; -}; - -export default connect(mapStateToProps)(SourceViewerIssuesIndicator); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js new file mode 100644 index 00000000000..b856b23bb53 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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. + */ +// @flow +import React from 'react'; +import type { SourceLine } from '../types'; + +type Props = { + line: SourceLine, + previousLine?: SourceLine, + onClick: (SourceLine, HTMLElement) => void +}; + +export default class LineSCM extends React.PureComponent { + props: Props; + + handleClick = (e: SyntheticInputEvent) => { + e.preventDefault(); + this.props.onClick(this.props.line, e.target); + }; + + isSCMChanged (s: SourceLine, p?: SourceLine) { + let changed = true; + if (p != null && s.scmAuthor != null && p.scmAuthor != null) { + changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate; + } + return changed; + } + + render () { + const { line, previousLine } = this.props; + const clickable = !!line.line; + return ( + <td + className="source-meta source-line-scm" + data-line-number={line.line} + role={clickable ? 'button' : undefined} + tabIndex={clickable ? 0 : undefined} + onClick={clickable ? this.handleClick : undefined}> + {this.isSCMChanged(line, previousLine) && + <div className="source-line-scm-inner" data-author={line.scmAuthor}/>} + </td> + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js new file mode 100644 index 00000000000..eedeb69252e --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +// import { click } from '../../../../helpers/testUtils'; +import LineCode from '../LineCode'; + +it('render code', () => { + const line = { + line: 3, + code: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {' + }; + const issueLocations = [{ from: 0, to: 5, line: 3 }]; + const secondaryIssueLocations = [{ from: 6, to: 9, line: 3 }]; + const secondaryIssueLocationMessages = [{ msg: 'Fix that', flowIndex: 0, locationIndex: 0 }]; + const selectedIssueLocation = { from: 6, to: 9, line: 3, flowIndex: 0, locationIndex: 0 }; + const wrapper = shallow( + <LineCode + highlightedSymbol="sym1" + issueKeys={['issue-1', 'issue-2']} + issueLocations={issueLocations} + line={line} + onIssueSelect={jest.fn()} + onSelectLocation={jest.fn()} + onSymbolClick={jest.fn()} + secondaryIssueLocations={secondaryIssueLocations} + secondaryIssueLocationMessages={secondaryIssueLocationMessages} + selectedIssue="issue-1" + selectedIssueLocation={selectedIssueLocation} + showIssues={true}/> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js new file mode 100644 index 00000000000..5dcee39a040 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineCoverage from '../LineCoverage'; + +it('render covered line', () => { + const line = { line: 3, coverageStatus: 'covered' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render uncovered line', () => { + const line = { line: 3, coverageStatus: 'uncovered' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render line with unknown coverage', () => { + const line = { line: 3 }; + const onClick = jest.fn(); + const wrapper = shallow(<LineCoverage line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js new file mode 100644 index 00000000000..e16dd8b6c0b --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js @@ -0,0 +1,43 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineDuplicationBlock from '../LineDuplicationBlock'; + +it('render duplicated line', () => { + const line = { line: 3, duplicated: true }; + const onClick = jest.fn(); + const wrapper = shallow( + <LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick}/> + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render not duplicated line', () => { + const line = { line: 3, duplicated: false }; + const onClick = jest.fn(); + const wrapper = shallow( + <LineDuplicationBlock index={1} duplicated={false} line={line} onClick={onClick}/> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js new file mode 100644 index 00000000000..1f11c8b9e37 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineDuplications from '../LineDuplications'; + +it('render duplicated line', () => { + const line = { line: 3, duplicated: true }; + const onClick = jest.fn(); + const wrapper = shallow(<LineDuplications line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render not duplicated line', () => { + const line = { line: 3, duplicated: false }; + const onClick = jest.fn(); + const wrapper = shallow(<LineDuplications line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js new file mode 100644 index 00000000000..c2bb88ec66b --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineIssuesIndicator from '../LineIssuesIndicator'; + +it('render highest severity', () => { + const line = { line: 3 }; + const issues = [{ severity: 'MINOR' }, { severity: 'CRITICAL' }]; + const onClick = jest.fn(); + const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); + + click(wrapper); + expect(onClick).toHaveBeenCalled(); + + const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }]; + wrapper.setProps({ issues: nextIssues }); + expect(wrapper).toMatchSnapshot(); +}); + +it('no issues', () => { + const line = { line: 3 }; + const issues = []; + const onClick = jest.fn(); + const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js new file mode 100644 index 00000000000..8f60222fb55 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import LineIssuesList from '../LineIssuesList'; + +it('render issues list', () => { + const line = { line: 3 }; + const issueKeys = ['foo', 'bar']; + const onIssueClick = jest.fn(); + const wrapper = shallow( + <LineIssuesList + issueKeys={issueKeys} + line={line} + onIssueClick={onIssueClick} + selectedIssue="foo"/> + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js new file mode 100644 index 00000000000..eb120a25a06 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineNumber from '../LineNumber'; + +it('render line 3', () => { + const line = { line: 3 }; + const onClick = jest.fn(); + const wrapper = shallow(<LineNumber line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render line 0', () => { + const line = { line: 0 }; + const onClick = jest.fn(); + const wrapper = shallow(<LineNumber line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js new file mode 100644 index 00000000000..f1a812d302a --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 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 React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineSCM from '../LineSCM'; + +it('render scm details', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine}/>); + expect(wrapper).toMatchSnapshot(); + click(wrapper); + expect(onClick).toHaveBeenCalled(); +}); + +it('render scm details for the first line', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineSCM line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); + +it('does not render scm details', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine}/>); + expect(wrapper).toMatchSnapshot(); +}); + +it('does not allow to click', () => { + const line = { scmAuthor: 'foo', scmDate: '2017-01-01' }; + const onClick = jest.fn(); + const wrapper = shallow(<LineSCM line={line} onClick={onClick}/>); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap new file mode 100644 index 00000000000..ecf619bfa06 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap @@ -0,0 +1,34 @@ +exports[`test render code 1`] = ` +<td + className="source-line-code code has-issues" + data-line-number={3}> + <div + className="source-line-code-inner"> + <pre + dangerouslySetInnerHTML={ + Object { + "__html": "<span class=\"k source-line-code-issue\">class</span><span class=\"\"> </span><span class=\"sym sym-1 issue-location\">Foo</span><span class=\"\"> {</span>", + } + } /> + <div + className="source-line-issue-locations"> + <a + className="source-viewer-issue-location issue-location-message selected" + href="#" + onClick={[Function]} + title="Fix that"> + Fix that + </a> + </div> + </div> + <LineIssuesList + issueKeys={ + Array [ + "issue-1", + "issue-2", + ] + } + onIssueClick={[Function]} + selectedIssue="issue-1" /> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap new file mode 100644 index 00000000000..ccf5c4d3c4f --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap @@ -0,0 +1,38 @@ +exports[`test render covered line 1`] = ` +<td + className="source-meta source-line-coverage source-line-covered" + data-line-number={3} + data-placement="right" + data-toggle="tooltip" + onClick={[Function]} + role="button" + tabIndex={0} + title="source_viewer.tooltip.covered"> + <div + className="source-line-bar" /> +</td> +`; + +exports[`test render line with unknown coverage 1`] = ` +<td + className="source-meta source-line-coverage" + data-line-number={3}> + <div + className="source-line-bar" /> +</td> +`; + +exports[`test render uncovered line 1`] = ` +<td + className="source-meta source-line-coverage source-line-uncovered" + data-line-number={3} + data-placement="right" + data-toggle="tooltip" + onClick={[Function]} + role="button" + tabIndex={0} + title="source_viewer.tooltip.uncovered"> + <div + className="source-line-bar" /> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap new file mode 100644 index 00000000000..b94d4b3bc09 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap @@ -0,0 +1,25 @@ +exports[`test render duplicated line 1`] = ` +<td + className="source-meta source-line-duplications-extra source-line-duplicated" + data-index={1} + data-line-number={3} + data-placement="right" + data-toggle="tooltip" + onClick={[Function]} + role="button" + tabIndex="0" + title="source_viewer.tooltip.duplicated_block"> + <div + className="source-line-bar" /> +</td> +`; + +exports[`test render not duplicated line 1`] = ` +<td + className="source-meta source-line-duplications-extra" + data-index={1} + data-line-number={3}> + <div + className="source-line-bar" /> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap new file mode 100644 index 00000000000..7e977c88442 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap @@ -0,0 +1,21 @@ +exports[`test render duplicated line 1`] = ` +<td + className="source-meta source-line-duplications source-line-duplicated" + data-placement="right" + data-toggle="tooltip" + onClick={[Function]} + role="button" + tabIndex={0} + title="source_viewer.tooltip.duplicated_line"> + <div + className="source-line-bar" /> +</td> +`; + +exports[`test render not duplicated line 1`] = ` +<td + className="source-meta source-line-duplications"> + <div + className="source-line-bar" /> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap new file mode 100644 index 00000000000..a945f7600ad --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap @@ -0,0 +1,37 @@ +exports[`test no issues 1`] = ` +<td + className="source-meta source-line-issues" + data-line-number={3} /> +`; + +exports[`test render highest severity 1`] = ` +<td + className="source-meta source-line-issues source-line-with-issues" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex="0"> + <severity-icon + severity="CRITICAL" /> + <span + className="source-line-issues-counter"> + 2 + </span> +</td> +`; + +exports[`test render highest severity 2`] = ` +<td + className="source-meta source-line-issues source-line-with-issues" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex="0"> + <severity-icon + severity="MINOR" /> + <span + className="source-line-issues-counter"> + 2 + </span> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap new file mode 100644 index 00000000000..9279cc173b3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap @@ -0,0 +1,13 @@ +exports[`test render issues list 1`] = ` +<div + className="issue-list"> + <Connect(Connect(Issue)) + issueKey="foo" + onClick={[Function]} + selected={true} /> + <Connect(Connect(Issue)) + issueKey="bar" + onClick={[Function]} + selected={false} /> +</div> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap new file mode 100644 index 00000000000..a14778f764a --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap @@ -0,0 +1,13 @@ +exports[`test render line 0 1`] = ` +<td + className="source-meta source-line-number" /> +`; + +exports[`test render line 3 1`] = ` +<td + className="source-meta source-line-number" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex={0} /> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap new file mode 100644 index 00000000000..34828c8c1ef --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap @@ -0,0 +1,43 @@ +exports[`test does not allow to click 1`] = ` +<td + className="source-meta source-line-scm"> + <div + className="source-line-scm-inner" + data-author="foo" /> +</td> +`; + +exports[`test does not render scm details 1`] = ` +<td + className="source-meta source-line-scm" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex={0} /> +`; + +exports[`test render scm details 1`] = ` +<td + className="source-meta source-line-scm" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex={0}> + <div + className="source-line-scm-inner" + data-author="foo" /> +</td> +`; + +exports[`test render scm details for the first line 1`] = ` +<td + className="source-meta source-line-scm" + data-line-number={3} + onClick={[Function]} + role="button" + tabIndex={0}> + <div + className="source-line-scm-inner" + data-author="foo" /> +</td> +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js index c0ca46bb9d1..c448c519b56 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js @@ -21,8 +21,8 @@ import escapeHtml from 'escape-html'; import type { LinearIssueLocation } from './indexing'; -type Token = { className: string, text: string }; -type Tokens = Array<Token>; +export type Token = { className: string, text: string }; +export type Tokens = Array<Token>; const ISSUE_LOCATION_CLASS = 'source-line-code-issue'; @@ -33,7 +33,7 @@ export const splitByTokens = (code: string, rootClassName: string = ''): Tokens [].forEach.call(container.childNodes, node => { if (node.nodeType === 1) { // ELEMENT NODE - const fullClassName = rootClassName ? (rootClassName + ' ' + node.className) : node.className; + const fullClassName = rootClassName ? rootClassName + ' ' + node.className : node.className; const innerTokens = splitByTokens(node.innerHTML, fullClassName); tokens = tokens.concat(innerTokens); } @@ -45,11 +45,13 @@ export const splitByTokens = (code: string, rootClassName: string = ''): Tokens return tokens; }; -export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => ( - tokens.map(token => token.className.includes(symbol) ? - { ...token, className: `${token.className} highlighted` } : - token -)); +export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => + tokens.map( + token => + token.className.includes(symbol) + ? { ...token, className: `${token.className} highlighted` } + : token + ); /** * Intersect two ranges @@ -58,7 +60,12 @@ export const highlightSymbol = (tokens: Tokens, symbol: string): Tokens => ( * @param s2 Start position of the second range * @param e2 End position of the second range */ -const intersect = (s1: number, e1: number, s2: number, e2: number): { from: number, to: number } => { +const intersect = ( + s1: number, + e1: number, + s2: number, + e2: number +): { from: number, to: number } => { return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; }; @@ -94,9 +101,9 @@ export const highlightIssueLocations = ( nextTokens.push({ className: token.className, text: p1 }); } if (p2.length) { - const newClassName = token.className.indexOf(rootClassName) === -1 ? - `${token.className} ${rootClassName}` : - token.className; + const newClassName = token.className.indexOf(rootClassName) === -1 + ? `${token.className} ${rootClassName}` + : token.className; nextTokens.push({ className: newClassName, text: p2 }); } if (p3.length) { @@ -110,7 +117,7 @@ export const highlightIssueLocations = ( }; export const generateHTML = (tokens: Tokens): string => { - return tokens.map(token => ( - `<span class="${token.className}">${escapeHtml(token.text)}</span>` - )).join(''); + return tokens + .map(token => `<span class="${token.className}">${escapeHtml(token.text)}</span>`) + .join(''); }; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js index dcfe2f273fa..13b2926d4ca 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js @@ -34,7 +34,7 @@ export type IndexedIssueLocation = { from: number, line: number, locationIndex: number, - to: number, + to: number }; export type IndexedIssueLocationMessage = { @@ -81,7 +81,9 @@ export const locationsByLine = (issues: Array<Issue>): { [number]: Array<LinearI return index; }; -export const locationsByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationsByIssueAndLine => { +export const locationsByIssueAndLine = ( + issues: Array<Issue> +): IndexedIssueLocationsByIssueAndLine => { const index = {}; issues.forEach(issue => { const byLine = {}; @@ -102,7 +104,9 @@ export const locationsByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocat return index; }; -export const locationMessagesByIssueAndLine = (issues: Array<Issue>): IndexedIssueLocationMessagesByIssueAndLine => { +export const locationMessagesByIssueAndLine = ( + issues: Array<Issue> +): IndexedIssueLocationMessagesByIssueAndLine => { const index = {}; issues.forEach(issue => { const byLine = {}; @@ -158,7 +162,8 @@ export const symbolsByLine = (sources: Array<SourceLine>) => { export const findLocationByIndex = ( locations: IndexedIssueLocationsByIssueAndLine, flowIndex: number, - locationIndex: number) => { + locationIndex: number +) => { const issueKeys = Object.keys(locations); for (const issueKey of issueKeys) { const lineNumbers = Object.keys(locations[issueKey]); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js index 70af97af1a5..54459e3534b 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js @@ -20,7 +20,9 @@ // @flow import type { TextRange, Issue } from '../../issue/types'; -export const getLinearLocations = (textRange?: TextRange): Array<{ line: number, from: number, to: number }> => { +export const getLinearLocations = ( + textRange?: TextRange +): Array<{ line: number, from: number, to: number }> => { if (!textRange) { return []; } @@ -36,7 +38,9 @@ export const getLinearLocations = (textRange?: TextRange): Array<{ line: number, return locations; }; -export const getIssueLocations = (issue: Issue): Array<{ +export const getIssueLocations = ( + issue: Issue +): Array<{ msg: string, flowIndex: number, locationIndex: number, diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js index ddc2963c0e7..3a9d00566ce 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js @@ -35,10 +35,17 @@ const buildQuery = (component: string): Query => ({ s: 'FILE_LINE' }); -export const loadPage = (query: Query, page: number, pageSize: number = PAGE_SIZE): Promise<Issues> => { - return searchIssues({ ...query, p: page, ps: pageSize }).then(r => ( - r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules)) - )); +export const loadPage = ( + query: Query, + page: number, + pageSize: number = PAGE_SIZE +): Promise<Issues> => { + return searchIssues({ + ...query, + p: page, + ps: pageSize + }).then(r => + r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules))); }; export const loadPageAndNext = ( diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js index 68fd0ccc388..145d5dbeb47 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/coverage-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js @@ -20,8 +20,7 @@ import $ from 'jquery'; import groupBy from 'lodash/groupBy'; import Popup from '../../common/popup'; -import Workspace from '../../workspace/main'; -import Template from '../templates/source-viewer-coverage-popup.hbs'; +import Template from './templates/source-viewer-coverage-popup.hbs'; export default Popup.extend({ template: Template, @@ -38,6 +37,7 @@ export default Popup.extend({ goToFile (e) { e.stopPropagation(); const key = $(e.currentTarget).data('key'); + const Workspace = require('../../workspace/main').default; Workspace.openComponent({ key }); }, diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js index da542333a30..d8ef03e1009 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/duplication-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js @@ -21,8 +21,7 @@ import $ from 'jquery'; import groupBy from 'lodash/groupBy'; import sortBy from 'lodash/sortBy'; import Popup from '../../common/popup'; -import Workspace from '../../workspace/main'; -import Template from '../templates/source-viewer-duplication-popup.hbs'; +import Template from './templates/source-viewer-duplication-popup.hbs'; export default Popup.extend({ template: Template, @@ -35,6 +34,7 @@ export default Popup.extend({ e.stopPropagation(); const key = $(e.currentTarget).data('key'); const line = $(e.currentTarget).data('line'); + const Workspace = require('../../workspace/main').default; Workspace.openComponent({ key, line }); }, diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js index a2d94f568b8..e65d748e0d1 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/line-actions-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import Popup from '../../common/popup'; -import Template from '../templates/source-viewer-line-options-popup.hbs'; +import Template from './templates/source-viewer-line-options-popup.hbs'; export default Popup.extend({ template: Template, diff --git a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js index f140e37c56b..06cbf45e182 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/popups/scm-popup.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import Popup from '../../common/popup'; -import Template from '../templates/source-viewer-scm-popup.hbs'; +import Template from './templates/source-viewer-scm-popup.hbs'; export default Popup.extend({ template: Template, diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs index 57c6301119e..57c6301119e 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-coverage-popup.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs index ea8fc2b2349..ea8fc2b2349 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-duplication-popup.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs index c6b9b418866..c6b9b418866 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-line-options-popup.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs index dd82aca528c..dd82aca528c 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-scm-popup.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs diff --git a/server/sonar-web/src/main/js/components/SourceViewer/styles.css b/server/sonar-web/src/main/js/components/SourceViewer/styles.css index 6371f9e8cb3..bd79e0fd79c 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -53,9 +53,8 @@ .issue-location-message { display: inline-block; vertical-align: top; - line-height: 16px; - height: 17px; - border: 1px solid #ffeaea; + line-height: 18px; + height: 18px; box-sizing: border-box; background-color: #ffeaea; } @@ -76,6 +75,7 @@ .issue-location-message { padding: 0 10px; + border: 1px solid #ffeaea; color: #444 !important; font-size: 12px; white-space: nowrap; diff --git a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js index 4baf170a2e8..6dee58b4c80 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/measures-overlay.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/measures-overlay.js @@ -21,11 +21,11 @@ import $ from 'jquery'; import groupBy from 'lodash/groupBy'; import sortBy from 'lodash/sortBy'; import toPairs from 'lodash/toPairs'; -import ModalView from '../common/modals'; +import ModalView from '../../common/modals'; import Template from './templates/source-viewer-measures.hbs'; -import { getMeasures } from '../../api/measures'; -import { getMetrics } from '../../api/metrics'; -import { formatMeasure } from '../../helpers/measures'; +import { getMeasures } from '../../../api/measures'; +import { getMetrics } from '../../../api/metrics'; +import { formatMeasure } from '../../../helpers/measures'; export default ModalView.extend({ template: Template, @@ -34,7 +34,7 @@ export default ModalView.extend({ initialize () { this.testsScroll = 0; const requests = [this.requestMeasures(), this.requestIssues()]; - if (this.model.get('q') === 'UTS') { + if (this.options.component.q === 'UTS') { requests.push(this.requestTests()); } Promise.all(requests).then(() => this.render()); @@ -150,8 +150,8 @@ export default ModalView.extend({ .filter(metric => metric.type !== 'DATA' && !metric.hidden) .map(metric => metric.key); - return getMeasures(this.model.key(), metricsToRequest).then(measures => { - let nextMeasures = this.model.get('measures') || {}; + return getMeasures(this.options.component.key, metricsToRequest).then(measures => { + let nextMeasures = this.options.component.measures || {}; measures.forEach(measure => { const metric = metrics.find(metric => metric.key === measure.metric); nextMeasures[metric.key] = formatMeasure(measure.value, metric.type); @@ -159,20 +159,17 @@ export default ModalView.extend({ metric.value = nextMeasures[metric.key]; }); nextMeasures = this.calcAdditionalMeasures(nextMeasures); - this.model.set({ - measures: nextMeasures, - measuresToDisplay: this.prepareMetrics(metrics) - }); + this.measures = nextMeasures; + this.measuresToDisplay = this.prepareMetrics(metrics); }); }); }, requestIssues () { return new Promise(resolve => { - const that = this; const url = window.baseUrl + '/api/issues/search'; const options = { - componentUuids: this.model.id, + componentKeys: this.options.component.key, resolved: false, ps: 1, facets: 'types,severities,tags' @@ -188,12 +185,10 @@ export default ModalView.extend({ const tagsFacet = data.facets.find(facet => facet.property === 'tags').values; - that.model.set({ - tagsFacet, - typesFacet: sortedTypesFacet, - severitiesFacet: sortedSeveritiesFacet, - issuesCount: data.total - }); + this.tagsFacet = tagsFacet; + this.typesFacet = sortedTypesFacet; + this.severitiesFacet = sortedSeveritiesFacet; + this.issuesCount = data.total; resolve(); }); @@ -202,28 +197,27 @@ export default ModalView.extend({ requestTests () { return new Promise(resolve => { - const that = this; const url = window.baseUrl + '/api/tests/list'; - const options = { testFileId: this.model.id }; + const options = { testFileKey: this.options.component.key }; $.get(url, options).done(data => { - that.model.set({ tests: data.tests }); - that.testSorting = 'status'; - that.testAsc = true; - that.sortTests(test => `${that.testsOrder.indexOf(test.status)}_______${test.name}`); + this.tests = data.tests; + this.testSorting = 'status'; + this.testAsc = true; + this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`); resolve(); }); }); }, sortTests (condition) { - let tests = this.model.get('tests'); + let tests = this.tests; if (Array.isArray(tests)) { tests = sortBy(tests, condition); if (!this.testAsc) { tests.reverse(); } - this.model.set({ tests }); + this.tests = tests; } }, @@ -246,25 +240,23 @@ export default ModalView.extend({ }, sortTestsByStatus () { - const that = this; if (this.testSorting === 'status') { this.testAsc = !this.testAsc; } - this.sortTests(test => `${that.testsOrder.indexOf(test.status)}_______${test.name}`); + this.sortTests(test => `${this.testsOrder.indexOf(test.status)}_______${test.name}`); this.testSorting = 'status'; this.render(); }, showTest (e) { - const that = this; const testId = $(e.currentTarget).data('id'); const url = window.baseUrl + '/api/tests/covered_files'; const options = { testId }; this.testsScroll = $(e.currentTarget).scrollParent().scrollTop(); return $.get(url, options).done(data => { - that.coveredFiles = data.files; - that.selectedTest = that.model.get('tests').find(test => test.id === testId); - that.render(); + this.coveredFiles = data.files; + this.selectedTest = this.tests.find(test => test.id === testId); + this.render(); }); }, @@ -276,6 +268,14 @@ export default ModalView.extend({ serializeData () { return { ...ModalView.prototype.serializeData.apply(this, arguments), + ...this.options.component, + measures: this.measures, + measuresToDisplay: this.measuresToDisplay, + tests: this.tests, + tagsFacet: this.tagsFacet, + typesFacet: this.typesFacet, + severitiesFacet: this.severitiesFacet, + issuesCount: this.issuesCount, testSorting: this.testSorting, selectedTest: this.selectedTest, coveredFiles: this.coveredFiles || [] diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-all.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs index cac99fea52d..cac99fea52d 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-all.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-all.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-coverage.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs index 2598e962d77..2598e962d77 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-coverage.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-coverage.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs index f6119c38fb6..f6119c38fb6 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-duplications.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-duplications.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-issues.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs index 30a39146750..30a39146750 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-issues.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-issues.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-lines.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs index f0c81d0349b..f0c81d0349b 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-lines.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-lines.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs index 6b0d5a110a6..6b0d5a110a6 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-test-cases.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-test-cases.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-tests.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs index c9b33c392ac..c9b33c392ac 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/measures/_source-viewer-measures-tests.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/_source-viewer-measures-tests.hbs diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs index 0df076390c9..ebcc2e79b13 100644 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-measures.hbs +++ b/server/sonar-web/src/main/js/components/SourceViewer/views/templates/source-viewer-measures.hbs @@ -22,34 +22,34 @@ {{#eq q 'UTS'}} <div class="source-viewer-measures"> <div class="source-viewer-measures-section"> - {{> 'measures/_source-viewer-measures-tests'}} + {{> '_source-viewer-measures-tests'}} </div> </div> <div class="source-viewer-measures"> - {{> 'measures/_source-viewer-measures-test-cases'}} + {{> '_source-viewer-measures-test-cases'}} </div> {{else}} <div class="source-viewer-measures"> <div class="source-viewer-measures-section"> <div class="source-viewer-measures-card"> - {{> 'measures/_source-viewer-measures-lines'}} + {{> '_source-viewer-measures-lines'}} </div> </div> <div class="source-viewer-measures-section"> - {{> 'measures/_source-viewer-measures-issues'}} + {{> '_source-viewer-measures-issues'}} </div> {{#if measures.coverage}} <div class="source-viewer-measures-section"> <div class="source-viewer-measures-card"> - {{> 'measures/_source-viewer-measures-coverage'}} + {{> '_source-viewer-measures-coverage'}} </div> </div> {{/if}} <div class="source-viewer-measures-section"> - {{> 'measures/_source-viewer-measures-duplications'}} + {{> '_source-viewer-measures-duplications'}} </div> </div> {{/eq}} @@ -59,7 +59,7 @@ <a class="js-show-all-measures">{{t 'component_viewer.show_all_measures'}}</a> <div class="source-viewer-measures source-viewer-measures-secondary js-all-measures hidden"> - {{> 'measures/_source-viewer-measures-all'}} + {{> '_source-viewer-measures-all'}} </div> </div> diff --git a/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js b/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js deleted file mode 100644 index 7b04a17d6fc..00000000000 --- a/server/sonar-web/src/main/js/components/__tests__/source-viewer-test.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 helper from '../source-viewer/helpers/code-with-issue-locations-helper'; - -describe('Code With Issue Locations Helper', () => { - it('should be a function', () => { - expect(helper).toBeTruthy(); - }); - - it('should mark one location', () => { - const code = '<span class="k">if</span> (<span class="sym-2 sym">a</span> + <span class="c">1</span>) {'; - const locations = [{ from: 1, to: 5 }]; - const result = helper(code, locations, 'x'); - expect(result).toBe([ - '<span class="k">i</span>', - '<span class="k x">f</span>', - '<span class=" x"> (</span>', - '<span class="sym-2 sym x">a</span>', - '<span class=""> + </span>', - '<span class="c">1</span>', - '<span class="">) {</span>' - ].join('')); - }); - - it('should mark two locations', () => { - const code = 'abcdefghijklmnopqrst'; - const locations = [ - { from: 1, to: 6 }, - { from: 11, to: 16 } - ]; - const result = helper(code, locations, 'x'); - expect(result).toBe([ - '<span class="">a</span>', - '<span class=" x">bcdef</span>', - '<span class="">ghijk</span>', - '<span class=" x">lmnop</span>', - '<span class="">qrst</span>' - ].join('')); - }); - - it('should mark one locations', () => { - const code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>'; - const locations = [{ from: 15, to: 20 }]; - const result = helper(code, locations, 'x'); - expect(result).toBe([ - '<span class="cppd"> * Copyright (C</span>', - '<span class="cppd x">) 200</span>', - '<span class="cppd">8-2014 SonarSource</span>' - ].join('')); - }); - - it('should mark two locations', () => { - const code = '<span class="cppd"> * Copyright (C) 2008-2014 SonarSource</span>'; - const locations = [ - { from: 24, to: 29 }, - { from: 15, to: 20 } - ]; - const result = helper(code, locations, 'x'); - expect(result).toBe([ - '<span class="cppd"> * Copyright (C</span>', - '<span class="cppd x">) 200</span>', - '<span class="cppd">8-20</span>', - '<span class="cppd x">14 So</span>', - '<span class="cppd">narSource</span>' - ].join('')); - }); - - it('should parse line with < and >', () => { - const code = '<span class="j">#include <stdio.h></span>'; - const result = helper(code, []); - expect(result).toBe('<span class="j">#include <stdio.h></span>'); - }); - - it('should parse syntax and usage highlighting', () => { - const code = '<span class="k"><span class="sym-3 sym">this</span></span>'; - const expected = '<span class="k sym-3 sym">this</span>'; - const result = helper(code, []); - expect(result).toBe(expected); - }); - - it('should parse nested tags', () => { - const code = '<span class="k"><span class="sym-3 sym">this</span> is</span>'; - const expected = '<span class="k sym-3 sym">this</span><span class="k"> is</span>'; - const result = helper(code, []); - expect(result).toBe(expected); - }); -}); - diff --git a/server/sonar-web/src/main/js/components/issue/issue-view.js b/server/sonar-web/src/main/js/components/issue/issue-view.js index cdd7d95de95..0bb7fbd1b31 100644 --- a/server/sonar-web/src/main/js/components/issue/issue-view.js +++ b/server/sonar-web/src/main/js/components/issue/issue-view.js @@ -29,7 +29,6 @@ import DeleteCommentView from './views/DeleteCommentView'; import SetSeverityFormView from './views/set-severity-form-view'; import SetTypeFormView from './views/set-type-form-view'; import TagsFormView from './views/tags-form-view'; -import Workspace from '../workspace/main'; import Template from './templates/issue.hbs'; import getCurrentUserFromStore from '../../app/utils/getCurrentUserFromStore'; @@ -242,6 +241,8 @@ export default Marionette.ItemView.extend({ e.preventDefault(); e.stopPropagation(); const ruleKey = this.model.get('rule'); + // lazy load Workspace + const Workspace = require('../workspace/main').default; Workspace.openRule({ key: ruleKey }); }, diff --git a/server/sonar-web/src/main/js/components/source-viewer/header.js b/server/sonar-web/src/main/js/components/source-viewer/header.js deleted file mode 100644 index 739037e5198..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/header.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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. - */ -/* @flow */ -import $ from 'jquery'; -import Marionette from 'backbone.marionette'; -import MoreActionsView from './more-actions'; -import MeasuresOverlay from './measures-overlay'; -import Template from './templates/source-viewer-header.hbs'; -import { addFavorite, removeFavorite } from '../../api/favorites'; - -export default Marionette.ItemView.extend({ - template: Template, - - events () { - return { - 'click .js-favorite': 'toggleFavorite', - 'click .js-actions': 'showMoreActions', - 'click .js-permalink': 'getPermalink' - }; - }, - - toggleFavorite () { - if (this.model.get('fav')) { - removeFavorite(this.model.get('key')).then(() => { - this.model.set('fav', false); - this.render(); - }); - } else { - addFavorite(this.model.get('key')).then(() => { - this.model.set('fav', true); - this.render(); - }); - } - }, - - showMoreActions (e) { - e.stopPropagation(); - $('body').click(); - const view = new MoreActionsView({ parent: this }); - view.render().$el.appendTo(this.$el); - }, - - getPermalink () { - let query = 'id=' + encodeURIComponent(this.model.get('key')); - const windowParams = 'resizable=1,scrollbars=1,status=1'; - if (this.options.viewer.highlightedLine) { - query = query + '&line=' + this.options.viewer.highlightedLine; - } - window.open(window.baseUrl + '/component/index?' + query, this.model.get('name'), windowParams); - }, - - showRawSources () { - const url = window.baseUrl + '/api/sources/raw?key=' + encodeURIComponent(this.model.get('key')); - const windowParams = 'resizable=1,scrollbars=1,status=1'; - window.open(url, this.model.get('name'), windowParams); - }, - - showMeasures () { - new MeasuresOverlay({ - model: this.model, - large: true - }).render(); - }, - - serializeData () { - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - path: this.model.get('path') || this.model.get('longName') - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js b/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js deleted file mode 100644 index 4b0307c0569..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/helpers/code-with-issue-locations-helper.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 escapeHtml from 'escape-html'; - -/** - * Intersect two ranges - * @param {number} s1 Start position of the first range - * @param {number} e1 End position of the first range - * @param {number} s2 Start position of the second range - * @param {number} e2 End position of the second range - * @returns {{from: number, to: number}} - */ -function intersect (s1, e1, s2, e2) { - return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; -} - -/** - * Get the substring of a string - * @param {string} str A string - * @param {number} from "From" offset - * @param {number} to "To" offset - * @param {number} acc Global offset to eliminate - * @returns {string} - */ -function part (str, from, to, acc) { - // we do not want negative number as the first argument of `substr` - return from >= acc ? str.substr(from - acc, to - from) : str.substr(0, to - from); -} - -/** - * Split a code html into tokens - * @param {string} code - * @param {string} rootClassName - * @returns {Array} - */ -function splitByTokens (code, rootClassName = '') { - const container = document.createElement('div'); - let tokens = []; - container.innerHTML = code; - [].forEach.call(container.childNodes, node => { - if (node.nodeType === 1) { - // ELEMENT NODE - const fullClassName = rootClassName ? (rootClassName + ' ' + node.className) : node.className; - const innerTokens = splitByTokens(node.innerHTML, fullClassName); - tokens = tokens.concat(innerTokens); - } - if (node.nodeType === 3) { - // TEXT NODE - tokens.push({ className: rootClassName, text: node.nodeValue }); - } - }); - return tokens; -} - -/** - * Highlight issue locations in the list of tokens - * @param {Array} tokens - * @param {Array} issueLocations - * @param {string} className - * @returns {Array} - */ -function highlightIssueLocations (tokens, issueLocations, className) { - issueLocations.forEach(location => { - const nextTokens = []; - let acc = 0; - tokens.forEach(token => { - const x = intersect(acc, acc + token.text.length, location.from, location.to); - const p1 = part(token.text, acc, x.from, acc); - const p2 = part(token.text, x.from, x.to, acc); - const p3 = part(token.text, x.to, acc + token.text.length, acc); - if (p1.length) { - nextTokens.push({ className: token.className, text: p1 }); - } - if (p2.length) { - const newClassName = token.className.indexOf(className) === -1 ? - [token.className, className].join(' ') : token.className; - nextTokens.push({ className: newClassName, text: p2 }); - } - if (p3.length) { - nextTokens.push({ className: token.className, text: p3 }); - } - acc += token.text.length; - }); - tokens = nextTokens.slice(); - }); - return tokens; -} - -/** - * Generate an html string from the list of tokens - * @param {Array} tokens - * @returns {string} - */ -function generateHTML (tokens) { - return tokens.map(token => ( - `<span class="${token.className}">${escapeHtml(token.text)}</span>` - )).join(''); -} - -/** - * Take the initial source code, split by tokens, - * highlight issues and generate result html - * @param {string} code - * @param {Array} issueLocations - * @param {string} [optionalClassName] - * @returns {string} - */ -function doTheStuff (code, issueLocations, optionalClassName) { - const _code = code || ' '; - const _issueLocations = issueLocations || []; - const _className = optionalClassName ? optionalClassName : 'source-line-code-issue'; - return generateHTML(highlightIssueLocations(splitByTokens(_code), _issueLocations, _className)); -} - -export default doTheStuff; - diff --git a/server/sonar-web/src/main/js/components/source-viewer/main.js b/server/sonar-web/src/main/js/components/source-viewer/main.js deleted file mode 100644 index 8b1725d09f3..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/main.js +++ /dev/null @@ -1,790 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 $ from 'jquery'; -import moment from 'moment'; -import sortBy from 'lodash/sortBy'; -import toPairs from 'lodash/toPairs'; -import Marionette from 'backbone.marionette'; -import Source from './source'; -import Issues from '../issue/collections/issues'; -import IssueView from '../issue/issue-view'; -import HeaderView from './header'; -import SCMPopupView from './popups/scm-popup'; -import CoveragePopupView from './popups/coverage-popup'; -import DuplicationPopupView from './popups/duplication-popup'; -import LineActionsPopupView from './popups/line-actions-popup'; -import highlightLocations from './helpers/code-with-issue-locations-helper'; -import Template from './templates/source-viewer.hbs'; -import IssueLocationTemplate from './templates/source-viewer-issue-location.hbs'; -import { translateWithParameters } from '../../helpers/l10n'; - -const HIGHLIGHTED_ROW_CLASS = 'source-line-highlighted'; - -export default Marionette.LayoutView.extend({ - className: 'source-viewer', - template: Template, - issueLocationTemplate: IssueLocationTemplate, - - ISSUES_LIMIT: 3000, - - LINES_AROUND: 500, - - // keep it twice bigger than LINES_AROUND - LINES_LIMIT: 1000, - TOTAL_LINES_LIMIT: 1000, - - regions: { - headerRegion: '.source-viewer-header' - }, - - ui: { - sourceBeforeSpinner: '.js-component-viewer-source-before', - sourceAfterSpinner: '.js-component-viewer-source-after' - }, - - events () { - return { - 'click .sym': 'highlightUsages', - 'click .source-line-scm': 'showSCMPopup', - 'click .source-line-covered': 'showCoveragePopup', - 'click .source-line-partially-covered': 'showCoveragePopup', - 'click .source-line-uncovered': 'showCoveragePopup', - 'click .source-line-duplications': 'showDuplications', - 'click .source-line-duplications-extra': 'showDuplicationPopup', - 'click .source-line-with-issues': 'onLineIssuesClick', - 'click .source-line-number[data-line-number]': 'onLineNumberClick', - 'mouseenter .source-line-filtered .source-line-filtered-container': 'showFilteredTooltip', - 'mouseleave .source-line-filtered .source-line-filtered-container': 'hideFilteredTooltip', - 'click @ui.sourceBeforeSpinner': 'loadSourceBefore', - 'click @ui.sourceAfterSpinner': 'loadSourceAfter' - }; - }, - - initialize () { - if (this.model == null) { - this.model = new Source(); - } - this.issues = new Issues(); - this.listenTo(this.issues, 'change:severity', this.onIssuesSeverityChange); - this.listenTo(this.issues, 'locations', this.toggleIssueLocations); - this.issueViews = []; - this.highlightedLine = null; - this.listenTo(this, 'loaded', this.onLoaded); - }, - - renderHeader () { - this.headerRegion.show(new HeaderView({ - viewer: this, - model: this.model - })); - }, - - onRender () { - this.renderHeader(); - this.renderIssues(); - if (this.model.has('filterLinesFunc')) { - this.filterLines(this.model.get('filterLinesFunc')); - } - this.$('[data-toggle="tooltip"]').tooltip({ container: 'body' }); - }, - - onDestroy () { - this.issueViews.forEach(view => view.destroy()); - this.issueViews = []; - this.clearTooltips(); - this.unbindScrollEvents(); - }, - - clearTooltips () { - this.$('[data-toggle="tooltip"]').tooltip('destroy'); - }, - - onLoaded () { - this.bindScrollEvents(); - }, - - open (id, options) { - const that = this; - const opts = typeof options === 'object' ? options : {}; - const finalize = function () { - that.requestIssues().done(() => { - if (!that.isDestroyed) { - that.render(); - that.trigger('loaded'); - } - }); - }; - Object.assign(this.options, { workspace: false, ...opts }); - this.model - .clear() - .set(this.model.defaults()) - .set({ uuid: id }); - this.requestComponent().done(() => { - that.requestSource(opts.aroundLine) - .done(finalize) - .fail(() => { - that.model.set({ - source: [ - { line: 0 } - ] - }); - finalize(); - }); - }); - return this; - }, - - requestComponent () { - const that = this; - const url = window.baseUrl + '/api/components/app'; - const data = { uuid: this.model.id }; - return $.ajax({ - url, - data, - type: 'GET', - statusCode: { - 404 () { - that.model.set({ exist: false }); - that.render(); - that.trigger('loaded'); - } - } - }).done(r => { - that.model.set(r); - that.model.set({ isUnitTest: r.q === 'UTS' }); - }); - }, - - linesLimit (aroundLine) { - if (aroundLine) { - return { - from: Math.max(1, aroundLine - this.LINES_AROUND), - to: aroundLine + this.LINES_AROUND - }; - } - return { from: 1, to: this.LINES_AROUND }; - }, - - getCoverageStatus (row) { - let status = null; - if (row.lineHits > 0) { - status = 'partially-covered'; - } - if (row.lineHits > 0 && row.conditions === row.coveredConditions) { - status = 'covered'; - } - if (row.lineHits === 0 || row.coveredConditions === 0) { - status = 'uncovered'; - } - return status; - }, - - requestSource (aroundLine) { - const that = this; - const url = window.baseUrl + '/api/sources/lines'; - const data = { uuid: this.model.id, ...this.linesLimit(aroundLine) }; - return $.ajax({ - url, - data, - statusCode: { - // don't display global error - 403: null - } - }).done(r => { - let source = (r.sources || []).slice(0); - if (source.length === 0 || (source.length > 0 && source[0].line === 1)) { - source.unshift({ line: 0 }); - } - source = source.map(row => { - return { ...row, coverageStatus: that.getCoverageStatus(row) }; - }); - const firstLine = source.length > 0 ? source[0].line : null; - const linesRequested = data.to - data.from + 1; - that.model.set({ - source, - hasCoverage: that.model.hasCoverage(source), - hasSourceBefore: firstLine > 1, - hasSourceAfter: r.sources.length === linesRequested - }); - that.model.checkIfHasDuplications(); - }).fail(request => { - if (request.status === 403) { - that.model.set({ - source: [], - hasSourceBefore: false, - hasSourceAfter: false, - canSeeCode: false - }); - } - }); - }, - - requestDuplications () { - const that = this; - const url = window.baseUrl + '/api/duplications/show'; - const options = { uuid: this.model.id }; - return $.get(url, options, data => { - const hasDuplications = data.duplications != null; - let duplications = []; - if (hasDuplications) { - duplications = {}; - data.duplications.forEach(d => { - d.blocks.forEach(b => { - if (b._ref === '1') { - const lineFrom = b.from; - const lineTo = b.from + b.size - 1; - for (let j = lineFrom; j <= lineTo; j++) { - duplications[j] = true; - } - } - }); - }); - duplications = toPairs(duplications).map(line => { - return { - line: +line[0], - duplicated: line[1] - }; - }); - } - that.model.addMeta(duplications); - that.model.addDuplications(data.duplications); - that.model.set({ - duplications: data.duplications, - duplicationsParsed: duplications, - duplicationFiles: data.files - }); - }); - }, - - requestIssues () { - const that = this; - const options = { - data: { - componentUuids: this.model.id, - f: 'component,componentId,project,subProject,rule,status,resolution,author,assignee,debt,' + - 'line,message,severity,creationDate,updateDate,closeDate,tags,comments,attr,actions,' + - 'transitions', - additionalFields: '_all', - resolved: false, - s: 'FILE_LINE', - asc: true, - ps: this.ISSUES_LIMIT - } - }; - return this.issues.fetch(options).done(() => { - that.addIssuesPerLineMeta(that.issues); - }); - }, - - _sortBySeverity (issues) { - const order = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO']; - return sortBy(issues, issue => order.indexOf(issue.severity)); - }, - - addIssuesPerLineMeta (issues) { - const that = this; - const lines = {}; - issues.forEach(issue => { - const line = issue.get('line') || 0; - if (!Array.isArray(lines[line])) { - lines[line] = []; - } - lines[line].push(issue.toJSON()); - }); - const issuesPerLine = toPairs(lines).map(line => { - return { - line: +line[0], - issues: that._sortBySeverity(line[1]) - }; - }); - this.model.addMeta(issuesPerLine); - this.addIssueLocationsMeta(issues); - }, - - addIssueLocationsMeta (issues) { - const issueLocations = []; - issues.forEach(issue => { - issue.getLinearLocations().forEach(location => { - const record = issueLocations.find(row => row.line === location.line); - if (record) { - record.issueLocations.push({ from: location.from, to: location.to }); - } else { - issueLocations.push({ - line: location.line, - issueLocations: [{ from: location.from, to: location.to }] - }); - } - }); - }); - this.model.addMeta(issueLocations); - }, - - renderIssues () { - this.$('.issue-list').addClass('hidden'); - }, - - renderIssue (issue) { - const issueView = new IssueView({ - el: '#issue-' + issue.get('key'), - model: issue - }); - this.issueViews.push(issueView); - issueView.render(); - }, - - addIssue (issue) { - const line = issue.get('line') || 0; - const code = this.$(`.source-line-code[data-line-number=${line}]`); - const issueBox = `<div class="issue" id="issue-${issue.get('key')}" data-key="${issue.get('key')}">`; - code.addClass('has-issues'); - let issueList = code.find('.issue-list'); - if (issueList.length === 0) { - code.append('<div class="issue-list"></div>'); - issueList = code.find('.issue-list'); - } - issueList - .append(issueBox) - .removeClass('hidden'); - this.renderIssue(issue); - }, - - showIssuesForLine (line) { - this.$(`.source-line-code[data-line-number="${line}"]`).find('.issue-list').removeClass('hidden'); - const issues = this.issues.filter(issue => ( - (issue.get('line') === line) || (!issue.get('line') && !line) - )); - issues.forEach(this.renderIssue, this); - }, - - onIssuesSeverityChange () { - const that = this; - this.addIssuesPerLineMeta(this.issues); - this.$('.source-line-with-issues').each(function () { - const line = +$(this).data('line-number'); - const row = that.model.get('source').find(row => row.line === line); - const issue = row.issues[0]; - $(this).html(`<i class="icon-severity-${issue.severity.toLowerCase()}"></i>`); - }); - }, - - highlightUsages (e) { - const highlighted = $(e.currentTarget).is('.highlighted'); - const key = e.currentTarget.className.match(/sym-\d+/); - if (key) { - this.$('.sym.highlighted').removeClass('highlighted'); - if (!highlighted) { - this.$('.sym.' + key[0]).addClass('highlighted'); - } - } - }, - - showSCMPopup (e) { - e.stopPropagation(); - $('body').click(); - const line = +$(e.currentTarget).data('line-number'); - const row = this.model.get('source').find(row => row.line === line); - const popup = new SCMPopupView({ - triggerEl: $(e.currentTarget), - line: row - }); - popup.render(); - }, - - showCoveragePopup (e) { - e.stopPropagation(); - $('body').click(); - this.clearTooltips(); - const line = $(e.currentTarget).data('line-number'); - const row = this.model.get('source').find(row => row.line === line); - const url = window.baseUrl + '/api/tests/list'; - const options = { - sourceFileId: this.model.id, - sourceFileLineNumber: line, - ps: 1000 - }; - return $.get(url, options).done(data => { - const popup = new CoveragePopupView({ - line: row, - tests: data.tests, - triggerEl: $(e.currentTarget) - }); - popup.render(); - }); - }, - - showDuplications (e) { - const that = this; - const lineNumber = $(e.currentTarget).closest('.source-line').data('line-number'); - this.clearTooltips(); - this.requestDuplications().done(() => { - that.render(); - that.$el.addClass('source-duplications-expanded'); - - // immediately show dropdown popup if there is only one duplicated block - if (that.model.get('duplications').length === 1) { - const dupsBlock = that.$(`.source-line[data-line-number=${lineNumber}]`) - .find('.source-line-duplications-extra'); - dupsBlock.click(); - } - }); - }, - - showDuplicationPopup (e) { - e.stopPropagation(); - $('body').click(); - this.clearTooltips(); - const index = $(e.currentTarget).data('index'); - const line = $(e.currentTarget).data('line-number'); - let blocks = this.model.get('duplications')[index - 1].blocks; - const inRemovedComponent = blocks.some(b => b._ref == null); - let foundOne = false; - blocks = blocks.filter(b => { - const outOfBounds = b.from > line || b.from + b.size < line; - const currentFile = b._ref === '1'; - const shouldDisplayForCurrentFile = outOfBounds || foundOne; - const shouldDisplay = !currentFile || shouldDisplayForCurrentFile; - const isOk = (b._ref != null) && shouldDisplay; - if (b._ref === '1' && !outOfBounds) { - foundOne = true; - } - return isOk; - }); - const popup = new DuplicationPopupView({ - blocks, - inRemovedComponent, - component: this.model.toJSON(), - files: this.model.get('duplicationFiles'), - triggerEl: $(e.currentTarget) - }); - popup.render(); - }, - - onLineIssuesClick (e) { - const line = $(e.currentTarget).data('line-number'); - const issuesList = $(e.currentTarget).parent().find('.issue-list'); - const areIssuesRendered = issuesList.find('.issue-inner').length > 0; - if (issuesList.is('.hidden')) { - if (areIssuesRendered) { - issuesList.removeClass('hidden'); - } else { - this.showIssuesForLine(line); - } - } else { - issuesList.addClass('hidden'); - } - }, - - showLineActionsPopup (e) { - e.stopPropagation(); - $('body').click(); - const line = $(e.currentTarget).data('line-number'); - const popup = new LineActionsPopupView({ - line, - triggerEl: $(e.currentTarget), - component: this.model.toJSON() - }); - popup.render(); - }, - - onLineNumberClick (e) { - const row = $(e.currentTarget).closest('.source-line'); - const line = row.data('line-number'); - const highlighted = row.is('.' + HIGHLIGHTED_ROW_CLASS); - if (!highlighted) { - this.highlightLine(line); - this.showLineActionsPopup(e); - } else { - this.removeHighlighting(); - } - }, - - removeHighlighting () { - this.highlightedLine = null; - this.$('.' + HIGHLIGHTED_ROW_CLASS).removeClass(HIGHLIGHTED_ROW_CLASS); - }, - - highlightLine (line) { - const row = this.$(`.source-line[data-line-number=${line}]`); - this.removeHighlighting(); - this.highlightedLine = line; - row.addClass(HIGHLIGHTED_ROW_CLASS); - return this; - }, - - bindScrollEvents () { - // no op - }, - - unbindScrollEvents () { - // no op - }, - - onScroll () { - // no op - }, - - scrollToLine (line) { - const row = this.$(`.source-line[data-line-number=${line}]`); - if (row.length > 0) { - let p = this.$el.scrollParent(); - if (p.is(document) || p.is('body')) { - p = $(window); - } - const pTopOffset = p.offset() != null ? p.offset().top : 0; - const pHeight = p.height(); - const goal = row.offset().top - pHeight / 3 - pTopOffset; - p.scrollTop(goal); - } - return this; - }, - - scrollToFirstLine (line) { - const row = this.$(`.source-line[data-line-number=${line}]`); - if (row.length > 0) { - let p = this.$el.scrollParent(); - if (p.is(document) || p.is('body')) { - p = $(window); - } - const pTopOffset = p.offset() != null ? p.offset().top : 0; - const goal = row.offset().top - pTopOffset; - p.scrollTop(goal); - } - return this; - }, - - scrollToLastLine (line) { - const row = this.$(`.source-line[data-line-number=${line}]`); - if (row.length > 0) { - let p = this.$el.scrollParent(); - if (p.is(document) || p.is('body')) { - p = $(window); - } - const pTopOffset = p.offset() != null ? p.offset().top : 0; - const pHeight = p.height(); - const goal = row.offset().top - pTopOffset - pHeight + row.height(); - p.scrollTop(goal); - } - return this; - }, - - loadSourceBefore (e) { - e.preventDefault(); - this.unbindScrollEvents(); - this.$('.js-component-viewer-loading-before').removeClass('hidden'); - this.$('.js-component-viewer-source-before').addClass('hidden'); - const that = this; - let source = this.model.get('source'); - const firstLine = source[0].line; - const url = window.baseUrl + '/api/sources/lines'; - const options = { - uuid: this.model.id, - from: Math.max(1, firstLine - this.LINES_AROUND), - to: firstLine - 1 - }; - return $.get(url, options).done(data => { - source = (data.sources || []).concat(source); - if (source.length > that.TOTAL_LINES_LIMIT + 1) { - source = source.slice(0, that.TOTAL_LINES_LIMIT); - that.model.set({ hasSourceAfter: true }); - } - if (source.length === 0 || (source.length > 0 && source[0].line === 1)) { - source.unshift({ line: 0 }); - } - source = source.map(row => { - return { ...row, coverageStatus: that.getCoverageStatus(row) }; - }); - that.model.set({ - source, - hasCoverage: that.model.hasCoverage(source), - hasSourceBefore: data.sources.length === that.LINES_AROUND && source.length > 0 && source[0].line > 0 - }); - that.addIssuesPerLineMeta(that.issues); - if (that.model.has('duplications')) { - that.model.addDuplications(that.model.get('duplications')); - that.model.addMeta(that.model.get('duplicationsParsed')); - } - that.model.checkIfHasDuplications(); - that.render(); - that.scrollToFirstLine(firstLine); - if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) { - that.bindScrollEvents(); - } - }); - }, - - loadSourceAfter (e) { - e.preventDefault(); - this.unbindScrollEvents(); - this.$('.js-component-viewer-loading-after').removeClass('hidden'); - this.$('.js-component-viewer-source-after').addClass('hidden'); - const that = this; - let source = this.model.get('source'); - const lastLine = source[source.length - 1].line; - const url = window.baseUrl + '/api/sources/lines'; - const options = { - uuid: this.model.id, - from: lastLine + 1, - to: lastLine + this.LINES_AROUND - }; - return $.get(url, options).done(data => { - source = source.concat(data.sources); - if (source.length > that.TOTAL_LINES_LIMIT + 1) { - source = source.slice(source.length - that.TOTAL_LINES_LIMIT); - that.model.set({ hasSourceBefore: true }); - } - source = source.map(row => { - return { ...row, coverageStatus: that.getCoverageStatus(row) }; - }); - that.model.set({ - source, - hasCoverage: that.model.hasCoverage(source), - hasSourceAfter: data.sources.length === that.LINES_AROUND - }); - that.addIssuesPerLineMeta(that.issues); - if (that.model.has('duplications')) { - that.model.addDuplications(that.model.get('duplications')); - that.model.addMeta(that.model.get('duplicationsParsed')); - } - that.model.checkIfHasDuplications(); - that.render(); - that.scrollToLastLine(lastLine); - if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) { - that.bindScrollEvents(); - } - }).fail(() => { - that.model.set({ - hasSourceAfter: false - }); - that.render(); - if (that.model.get('hasSourceBefore') || that.model.get('hasSourceAfter')) { - that.bindScrollEvents(); - } - }); - }, - - filterLines (func) { - const lines = this.model.get('source'); - const $lines = this.$('.source-line'); - this.model.set('filterLinesFunc', func); - lines.forEach((line, idx) => { - const $line = $($lines[idx]); - const filtered = func(line) && line.line > 0; - $line.toggleClass('source-line-shadowed', !filtered); - $line.toggleClass('source-line-filtered', filtered); - }); - }, - - filterLinesByDate (date, label) { - const sinceDate = moment(date).toDate(); - this.sinceLabel = label; - this.filterLines(line => { - const scmDate = moment(line.scmDate).toDate(); - return scmDate >= sinceDate; - }); - }, - - showFilteredTooltip (e) { - $(e.currentTarget).tooltip({ - container: 'body', - placement: 'right', - title: translateWithParameters('source_viewer.tooltip.new_code', this.sinceLabel), - trigger: 'manual' - }).tooltip('show'); - }, - - hideFilteredTooltip (e) { - $(e.currentTarget).tooltip('destroy'); - }, - - toggleIssueLocations (issue) { - if (this.locationsShowFor === issue) { - this.hideIssueLocations(); - } else { - this.hideIssueLocations(); - this.showIssueLocations(issue); - } - }, - - showIssueLocations (issue) { - this.locationsShowFor = issue; - const primaryLocation = { - msg: issue.get('message'), - textRange: issue.get('textRange') - }; - let _locations = [primaryLocation]; - issue.get('flows').forEach(flow => { - const flowLocationsCount = Array.isArray(flow.locations) ? flow.locations.length : 0; - const flowLocations = flow.locations.map((location, index) => { - const _location = { ...location }; - if (flowLocationsCount > 1) { - Object.assign(_location, { index: flowLocationsCount - index }); - } - return _location; - }); - _locations = [].concat(_locations, flowLocations); - }); - _locations.forEach(this.showIssueLocation, this); - }, - - showIssueLocation (location, index) { - if (location && location.textRange) { - const line = location.textRange.startLine; - const row = this.$(`.source-line-code[data-line-number="${line}"]`); - - if (index > 0 && location.msg) { - // render location marker only for - // secondary locations and execution flows - // and only if message is not empty - const renderedFlowLocation = this.renderIssueLocation(location); - row.find('.source-line-issue-locations').prepend(renderedFlowLocation); - } - - this.highlightIssueLocationInCode(location); - } - }, - - renderIssueLocation (location) { - location.msg = location.msg ? location.msg : ' '; - return this.issueLocationTemplate(location); - }, - - highlightIssueLocationInCode (location) { - for (let line = location.textRange.startLine; line <= location.textRange.endLine; line++) { - const row = this.$(`.source-line-code[data-line-number="${line}"]`); - - // get location for the current line - const from = line === location.textRange.startLine ? location.textRange.startOffset : 0; - const to = line === location.textRange.endLine ? location.textRange.endOffset : 999999; - const _location = { from, to }; - - // mark issue location in the source code - const codeEl = row.find('.source-line-code-inner > pre'); - const code = codeEl.html(); - const newCode = highlightLocations(code, [_location], 'source-line-code-secondary-issue'); - codeEl.html(newCode); - } - }, - - hideIssueLocations () { - this.locationsShowFor = null; - this.$('.source-line-issue-locations').empty(); - this.$('.source-line-code-secondary-issue').removeClass('source-line-code-secondary-issue'); - } -}); diff --git a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js b/server/sonar-web/src/main/js/components/source-viewer/more-actions.js deleted file mode 100644 index aba02a8e1de..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/more-actions.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 $ from 'jquery'; -import Marionette from 'backbone.marionette'; -import Workspace from '../workspace/main'; -import Template from './templates/source-viewer-more-actions.hbs'; - -export default Marionette.ItemView.extend({ - className: 'source-viewer-header-more-actions', - template: Template, - - events: { - 'click .js-measures': 'showMeasures', - 'click .js-new-window': 'openNewWindow', - 'click .js-workspace': 'openInWorkspace', - 'click .js-raw-source': 'showRawSource' - }, - - onRender () { - const that = this; - $('body').on('click.component-viewer-more-actions', () => { - $('body').off('click.component-viewer-more-actions'); - that.destroy(); - }); - }, - - showMeasures () { - this.options.parent.showMeasures(); - }, - - openNewWindow () { - this.options.parent.getPermalink(); - }, - - openInWorkspace () { - const key = this.options.parent.model.get('key'); - Workspace.openComponent({ key }); - }, - - showRawSource () { - this.options.parent.showRawSources(); - }, - - serializeData () { - const options = this.options.parent.options.viewer.options; - return { - ...Marionette.ItemView.prototype.serializeData.apply(this, arguments), - options - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/source-viewer/source.js b/server/sonar-web/src/main/js/components/source-viewer/source.js deleted file mode 100644 index 3cb1198e328..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/source.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 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 Backbone from 'backbone'; - -export default Backbone.Model.extend({ - idAttribute: 'uuid', - - defaults () { - return { - exist: true, - - hasSource: false, - hasCoverage: false, - hasDuplications: false, - hasSCM: false, - - canSeeCode: true - }; - }, - - key () { - return this.get('key'); - }, - - addMeta (meta) { - const source = this.get('source'); - let metaIdx = 0; - let metaLine = meta[metaIdx]; - source.forEach(line => { - while (metaLine != null && line.line > metaLine.line) { - metaLine = meta[++metaIdx]; - } - if (metaLine != null && line.line === metaLine.line) { - Object.assign(line, metaLine); - metaLine = meta[++metaIdx]; - } - }); - this.set({ source }); - }, - - addDuplications (duplications) { - const source = this.get('source'); - if (source != null) { - source.forEach(line => { - const lineDuplications = []; - duplications.forEach((d, i) => { - let duplicated = false; - d.blocks.forEach(b => { - if (b._ref === '1') { - const lineFrom = b.from; - const lineTo = b.from + b.size - 1; - if (line.line >= lineFrom && line.line <= lineTo) { - duplicated = true; - } - } - }); - lineDuplications.push(duplicated ? i + 1 : false); - }); - line.duplications = lineDuplications; - }); - } - this.set({ source }); - }, - - checkIfHasDuplications () { - const source = this.get('source'); - let hasDuplications = false; - if (source != null) { - source.forEach(line => { - if (line.duplicated) { - hasDuplications = true; - } - }); - } - this.set({ hasDuplications }); - }, - - hasCoverage (source) { - return source.some(line => line.coverageStatus != null); - } -}); diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs deleted file mode 100644 index a4532354481..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-header.hbs +++ /dev/null @@ -1,74 +0,0 @@ -<div class="source-viewer-header-component"> - <div class="component-name"> - - {{#unless removed}} - {{#if projectName}} - <div class="component-name-parent"> - {{qualifierIcon 'TRK'}} <a href="{{dashboardUrl project}}">{{projectName}}</a> - </div> - {{#if subProjectName}} - <div class="component-name-parent"> - {{qualifierIcon 'TRK'}} <a href="{{dashboardUrl subProject}}">{{subProjectName}}</a> - </div> - {{/if}} - {{/if}} - - <div class="component-name-path"> - {{qualifierIcon q}} <span>{{collapsedDirFromPath path}}</span><span class="component-name-file">{{fileFromPath path}}</span> - - {{#if canMarkAsFavorite}} - <a class="js-favorite component-name-favorite {{#if fav}}icon-favorite{{else}}icon-not-favorite{{/if}}" - title="{{#if fav}}{{t 'click_to_remove_from_favorites'}}{{else}}{{t 'click_to_add_to_favorites'}}{{/if}}"> - </a> - {{/if}} - </div> - {{else}} - <div class="source-viewer-header-component-project removed">{{removedMessage}}</div> - {{/unless}} - </div> -</div> - -{{#unless removed}} - <a class="js-actions source-viewer-header-actions icon-list" title="{{t 'component_viewer.more_actions'}}"></a> - - <div class="source-viewer-header-measures"> - {{#if isUnitTest}} - <div class="source-viewer-header-measure"> - <span class="source-viewer-header-measure-value">{{formatMeasure measures.tests 'SHORT_INT'}}</span> - <span class="source-viewer-header-measure-label">{{t 'metric.tests.name'}}</span> - </div> - {{/if}} - - {{#unless isUnitTest}} - <div class="source-viewer-header-measure"> - <span class="source-viewer-header-measure-value">{{formatMeasure measures.lines 'SHORT_INT'}}</span> - <span class="source-viewer-header-measure-label">{{t 'metric.lines.name'}}</span> - </div> - {{/unless}} - - <div class="source-viewer-header-measure"> - <span class="source-viewer-header-measure-value"> - <a class="source-viewer-header-external-link" target="_blank" - href="{{link '/issues/search#resolved=false|fileUuids=' uuid}}"> - {{#if measures.issues}}{{formatMeasure measures.issues 'SHORT_INT'}}{{else}}0{{/if}} <i class="icon-detach"></i> - </a> - </span> - <span class="source-viewer-header-measure-label">{{t 'metric.violations.name'}}</span> - </div> - - {{#notNull measures.coverage}} - <div class="source-viewer-header-measure"> - <span class="source-viewer-header-measure-value">{{formatMeasure measures.coverage 'PERCENT'}}</span> - <span class="source-viewer-header-measure-label">{{t 'metric.coverage.name'}}</span> - </div> - {{/notNull}} - - {{#notNull measures.duplicationDensity}} - <div class="source-viewer-header-measure"> - <span class="source-viewer-header-measure-value">{{formatMeasure measures.duplicationDensity 'PERCENT'}}</span> - <span class="source-viewer-header-measure-label">{{t 'duplications'}}</span> - </div> - {{/notNull}} - - </div> -{{/unless}} diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs deleted file mode 100644 index 181e85b2497..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-issue-location.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="source-viewer-issue-location" title="{{msg}}"> - {{#if index}}<strong>{{index}}: </strong>{{/if}} - {{limitString msg}} -</div> diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs deleted file mode 100644 index c5a85413424..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer-more-actions.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<a class="js-measures">{{t 'component_viewer.show_details'}}</a> -<br> -<a class="js-new-window">{{t 'component_viewer.new_window'}}</a> -{{#unless options.workspace}} - <br> - <a class="js-workspace">{{t 'component_viewer.open_in_workspace'}}</a> -{{/unless}} -<br> -<a class="js-raw-source">{{t 'component_viewer.show_raw_source'}}</a> diff --git a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs b/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs deleted file mode 100644 index ede6aeddde8..00000000000 --- a/server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs +++ /dev/null @@ -1,111 +0,0 @@ -<div class="source-viewer-header"></div> - -{{#if canSeeCode}} - - {{#if exist}} - - {{#if hasSourceBefore}} - <div class="source-viewer-more-code"> - <button class="js-component-viewer-source-before"> - {{t 'source_viewer.load_more_code'}} - </button> - <div class="js-component-viewer-loading-before hidden"> - <i class="spinner"></i> - <span class="note spacer-left">{{t 'source_viewer.loading_more_code'}}</span> - </div> - </div> - {{/if}} - - <table class="source-table"> - {{#eachWithPrevious source}} - <tr class="source-line {{#eq line 0}}{{#empty issues}}hidden{{/empty}}{{/eq}}" {{#if line}}data-line-number="{{line}}"{{/if}}> - <td class="source-meta source-line-number" {{#if line}}data-line-number="{{line}}"{{/if}}></td> - - <td class="source-meta source-line-scm" {{#if line}}data-line-number="{{line}}"{{/if}}> - {{#ifSCMChanged2 this _previous}} - <div class="source-line-scm-inner" data-author="{{scmAuthor}}"></div> - {{/ifSCMChanged2}} - </td> - - {{#if ../hasCoverage}} - <td class="source-meta source-line-coverage {{#notNull coverageStatus}}source-line-{{coverageStatus}}{{/notNull}}" - data-line-number="{{line}}" {{#notNull coverageStatus}}title="{{t 'source_viewer.tooltip' coverageStatus}}" data-placement="right" data-toggle="tooltip"{{/notNull}}> - <div class="source-line-bar"></div> - </td> - {{/if}} - - {{#if ../hasDuplications}} - <td class="source-meta source-line-duplications {{#if duplicated}}source-line-duplicated{{/if}}" - {{#if duplicated}}title="{{t 'source_viewer.tooltip.duplicated_line'}}" data-placement="right" data-toggle="tooltip"{{/if}}> - <div class="source-line-bar"></div> - </td> - - {{#each duplications}} - <td class="source-meta source-line-duplications-extra {{#if this}}source-line-duplicated{{/if}}" - data-index="{{this}}" data-line-number="{{../line}}" - {{#if this}}title="{{t 'source_viewer.tooltip.duplicated_block'}}" data-placement="right" data-toggle="tooltip"{{/if}}> - <div class="source-line-bar"></div> - </td> - {{/each}} - {{/if}} - - <td class="source-meta source-line-issues {{#notEmpty issues}}source-line-with-issues{{/notEmpty}}" - data-line-number="{{line}}"> - {{#withFirst issues}} - {{severityIcon severity}} - {{/withFirst}} - {{#ifLengthGT issues 1}} - <span class="source-line-issues-counter">{{length issues}}</span> - {{/ifLengthGT}} - </td> - - <td class="source-meta source-line-filtered-container" data-line-number="{{line}}"> - <div class="source-line-bar"></div> - </td> - - <td class="source-line-code code {{#notEmpty issues}}has-issues{{/notEmpty}}" data-line-number="{{line}}"> - <div class="source-line-code-inner"> - {{#notNull code}} - <pre>{{{codeWithIssueLocations code issueLocations}}}</pre> - {{/notNull}} - - <div class="source-line-issue-locations"></div> - </div> - - {{#notEmpty issues}} - <div class="issue-list"> - {{#each issues}} - <div class="issue" id="issue-{{key}}"></div> - {{/each}} - </div> - {{/notEmpty}} - </td> - </tr> - {{/eachWithPrevious}} - </table> - - {{#if hasSourceAfter}} - <div class="source-viewer-more-code"> - <button class="js-component-viewer-source-after"> - {{t 'source_viewer.load_more_code'}} - </button> - <div class="js-component-viewer-loading-after hidden"> - <i class="spinner"></i> - <span class="note spacer-left">{{t 'source_viewer.loading_more_code'}}</span> - </div> - </div> - {{/if}} - - {{else}} - - {{! does not exist }} - <div class="alert alert-warning spacer-top">{{t 'component_viewer.no_component'}}</div> - - {{/if}} - -{{else}} - - {{! can't see code }} - <div class="alert alert-warning spacer-top">{{t 'code_viewer.no_source_code_displayed_due_to_security'}}</div> - -{{/if}} diff --git a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js index 7ab96e7c683..188b08cab60 100644 --- a/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js +++ b/server/sonar-web/src/main/js/components/workspace/views/viewer-view.js @@ -21,7 +21,7 @@ import $ from 'jquery'; import React from 'react'; import { render } from 'react-dom'; import BaseView from './base-viewer-view'; -import SourceViewer from '../../SourceViewer/StandaloneSourceViewer'; +import SourceViewer from '../../SourceViewer/SourceViewer'; import Template from '../templates/workspace-viewer.hbs'; import WithStore from '../../shared/WithStore'; |