From: Stas Vilchik Date: Fri, 2 Mar 2018 15:24:37 +0000 (+0100) Subject: rewrite remaining popups in react (#3109) X-Git-Tag: 7.5~1584 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=299cebedac5ef4a6a17dd18782c2b1a2a79f08d5;p=sonarqube.git rewrite remaining popups in react (#3109) * extract baseFontFamily * rewrite favorites store in ts * add new types and change existing ones * rewrite SourceViewer helpers in ts * rewrite SourceViewer in ts and its popups in react * drop popups * fix iterating over nodelist * fix quality flaws --- diff --git a/server/sonar-web/config/webpack.config.js b/server/sonar-web/config/webpack.config.js index f120681011f..3d1e7aba4e6 100644 --- a/server/sonar-web/config/webpack.config.js +++ b/server/sonar-web/config/webpack.config.js @@ -68,7 +68,7 @@ module.exports = ({ production = true, fast = false }) => ({ app: [ './src/main/js/app/utils/setPublicPath.js', './src/main/js/app/index.js', - './src/main/js/components/SourceViewer/SourceViewer.js' + './src/main/js/components/SourceViewer/SourceViewer' ] }, output: { diff --git a/server/sonar-web/src/main/js/app/styles/init/type.css b/server/sonar-web/src/main/js/app/styles/init/type.css index 80b61e7dbf3..28ab828201f 100644 --- a/server/sonar-web/src/main/js/app/styles/init/type.css +++ b/server/sonar-web/src/main/js/app/styles/init/type.css @@ -23,7 +23,7 @@ body { } body { - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--baseFontSize); line-height: 1.23076923; } diff --git a/server/sonar-web/src/main/js/app/theme.js b/server/sonar-web/src/main/js/app/theme.js index 04f518b8060..dd39fe0a445 100644 --- a/server/sonar-web/src/main/js/app/theme.js +++ b/server/sonar-web/src/main/js/app/theme.js @@ -73,6 +73,7 @@ module.exports = { pagePadding: '20px', // different + baseFontFamily: "'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif", defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)', // z-index diff --git a/server/sonar-web/src/main/js/app/types.ts b/server/sonar-web/src/main/js/app/types.ts index 252f6c135a2..92136630ff4 100644 --- a/server/sonar-web/src/main/js/app/types.ts +++ b/server/sonar-web/src/main/js/app/types.ts @@ -112,6 +112,25 @@ export interface CustomMeasure { updatedAt?: string; } +export interface Duplication { + blocks: DuplicationBlock[]; +} + +export interface DuplicationBlock { + _ref: string; + from: number; + size: number; +} + +export interface DuplicatedFile { + key: string; + name: string; + project: string; + projectName: string; + subProject?: string; + subProjectName?: string; +} + export interface Extension { key: string; name: string; @@ -122,6 +141,11 @@ export interface FacetValue { val: string; } +export interface FlowLocation { + msg: string; + textRange: TextRange; +} + export interface Group { default?: boolean; description?: string; @@ -174,12 +198,72 @@ export function isSameHomePage(a: HomePage, b: HomePage) { ); } +export interface Issue { + actions?: string[]; + assignee?: string; + assigneeActive?: string; + assigneeAvatar?: string; + assigneeLogin?: string; + assigneeName?: string; + author?: string; + comments?: IssueComment[]; + component: string; + componentLongName: string; + componentQualifier: string; + componentUuid: string; + creationDate: string; + effort?: string; + key: string; + flows: FlowLocation[][]; + line?: number; + message: string; + organization: string; + project: string; + projectName: string; + projectOrganization: string; + projectUuid: string; + resolution?: string; + rule: string; + ruleName: string; + secondaryLocations: FlowLocation[]; + severity: string; + status: string; + subProject?: string; + subProjectName?: string; + subProjectUuid?: string; + tags?: string[]; + textRange?: TextRange; + transitions?: string[]; + type: string; +} + +export interface IssueComment { + author?: string; + authorActive?: boolean; + authorAvatar?: string; + authorLogin?: string; + authorName?: string; + createdAt: string; + htmlText: string; + key: string; + markdown: string; + updatable: boolean; +} + export interface LightComponent { key: string; organization: string; qualifier: string; } +export interface LinearIssueLocation { + from: number; + index?: number; + line: number; + startLine?: number; + to: number; +} + export interface LoggedInUser extends CurrentUser { avatar?: string; email?: string; @@ -349,9 +433,24 @@ export interface ShortLivingBranch { type: BranchType.SHORT; } +export interface SourceLine { + code?: string; + conditions?: number; + coverageStatus?: string; + coveredConditions?: number; + duplicated?: boolean; + line: number; + lineHits?: number; + scmAuthor?: string; + scmDate?: string; + scmRevision?: string; +} + export interface SourceViewerFile { canMarkAsFavorite?: boolean; + fav?: boolean; key: string; + leakPeriodDate?: string; measures: { coverage?: string; duplicationDensity?: string; @@ -381,6 +480,13 @@ export interface TestCase { status: string; } +export interface TextRange { + startLine: number; + startOffset: number; + endLine: number; + endOffset: number; +} + export interface User { active: boolean; avatar?: string; diff --git a/server/sonar-web/src/main/js/apps/component/components/App.tsx b/server/sonar-web/src/main/js/apps/component/components/App.tsx index 06d009a07fa..96ff380b906 100644 --- a/server/sonar-web/src/main/js/apps/component/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/component/components/App.tsx @@ -47,7 +47,7 @@ export default class App extends React.PureComponent { render() { const { branch, id, line } = this.props.location.query; - const finalLine = line != null ? Number(line) : null; + const finalLine = line ? Number(line) : undefined; return (
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js deleted file mode 100644 index 974eb8e4b0f..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { connect } from 'react-redux'; -import SourceViewerBase from './SourceViewerBase'; -import { receiveFavorites } from '../../store/favorites/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 }); - } - dispatch(receiveFavorites(favorites, notFavorites)); - } -}; - -const mapDispatchToProps = { onReceiveComponent }; - -export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx new file mode 100644 index 00000000000..3b40ed80dae --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewer.tsx @@ -0,0 +1,47 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import SourceViewerBase from './SourceViewerBase'; +import { SourceViewerFile } from '../../app/types'; +import { receiveFavorites } from '../../store/favorites/duck'; + +const mapStateToProps = null; + +interface DispatchProps { + onReceiveComponent: (component: SourceViewerFile) => void; +} + +const onReceiveComponent = (component: SourceViewerFile) => (dispatch: 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 mapDispatchToProps: DispatchProps = { onReceiveComponent }; + +export default connect(mapStateToProps, mapDispatchToProps)(SourceViewerBase); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js deleted file mode 100644 index 6f267452fa0..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ /dev/null @@ -1,694 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { intersection, uniqBy } from 'lodash'; -import SourceViewerHeader from './SourceViewerHeader'; -import SourceViewerCode from './SourceViewerCode'; -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 loadIssues from './helpers/loadIssues'; -import getCoverageStatus from './helpers/getCoverageStatus'; -import { - issuesByLine, - locationsByLine, - duplicationsByLine, - symbolsByLine -} from './helpers/indexing'; -/*:: import type { LinearIssueLocation } from './helpers/indexing'; */ -import { - getComponentForSourceViewer, - getComponentData, - getSources, - getDuplications, - getTests -} from '../../api/components'; -import { parseDate } from '../../helpers/dates'; -import { translate } from '../../helpers/l10n'; -import { scrollToElement } from '../../helpers/scrolling'; -/*:: import type { SourceLine } from './types'; */ -/*:: import type { Issue, FlowLocation } from '../issue/types'; */ -import './styles.css'; - -// TODO react-virtualized - -/*:: -type Props = { - aroundLine?: number, - branch?: string, - component: string, - displayAllIssues: boolean, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - displayLocationMarkers?: boolean; - highlightedLine?: number, - highlightedLocations?: Array, - highlightedLocationMessage?: { index: number, text: string }, - loadComponent: (component: string, branch?: string) => Promise<*>, - loadIssues: (component: string, from: number, to: number, branch?: string) => Promise<*>, - loadSources: (component: string, from: number, to: number, branch?: string) => Promise<*>, - onLoaded?: (component: Object, sources: Array<*>, issues: Array<*>) => void, - onLocationSelect?: number => void, - onIssueChange?: Issue => void, - onIssueSelect?: string => void, - onIssueUnselect?: () => void, - onReceiveComponent: ({ canMarkAsFavorite: boolean, fav: boolean, key: string }) => void, - scroll?: HTMLElement => void, - selectedIssue?: string -}; -*/ - -/*:: -type State = { - component?: Object, - displayDuplications: boolean, - duplications?: Array<{ - blocks: Array<{ - _ref: string, - from: number, - size: number - }> - }>, - duplicationsByLine: { [number]: Array }, - duplicatedFiles?: Array<{ key: string }>, - hasSourcesAfter: boolean, - highlightedLine: number | null, - highlightedSymbols: Array, - issues?: Array, - issuesByLine: { [number]: Array }, - issueLocationsByLine: { [number]: Array }, - loading: boolean, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - notAccessible: boolean, - notExist: boolean, - openIssuesByLine: { [number]: boolean }, - openPopup: ?{ - issue: string, - name: string - }, - selectedIssue?: string, - sources?: Array, - sourceRemoved: boolean, - symbolsByLine: { [number]: Array } -}; -*/ - -const LINES = 500; - -function loadComponent(key /*: string */, branch /*: string | void */) /*: Promise<*> */ { - return Promise.all([ - getComponentForSourceViewer(key, branch), - getComponentData(key, branch) - ]).then(([component, data]) => ({ - ...component, - leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate) - })); -} - -function loadSources( - key /*: string */, - from /*: ?number */, - to /*: ?number */, - branch /*: string | void */ -) /*: Promise> */ { - return getSources(key, from, to, branch); -} - -export default class SourceViewerBase extends React.PureComponent { - /*:: mounted: boolean; */ - /*:: node: HTMLElement; */ - /*:: props: Props; */ - /*:: state: State; */ - - static defaultProps = { - displayAllIssues: false, - displayIssueLocationsCount: true, - displayIssueLocationsLink: true, - displayLocationMarkers: true, - loadComponent, - loadIssues, - loadSources - }; - - constructor(props /*: Props */) { - super(props); - this.state = { - displayDuplications: false, - duplicationsByLine: {}, - hasSourcesAfter: false, - highlightedLine: props.highlightedLine || null, - highlightedSymbols: [], - issuesByLine: {}, - issueLocationsByLine: {}, - issueSecondaryLocationsByIssueByLine: {}, - issueSecondaryLocationMessagesByIssueByLine: {}, - loading: true, - loadingSourcesAfter: false, - loadingSourcesBefore: false, - notAccessible: false, - notExist: false, - openIssuesByLine: {}, - openPopup: null, - selectedIssue: props.selectedIssue, - selectedIssueLocation: null, - sourceRemoved: false, - symbolsByLine: {} - }; - } - - componentDidMount() { - this.mounted = true; - this.fetchComponent(); - } - - componentWillReceiveProps(nextProps /*: Props */) { - if (nextProps.onIssueSelect != null && nextProps.selectedIssue !== this.props.selectedIssue) { - this.setState({ selectedIssue: nextProps.selectedIssue }); - } - } - - componentDidUpdate(prevProps /*: Props */) { - if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { - this.fetchComponent(); - } else if ( - this.props.aroundLine != null && - prevProps.aroundLine !== this.props.aroundLine && - this.isLineOutsideOfRange(this.props.aroundLine) - ) { - this.fetchSources(); - } else { - const { selectedIssue } = this.props; - const { issues } = this.state; - if ( - selectedIssue != null && - issues != null && - issues.find(issue => issue.key === selectedIssue) == null - ) { - this.reloadIssues(); - } - } - } - - componentWillUnmount() { - this.mounted = false; - } - - scrollToLine(line /*: number */) { - const lineElement = this.node.querySelector( - `.source-line-code[data-line-number="${line}"] .source-line-issue-locations` - ); - if (lineElement) { - scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 }); - } - } - - computeCoverageStatus(lines /*: Array */) /*: Array */ { - return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) })); - } - - isLineOutsideOfRange(lineNumber /*: number */) { - const { sources } = this.state; - if (sources != null && sources.length > 0) { - const firstLine = sources[0]; - const lastList = sources[sources.length - 1]; - return lineNumber < firstLine.line || lineNumber > lastList.line; - } else { - return true; - } - } - - fetchComponent() { - this.setState({ loading: true }); - const loadIssues = (component, sources) => { - this.props.loadIssues(this.props.component, 1, LINES, this.props.branch).then(issues => { - if (this.mounted) { - const finalSources = sources.slice(0, LINES); - this.setState( - { - component, - issues, - issuesByLine: issuesByLine(issues), - issueLocationsByLine: locationsByLine(issues), - loading: false, - notAccessible: false, - notExist: false, - hasSourcesAfter: sources.length > LINES, - sources: this.computeCoverageStatus(finalSources), - sourceRemoved: false, - symbolsByLine: symbolsByLine(sources.slice(0, LINES)) - }, - () => { - if (this.props.onLoaded) { - this.props.onLoaded(component, finalSources, issues); - } - } - ); - } - }); - }; - - const onFailLoadComponent = ({ response }) => { - // TODO handle other statuses - if (this.mounted) { - if (response.status === 403) { - this.setState({ loading: false, notAccessible: true }); - } else if (response.status === 404) { - this.setState({ loading: false, notExist: true }); - } - } - }; - - const onFailLoadSources = (response, component) => { - // TODO handle other statuses - if (this.mounted) { - if (response.status === 403) { - this.setState({ component, loading: false, notAccessible: true }); - } else if (response.status === 404) { - this.setState({ component, loading: false, sourceRemoved: true }); - } - } - }; - - const onResolve = component => { - this.props.onReceiveComponent(component); - const sourcesRequest = - component.q === 'FIL' || component.q === 'UTS' ? this.loadSources() : Promise.resolve([]); - sourcesRequest.then( - sources => loadIssues(component, sources), - response => onFailLoadSources(response, component) - ); - }; - - this.props - .loadComponent(this.props.component, this.props.branch) - .then(onResolve, onFailLoadComponent); - } - - fetchSources() { - 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); - } - } - ); - } - }); - } - - reloadIssues() { - if (!this.state.sources) { - return; - } - const firstSourceLine = this.state.sources[0]; - const lastSourceLine = this.state.sources[this.state.sources.length - 1]; - this.props - .loadIssues( - this.props.component, - firstSourceLine && firstSourceLine.line, - lastSourceLine && lastSourceLine.line - ) - .then(issues => { - if (this.mounted) { - this.setState({ - issues, - issuesByLine: issuesByLine(issues), - issueLocationsByLine: locationsByLine(issues) - }); - } - }); - } - - loadSources() { - return new Promise((resolve, reject) => { - const onFailLoadSources = ({ response }) => { - // TODO handle other statuses - if (this.mounted) { - if ([403, 404].includes(response.status)) { - reject(response); - } else { - resolve([]); - } - } - }; - - const from = this.props.aroundLine ? Math.max(1, this.props.aroundLine - LINES / 2 + 1) : 1; - - let to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1; - // make sure we try to download `LINES` lines - if (from === 1 && to < LINES) { - to = LINES; - } - // request one additional line to define `hasSourcesAfter` - to++; - - return this.props - .loadSources(this.props.component, from, to, this.props.branch) - .then(sources => resolve(sources), onFailLoadSources); - }); - } - - loadSourcesBefore = () => { - if (!this.state.sources) { - return; - } - const firstSourceLine = this.state.sources[0]; - this.setState({ loadingSourcesBefore: true }); - const from = Math.max(1, firstSourceLine.line - LINES); - this.props - .loadSources(this.props.component, from, firstSourceLine.line - 1, this.props.branch) - .then(sources => { - this.props.loadIssues(this.props.component, from, firstSourceLine.line - 1).then(issues => { - if (this.mounted) { - this.setState(prevState => { - const nextIssues = uniqBy([...issues, ...prevState.issues], issue => issue.key); - return { - issues: nextIssues, - issuesByLine: issuesByLine(nextIssues), - issueLocationsByLine: locationsByLine(nextIssues), - loadingSourcesBefore: false, - sources: [...this.computeCoverageStatus(sources), ...prevState.sources], - symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } - }; - }); - } - }); - }); - }; - - loadSourcesAfter = () => { - if (!this.state.sources) { - return; - } - const lastSourceLine = this.state.sources[this.state.sources.length - 1]; - this.setState({ loadingSourcesAfter: true }); - const fromLine = lastSourceLine.line + 1; - // request one additional line to define `hasSourcesAfter` - const toLine = lastSourceLine.line + LINES + 1; - this.props - .loadSources(this.props.component, fromLine, toLine, this.props.branch) - .then(sources => { - this.props.loadIssues(this.props.component, fromLine, toLine).then(issues => { - if (this.mounted) { - this.setState(prevState => { - const nextIssues = uniqBy([...prevState.issues, ...issues], issue => issue.key); - return { - issues: nextIssues, - issuesByLine: issuesByLine(nextIssues), - issueLocationsByLine: locationsByLine(nextIssues), - hasSourcesAfter: sources.length > LINES, - loadingSourcesAfter: false, - sources: [ - ...prevState.sources, - ...this.computeCoverageStatus(sources.slice(0, LINES)) - ], - symbolsByLine: { - ...prevState.symbolsByLine, - ...symbolsByLine(sources.slice(0, LINES)) - } - }; - }); - } - }); - }); - }; - - loadDuplications = (line /*: SourceLine */) => { - getDuplications(this.props.component, this.props.branch).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); - } - } - ); - } - }); - }; - - handleCoverageClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - getTests(this.props.component, line.line, this.props.branch).then(tests => { - const popup = new CoveragePopupView({ - line, - tests, - triggerEl: element, - branch: this.props.branch - }); - popup.render(); - }); - }; - - handleDuplicationClick = (index /*: number */, line /*: number */) => { - const duplication = this.state.duplications && this.state.duplications[index]; - let blocks = (duplication && duplication.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 element = this.node.querySelector( - `.source-line-duplications-extra[data-line-number="${line}"]` - ); - if (element) { - const popup = new DuplicationPopupView({ - blocks, - inRemovedComponent, - component: this.state.component, - files: this.state.duplicatedFiles, - triggerEl: element, - branch: this.props.branch - }); - popup.render(); - } - }; - - handlePopupToggle = (issue /*: string */, popupName /*: string */, open /*: ?boolean */) => { - this.setState((state /*: State */) => { - const samePopup = - state.openPopup && state.openPopup.name === popupName && state.openPopup.issue === issue; - if (open !== false && !samePopup) { - return { openPopup: { issue, name: popupName } }; - } else if (open !== true && samePopup) { - return { openPopup: null }; - } - return state; - }); - }; - - displayLinePopup(line /*: number */, element /*: HTMLElement */) { - const popup = new LineActionsPopupView({ - line, - triggerEl: element, - component: this.state.component, - branch: this.props.branch - }); - popup.render(); - } - - handleLineClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - this.setState(prevState => ({ - highlightedLine: prevState.highlightedLine === line.line ? null : line - })); - this.displayLinePopup(line.line, element); - }; - - handleSymbolClick = (symbols /*: Array */) => { - this.setState(state => { - const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0; - const highlightedSymbols = shouldDisable ? [] : symbols; - return { highlightedSymbols }; - }); - }; - - handleSCMClick = (line /*: SourceLine */, element /*: HTMLElement */) => { - const popup = new SCMPopupView({ triggerEl: element, line }); - popup.render(); - }; - - handleIssueSelect = (issue /*: string */) => { - if (this.props.onIssueSelect) { - this.props.onIssueSelect(issue); - } else { - this.setState({ selectedIssue: issue }); - } - }; - - handleIssueUnselect = () => { - if (this.props.onIssueUnselect) { - this.props.onIssueUnselect(); - } else { - this.setState({ selectedIssue: undefined }); - } - }; - - handleOpenIssues = (line /*: SourceLine */) => { - this.setState(state => ({ - openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } - })); - }; - - handleCloseIssues = (line /*: SourceLine */) => { - this.setState(state => ({ - openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false } - })); - }; - - handleIssueChange = (issue /*: Issue */) => { - this.setState(state => { - const issues = state.issues.map( - candidate => (candidate.key === issue.key ? issue : candidate) - ); - return { issues, issuesByLine: issuesByLine(issues) }; - }); - if (this.props.onIssueChange) { - this.props.onIssueChange(issue); - } - }; - - handleFilterLine = (line /*: SourceLine */) => { - const { component } = this.state; - const leakPeriodDate = component && component.leakPeriodDate; - return leakPeriodDate - ? line.scmDate != null && parseDate(line.scmDate) > leakPeriodDate - : false; - }; - - renderCode(sources /*: Array */) { - const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; - return ( - - ); - } - - render() { - const { component, loading, sources, notAccessible, sourceRemoved } = this.state; - - if (loading) { - return null; - } - - if (this.state.notExist) { - return ( -
- {translate('component_viewer.no_component')} -
- ); - } - - if (notAccessible) { - return ( -
- {translate('code_viewer.no_source_code_displayed_due_to_security')} -
- ); - } - - if (component == null) { - return null; - } - - const className = classNames('source-viewer', { - 'source-duplications-expanded': this.state.displayDuplications - }); - - return ( -
(this.node = node)}> - - {sourceRemoved && ( -
- {translate('code_viewer.no_source_code_displayed_due_to_source_removed')} -
- )} - {!sourceRemoved && sources != null && this.renderCode(sources)} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx new file mode 100644 index 00000000000..6e487e2558a --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.tsx @@ -0,0 +1,739 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { intersection, uniqBy } from 'lodash'; +import SourceViewerHeader from './SourceViewerHeader'; +import SourceViewerCode from './SourceViewerCode'; +import DuplicationPopup from './components/DuplicationPopup'; +import defaultLoadIssues from './helpers/loadIssues'; +import getCoverageStatus from './helpers/getCoverageStatus'; +import { + duplicationsByLine, + issuesByLine, + locationsByLine, + symbolsByLine +} from './helpers/indexing'; +import { + getComponentData, + getComponentForSourceViewer, + getDuplications, + getSources +} from '../../api/components'; +import { + Duplication, + FlowLocation, + Issue, + LinearIssueLocation, + SourceLine, + SourceViewerFile, + DuplicatedFile +} from '../../app/types'; +import { parseDate } from '../../helpers/dates'; +import { translate } from '../../helpers/l10n'; +import './styles.css'; + +// TODO react-virtualized + +interface Props { + aroundLine?: number; + branch: string | undefined; + component: string; + displayAllIssues?: boolean; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayLocationMarkers?: boolean; + highlightedLine?: number; + highlightedLocations?: FlowLocation[]; + highlightedLocationMessage?: { index: number; text: string }; + loadComponent?: (component: string, branch: string | undefined) => Promise; + loadIssues?: ( + component: string, + from: number, + to: number, + branch: string | undefined + ) => Promise; + loadSources?: ( + component: string, + from: number, + to: number, + branch: string | undefined + ) => Promise; + onLoaded?: (component: SourceViewerFile, sources: SourceLine[], issues: Issue[]) => void; + onLocationSelect?: (index: number) => void; + onIssueChange?: (issue: Issue) => void; + onIssueSelect?: (issueKey: string) => void; + onIssueUnselect?: () => void; + onReceiveComponent: (component: SourceViewerFile) => void; + scroll?: (element: HTMLElement) => void; + selectedIssue?: string; +} + +interface State { + component?: SourceViewerFile; + displayDuplications: boolean; + duplications?: Duplication[]; + duplicationsByLine: { [line: number]: number[] }; + duplicatedFiles?: { [ref: string]: DuplicatedFile }; + hasSourcesAfter: boolean; + highlightedLine?: number; + highlightedSymbols: string[]; + issues?: Issue[]; + issuesByLine: { [line: number]: Issue[] }; + issueLocationsByLine: { [line: number]: LinearIssueLocation[] }; + linePopup?: { index?: number; line: number; name: string }; + loading: boolean; + loadingSourcesAfter: boolean; + loadingSourcesBefore: boolean; + notAccessible: boolean; + notExist: boolean; + openIssuesByLine: { [line: number]: boolean }; + issuePopup?: { issue: string; name: string }; + selectedIssue?: string; + sources?: SourceLine[]; + sourceRemoved: boolean; + symbolsByLine: { [line: number]: string[] }; +} + +const LINES = 500; + +export default class SourceViewerBase extends React.PureComponent { + node?: HTMLElement | null; + mounted = false; + + static defaultProps = { + displayAllIssues: false, + displayIssueLocationsCount: true, + displayIssueLocationsLink: true, + displayLocationMarkers: true + }; + + constructor(props: Props) { + super(props); + this.state = { + displayDuplications: false, + duplicationsByLine: {}, + hasSourcesAfter: false, + highlightedLine: props.highlightedLine, + highlightedSymbols: [], + issuesByLine: {}, + issueLocationsByLine: {}, + loading: true, + loadingSourcesAfter: false, + loadingSourcesBefore: false, + notAccessible: false, + notExist: false, + openIssuesByLine: {}, + selectedIssue: props.selectedIssue, + sourceRemoved: false, + symbolsByLine: {} + }; + } + + componentDidMount() { + this.mounted = true; + this.fetchComponent(); + } + + componentWillReceiveProps(nextProps: Props) { + if ( + nextProps.onIssueSelect !== undefined && + nextProps.selectedIssue !== this.props.selectedIssue + ) { + this.setState({ selectedIssue: nextProps.selectedIssue }); + } + } + + componentDidUpdate(prevProps: Props) { + if (prevProps.component !== this.props.component || prevProps.branch !== this.props.branch) { + this.fetchComponent(); + } else if ( + this.props.aroundLine !== undefined && + prevProps.aroundLine !== this.props.aroundLine && + this.isLineOutsideOfRange(this.props.aroundLine) + ) { + this.fetchSources(); + } else { + const { selectedIssue } = this.props; + const { issues } = this.state; + if ( + selectedIssue !== undefined && + issues !== undefined && + issues.find(issue => issue.key === selectedIssue) === undefined + ) { + this.reloadIssues(); + } + } + } + + componentWillUnmount() { + this.mounted = false; + } + + // react typings do not take `defaultProps` into account, + // so use these getters to get type-safe methods + + get safeLoadComponent() { + return this.props.loadComponent || defaultLoadComponent; + } + + get safeLoadIssues() { + return this.props.loadIssues || defaultLoadIssues; + } + + get safeLoadSources() { + return this.props.loadSources || defaultLoadSources; + } + + computeCoverageStatus(lines: SourceLine[]) { + return lines.map(line => ({ ...line, coverageStatus: getCoverageStatus(line) })); + } + + isLineOutsideOfRange(lineNumber: number) { + const { sources } = this.state; + if (sources && sources.length > 0) { + const firstLine = sources[0]; + const lastList = sources[sources.length - 1]; + return lineNumber < firstLine.line || lineNumber > lastList.line; + } else { + return true; + } + } + + fetchComponent() { + this.setState({ loading: true }); + const loadIssues = (component: SourceViewerFile, sources: SourceLine[]) => { + this.safeLoadIssues(this.props.component, 1, LINES, this.props.branch).then( + issues => { + if (this.mounted) { + const finalSources = sources.slice(0, LINES); + this.setState( + { + component, + issues, + issuesByLine: issuesByLine(issues), + issueLocationsByLine: locationsByLine(issues), + loading: false, + notAccessible: false, + notExist: false, + hasSourcesAfter: sources.length > LINES, + sources: this.computeCoverageStatus(finalSources), + sourceRemoved: false, + symbolsByLine: symbolsByLine(sources.slice(0, LINES)) + }, + () => { + if (this.props.onLoaded) { + this.props.onLoaded(component, finalSources, issues); + } + } + ); + } + }, + () => { + // TODO + } + ); + }; + + const onFailLoadComponent = ({ response }: { response: Response }) => { + // TODO handle other statuses + if (this.mounted) { + if (response.status === 403) { + this.setState({ loading: false, notAccessible: true }); + } else if (response.status === 404) { + this.setState({ loading: false, notExist: true }); + } + } + }; + + const onFailLoadSources = (response: Response, component: SourceViewerFile) => { + // TODO handle other statuses + if (this.mounted) { + if (response.status === 403) { + this.setState({ component, loading: false, notAccessible: true }); + } else if (response.status === 404) { + this.setState({ component, loading: false, sourceRemoved: true }); + } + } + }; + + const onResolve = (component: SourceViewerFile) => { + this.props.onReceiveComponent(component); + const sourcesRequest = + component.q === 'FIL' || component.q === 'UTS' ? this.loadSources() : Promise.resolve([]); + sourcesRequest.then( + sources => loadIssues(component, sources), + response => onFailLoadSources(response, component) + ); + }; + + this.safeLoadComponent(this.props.component, this.props.branch).then( + onResolve, + onFailLoadComponent + ); + } + + fetchSources() { + 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 && this.state.component && this.state.issues) { + this.props.onLoaded(this.state.component, finalSources, this.state.issues); + } + } + ); + } + }, + () => { + // TODO + } + ); + } + + reloadIssues() { + if (!this.state.sources) { + return; + } + const firstSourceLine = this.state.sources[0]; + const lastSourceLine = this.state.sources[this.state.sources.length - 1]; + this.safeLoadIssues( + this.props.component, + firstSourceLine && firstSourceLine.line, + lastSourceLine && lastSourceLine.line, + this.props.branch + ).then( + issues => { + if (this.mounted) { + this.setState({ + issues, + issuesByLine: issuesByLine(issues), + issueLocationsByLine: locationsByLine(issues) + }); + } + }, + () => { + // TODO + } + ); + } + + loadSources = (): Promise => { + return new Promise((resolve, reject) => { + const onFailLoadSources = ({ response }: { response: Response }) => { + // TODO handle other statuses + if (this.mounted) { + if ([403, 404].includes(response.status)) { + reject(response); + } else { + resolve([]); + } + } + }; + + const from = this.props.aroundLine ? Math.max(1, this.props.aroundLine - LINES / 2 + 1) : 1; + + let to = this.props.aroundLine ? this.props.aroundLine + LINES / 2 + 1 : LINES + 1; + // make sure we try to download `LINES` lines + if (from === 1 && to < LINES) { + to = LINES; + } + // request one additional line to define `hasSourcesAfter` + to++; + + return this.safeLoadSources(this.props.component, from, to, this.props.branch).then( + sources => resolve(sources), + onFailLoadSources + ); + }); + }; + + loadSourcesBefore = () => { + if (!this.state.sources) { + return; + } + const firstSourceLine = this.state.sources[0]; + this.setState({ loadingSourcesBefore: true }); + const from = Math.max(1, firstSourceLine.line - LINES); + this.safeLoadSources( + this.props.component, + from, + firstSourceLine.line - 1, + this.props.branch + ).then( + sources => { + this.safeLoadIssues( + this.props.component, + from, + firstSourceLine.line - 1, + this.props.branch + ).then( + issues => { + if (this.mounted) { + this.setState(prevState => { + const nextIssues = uniqBy( + [...issues, ...(prevState.issues || [])], + issue => issue.key + ); + return { + issues: nextIssues, + issuesByLine: issuesByLine(nextIssues), + issueLocationsByLine: locationsByLine(nextIssues), + loadingSourcesBefore: false, + sources: [...this.computeCoverageStatus(sources), ...(prevState.sources || [])], + symbolsByLine: { ...prevState.symbolsByLine, ...symbolsByLine(sources) } + }; + }); + } + }, + () => { + // TODO + } + ); + }, + () => { + // TODO + } + ); + }; + + loadSourcesAfter = () => { + if (!this.state.sources) { + return; + } + const lastSourceLine = this.state.sources[this.state.sources.length - 1]; + this.setState({ loadingSourcesAfter: true }); + const fromLine = lastSourceLine.line + 1; + // request one additional line to define `hasSourcesAfter` + const toLine = lastSourceLine.line + LINES + 1; + this.safeLoadSources(this.props.component, fromLine, toLine, this.props.branch).then( + sources => { + this.safeLoadIssues(this.props.component, fromLine, toLine, this.props.branch).then( + issues => { + if (this.mounted) { + this.setState(prevState => { + const nextIssues = uniqBy( + [...(prevState.issues || []), ...issues], + issue => issue.key + ); + return { + issues: nextIssues, + issuesByLine: issuesByLine(nextIssues), + issueLocationsByLine: locationsByLine(nextIssues), + hasSourcesAfter: sources.length > LINES, + loadingSourcesAfter: false, + sources: [ + ...(prevState.sources || []), + ...this.computeCoverageStatus(sources.slice(0, LINES)) + ], + symbolsByLine: { + ...prevState.symbolsByLine, + ...symbolsByLine(sources.slice(0, LINES)) + } + }; + }); + } + }, + () => { + // TODO + } + ); + }, + () => { + // TODO + } + ); + }; + + loadDuplications = (line: SourceLine) => { + getDuplications(this.props.component, this.props.branch).then( + r => { + if (this.mounted) { + this.setState(() => { + const changes: Partial = { + displayDuplications: true, + duplications: r.duplications, + duplicationsByLine: duplicationsByLine(r.duplications), + duplicatedFiles: r.files + }; + if (r.duplications.length === 1) { + changes.linePopup = { index: 0, line: line.line, name: 'duplications' }; + } + return changes; + }); + } + }, + () => { + // TODO + } + ); + }; + + handleLinePopupToggle = ({ + index, + line, + name, + open + }: { + index?: number; + line: number; + name: string; + open?: boolean; + }) => { + this.setState((state: State) => { + const samePopup = + state.linePopup !== undefined && + state.linePopup.name === name && + state.linePopup.line === line && + state.linePopup.index === index; + if (open !== false && !samePopup) { + return { linePopup: { index, line, name } }; + } else if (open !== true && samePopup) { + return { linePopup: undefined }; + } + return null; + }); + }; + + closeLinePopup = () => { + this.setState({ linePopup: undefined }); + }; + + handleIssuePopupToggle = (issue: string, popupName: string, open?: boolean) => { + this.setState((state: State) => { + const samePopup = + state.issuePopup && state.issuePopup.name === popupName && state.issuePopup.issue === issue; + if (open !== false && !samePopup) { + return { issuePopup: { issue, name: popupName } }; + } else if (open !== true && samePopup) { + return { issuePopup: undefined }; + } + return null; + }); + }; + + handleSymbolClick = (symbols: string[]) => { + this.setState(state => { + const shouldDisable = intersection(state.highlightedSymbols, symbols).length > 0; + const highlightedSymbols = shouldDisable ? [] : symbols; + return { highlightedSymbols }; + }); + }; + + handleIssueSelect = (issue: string) => { + if (this.props.onIssueSelect) { + this.props.onIssueSelect(issue); + } else { + this.setState({ selectedIssue: issue }); + } + }; + + handleIssueUnselect = () => { + if (this.props.onIssueUnselect) { + this.props.onIssueUnselect(); + } else { + this.setState({ selectedIssue: undefined }); + } + }; + + handleOpenIssues = (line: SourceLine) => { + this.setState(state => ({ + openIssuesByLine: { ...state.openIssuesByLine, [line.line]: true } + })); + }; + + handleCloseIssues = (line: SourceLine) => { + this.setState(state => ({ + openIssuesByLine: { ...state.openIssuesByLine, [line.line]: false } + })); + }; + + handleIssueChange = (issue: Issue) => { + this.setState(({ issues = [] }) => { + const newIssues = issues.map(candidate => (candidate.key === issue.key ? issue : candidate)); + return { issues: newIssues, issuesByLine: issuesByLine(newIssues) }; + }); + if (this.props.onIssueChange) { + this.props.onIssueChange(issue); + } + }; + + handleFilterLine = (line: SourceLine) => { + const { component } = this.state; + const leakPeriodDate = component && component.leakPeriodDate; + return leakPeriodDate + ? line.scmDate !== undefined && parseDate(line.scmDate) > parseDate(leakPeriodDate) + : false; + }; + + renderDuplicationPopup = (index: number, line: number) => { + const { component, duplicatedFiles, duplications } = this.state; + + if (!component || !duplicatedFiles) return <>; + + const duplication = duplications && duplications[index]; + let blocks = (duplication && duplication.blocks) || []; + /* eslint-disable no-underscore-dangle */ + const inRemovedComponent = blocks.some(b => b._ref === undefined); + 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 !== undefined && shouldDisplay; + if (b._ref === '1' && !outOfBounds) { + foundOne = true; + } + return isOk; + }); + /* eslint-enable no-underscore-dangle */ + + return ( + + ); + }; + + renderCode(sources: SourceLine[]) { + const hasSourcesBefore = sources.length > 0 && sources[0].line > 1; + return ( + + ); + } + + render() { + const { component, loading, sources, notAccessible, sourceRemoved } = this.state; + + if (loading) { + return null; + } + + if (this.state.notExist) { + return ( +
+ {translate('component_viewer.no_component')} +
+ ); + } + + if (notAccessible) { + return ( +
+ {translate('code_viewer.no_source_code_displayed_due_to_security')} +
+ ); + } + + if (!component) { + return null; + } + + const className = classNames('source-viewer', { + 'source-duplications-expanded': this.state.displayDuplications + }); + + return ( +
(this.node = node)}> + {this.state.component && ( + + )} + {sourceRemoved && ( +
+ {translate('code_viewer.no_source_code_displayed_due_to_source_removed')} +
+ )} + {!sourceRemoved && sources !== undefined && this.renderCode(sources)} +
+ ); + } +} + +function defaultLoadComponent(key: string, branch: string | undefined) { + return Promise.all([ + getComponentForSourceViewer(key, branch), + getComponentData(key, branch) + ]).then(([component, data]) => ({ + ...component, + leakPeriodDate: data.leakPeriodDate && parseDate(data.leakPeriodDate) + })); +} + +function defaultLoadSources( + key: string, + from: number | undefined, + to: number | undefined, + branch: string | undefined +) { + return getSources(key, from, to, branch); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js deleted file mode 100644 index b3285d2ff0c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.js +++ /dev/null @@ -1,256 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { intersection } from 'lodash'; -import Line from './components/Line'; -import { getLinearLocations } from './helpers/issueLocations'; -import { translate } from '../../helpers/l10n'; -/*:: import type { Duplication, SourceLine } from './types'; */ -/*:: import type { Issue, FlowLocation } from '../issue/types'; */ -/*:: import type { LinearIssueLocation } from './helpers/indexing'; */ - -const EMPTY_ARRAY = []; - -const ZERO_LINE = { - code: '', - duplicated: false, - line: 0 -}; - -export default class SourceViewerCode extends React.PureComponent { - /*:: props: {| - branch?: string, - displayAllIssues: boolean, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - displayLocationMarkers?: boolean; - duplications?: Array, - duplicationsByLine: { [number]: Array }, - duplicatedFiles?: Array<{ key: string }>, - filterLine?: SourceLine => boolean, - hasSourcesAfter: boolean, - hasSourcesBefore: boolean, - highlightedLine: number | null, - highlightedLocations?: Array, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols: Array, - issues: Array, - issuesByLine: { [number]: Array }, - issueLocationsByLine: { [number]: Array }, - loadDuplications: SourceLine => void, - loadSourcesAfter: () => void, - loadSourcesBefore: () => void, - loadingSourcesAfter: boolean, - loadingSourcesBefore: boolean, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueChange: Issue => void, - onIssueSelect: string => void, - onIssueUnselect: () => void, - onIssuesOpen: SourceLine => void, - onIssuesClose: SourceLine => void, - onLineClick: (SourceLine, HTMLElement) => void, - onLocationSelect?: number => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSymbolClick: (Array) => void, - openIssuesByLine: { [number]: boolean }, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - scroll?: HTMLElement => void, - selectedIssue: string | null, - sources: Array, - symbolsByLine: { [number]: Array } - |}; -*/ - - getDuplicationsForLine(line /*: SourceLine */) { - return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; - } - - getIssuesForLine(line /*: SourceLine */) /*: Array */ { - return this.props.issuesByLine[line.line] || EMPTY_ARRAY; - } - - getIssueLocationsForLine(line /*: SourceLine */) { - return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY; - } - - getSecondaryIssueLocationsForLine( - line /*: SourceLine */ - ) /*: Array<{ from: number, to: number, line: number, index: number, startLine: number }> */ { - const { highlightedLocations } = this.props; - if (!highlightedLocations) { - return EMPTY_ARRAY; - } - return highlightedLocations.reduce((locations, location, index) => { - const linearLocations = getLinearLocations(location.textRange) - .filter(l => l.line === line.line) - .map(l => ({ ...l, startLine: location.textRange.startLine, index })); - return [...locations, ...linearLocations]; - }, []); - } - - renderLine = ( - line /*: SourceLine */, - index /*: number */, - displayCoverage /*: boolean */, - displayDuplications /*: boolean */, - displayIssues /*: boolean */ - ) => { - const { filterLine, highlightedLocationMessage, selectedIssue, sources } = this.props; - const filtered = filterLine ? filterLine(line) : null; - - const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line); - - const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0; - - const issuesForLine = this.getIssuesForLine(line); - - // for the following properties pass null if the line for sure is not impacted - const symbolsForLine = this.props.symbolsByLine[line.line] || []; - const { highlightedSymbols } = this.props; - let optimizedHighlightedSymbols = intersection(symbolsForLine, highlightedSymbols); - if (!optimizedHighlightedSymbols.length) { - optimizedHighlightedSymbols = undefined; - } - - const optimizedSelectedIssue = - selectedIssue != null && issuesForLine.find(issue => issue.key === selectedIssue) - ? selectedIssue - : null; - - const optimizedSecondaryIssueLocations = - secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY; - - const optimizedLocationMessage = - highlightedLocationMessage != null && - optimizedSecondaryIssueLocations.some( - location => location.index === highlightedLocationMessage.index - ) - ? highlightedLocationMessage - : undefined; - - return ( - 0 ? sources[index - 1] : undefined} - scroll={this.props.scroll} - secondaryIssueLocations={optimizedSecondaryIssueLocations} - selectedIssue={optimizedSelectedIssue} - /> - ); - }; - - render() { - const { sources } = this.props; - - const hasCoverage = sources.some(s => s.coverageStatus != null); - const hasDuplications = sources.some(s => s.duplicated); - const hasIssues = this.props.issues.length > 0; - - const hasFileIssues = hasIssues && this.props.issues.some(issue => !issue.textRange); - - return ( -
- {this.props.hasSourcesBefore && ( -
- {this.props.loadingSourcesBefore ? ( -
- - - {translate('source_viewer.loading_more_code')} - -
- ) : ( - - )} -
- )} - - - - {hasFileIssues && - this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, hasIssues)} - {sources.map((line, index) => - this.renderLine(line, index, hasCoverage, hasDuplications, hasIssues) - )} - -
- - {this.props.hasSourcesAfter && ( -
- {this.props.loadingSourcesAfter ? ( -
- - - {translate('source_viewer.loading_more_code')} - -
- ) : ( - - )} -
- )} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx new file mode 100644 index 00000000000..15db6dd0961 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerCode.tsx @@ -0,0 +1,265 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { intersection } from 'lodash'; +import Line from './components/Line'; +import { getLinearLocations } from './helpers/issueLocations'; +import { Duplication, FlowLocation, Issue, LinearIssueLocation, SourceLine } from '../../app/types'; +import { translate } from '../../helpers/l10n'; +import { Button } from '../ui/buttons'; + +const EMPTY_ARRAY: any[] = []; + +const ZERO_LINE = { + code: '', + duplicated: false, + line: 0 +}; + +interface Props { + branch: string | undefined; + componentKey: string; + displayAllIssues?: boolean; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayLocationMarkers?: boolean; + duplications: Duplication[] | undefined; + duplicationsByLine: { [line: number]: number[] }; + filterLine?: (line: SourceLine) => boolean; + hasSourcesAfter: boolean; + hasSourcesBefore: boolean; + highlightedLine: number | undefined; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedLocations: FlowLocation[] | undefined; + highlightedSymbols: string[]; + issueLocationsByLine: { [line: number]: LinearIssueLocation[] }; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[] | undefined; + issuesByLine: { [line: number]: Issue[] }; + linePopup: { index?: number; line: number; name: string } | undefined; + loadDuplications: (line: SourceLine) => void; + loadingSourcesAfter: boolean; + loadingSourcesBefore: boolean; + loadSourcesAfter: () => void; + loadSourcesBefore: () => void; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + onIssuesClose: (line: SourceLine) => void; + onIssueSelect: (issueKey: string) => void; + onIssuesOpen: (line: SourceLine) => void; + onIssueUnselect: () => void; + onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onLocationSelect: ((index: number) => void) | undefined; + onSymbolClick: (symbols: string[]) => void; + openIssuesByLine: { [line: number]: boolean }; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; + scroll?: (element: HTMLElement) => void; + selectedIssue: string | undefined; + sources: SourceLine[]; + symbolsByLine: { [line: number]: string[] }; +} + +export default class SourceViewerCode extends React.PureComponent { + getDuplicationsForLine = (line: SourceLine): number[] => { + return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY; + }; + + getIssuesForLine = (line: SourceLine): Issue[] => { + return this.props.issuesByLine[line.line] || EMPTY_ARRAY; + }; + + getIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => { + return this.props.issueLocationsByLine[line.line] || EMPTY_ARRAY; + }; + + getSecondaryIssueLocationsForLine = (line: SourceLine): LinearIssueLocation[] => { + const { highlightedLocations } = this.props; + if (!highlightedLocations) { + return EMPTY_ARRAY; + } + return highlightedLocations.reduce((locations, location, index) => { + const linearLocations: LinearIssueLocation[] = getLinearLocations(location.textRange) + .filter(l => l.line === line.line) + .map(l => ({ ...l, startLine: location.textRange.startLine, index })); + return [...locations, ...linearLocations]; + }, []); + }; + + renderLine = ({ + line, + index, + displayCoverage, + displayDuplications, + displayIssues + }: { + line: SourceLine; + index: number; + displayCoverage: boolean; + displayDuplications: boolean; + displayIssues: boolean; + }) => { + const { filterLine, highlightedLocationMessage, selectedIssue, sources } = this.props; + const filtered = filterLine && filterLine(line); + + const secondaryIssueLocations = this.getSecondaryIssueLocationsForLine(line); + + const duplicationsCount = this.props.duplications ? this.props.duplications.length : 0; + + const issuesForLine = this.getIssuesForLine(line); + + // for the following properties pass null if the line for sure is not impacted + const symbolsForLine = this.props.symbolsByLine[line.line] || []; + const { highlightedSymbols } = this.props; + let optimizedHighlightedSymbols: string[] | undefined = intersection( + symbolsForLine, + highlightedSymbols + ); + if (!optimizedHighlightedSymbols.length) { + optimizedHighlightedSymbols = undefined; + } + + const optimizedSelectedIssue = + selectedIssue !== undefined && issuesForLine.find(issue => issue.key === selectedIssue) + ? selectedIssue + : undefined; + + const optimizedSecondaryIssueLocations = + secondaryIssueLocations.length > 0 ? secondaryIssueLocations : EMPTY_ARRAY; + + const optimizedLocationMessage = + highlightedLocationMessage != null && + optimizedSecondaryIssueLocations.some( + location => location.index === highlightedLocationMessage.index + ) + ? highlightedLocationMessage + : undefined; + + return ( + 0 ? sources[index - 1] : undefined} + renderDuplicationPopup={this.props.renderDuplicationPopup} + scroll={this.props.scroll} + secondaryIssueLocations={optimizedSecondaryIssueLocations} + selectedIssue={optimizedSelectedIssue} + /> + ); + }; + + render() { + const { issues = [], sources } = this.props; + + const displayCoverage = sources.some(s => s.coverageStatus != null); + const displayDuplications = sources.some(s => !!s.duplicated); + const displayIssues = issues.length > 0; + + const hasFileIssues = displayIssues && issues.some(issue => !issue.textRange); + + return ( +
+ {this.props.hasSourcesBefore && ( +
+ {this.props.loadingSourcesBefore ? ( +
+ + + {translate('source_viewer.loading_more_code')} + +
+ ) : ( + + )} +
+ )} + + + + {hasFileIssues && + this.renderLine({ + line: ZERO_LINE, + index: -1, + displayCoverage, + displayDuplications, + displayIssues + })} + {sources.map((line, index) => + this.renderLine({ line, index, displayCoverage, displayDuplications, displayIssues }) + )} + +
+ + {this.props.hasSourcesAfter && ( +
+ {this.props.loadingSourcesAfter ? ( +
+ + + {translate('source_viewer.loading_more_code')} + +
+ ) : ( + + )} +
+ )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx new file mode 100644 index 00000000000..2edff725967 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/CoveragePopup.tsx @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { groupBy } from 'lodash'; +import { getTests } from '../../../api/components'; +import { SourceLine, TestCase } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import TestStatusIcon from '../../shared/TestStatusIcon'; +import { translate } from '../../../helpers/l10n'; +import { collapsePath } from '../../../helpers/path'; + +interface Props { + branch: string | undefined; + componentKey: string; + line: SourceLine; + onClose: () => void; + popupPosition?: any; +} + +interface State { + loading: boolean; + testCases: TestCase[]; +} + +export default class CoveragePopup extends React.PureComponent { + mounted = false; + state: State = { loading: true, testCases: [] }; + + componentDidMount() { + this.mounted = true; + this.fetchTests(); + } + + componentDidUpdate(prevProps: Props) { + // TODO use branchLike + if ( + prevProps.branch !== this.props.branch || + prevProps.componentKey !== this.props.componentKey || + prevProps.line.line !== this.props.line.line + ) { + this.fetchTests(); + } + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchTests = () => { + this.setState({ loading: true }); + getTests(this.props.componentKey, this.props.line.line, this.props.branch).then( + testCases => { + if (this.mounted) { + this.setState({ loading: false, testCases }); + } + }, + () => { + if (this.mounted) { + this.setState({ loading: false }); + } + } + ); + }; + + handleTestClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + const { key } = event.currentTarget.dataset; + const Workspace = require('../../workspace/main').default; + Workspace.openComponent({ key, branch: this.props.branch }); + this.props.onClose(); + }; + + render() { + const { line } = this.props; + const testCasesByFile = groupBy(this.state.testCases || [], 'fileKey'); + const testFiles = Object.keys(testCasesByFile).map(fileKey => { + const testSet = testCasesByFile[fileKey]; + const test = testSet[0]; + return { + file: { key: test.fileKey, longName: test.fileName }, + tests: testSet + }; + }); + + return ( + +
+ {translate('source_viewer.covered')} + {!!line.conditions && ( +
+ {'('} + {line.coveredConditions || '0'} + {' of '} + {line.conditions} {translate('source_viewer.conditions')} + {')'} +
+ )} +
+ {this.state.loading ? ( + + ) : ( + <> + {testFiles.length === 0 && + translate('source_viewer.tooltip.no_information_about_tests')} + {testFiles.map(testFile => ( +
+ + {collapsePath(testFile.file.longName)} + +
    + {testFile.tests.map(testCase => ( +
  • + + {testCase.name} + {testCase.status !== 'SKIPPED' && ( + {testCase.durationInMs}ms + )} +
  • + ))} +
+
+ ))} + + )} +
+ ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx new file mode 100644 index 00000000000..9c58a3e01ad --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/DuplicationPopup.tsx @@ -0,0 +1,158 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Link } from 'react-router'; +import { groupBy, sortBy } from 'lodash'; +import { SourceViewerFile, DuplicationBlock, DuplicatedFile } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import QualifierIcon from '../../shared/QualifierIcon'; +import { translate } from '../../../helpers/l10n'; +import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; +import { getProjectUrl } from '../../../helpers/urls'; + +interface Props { + blocks: DuplicationBlock[]; + // TODO use branchLike + branch: string | undefined; + duplicatedFiles?: { [ref: string]: DuplicatedFile }; + inRemovedComponent: boolean; + onClose: () => void; + popupPosition?: any; + sourceViewerFile: SourceViewerFile; +} + +export default class DuplicationPopup extends React.PureComponent { + isDifferentComponent = ( + a: { project: string; subProject?: string }, + b: { project: string; subProject?: string } + ) => { + return Boolean(a && b && (a.project !== b.project || a.subProject !== b.subProject)); + }; + + handleFileClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.currentTarget.blur(); + const Workspace = require('../../workspace/main').default; + const { key, line } = event.currentTarget.dataset; + Workspace.openComponent({ key, line, branch: this.props.branch }); + this.props.onClose(); + }; + + render() { + const { duplicatedFiles = {}, sourceViewerFile } = this.props; + + const groupedBlocks = groupBy(this.props.blocks, '_ref'); + let duplications = Object.keys(groupedBlocks).map(fileRef => { + return { + blocks: groupedBlocks[fileRef], + file: duplicatedFiles[fileRef] + }; + }); + + // first duplications in the same file + // then duplications in the same sub-project + // then duplications in the same project + // then duplications in other projects + duplications = sortBy( + duplications, + d => d.file.projectName !== sourceViewerFile.projectName, + d => d.file.subProjectName !== sourceViewerFile.subProjectName, + d => d.file.key !== sourceViewerFile.key + ); + + return ( + +
+ {this.props.inRemovedComponent && ( +
+ {translate('duplications.dups_found_on_deleted_resource')} +
+ )} + {duplications.length > 0 && ( + <> +
+ {translate('component_viewer.transition.duplication')} +
+ {duplications.map(duplication => ( +
+
+ {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && ( + <> +
+ + + {duplication.file.projectName} + +
+ {duplication.file.subProject && + duplication.file.subProjectName && ( +
+ + + {duplication.file.subProjectName} + +
+ )} + + )} + + {duplication.file.key !== this.props.sourceViewerFile.key && ( + + )} + +
+ {'Lines: '} + {duplication.blocks.map((block, index) => ( + + + {block.from} + {' – '} + {block.from + block.size - 1} + + {index < duplication.blocks.length - 1 && ', '} + + ))} +
+
+
+ ))} + + )} +
+
+ ); + } +} 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 deleted file mode 100644 index ff74df479b1..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js +++ /dev/null @@ -1,172 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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'; -import LineNumber from './LineNumber'; -import LineSCM from './LineSCM'; -import LineCoverage from './LineCoverage'; -import LineDuplications from './LineDuplications'; -import LineDuplicationBlock from './LineDuplicationBlock'; -import LineIssuesIndicator from './LineIssuesIndicator'; -import LineCode from './LineCode'; -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */ -/*:: import type { Issue } from '../../issue/types'; */ - -/*:: -type Props = {| - branch?: string, - displayAllIssues: boolean, - displayCoverage: boolean, - displayDuplications: boolean, - displayIssues: boolean, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - displayLocationMarkers?: boolean; - duplications: Array, - duplicationsCount: number, - filtered: boolean | null, - highlighted: boolean, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols?: Array, - issueLocations: Array, - issues: Array, - last: boolean, - line: SourceLine, - loadDuplications: SourceLine => void, - onClick: (SourceLine, HTMLElement) => void, - onCoverageClick: (SourceLine, HTMLElement) => void, - onDuplicationClick: (number, number) => void, - onIssueChange: Issue => void, - onIssueSelect: string => void, - onIssueUnselect: () => void, - onIssuesOpen: SourceLine => void, - onIssuesClose: SourceLine => void, - onLocationSelect?: number => void, - onSCMClick: (SourceLine, HTMLElement) => void, - onSymbolClick: (Array) => void, - openIssues: boolean, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - previousLine?: SourceLine, - scroll?: HTMLElement => void, - secondaryIssueLocations: Array<{ - from: number, - to: number, - line: number, - index: number, - startLine: number - }>, - selectedIssue: string | 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].key); - } - } - }; - - render() { - const { line, duplications, displayCoverage, duplicationsCount, filtered } = this.props; - const className = classNames('source-line', { - 'source-line-highlighted': this.props.highlighted, - 'source-line-filtered': filtered === true, - 'source-line-filtered-dark': - displayCoverage && - (line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'), - 'source-line-last': this.props.last - }); - - return ( - - - - - - {this.props.displayCoverage && ( - - )} - - {this.props.displayDuplications && ( - - )} - - {times(duplicationsCount).map(index => ( - - ))} - - {this.props.displayIssues && - !this.props.displayAllIssues && ( - - )} - - - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx new file mode 100644 index 00000000000..a29255557dd --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.tsx @@ -0,0 +1,197 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { times } from 'lodash'; +import LineNumber from './LineNumber'; +import LineSCM from './LineSCM'; +import LineCoverage from './LineCoverage'; +import LineDuplications from './LineDuplications'; +import LineDuplicationBlock from './LineDuplicationBlock'; +import LineIssuesIndicator from './LineIssuesIndicator'; +import LineCode from './LineCode'; +import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; + +interface Props { + branch: string | undefined; + componentKey: string; + displayAllIssues?: boolean; + displayCoverage: boolean; + displayDuplications: boolean; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayIssues: boolean; + displayLocationMarkers?: boolean; + duplications: number[]; + duplicationsCount: number; + filtered: boolean | undefined; + highlighted: boolean; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedSymbols: string[] | undefined; + issueLocations: LinearIssueLocation[]; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[]; + last: boolean; + line: SourceLine; + linePopup: { index?: number; line: number; name: string } | undefined; + loadDuplications: (line: SourceLine) => void; + onLinePopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issueKey: string, popupName: string, open?: boolean) => void; + onIssuesClose: (line: SourceLine) => void; + onIssueSelect: (issueKey: string) => void; + onIssuesOpen: (line: SourceLine) => void; + onIssueUnselect: () => void; + onLocationSelect: ((x: number) => void) | undefined; + onSymbolClick: (symbols: string[]) => void; + openIssues: boolean; + previousLine: SourceLine | undefined; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; + scroll?: (element: HTMLElement) => void; + secondaryIssueLocations: Array<{ + from: number; + to: number; + line: number; + index: number; + startLine: number; + }>; + selectedIssue: string | undefined; +} + +export default class Line extends React.PureComponent { + isPopupOpen = (name: string, index?: number) => { + const { line, linePopup } = this.props; + return ( + linePopup !== undefined && + linePopup.index === index && + linePopup.line === line.line && + linePopup.name === name + ); + }; + + handleIssuesIndicatorClick = () => { + if (this.props.openIssues) { + this.props.onIssuesClose(this.props.line); + this.props.onIssueUnselect(); + } else { + this.props.onIssuesOpen(this.props.line); + + const { issues } = this.props; + if (issues.length > 0) { + this.props.onIssueSelect(issues[0].key); + } + } + }; + + render() { + const { + displayCoverage, + duplications, + duplicationsCount, + filtered, + issuePopup, + line + } = this.props; + const className = classNames('source-line', { + 'source-line-highlighted': this.props.highlighted, + 'source-line-filtered': filtered === true, + 'source-line-filtered-dark': + displayCoverage && + (line.coverageStatus === 'uncovered' || line.coverageStatus === 'partially-covered'), + 'source-line-last': this.props.last + }); + + return ( + + + + + + {this.props.displayCoverage && ( + + )} + + {this.props.displayDuplications && ( + + )} + + {times(duplicationsCount).map(index => ( + + ))} + + {this.props.displayIssues && + !this.props.displayAllIssues && ( + + )} + + + + ); + } +} 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 deleted file mode 100644 index fc197bcd217..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.js +++ /dev/null @@ -1,250 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 LocationIndex from '../../common/LocationIndex'; -import LocationMessage from '../../common/LocationMessage'; -import { splitByTokens, highlightSymbol, highlightIssueLocations } from '../helpers/highlight'; -/*:: import type { Tokens } from '../helpers/highlight'; */ -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { LinearIssueLocation } from '../helpers/indexing'; */ -/*:: import type { Issue } from '../../issue/types'; */ - -/*:: -type Props = {| - branch?: string, - displayIssueLocationsCount?: boolean, - displayIssueLocationsLink?: boolean, - displayLocationMarkers?: boolean, - highlightedLocationMessage?: { index: number, text: string }, - highlightedSymbols?: Array, - issues: Array, - issueLocations: Array, - line: SourceLine, - onIssueChange: Issue => void, - onIssueSelect: (issueKey: string) => void, - onLocationSelect?: number => void, - onSymbolClick: (Array) => void, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - scroll?: HTMLElement => void, - secondaryIssueLocations: Array<{ - from: number, - to: number, - line: number, - index: number, - startLine: number - }>, - selectedIssue: string | null, - showIssues: boolean -|}; -*/ - -/*:: -type State = { - tokens: Tokens -}; -*/ - -export default class LineCode extends React.PureComponent { - /*:: activeMarkerNode: ?HTMLElement; */ - /*:: codeNode: HTMLElement; */ - /*:: props: Props; */ - /*:: state: State; */ - /*:: symbols: NodeList; */ - - constructor(props /*: Props */) { - super(props); - this.state = { - tokens: splitByTokens(props.line.code || '') - }; - } - - componentDidMount() { - this.attachEvents(); - if (this.props.highlightedLocationMessage && this.activeMarkerNode && this.props.scroll) { - this.props.scroll(this.activeMarkerNode); - } - } - - componentWillReceiveProps(nextProps /*: Props */) { - if (nextProps.line.code !== this.props.line.code) { - this.setState({ - tokens: splitByTokens(nextProps.line.code || '') - }); - } - } - - componentWillUpdate() { - this.detachEvents(); - } - - componentDidUpdate(prevProps /*: Props */) { - this.attachEvents(); - if ( - this.props.highlightedLocationMessage && - prevProps.highlightedLocationMessage !== this.props.highlightedLocationMessage && - this.activeMarkerNode && - this.props.scroll - ) { - this.props.scroll(this.activeMarkerNode); - } - } - - componentWillUnmount() { - this.detachEvents(); - } - - attachEvents() { - if (this.codeNode) { - 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 keys = e.currentTarget.className.match(/sym-\d+/g); - if (keys.length > 0) { - this.props.onSymbolClick(keys); - } - }; - - renderMarker(index /*: number */, message /*: ?string */, leading /*: boolean */ = false) { - const { onLocationSelect } = this.props; - const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined; - const ref = message != null ? node => (this.activeMarkerNode = node) : undefined; - return ( - - - {index + 1} - - {message != null && {message}} - - ); - } - - render() { - const { - highlightedLocationMessage, - highlightedSymbols, - issues, - issueLocations, - line, - onIssueSelect, - secondaryIssueLocations, - selectedIssue, - showIssues - } = this.props; - - let tokens = [...this.state.tokens]; - - if (highlightedSymbols) { - highlightedSymbols.forEach(symbol => { - tokens = highlightSymbol(tokens, symbol); - }); - } - - if (issueLocations.length > 0) { - tokens = highlightIssueLocations(tokens, issueLocations); - } - - if (secondaryIssueLocations) { - tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location'); - - if (highlightedLocationMessage) { - const location = secondaryIssueLocations.find( - location => location.index === highlightedLocationMessage.index - ); - if (location) { - tokens = highlightIssueLocations(tokens, [location], 'selected'); - } - } - } - - const className = classNames('source-line-code', 'code', { - 'has-issues': issues.length > 0 - }); - - const renderedTokens = []; - - // track if the first marker is displayed before the source code - // set `false` for the first token in a row - let leadingMarker = false; - - tokens.forEach((token, index) => { - if (this.props.displayLocationMarkers && token.markers.length > 0) { - token.markers.forEach(marker => { - const message = - highlightedLocationMessage != null && highlightedLocationMessage.index === marker - ? highlightedLocationMessage.text - : null; - renderedTokens.push(this.renderMarker(marker, message, leadingMarker)); - }); - } - renderedTokens.push( - - {token.text} - - ); - - // keep leadingMarker truthy if previous token has only whitespaces - leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length; - }); - - return ( - -
-
 (this.codeNode = node)}>{renderedTokens}
-
- {showIssues && - issues.length > 0 && ( - - )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx new file mode 100644 index 00000000000..c91f58c43c2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCode.tsx @@ -0,0 +1,248 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import LineIssuesList from './LineIssuesList'; +import { Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; +import LocationIndex from '../../common/LocationIndex'; +import LocationMessage from '../../common/LocationMessage'; +import { + highlightIssueLocations, + highlightSymbol, + splitByTokens, + Token +} from '../helpers/highlight'; + +interface Props { + branch: string | undefined; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + displayLocationMarkers?: boolean; + highlightedLocationMessage: { index: number; text: string } | undefined; + highlightedSymbols: string[] | undefined; + issueLocations: LinearIssueLocation[]; + issuePopup: { issue: string; name: string } | undefined; + issues: Issue[]; + line: SourceLine; + onIssueChange: (issue: Issue) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + onIssueSelect: (issueKey: string) => void; + onLocationSelect: ((index: number) => void) | undefined; + onSymbolClick: (symbols: Array) => void; + scroll?: (element: HTMLElement) => void; + secondaryIssueLocations: Array<{ + from: number; + to: number; + line: number; + index: number; + startLine: number; + }>; + selectedIssue: string | undefined; + showIssues?: boolean; +} + +interface State { + tokens: Token[]; +} + +export default class LineCode extends React.PureComponent { + activeMarkerNode?: HTMLElement | null; + codeNode?: HTMLElement | null; + symbols?: NodeListOf; + + constructor(props: Props) { + super(props); + this.state = { + tokens: splitByTokens(props.line.code || '') + }; + } + + componentDidMount() { + this.attachEvents(); + if (this.props.highlightedLocationMessage && this.activeMarkerNode && this.props.scroll) { + this.props.scroll(this.activeMarkerNode); + } + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.line.code !== this.props.line.code) { + this.setState({ + tokens: splitByTokens(nextProps.line.code || '') + }); + } + } + + componentWillUpdate() { + this.detachEvents(); + } + + componentDidUpdate(prevProps: Props) { + this.attachEvents(); + if ( + this.props.highlightedLocationMessage && + prevProps.highlightedLocationMessage !== this.props.highlightedLocationMessage && + this.activeMarkerNode && + this.props.scroll + ) { + this.props.scroll(this.activeMarkerNode); + } + } + + componentWillUnmount() { + this.detachEvents(); + } + + attachEvents() { + if (this.codeNode) { + this.symbols = this.codeNode.querySelectorAll('.sym'); + if (this.symbols) { + for (let i = 0; i < this.symbols.length; i++) { + const symbol = this.symbols[i]; + symbol.addEventListener('click', this.handleSymbolClick); + } + } + } + } + + detachEvents() { + if (this.symbols) { + for (let i = 0; i < this.symbols.length; i++) { + const symbol = this.symbols[i]; + symbol.addEventListener('click', this.handleSymbolClick); + } + } + } + + handleSymbolClick = (event: MouseEvent) => { + event.preventDefault(); + const keys = (event.currentTarget as HTMLElement).className.match(/sym-\d+/g); + if (keys && keys.length > 0) { + this.props.onSymbolClick(keys); + } + }; + + renderMarker(index: number, message: string | undefined, leading = false) { + const { onLocationSelect } = this.props; + const onClick = onLocationSelect ? () => onLocationSelect(index) : undefined; + const ref = + message != null ? (node: HTMLElement | null) => (this.activeMarkerNode = node) : undefined; + return ( + + {index + 1} + {message != null && {message}} + + ); + } + + render() { + const { + highlightedLocationMessage, + highlightedSymbols, + issues, + issueLocations, + line, + onIssueSelect, + secondaryIssueLocations, + selectedIssue, + showIssues + } = this.props; + + let tokens = [...this.state.tokens]; + + if (highlightedSymbols) { + highlightedSymbols.forEach(symbol => { + tokens = highlightSymbol(tokens, symbol); + }); + } + + if (issueLocations.length > 0) { + tokens = highlightIssueLocations(tokens, issueLocations); + } + + if (secondaryIssueLocations) { + tokens = highlightIssueLocations(tokens, secondaryIssueLocations, 'issue-location'); + + if (highlightedLocationMessage) { + const location = secondaryIssueLocations.find( + location => location.index === highlightedLocationMessage.index + ); + if (location) { + tokens = highlightIssueLocations(tokens, [location], 'selected'); + } + } + } + + const className = classNames('source-line-code', 'code', { + 'has-issues': issues.length > 0 + }); + + const renderedTokens: React.ReactNode[] = []; + + // track if the first marker is displayed before the source code + // set `false` for the first token in a row + let leadingMarker = false; + + tokens.forEach((token, index) => { + if (this.props.displayLocationMarkers && token.markers.length > 0) { + token.markers.forEach(marker => { + const message = + highlightedLocationMessage != null && highlightedLocationMessage.index === marker + ? highlightedLocationMessage.text + : undefined; + renderedTokens.push(this.renderMarker(marker, message, leadingMarker)); + }); + } + renderedTokens.push( + + {token.text} + + ); + + // keep leadingMarker truthy if previous token has only whitespaces + leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length; + }); + + return ( + +
+
 (this.codeNode = node)}>{renderedTokens}
+
+ {showIssues && + issues.length > 0 && ( + + )} + + ); + } +} 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 deleted file mode 100644 index 30fbf0d36a1..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Tooltip from '../../controls/Tooltip'; -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 hasPopup = - line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; - const cell = ( - -
- - ); - - return line.coverageStatus != null ? ( - - {cell} - - ) : ( - cell - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx new file mode 100644 index 00000000000..c6b9efb895d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.tsx @@ -0,0 +1,102 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import CoveragePopup from './CoveragePopup'; +import { SourceLine } from '../../../app/types'; +import Tooltip from '../../controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + branch: string | undefined; + componentKey: string; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; +} + +export default class LineCoverage extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open }); + }; + + closePopup = () => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'coverage', open: false }); + }; + + render() { + const { branch, componentKey, line, popupOpen } = this.props; + + const className = + 'source-meta source-line-coverage' + + (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : ''); + + const hasPopup = + line.coverageStatus === 'covered' || line.coverageStatus === 'partially-covered'; + + const cell = line.coverageStatus ? ( + +
+ + ) : ( +
+ ); + + if (hasPopup) { + return ( + + {cell} + + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ); + } + + return ( + + {cell} + + ); + } +} 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 deleted file mode 100644 index 51c83c9c99c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Tooltip from '../../controls/Tooltip'; -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 - }); - - const cell = ( - -
- - ); - - return duplicated ? ( - - {cell} - - ) : ( - cell - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx new file mode 100644 index 00000000000..0121bfa868d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.tsx @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { SourceLine } from '../../../app/types'; +import Tooltip from '../../controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + duplicated: boolean; + index: number; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; + renderDuplicationPopup: (index: number, line: number) => JSX.Element; +} + +export default class LineDuplicationBlock extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ + index: this.props.index, + line: this.props.line.line, + name: 'duplications' + }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ + index: this.props.index, + line: this.props.line.line, + name: 'duplications', + open + }); + }; + + render() { + const { duplicated, index, line, popupOpen } = this.props; + const className = classNames('source-meta', 'source-line-duplications-extra', { + 'source-line-duplicated': duplicated + }); + + const cell =
; + + return duplicated ? ( + + + {cell} + + + + ) : ( + + {cell} + + ); + } +} 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 deleted file mode 100644 index 7e463eab0b2..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Tooltip from '../../controls/Tooltip'; -import { translate } from '../../../helpers/l10n'; -/*:: import type { SourceLine } from '../types'; */ - -/*:: -type Props = { - line: SourceLine, - onClick: SourceLine => void -}; -*/ - -export default class LineDuplications extends React.PureComponent { - /*:: props: Props; */ - - handleClick = (e /*: SyntheticInputEvent */) => { - e.preventDefault(); - this.props.onClick(this.props.line); - }; - - render() { - const { line } = this.props; - const className = classNames('source-meta', 'source-line-duplications', { - 'source-line-duplicated': line.duplicated - }); - - const cell = ( - -
- - ); - - return line.duplicated ? ( - - {cell} - - ) : ( - cell - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx new file mode 100644 index 00000000000..c51c49dcdca --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.tsx @@ -0,0 +1,61 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import { SourceLine } from '../../../app/types'; +import Tooltip from '../../controls/Tooltip'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + line: SourceLine; + onClick: (line: SourceLine) => void; +} + +export default class LineDuplications extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + this.props.onClick(this.props.line); + }; + + render() { + const { line } = this.props; + const className = classNames('source-meta', 'source-line-duplications', { + 'source-line-duplicated': line.duplicated + }); + + const cell = ( + +
+ + ); + + return line.duplicated ? ( + + {cell} + + ) : ( + cell + ); + } +} 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 deleted file mode 100644 index 2a7159bfc6b..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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/SeverityIcon'; -import { sortBySeverity } from '../../../helpers/issues'; -/*:: import type { SourceLine } from '../types'; */ -/*:: import type { Issue } from '../../issue/types'; */ - -/*:: -type Props = { - issues: Array, - 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 ( - - {mostImportantIssue != null && } - {issues.length > 1 && {issues.length}} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx new file mode 100644 index 00000000000..e5434c0388c --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesIndicator.tsx @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import * as classNames from 'classnames'; +import SeverityIcon from '../../shared/SeverityIcon'; +import { sortBySeverity } from '../../../helpers/issues'; +import { Issue, SourceLine } from '../../../app/types'; + +interface Props { + issues: Issue[]; + line: SourceLine; + onClick: () => void; +} + +export default class LineIssuesIndicator extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.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 ( + + {mostImportantIssue != null && } + {issues.length > 1 && {issues.length}} + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js deleted file mode 100644 index 66ef7584321..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Issue from '../../issue/Issue'; -/*:: import type { Issue as IssueType } from '../../issue/types'; */ - -/*:: -type Props = { - branch?: string, - displayIssueLocationsCount?: boolean; - displayIssueLocationsLink?: boolean; - issues: Array, - onIssueChange: IssueType => void, - onIssueClick: (issueKey: string) => void, - onPopupToggle: (issue: string, popupName: string, open: ?boolean ) => void, - openPopup: ?{ issue: string, name: string}, - selectedIssue: string | null -}; -*/ - -export default class LineIssuesList extends React.PureComponent { - /*:: props: Props; */ - - render() { - const { branch, issues, onIssueClick, openPopup, selectedIssue } = this.props; - - return ( -
- {issues.map(issue => ( - - ))} -
- ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx new file mode 100644 index 00000000000..231cc639171 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineIssuesList.tsx @@ -0,0 +1,57 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Issue as IssueType } from '../../../app/types'; +import Issue from '../../issue/Issue'; + +interface Props { + branch: string | undefined; + displayIssueLocationsCount?: boolean; + displayIssueLocationsLink?: boolean; + issuePopup: { issue: string; name: string } | undefined; + issues: IssueType[]; + onIssueChange: (issue: IssueType) => void; + onIssueClick: (issueKey: string) => void; + onIssuePopupToggle: (issue: string, popupName: string, open?: boolean) => void; + selectedIssue: string | undefined; +} + +export default function LineIssuesList(props: Props) { + const { issuePopup } = props; + + return ( +
+ {props.issues.map(issue => ( + + ))} +
+ ); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js deleted file mode 100644 index 801830c5e29..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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, - 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 ( - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx new file mode 100644 index 00000000000..f6126f5e7ec --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineNumber.tsx @@ -0,0 +1,69 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import LineOptionsPopup from './LineOptionsPopup'; +import { SourceLine } from '../../../app/types'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + // TODO use branchLike + branch: string | undefined; + componentKey: string; + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; +} + +export default class LineNumber extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'line-number', open }); + }; + + render() { + const { branch, componentKey, line, popupOpen } = this.props; + const { line: lineNumber } = line; + const hasLineNumber = !!lineNumber; + return hasLineNumber ? ( + + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ) : ( + + ); + } +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx new file mode 100644 index 00000000000..db4634b2c28 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineOptionsPopup.tsx @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Link } from 'react-router'; +import { SourceLine } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + // TODO use branchLike + branch: string | undefined; + componentKey: string; + line: SourceLine; + popupPosition?: any; +} + +export default function LineOptionsPopup({ branch, componentKey, line, popupPosition }: Props) { + const permalink = { + pathname: '/component', + query: { branch, id: componentKey, line: line.line } + }; + return ( + +
+ + {translate('component_viewer.get_permalink')} + +
+
+ ); +} 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 deleted file mode 100644 index 274add2c12d..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 ( - - {this.isSCMChanged(line, previousLine) && ( -
- )} - - ); - } -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx new file mode 100644 index 00000000000..c1de61e6ee2 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx @@ -0,0 +1,80 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import SCMPopup from './SCMPopup'; +import { SourceLine } from '../../../app/types'; +import BubblePopupHelper from '../../common/BubblePopupHelper'; + +interface Props { + line: SourceLine; + onPopupToggle: (x: { index?: number; line: number; name: string; open?: boolean }) => void; + popupOpen: boolean; + previousLine: SourceLine | undefined; +} + +export default class LineSCM extends React.PureComponent { + handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + this.props.onPopupToggle({ line: this.props.line.line, name: 'scm' }); + }; + + handleTogglePopup = (open: boolean) => { + this.props.onPopupToggle({ line: this.props.line.line, name: 'scm', open }); + }; + + render() { + const { line, popupOpen, previousLine } = this.props; + const hasPopup = !!line.line; + const cell = isSCMChanged(line, previousLine) && ( +
+ ); + return hasPopup ? ( + + {cell} + } + position="bottomright" + togglePopup={this.handleTogglePopup} + /> + + ) : ( + + {cell} + + ); + } +} + +function isSCMChanged(s: SourceLine, p: SourceLine | undefined) { + let changed = true; + if (p != null && s.scmAuthor != null && p.scmAuthor != null) { + changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate; + } + return changed; +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx new file mode 100644 index 00000000000..1a94d48e657 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { SourceLine } from '../../../app/types'; +import BubblePopup from '../../common/BubblePopup'; +import DateFormatter from '../../intl/DateFormatter'; + +interface Props { + line: SourceLine; + popupPosition?: any; +} + +export default function SCMPopup({ line, popupPosition }: Props) { + return ( + +
{line.scmAuthor}
+ {line.scmDate && ( +
+ +
+ )} + {line.scmRevision &&
{line.scmRevision}
} +
+ ); +} 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 deleted file mode 100644 index 3dc8487e896..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 LineCode from '../LineCode'; - -it('render code', () => { - const line = { - line: 3, - code: 'class Foo {' - }; - const issueLocations = [{ from: 0, to: 5, line: 3 }]; - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx new file mode 100644 index 00000000000..a9127eae0ce --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCode-test.tsx @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LineCode from '../LineCode'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; + +it('render code', () => { + const line = { + line: 3, + code: 'class Foo {' + }; + const issueLocations = [{ from: 0, to: 5, line: 3 }]; + const wrapper = shallow( + + ); + 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 deleted file mode 100644 index 35cac165cd4..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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(); - expect(wrapper).toMatchSnapshot(); - click(wrapper.find('[tabIndex]')); - expect(onClick).toHaveBeenCalled(); -}); - -it('render uncovered line', () => { - const line = { line: 3, coverageStatus: 'uncovered' }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); - -it('render line with unknown coverage', () => { - const line = { line: 3 }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx new file mode 100644 index 00000000000..a737504b3bd --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.tsx @@ -0,0 +1,83 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LineCoverage from '../LineCoverage'; +import { click } from '../../../../helpers/testUtils'; +import { SourceLine } from '../../../../app/types'; + +it('render covered line', () => { + const line: SourceLine = { line: 3, coverageStatus: 'covered' }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('[tabIndex]')); +}); + +it('render uncovered line', () => { + const line: SourceLine = { line: 3, coverageStatus: 'uncovered' }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('render line with unknown coverage', () => { + const line: SourceLine = { line: 3 }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should open coverage popup', () => { + const line: SourceLine = { line: 3, coverageStatus: 'covered' }; + const onPopupToggle = jest.fn(); + const wrapper = shallow( + + ); + click(wrapper.find('[role="button"]')); + expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'coverage' }); +}); 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 deleted file mode 100644 index cc2d147cd06..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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( - - ); - expect(wrapper).toMatchSnapshot(); - click(wrapper.find('[tabIndex]')); - expect(onClick).toHaveBeenCalled(); -}); - -it('render not duplicated line', () => { - const line = { line: 3, duplicated: false }; - const onClick = jest.fn(); - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx new file mode 100644 index 00000000000..0854c4c4f95 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.tsx @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineDuplicationBlock from '../LineDuplicationBlock'; + +it('render duplicated line', () => { + const line = { line: 3, duplicated: true }; + const onPopupToggle = jest.fn(); + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('[tabIndex]')); + expect(onPopupToggle).toHaveBeenCalled(); +}); + +it('render not duplicated line', () => { + const line = { line: 3, duplicated: false }; + const wrapper = shallow( + + ); + 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 deleted file mode 100644 index aaa463ec776..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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(); - expect(wrapper).toMatchSnapshot(); - click(wrapper.find('[tabIndex]')); - expect(onClick).toHaveBeenCalled(); -}); - -it('render not duplicated line', () => { - const line = { line: 3, duplicated: false }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx new file mode 100644 index 00000000000..b5fac8ef47d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { 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(); + expect(wrapper).toMatchSnapshot(); + click(wrapper.find('[tabIndex]')); + expect(onClick).toHaveBeenCalled(); +}); + +it('render not duplicated line', () => { + const line = { line: 3, duplicated: false }; + const onClick = jest.fn(); + const wrapper = shallow(); + 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 deleted file mode 100644 index 9f22629af08..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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(); - 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(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx new file mode 100644 index 00000000000..b4fd9fc6e70 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesIndicator-test.tsx @@ -0,0 +1,72 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineIssuesIndicator from '../LineIssuesIndicator'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; + +it('render highest severity', () => { + const line = { line: 3 }; + const issues = [ + { ...issueBase, key: 'foo', severity: 'MINOR' }, + { ...issueBase, key: 'bar', severity: 'CRITICAL' } + ]; + const onClick = jest.fn(); + const wrapper = shallow(); + 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: Issue[] = []; + const onClick = jest.fn(); + const wrapper = shallow(); + 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 deleted file mode 100644 index 354c3deab9d..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 issues = [{ key: 'foo' }, { key: 'bar' }]; - const onIssueClick = jest.fn(); - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx new file mode 100644 index 00000000000..8ab04fcda21 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineIssuesList-test.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LineIssuesList from '../LineIssuesList'; +import { Issue } from '../../../../app/types'; + +const issueBase: Issue = { + component: '', + componentLongName: '', + componentQualifier: '', + componentUuid: '', + creationDate: '', + key: '', + flows: [], + message: '', + organization: '', + project: '', + projectName: '', + projectOrganization: '', + projectUuid: '', + rule: '', + ruleName: '', + secondaryLocations: [], + severity: '', + status: '', + type: '' +}; + +it('render issues list', () => { + const issues: Issue[] = [{ ...issueBase, key: 'foo' }, { ...issueBase, key: 'bar' }]; + const onIssueClick = jest.fn(); + const wrapper = shallow( + + ); + 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 deleted file mode 100644 index 2afee7b5898..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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(); - expect(wrapper).toMatchSnapshot(); - click(wrapper); - expect(onClick).toHaveBeenCalled(); -}); - -it('render line 0', () => { - const line = { line: 0 }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx new file mode 100644 index 00000000000..bf40ca900c8 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineNumber-test.tsx @@ -0,0 +1,52 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { click } from '../../../../helpers/testUtils'; +import LineNumber from '../LineNumber'; + +it('render line 3', () => { + const line = { line: 3 }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + click(wrapper); +}); + +it('render line 0', () => { + const line = { line: 0 }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx new file mode 100644 index 00000000000..a5de2ec635e --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineOptionsPopup-test.tsx @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LineOptionsPopup from '../LineOptionsPopup'; + +it('should render', () => { + const line = { line: 3 }; + const wrapper = shallow(); + 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 deleted file mode 100644 index 99db276beba..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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(); - 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(); - 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(); - expect(wrapper).toMatchSnapshot(); -}); - -it('does not allow to click', () => { - const line = { scmAuthor: 'foo', scmDate: '2017-01-01' }; - const onClick = jest.fn(); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx new file mode 100644 index 00000000000..29e82eb3d61 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import LineSCM from '../LineSCM'; +import { click } from '../../../../helpers/testUtils'; + +it('render scm details', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const previousLine = { line: 2, scmAuthor: 'bar', scmDate: '2017-01-02' }; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('render scm details for the first line', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const wrapper = shallow( + + ); + 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 wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); +}); + +it('should open popup', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + const onPopupToggle = jest.fn(); + const wrapper = shallow( + + ); + click(wrapper.find('[role="button"]')); + expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx new file mode 100644 index 00000000000..6a6fba2acc3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/SCMPopup-test.tsx @@ -0,0 +1,27 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import SCMPopup from '../SCMPopup'; + +it('should render', () => { + const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' }; + expect(shallow()).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 deleted file mode 100644 index 1cb641e3679..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.js.snap +++ /dev/null @@ -1,55 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render code 1`] = ` - -
-
-      
-        class
-      
-      
-         
-      
-      
-        Foo
-      
-      
-         {
-      
-    
-
- - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap new file mode 100644 index 00000000000..f5e24cd2736 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCode-test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render code 1`] = ` + +
+
+      
+        class
+      
+      
+         
+      
+      
+        Foo
+      
+      
+         {
+      
+    
+
+ + +`; 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 deleted file mode 100644 index abef0b03565..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render covered line 1`] = ` - - -
- - -`; - -exports[`render line with unknown coverage 1`] = ` - -
- -`; - -exports[`render uncovered line 1`] = ` - - -
- - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap new file mode 100644 index 00000000000..664a60ddf64 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.tsx.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render covered line 1`] = ` + + +
+ + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render line with unknown coverage 1`] = ` + +
+ +`; + +exports[`render uncovered line 1`] = ` + + +
+ + +`; 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 deleted file mode 100644 index c8a4ee23981..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap +++ /dev/null @@ -1,35 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render duplicated line 1`] = ` - - -
- - -`; - -exports[`render not duplicated line 1`] = ` - -
- -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap new file mode 100644 index 00000000000..9ea77716f15 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.tsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render duplicated line 1`] = ` + + +
+ + + +`; + +exports[`render not duplicated line 1`] = ` + +
+ +`; 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 deleted file mode 100644 index d40e9e6d111..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render duplicated line 1`] = ` - - -
- - -`; - -exports[`render not duplicated line 1`] = ` - -
- -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap new file mode 100644 index 00000000000..d40e9e6d111 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render duplicated line 1`] = ` + + +
+ + +`; + +exports[`render not duplicated line 1`] = ` + +
+ +`; 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 deleted file mode 100644 index 5636d7a0381..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.js.snap +++ /dev/null @@ -1,46 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`no issues 1`] = ` - -`; - -exports[`render highest severity 1`] = ` - - - - 2 - - -`; - -exports[`render highest severity 2`] = ` - - - - 2 - - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap new file mode 100644 index 00000000000..e941c781cac --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesIndicator-test.tsx.snap @@ -0,0 +1,46 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`no issues 1`] = ` + +`; + +exports[`render highest severity 1`] = ` + + + + 2 + + +`; + +exports[`render highest severity 2`] = ` + + + + 2 + + +`; 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 deleted file mode 100644 index 6612498bbc8..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.js.snap +++ /dev/null @@ -1,36 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render issues list 1`] = ` -
- - -
-`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap new file mode 100644 index 00000000000..d19a4e96ef0 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineIssuesList-test.tsx.snap @@ -0,0 +1,72 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render issues list 1`] = ` +
+ + +
+`; 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 deleted file mode 100644 index 86e1095667a..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.js.snap +++ /dev/null @@ -1,17 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`render line 0 1`] = ` - -`; - -exports[`render line 3 1`] = ` - -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap new file mode 100644 index 00000000000..477d94d1c3f --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineNumber-test.tsx.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`render line 0 1`] = ` + +`; + +exports[`render line 3 1`] = ` + + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap new file mode 100644 index 00000000000..13d7b6ee859 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineOptionsPopup-test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + +
+ + component_viewer.get_permalink + +
+
+`; 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 deleted file mode 100644 index ce8c0369fb1..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.js.snap +++ /dev/null @@ -1,52 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`does not allow to click 1`] = ` - -
- -`; - -exports[`does not render scm details 1`] = ` - -`; - -exports[`render scm details 1`] = ` - -
- -`; - -exports[`render scm details for the first line 1`] = ` - -
- -`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap new file mode 100644 index 00000000000..dcbaf7e2c9c --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`does not render scm details 1`] = ` + + + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render scm details 1`] = ` + +
+ + } + position="bottomright" + togglePopup={[Function]} + /> + +`; + +exports[`render scm details for the first line 1`] = ` + +
+ + } + position="bottomright" + togglePopup={[Function]} + /> + +`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap new file mode 100644 index 00000000000..d8460698802 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/SCMPopup-test.tsx.snap @@ -0,0 +1,20 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` + +
+ foo +
+
+ +
+
+`; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js deleted file mode 100644 index f252b171782..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { highlightSymbol } from '../highlight'; - -describe('highlightSymbol', () => { - it('should not highlight symbols with similar beginning', () => { - // test all positions of sym-X in the string: beginning, middle and ending - const tokens = [ - { className: 'sym-18 b', markers: [], text: 'foo' }, - { className: 'a sym-18', markers: [], text: 'foo' }, - { className: 'a sym-18 b', markers: [], text: 'foo' }, - { className: 'sym-1 d', markers: [], text: 'bar' }, - { className: 'c sym-1', markers: [], text: 'bar' }, - { className: 'c sym-1 d', markers: [], text: 'bar' } - ]; - expect(highlightSymbol(tokens, 'sym-1')).toEqual([ - { className: 'sym-18 b', markers: [], text: 'foo' }, - { className: 'a sym-18', markers: [], text: 'foo' }, - { className: 'a sym-18 b', markers: [], text: 'foo' }, - { className: 'sym-1 d highlighted', markers: [], text: 'bar' }, - { className: 'c sym-1 highlighted', markers: [], text: 'bar' }, - { className: 'c sym-1 d highlighted', markers: [], text: 'bar' } - ]); - }); - - it('should highlight symbols marked twice', () => { - const tokens = [ - { className: 'sym sym-1 sym sym-2', markers: [], text: 'foo' }, - { className: 'sym sym-1', markers: [], text: 'bar' }, - { className: 'sym sym-2', markers: [], text: 'qux' } - ]; - expect(highlightSymbol(tokens, 'sym-1')).toEqual([ - { className: 'sym sym-1 sym sym-2 highlighted', markers: [], text: 'foo' }, - { className: 'sym sym-1 highlighted', markers: [], text: 'bar' }, - { className: 'sym sym-2', markers: [], text: 'qux' } - ]); - }); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts new file mode 100644 index 00000000000..47900d5ef8f --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/highlight-test.ts @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { highlightSymbol } from '../highlight'; + +describe('highlightSymbol', () => { + it('should not highlight symbols with similar beginning', () => { + // test all positions of sym-X in the string: beginning, middle and ending + const tokens = [ + { className: 'sym-18 b', markers: [], text: 'foo' }, + { className: 'a sym-18', markers: [], text: 'foo' }, + { className: 'a sym-18 b', markers: [], text: 'foo' }, + { className: 'sym-1 d', markers: [], text: 'bar' }, + { className: 'c sym-1', markers: [], text: 'bar' }, + { className: 'c sym-1 d', markers: [], text: 'bar' } + ]; + expect(highlightSymbol(tokens, 'sym-1')).toEqual([ + { className: 'sym-18 b', markers: [], text: 'foo' }, + { className: 'a sym-18', markers: [], text: 'foo' }, + { className: 'a sym-18 b', markers: [], text: 'foo' }, + { className: 'sym-1 d highlighted', markers: [], text: 'bar' }, + { className: 'c sym-1 highlighted', markers: [], text: 'bar' }, + { className: 'c sym-1 d highlighted', markers: [], text: 'bar' } + ]); + }); + + it('should highlight symbols marked twice', () => { + const tokens = [ + { className: 'sym sym-1 sym sym-2', markers: [], text: 'foo' }, + { className: 'sym sym-1', markers: [], text: 'bar' }, + { className: 'sym sym-2', markers: [], text: 'qux' } + ]; + expect(highlightSymbol(tokens, 'sym-1')).toEqual([ + { className: 'sym sym-1 sym sym-2 highlighted', markers: [], text: 'foo' }, + { className: 'sym sym-1 highlighted', markers: [], text: 'bar' }, + { className: 'sym sym-2', markers: [], text: 'qux' } + ]); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js deleted file mode 100644 index 2e88dd661a5..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { symbolsByLine } from '../indexing'; - -describe('symbolsByLine', () => { - it('should highlight symbols marked twice', () => { - const lines = [ - { line: 1, code: 'foo' }, - { line: 2, code: 'bar' }, - { line: 3, code: 'qux' } - ]; - expect(symbolsByLine(lines)).toEqual({ - 1: ['sym-54', 'sym-56'], - 2: ['sym-56'], - 3: [] - }); - }); -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts new file mode 100644 index 00000000000..2e88dd661a5 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/indexing-test.ts @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { symbolsByLine } from '../indexing'; + +describe('symbolsByLine', () => { + it('should highlight symbols marked twice', () => { + const lines = [ + { line: 1, code: 'foo' }, + { line: 2, code: 'bar' }, + { line: 3, code: 'qux' } + ]; + expect(symbolsByLine(lines)).toEqual({ + 1: ['sym-54', 'sym-56'], + 2: ['sym-56'], + 3: [] + }); + }); +}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js deleted file mode 100644 index 5a0ba749ca0..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 type { SourceLine } from '../types'; */ - -export default function getCoverageStatus(s /*: SourceLine */) /*: string | null */ { - let status = null; - if (s.lineHits != null && s.lineHits > 0) { - status = 'partially-covered'; - } - if (s.lineHits != null && s.lineHits > 0 && s.conditions === s.coveredConditions) { - status = 'covered'; - } - if (s.lineHits === 0 || s.coveredConditions === 0) { - status = 'uncovered'; - } - return status; -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx new file mode 100644 index 00000000000..88fd50b2f1d --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/getCoverageStatus.tsx @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { SourceLine } from '../../../app/types'; + +export default function getCoverageStatus(s: SourceLine): string | undefined { + let status: string | undefined; + if (s.lineHits != null && s.lineHits > 0) { + status = 'partially-covered'; + } + if (s.lineHits != null && s.lineHits > 0 && s.conditions === s.coveredConditions) { + status = 'covered'; + } + if (s.lineHits === 0 || s.coveredConditions === 0) { + status = 'uncovered'; + } + return status; +} 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 deleted file mode 100644 index afdd574fbb7..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 escapeHtml from 'escape-html'; -import { uniq } from 'lodash'; - -/*:: -export type Token = { className: string, markers: Array, text: string }; -*/ -/*:: -export type Tokens = Array; */ - -const ISSUE_LOCATION_CLASS = 'source-line-code-issue'; - -export function splitByTokens(code /*: string */, rootClassName /*: string */ = '') /*: Tokens */ { - 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, markers: [], text: node.nodeValue }); - } - }); - return tokens; -} - -export function highlightSymbol(tokens /*: Tokens */, symbol /*: string */) /*: Tokens */ { - const symbolRegExp = new RegExp(`\\b${symbol}\\b`); - return tokens.map( - token => - symbolRegExp.test(token.className) - ? { ...token, className: `${token.className} highlighted` } - : token - ); -} - -/** - * Intersect two ranges - * @param s1 Start position of the first range - * @param e1 End position of the first range - * @param s2 Start position of the second range - * @param e2 End position of the second range - */ -function intersect( - s1 /*: number */, - e1 /*: number */, - s2 /*: number */, - e2 /*: number */ -) /*: { from: number, to: number } */ { - return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; -} - -/** - * Get the substring of a string - * @param str A string - * @param from "From" offset - * @param to "To" offset - * @param acc Global offset to eliminate - */ -function part( - str /*: string */, - from /*: number */, - to /*: number */, - acc /*: number */ -) /*: string */ { - // 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); -} - -/** - * Highlight issue locations in the list of tokens - */ -export function highlightIssueLocations( - tokens /*: Tokens */, - issueLocations /*: Array<*> */, - rootClassName /*: string */ = ISSUE_LOCATION_CLASS -) /*: Tokens */ { - issueLocations.forEach(location => { - const nextTokens = []; - let acc = 0; - let markerAdded = location.line !== location.startLine; - 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({ ...token, text: p1 }); - } - if (p2.length) { - const newClassName = - token.className.indexOf(rootClassName) === -1 - ? `${token.className} ${rootClassName}` - : token.className; - nextTokens.push({ - className: newClassName, - markers: - !markerAdded && location.index != null - ? uniq([...token.markers, location.index]) - : token.markers, - text: p2 - }); - markerAdded = true; - } - if (p3.length) { - nextTokens.push({ ...token, text: p3 }); - } - acc += token.text.length; - }); - tokens = nextTokens.slice(); - }); - return tokens; -} - -export function generateHTML(tokens /*: Tokens */) /*: string */ { - return tokens - .map(token => `${escapeHtml(token.text)}`) - .join(''); -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts new file mode 100644 index 00000000000..2f105f74884 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/highlight.ts @@ -0,0 +1,126 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { uniq } from 'lodash'; +import { LinearIssueLocation } from '../../../app/types'; + +export interface Token { + className: string; + markers: number[]; + text: string; +} + +const ISSUE_LOCATION_CLASS = 'source-line-code-issue'; + +export function splitByTokens(code: string, rootClassName = ''): Token[] { + const container = document.createElement('div'); + let tokens: Token[] = []; + container.innerHTML = code; + [].forEach.call(container.childNodes, (node: Element) => { + 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 && node.nodeValue) { + // TEXT NODE + tokens.push({ className: rootClassName, markers: [], text: node.nodeValue }); + } + }); + return tokens; +} + +export function highlightSymbol(tokens: Token[], symbol: string): Token[] { + const symbolRegExp = new RegExp(`\\b${symbol}\\b`); + return tokens.map( + token => + symbolRegExp.test(token.className) + ? { ...token, className: `${token.className} highlighted` } + : token + ); +} + +/** + * Intersect two ranges + * @param s1 Start position of the first range + * @param e1 End position of the first range + * @param s2 Start position of the second range + * @param e2 End position of the second range + */ +function intersect(s1: number, e1: number, s2: number, e2: number) { + return { from: Math.max(s1, s2), to: Math.min(e1, e2) }; +} + +/** + * Get the substring of a string + * @param str A string + * @param from "From" offset + * @param to "To" offset + * @param acc Global offset to eliminate + */ +function part(str: string, from: number, to: number, acc: number): string { + // 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); +} + +/** + * Highlight issue locations in the list of tokens + */ +export function highlightIssueLocations( + tokens: Token[], + issueLocations: LinearIssueLocation[], + rootClassName: string = ISSUE_LOCATION_CLASS +): Token[] { + issueLocations.forEach(location => { + const nextTokens: Token[] = []; + let acc = 0; + let markerAdded = location.line !== location.startLine; + 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({ ...token, text: p1 }); + } + if (p2.length) { + const newClassName = + token.className.indexOf(rootClassName) === -1 + ? `${token.className} ${rootClassName}` + : token.className; + nextTokens.push({ + className: newClassName, + markers: + !markerAdded && location.index != null + ? uniq([...token.markers, location.index]) + : token.markers, + text: p2 + }); + markerAdded = true; + } + if (p3.length) { + nextTokens.push({ ...token, text: p3 }); + } + acc += token.text.length; + }); + tokens = nextTokens.slice(); + }); + return tokens; +} 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 deleted file mode 100644 index c9364268e23..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.js +++ /dev/null @@ -1,115 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { flatten } from 'lodash'; -import { splitByTokens } from './highlight'; -import { getLinearLocations } from './issueLocations'; -/*:: import type { Issue } from '../../issue/types'; */ -/*:: import type { SourceLine } from '../types'; */ - -/*:: -export type LinearIssueLocation = { - from: number, - line: number, - to: number, - index?: number -}; -*/ - -/*:: -export type IndexedIssueLocation = { - from: number, - line: number, - to: number -}; -*/ - -/*:: -export type IndexedIssueLocationMessage = { - flowIndex: number, - locationIndex: number, - msg?: string -}; -*/ - -export const issuesByLine = (issues /*: Array */) => { - const index = {}; - issues.forEach(issue => { - const line = issue.textRange ? issue.textRange.endLine : 0; - if (!(line in index)) { - index[line] = []; - } - index[line].push(issue); - }); - return index; -}; - -export function locationsByLine( - issues /*: Array */ -) /*: { [number]: Array } */ { - const index = {}; - issues.forEach(issue => { - getLinearLocations(issue.textRange).forEach(location => { - if (!(location.line in index)) { - index[location.line] = []; - } - index[location.line].push(location); - }); - }); - return index; -} - -export const duplicationsByLine = (duplications /*: Array<*> | null */) => { - if (duplications == null) { - return {}; - } - - const duplicationsByLine = {}; - - duplications.forEach(({ blocks }, duplicationIndex) => { - blocks.forEach(block => { - if (block._ref === '1') { - for (let line = block.from; line < block.from + block.size; line++) { - if (!(line in duplicationsByLine)) { - duplicationsByLine[line] = []; - } - duplicationsByLine[line].push(duplicationIndex); - } - } - }); - }); - - return duplicationsByLine; -}; - -export const symbolsByLine = (sources /*: Array */) => { - const index = {}; - sources.forEach(line => { - const tokens = splitByTokens(line.code); - const symbols = flatten( - tokens.map(token => { - const keys = token.className.match(/sym-\d+/g); - return keys != null ? keys : []; - }) - ); - index[line.line] = symbols.filter(key => key); - }); - return index; -}; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts new file mode 100644 index 00000000000..bf103f0cf34 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/indexing.ts @@ -0,0 +1,87 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { flatten } from 'lodash'; +import { splitByTokens } from './highlight'; +import { getLinearLocations } from './issueLocations'; +import { Duplication, Issue, LinearIssueLocation, SourceLine } from '../../../app/types'; + +export function issuesByLine(issues: Issue[]) { + const index: { [line: number]: Issue[] } = {}; + issues.forEach(issue => { + const line = issue.textRange ? issue.textRange.endLine : 0; + if (!(line in index)) { + index[line] = []; + } + index[line].push(issue); + }); + return index; +} + +export function locationsByLine(issues: Issue[]) { + const index: { [line: number]: LinearIssueLocation[] } = {}; + issues.forEach(issue => { + getLinearLocations(issue.textRange).forEach(location => { + if (!(location.line in index)) { + index[location.line] = []; + } + index[location.line].push(location); + }); + }); + return index; +} + +export function duplicationsByLine(duplications: Duplication[] | undefined) { + if (duplications == null) { + return {}; + } + + const duplicationsByLine: { [line: number]: number[] } = {}; + + duplications.forEach(({ blocks }, duplicationIndex) => { + blocks.forEach(block => { + // eslint-disable-next-line no-underscore-dangle + if (block._ref === '1') { + for (let line = block.from; line < block.from + block.size; line++) { + if (!(line in duplicationsByLine)) { + duplicationsByLine[line] = []; + } + duplicationsByLine[line].push(duplicationIndex); + } + } + }); + }); + + return duplicationsByLine; +} + +export function symbolsByLine(sources: SourceLine[]) { + const index: { [line: number]: string[] } = {}; + sources.forEach(line => { + const tokens = splitByTokens(line.code || ''); + const symbols = flatten( + tokens.map(token => { + const keys = token.className.match(/sym-\d+/g); + return keys != null ? keys : []; + }) + ); + index[line.line] = symbols.filter(key => key); + }); + return index; +} 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 deleted file mode 100644 index 2e47fa64e8b..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.js +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 type { TextRange, Issue } from '../../issue/types'; */ - -export function getLinearLocations( - textRange /*: ?TextRange */ -) /*: Array<{ line: number, from: number, to: number }> */ { - if (!textRange) { - return []; - } - const locations = []; - - // go through all lines of the `textRange` - for (let line = textRange.startLine; line <= textRange.endLine; line++) { - // TODO fix 999999 - const from = line === textRange.startLine ? textRange.startOffset : 0; - const to = line === textRange.endLine ? textRange.endOffset : 999999; - locations.push({ line, from, to }); - } - return locations; -} - -/*:: -type Location = { - msg: string, - flowIndex: number, - locationIndex: number, - textRange?: TextRange, - index?: number -} -*/ - -export function getIssueLocations(issue /*: Issue */) /*: Array */ { - const allLocations = []; - issue.flows.forEach((locations, flowIndex) => { - if (locations) { - const locationsCount = locations.length; - locations.forEach((location, index) => { - const flowLocation = { - ...location, - flowIndex, - locationIndex: index, - // set index only for real flows, do not set for just secondary locations - index: locationsCount > 1 ? locationsCount - index : undefined - }; - allLocations.push(flowLocation); - }); - } - }); - return allLocations; -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx new file mode 100644 index 00000000000..5c0f7ee9616 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/issueLocations.tsx @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { LinearIssueLocation, TextRange } from '../../../app/types'; + +export function getLinearLocations(textRange: TextRange | undefined): LinearIssueLocation[] { + if (!textRange) { + return []; + } + const locations = []; + + // go through all lines of the `textRange` + for (let line = textRange.startLine; line <= textRange.endLine; line++) { + // TODO fix 999999 + const from = line === textRange.startLine ? textRange.startOffset : 0; + const to = line === textRange.endLine ? textRange.endOffset : 999999; + locations.push({ line, from, to }); + } + return locations; +} 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 deleted file mode 100644 index f1950d1f583..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { searchIssues } from '../../../api/issues'; -import { parseIssueFromResponse } from '../../../helpers/issues'; - -/*:: -export type Query = { [string]: string | void }; -*/ - -/*:: -export type Issues = Array<*>; */ - -// maximum possible value -const PAGE_SIZE = 500; - -function buildQuery(component /*: string */, branch /*: string | void */) /*: Query */ { - return { - additionalFields: '_all', - resolved: 'false', - componentKeys: component, - branch, - s: 'FILE_LINE' - }; -} - -export function loadPage( - query /*: Query */, - page /*: number */, - pageSize /*: number */ = PAGE_SIZE -) /*: Promise */ { - return searchIssues({ - ...query, - p: page, - ps: pageSize - }).then(r => - r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules)) - ); -} - -export function loadPageAndNext( - query /*: Query */, - toLine /*: number */, - page /*: number */, - pageSize /*: number */ = PAGE_SIZE -) /*: Promise */ { - return loadPage(query, page).then(issues => { - if (issues.length === 0) { - return []; - } - - const lastIssue = issues[issues.length - 1]; - - if ( - (lastIssue.textRange != null && lastIssue.textRange.endLine > toLine) || - issues.length < pageSize - ) { - return issues; - } - - return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => { - return [...issues, ...nextIssues]; - }); - }); -} - -export default function loadIssues( - component /*: string */, - fromLine /*: number */, - toLine /*: number */, - branch /*: string | void */ -) /*: Promise */ { - const query = buildQuery(component, branch); - return new Promise(resolve => { - loadPageAndNext(query, toLine, 1).then(issues => { - resolve(issues); - }); - }); -} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx new file mode 100644 index 00000000000..8354099e6f3 --- /dev/null +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.tsx @@ -0,0 +1,86 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { searchIssues } from '../../../api/issues'; +import { Issue } from '../../../app/types'; +import { parseIssueFromResponse } from '../../../helpers/issues'; +import { RawQuery } from '../../../helpers/query'; + +// maximum possible value +const PAGE_SIZE = 500; + +function buildQuery(component: string, branch: string | undefined) { + return { + additionalFields: '_all', + resolved: 'false', + componentKeys: component, + branch, + s: 'FILE_LINE' + }; +} + +export function loadPage(query: RawQuery, page: number, pageSize = PAGE_SIZE): Promise { + return searchIssues({ + ...query, + p: page, + ps: pageSize + }).then(r => + r.issues.map(issue => parseIssueFromResponse(issue, r.components, r.users, r.rules)) + ); +} + +export function loadPageAndNext( + query: RawQuery, + toLine: number, + page: number, + pageSize = PAGE_SIZE +): Promise { + return loadPage(query, page).then(issues => { + if (issues.length === 0) { + return []; + } + + const lastIssue = issues[issues.length - 1]; + + if ( + (lastIssue.textRange != null && lastIssue.textRange.endLine > toLine) || + issues.length < pageSize + ) { + return issues; + } + + return loadPageAndNext(query, toLine, page + 1, pageSize).then(nextIssues => { + return [...issues, ...nextIssues]; + }); + }); +} + +export default function loadIssues( + component: string, + _fromLine: number, + toLine: number, + branch: string | undefined +): Promise { + const query = buildQuery(component, branch); + return new Promise(resolve => { + loadPageAndNext(query, toLine, 1).then(issues => { + resolve(issues); + }); + }); +} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js deleted file mode 100644 index 694182c0c60..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/coverage-popup.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { groupBy } from 'lodash'; -import Template from './templates/source-viewer-coverage-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - events: { - 'click a[data-key]': 'goToFile' - }, - - onRender() { - Popup.prototype.onRender.apply(this, arguments); - this.$('.bubble-popup-container').isolatedScroll(); - }, - - goToFile(e) { - e.stopPropagation(); - const key = $(e.currentTarget).data('key'); - const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, branch: this.options.branch }); - }, - - serializeData() { - const row = this.options.line || {}; - const tests = groupBy(this.options.tests, 'fileKey'); - const testFiles = Object.keys(tests).map(fileKey => { - const testSet = tests[fileKey]; - const test = testSet[0]; - return { - file: { - key: test.fileKey, - longName: test.fileName - }, - tests: testSet - }; - }); - return { testFiles, row }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js deleted file mode 100644 index f45a5dc25b2..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/duplication-popup.js +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { groupBy, sortBy } from 'lodash'; -import Template from './templates/source-viewer-duplication-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - events: { - 'click a[data-key]': 'goToFile' - }, - - goToFile(e) { - e.stopPropagation(); - const key = $(e.currentTarget).data('key'); - const line = $(e.currentTarget).data('line'); - const Workspace = require('../../workspace/main').default; - Workspace.openComponent({ key, line, branch: this.options.branch }); - }, - - serializeData() { - const that = this; - const groupedBlocks = groupBy(this.options.blocks, '_ref'); - let duplications = Object.keys(groupedBlocks).map(fileRef => { - return { - blocks: groupedBlocks[fileRef], - file: this.options.files[fileRef] - }; - }); - duplications = sortBy(duplications, d => { - const a = d.file.projectName !== that.options.component.projectName; - const b = d.file.subProjectName !== that.options.component.subProjectName; - const c = d.file.key !== that.options.component.key; - return '' + a + b + c; - }); - return { - duplications, - component: this.options.component, - inRemovedComponent: this.options.inRemovedComponent - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js deleted file mode 100644 index 33d44cfb1f5..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/line-actions-popup.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Template from './templates/source-viewer-line-options-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - serializeData() { - const { component, line, branch } = this.options; - let permalink = - window.baseUrl + `/component?id=${encodeURIComponent(component.key)}&line=${line}`; - if (branch) { - permalink += `&branch=${encodeURIComponent(branch)}`; - } - return { permalink }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js b/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js deleted file mode 100644 index 5478c6fc533..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/scm-popup.js +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 Template from './templates/source-viewer-scm-popup.hbs'; -import Popup from '../../common/popup'; - -export default Popup.extend({ - template: Template, - - events: { - click: 'onClick' - }, - - onRender() { - Popup.prototype.onRender.apply(this, arguments); - this.$('.bubble-popup-container').isolatedScroll(); - }, - - onClick(e) { - e.stopPropagation(); - }, - - serializeData() { - return { - ...Popup.prototype.serializeData.apply(this, arguments), - line: this.options.line - }; - } -}); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs deleted file mode 100644 index bad2779d285..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-coverage-popup.hbs +++ /dev/null @@ -1,33 +0,0 @@ -
-
- {{t 'source_viewer.covered'}} - {{#if row.conditions}} - ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}}) - {{/if}} -
- {{#each testFiles}} -
- - {{collapsePath file.longName}} - -
    - {{#each tests}} -
  • - - - - {{name}} - - - {{durationInMs}}ms -
  • - {{/each}} -
-
- {{else}} - {{t 'source_viewer.tooltip.no_information_about_tests'}} - {{/each}} -
- -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs deleted file mode 100644 index ea8fc2b2349..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-duplication-popup.hbs +++ /dev/null @@ -1,45 +0,0 @@ -
- {{#if inRemovedComponent}} -
{{t 'duplications.dups_found_on_deleted_resource'}}
- {{/if}} - {{#notEmpty duplications}} -
{{t 'component_viewer.transition.duplication'}}
- {{#each duplications}} -
-
- {{#notEqComponents file ../component}} - - {{#if file.subProjectName}} - - {{/if}} - {{/notEqComponents}} - - {{#notEq file.key ../component.key}} - - {{/notEq}} - -
- Lines: - {{#joinEach blocks ','}} - - {{this.from}} – {{sum from size -1}} - - {{/joinEach}} -
-
-
- {{/each}} - {{/notEmpty}} -
- -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs deleted file mode 100644 index cefd31210bb..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-line-options-popup.hbs +++ /dev/null @@ -1,7 +0,0 @@ - - -
diff --git a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs b/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs deleted file mode 100644 index dd82aca528c..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/popups/templates/source-viewer-scm-popup.hbs +++ /dev/null @@ -1,15 +0,0 @@ -
-
- {{line.scmAuthor}} -
-
- {{dt line.scmDate}} -
- {{#if line.scmRevision}} -
- {{line.scmRevision}} -
- {{/if}} -
- -
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 3bcdcdf4415..73521086ad3 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/styles.css +++ b/server/sonar-web/src/main/js/components/SourceViewer/styles.css @@ -94,7 +94,8 @@ } .source-viewer pre, -.source-meta { +.source-line-number, +.source-line-scm { line-height: 18px; font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-size: var(--smallFontSize); @@ -134,6 +135,7 @@ } .source-meta { + position: relative; vertical-align: top; width: 1px; background-clip: padding-box; @@ -501,6 +503,15 @@ border-top: 1px solid var(--barBorderColor); } +.source-viewer-bubble-popup { + top: -16px; + left: 100%; + width: 480px; + font-family: var(--baseFontFamily); + font-size: var(--baseFontSize); + text-align: left; +} + .issue-location { display: inline-block; vertical-align: top; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/types.js b/server/sonar-web/src/main/js/components/SourceViewer/types.js deleted file mode 100644 index 63b638a93e6..00000000000 --- a/server/sonar-web/src/main/js/components/SourceViewer/types.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 -/*:: -export type SourceLine = { - code: string, - conditions?: number, - coverageStatus?: string | null, - coveredConditions?: number, - duplicated: boolean, - line: number, - lineHits?: number, - scmAuthor?: string, - scmDate?: string, - scmRevision?: string -}; -*/ - -/*:: -export type Duplication = { - blocks: Array<{ - _ref: string, - from: number, - size: number - }> -}; -*/ diff --git a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx index 2cfb88cee4c..627dad9cde5 100644 --- a/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx +++ b/server/sonar-web/src/main/js/components/common/BubblePopupHelper.tsx @@ -25,7 +25,7 @@ interface Props { children?: React.ReactNode; isOpen: boolean; offset?: { vertical: number; horizontal: number }; - popup: React.ReactElement; + popup: JSX.Element; position: 'bottomleft' | 'bottomright'; togglePopup: (show: boolean) => void; } @@ -92,10 +92,10 @@ export default class BubblePopupHelper extends React.PureComponent return (
(this.container = container)} onClick={this.handleClick} - tabIndex={0} - role="tooltip"> + ref={container => (this.container = container)} + role="tooltip" + tabIndex={0}> {this.props.children} {this.props.isOpen && (
(this.popupContainer = popupContainer)}> diff --git a/server/sonar-web/src/main/js/components/common/LocationIndex.css b/server/sonar-web/src/main/js/components/common/LocationIndex.css index 21e7535b690..97bdefa079a 100644 --- a/server/sonar-web/src/main/js/components/common/LocationIndex.css +++ b/server/sonar-web/src/main/js/components/common/LocationIndex.css @@ -27,7 +27,7 @@ border-radius: 2px; background-color: #d18582; color: #fff; - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--smallFontSize); transition: background-color 0.3s ease; } diff --git a/server/sonar-web/src/main/js/components/common/LocationMessage.css b/server/sonar-web/src/main/js/components/common/LocationMessage.css index 19a9af84071..29fa3a17c3f 100644 --- a/server/sonar-web/src/main/js/components/common/LocationMessage.css +++ b/server/sonar-web/src/main/js/components/common/LocationMessage.css @@ -25,7 +25,7 @@ border-radius: 2px; background-color: #9e9e9e; color: #fff; - font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif; + font-family: var(--baseFontFamily); font-size: var(--smallFontSize); text-overflow: ellipsis; overflow: hidden; diff --git a/server/sonar-web/src/main/js/components/common/popup.js b/server/sonar-web/src/main/js/components/common/popup.js deleted file mode 100644 index b9a988b1a9a..00000000000 --- a/server/sonar-web/src/main/js/components/common/popup.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 key from 'keymaster'; - -export default Marionette.ItemView.extend({ - className: 'bubble-popup', - - onRender() { - this.$el.detach().appendTo($('body')); - const triggerEl = $(this.options.triggerEl); - if (this.options.bottom) { - this.$el.addClass('bubble-popup-bottom'); - this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - left: triggerEl.offset().left - }); - } else if (this.options.bottomRight) { - this.$el.addClass('bubble-popup-bottom-right'); - this.$el.css({ - top: triggerEl.offset().top + triggerEl.outerHeight(), - right: $(window).width() - triggerEl.offset().left - triggerEl.outerWidth() - }); - } else { - this.$el.css({ - top: triggerEl.offset().top, - left: triggerEl.offset().left + triggerEl.outerWidth() - }); - } - this.attachCloseEvents(); - }, - - attachCloseEvents() { - const that = this; - const triggerEl = $(this.options.triggerEl); - key('escape', () => { - that.destroy(); - }); - $('body').on('click.bubble-popup', () => { - $('body').off('click.bubble-popup'); - that.destroy(); - }); - triggerEl.on('click.bubble-popup', e => { - triggerEl.off('click.bubble-popup'); - e.stopPropagation(); - that.destroy(); - }); - }, - - onDestroy() { - $('body').off('click.bubble-popup'); - const triggerEl = $(this.options.triggerEl); - triggerEl.off('click.bubble-popup'); - } -}); diff --git a/server/sonar-web/src/main/js/components/issue/Issue.d.ts b/server/sonar-web/src/main/js/components/issue/Issue.d.ts new file mode 100644 index 00000000000..5abf041951b --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/Issue.d.ts @@ -0,0 +1,38 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { Issue as IssueType } from '../../app/types'; + +interface IssueProps { + branch?: string; + checked?: boolean; + displayLocationsCount?: boolean; + displayLocationsLink?: boolean; + issue: IssueType; + onChange: (issue: IssueType) => void; + onCheck?: (issueKey: string) => void; + onClick: (issueKey: string) => void; + onFilter?: (property: string, issue: IssueType) => void; + onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; + openPopup?: string; + selected: boolean; +} + +export default class Issue extends React.PureComponent {} diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index e7910c837e3..a1ccedd665c 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -19,6 +19,7 @@ */ import { flatten, sortBy } from 'lodash'; import { SEVERITIES } from './constants'; +import { Issue } from '../app/types'; interface TextRange { startLine: number; @@ -67,8 +68,6 @@ export interface RawIssue extends IssueBase { textRange?: TextRange; } -interface Issue extends IssueBase {} - export function sortBySeverity(issues: Issue[]): Issue[] { return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity)); } @@ -173,5 +172,5 @@ export function parseIssueFromResponse( ...ensureTextRange(issue), secondaryLocations, flows - }; + } as Issue; } diff --git a/server/sonar-web/src/main/js/store/favorites/duck.js b/server/sonar-web/src/main/js/store/favorites/duck.js deleted file mode 100644 index 3015e256953..00000000000 --- a/server/sonar-web/src/main/js/store/favorites/duck.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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 { uniq, without } from 'lodash'; - -/*:: -type Favorite = { key: string }; -*/ - -/*:: -type ReceiveFavoritesAction = { - type: 'RECEIVE_FAVORITES', - favorites: Array, - notFavorites: Array -}; -*/ - -/*:: -type AddFavoriteAction = { - type: 'ADD_FAVORITE', - componentKey: string -}; -*/ - -/*:: -type RemoveFavoriteAction = { - type: 'REMOVE_FAVORITE', - componentKey: string -}; -*/ - -/*:: -type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction; -*/ - -/*:: -type State = Array; -*/ - -export const actions = { - RECEIVE_FAVORITES: 'RECEIVE_FAVORITES', - ADD_FAVORITE: 'ADD_FAVORITE', - REMOVE_FAVORITE: 'REMOVE_FAVORITE' -}; - -export function receiveFavorites( - favorites /*: Array */, - notFavorites /*: Array */ = [] -) /*: ReceiveFavoritesAction */ { - return { - type: actions.RECEIVE_FAVORITES, - favorites, - notFavorites - }; -} - -export function addFavorite(componentKey /*: string */) /*: AddFavoriteAction */ { - return { - type: actions.ADD_FAVORITE, - componentKey - }; -} - -export function removeFavorite(componentKey /*: string */) /*: RemoveFavoriteAction */ { - return { - type: actions.REMOVE_FAVORITE, - componentKey - }; -} - -export default function(state /*: State */ = [], action /*: Action */) /*: State */ { - if (action.type === actions.RECEIVE_FAVORITES) { - const toAdd = action.favorites.map(f => f.key); - const toRemove = action.notFavorites.map(f => f.key); - return without(uniq([...state, ...toAdd]), ...toRemove); - } - - if (action.type === actions.ADD_FAVORITE) { - return uniq([...state, action.componentKey]); - } - - if (action.type === actions.REMOVE_FAVORITE) { - return without(state, action.componentKey); - } - - return state; -} - -export function isFavorite(state /*: State */, componentKey /*: string */) { - return state.includes(componentKey); -} diff --git a/server/sonar-web/src/main/js/store/favorites/duck.ts b/server/sonar-web/src/main/js/store/favorites/duck.ts new file mode 100644 index 00000000000..710151e4eb2 --- /dev/null +++ b/server/sonar-web/src/main/js/store/favorites/duck.ts @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { uniq, without } from 'lodash'; + +interface Favorite { + key: string; +} + +interface ReceiveFavoritesAction { + type: 'RECEIVE_FAVORITES'; + favorites: Array; + notFavorites: Array; +} + +interface AddFavoriteAction { + type: 'ADD_FAVORITE'; + componentKey: string; +} + +interface RemoveFavoriteAction { + type: 'REMOVE_FAVORITE'; + componentKey: string; +} + +type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction; + +type State = string[]; + +export function receiveFavorites( + favorites: Favorite[], + notFavorites: Favorite[] = [] +): ReceiveFavoritesAction { + return { type: 'RECEIVE_FAVORITES', favorites, notFavorites }; +} + +export function addFavorite(componentKey: string): AddFavoriteAction { + return { type: 'ADD_FAVORITE', componentKey }; +} + +export function removeFavorite(componentKey: string): RemoveFavoriteAction { + return { type: 'REMOVE_FAVORITE', componentKey }; +} + +export default function(state: State = [], action: Action): State { + if (action.type === 'RECEIVE_FAVORITES') { + const toAdd = action.favorites.map(f => f.key); + const toRemove = action.notFavorites.map(f => f.key); + return without(uniq([...state, ...toAdd]), ...toRemove); + } + + if (action.type === 'ADD_FAVORITE') { + return uniq([...state, action.componentKey]); + } + + if (action.type === 'REMOVE_FAVORITE') { + return without(state, action.componentKey); + } + + return state; +} + +export function isFavorite(state: State, componentKey: string) { + return state.includes(componentKey); +}