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: {
}
body {
- font-family: 'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif;
+ font-family: var(--baseFontFamily);
font-size: var(--baseFontSize);
line-height: 1.23076923;
}
pagePadding: '20px',
// different
+ baseFontFamily: "'Helvetica Neue', 'Segoe UI', Helvetica, Arial, sans-serif",
defaultShadow: '0 6px 12px rgba(0, 0, 0, 0.175)',
// z-index
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;
val: string;
}
+export interface FlowLocation {
+ msg: string;
+ textRange: TextRange;
+}
+
export interface Group {
default?: boolean;
description?: string;
);
}
+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;
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;
status: string;
}
+export interface TextRange {
+ startLine: number;
+ startOffset: number;
+ endLine: number;
+ endOffset: number;
+}
+
export interface User {
active: boolean;
avatar?: string;
render() {
const { branch, id, line } = this.props.location.query;
- const finalLine = line != null ? Number(line) : null;
+ const finalLine = line ? Number(line) : undefined;
return (
<div className="page page-limited">
+++ /dev/null
-/*
- * 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);
--- /dev/null
+/*
+ * 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<any>) => {
+ 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);
+++ /dev/null
-/*
- * 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<FlowLocation>,
- 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<number> },
- duplicatedFiles?: Array<{ key: string }>,
- hasSourcesAfter: boolean,
- highlightedLine: number | null,
- highlightedSymbols: Array<string>,
- issues?: Array<Issue>,
- issuesByLine: { [number]: Array<Issue> },
- issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
- loading: boolean,
- loadingSourcesAfter: boolean,
- loadingSourcesBefore: boolean,
- notAccessible: boolean,
- notExist: boolean,
- openIssuesByLine: { [number]: boolean },
- openPopup: ?{
- issue: string,
- name: string
- },
- selectedIssue?: string,
- sources?: Array<SourceLine>,
- sourceRemoved: boolean,
- symbolsByLine: { [number]: Array<string> }
-};
-*/
-
-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<Array<*>> */ {
- 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<SourceLine> */) /*: Array<SourceLine> */ {
- 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<string> */) => {
- 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<SourceLine> */) {
- const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
- return (
- <SourceViewerCode
- branch={this.props.branch}
- displayAllIssues={this.props.displayAllIssues}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- duplications={this.state.duplications}
- duplicationsByLine={this.state.duplicationsByLine}
- duplicatedFiles={this.state.duplicatedFiles}
- hasSourcesBefore={hasSourcesBefore}
- hasSourcesAfter={this.state.hasSourcesAfter}
- filterLine={this.handleFilterLine}
- highlightedLine={this.state.highlightedLine}
- highlightedLocations={this.props.highlightedLocations}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- highlightedSymbols={this.state.highlightedSymbols}
- issues={this.state.issues}
- issuesByLine={this.state.issuesByLine}
- issueLocationsByLine={this.state.issueLocationsByLine}
- loadDuplications={this.loadDuplications}
- loadSourcesAfter={this.loadSourcesAfter}
- loadSourcesBefore={this.loadSourcesBefore}
- loadingSourcesAfter={this.state.loadingSourcesAfter}
- loadingSourcesBefore={this.state.loadingSourcesBefore}
- onCoverageClick={this.handleCoverageClick}
- onDuplicationClick={this.handleDuplicationClick}
- onIssueChange={this.handleIssueChange}
- onIssueSelect={this.handleIssueSelect}
- onIssueUnselect={this.handleIssueUnselect}
- onIssuesOpen={this.handleOpenIssues}
- onIssuesClose={this.handleCloseIssues}
- onLineClick={this.handleLineClick}
- onLocationSelect={this.props.onLocationSelect}
- onPopupToggle={this.handlePopupToggle}
- openPopup={this.state.openPopup}
- onSCMClick={this.handleSCMClick}
- onSymbolClick={this.handleSymbolClick}
- openIssuesByLine={this.state.openIssuesByLine}
- scroll={this.props.scroll}
- selectedIssue={this.state.selectedIssue}
- sources={sources}
- symbolsByLine={this.state.symbolsByLine}
- />
- );
- }
-
- render() {
- const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
-
- if (loading) {
- return null;
- }
-
- if (this.state.notExist) {
- return (
- <div className="alert alert-warning spacer-top">
- {translate('component_viewer.no_component')}
- </div>
- );
- }
-
- if (notAccessible) {
- return (
- <div className="alert alert-warning spacer-top">
- {translate('code_viewer.no_source_code_displayed_due_to_security')}
- </div>
- );
- }
-
- if (component == null) {
- return null;
- }
-
- const className = classNames('source-viewer', {
- 'source-duplications-expanded': this.state.displayDuplications
- });
-
- return (
- <div className={className} ref={node => (this.node = node)}>
- <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
- {sourceRemoved && (
- <div className="alert alert-warning spacer-top">
- {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
- </div>
- )}
- {!sourceRemoved && sources != null && this.renderCode(sources)}
- </div>
- );
- }
-}
--- /dev/null
+/*
+ * 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<SourceViewerFile>;
+ loadIssues?: (
+ component: string,
+ from: number,
+ to: number,
+ branch: string | undefined
+ ) => Promise<Issue[]>;
+ loadSources?: (
+ component: string,
+ from: number,
+ to: number,
+ branch: string | undefined
+ ) => Promise<SourceLine[]>;
+ 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<Props, State> {
+ 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<SourceLine[]> => {
+ 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<State> = {
+ 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 (
+ <DuplicationPopup
+ blocks={blocks}
+ branch={this.props.branch}
+ duplicatedFiles={duplicatedFiles}
+ inRemovedComponent={inRemovedComponent}
+ onClose={this.closeLinePopup}
+ sourceViewerFile={component}
+ />
+ );
+ };
+
+ renderCode(sources: SourceLine[]) {
+ const hasSourcesBefore = sources.length > 0 && sources[0].line > 1;
+ return (
+ <SourceViewerCode
+ branch={this.props.branch}
+ componentKey={this.props.component}
+ displayAllIssues={this.props.displayAllIssues}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ displayLocationMarkers={this.props.displayLocationMarkers}
+ duplications={this.state.duplications}
+ duplicationsByLine={this.state.duplicationsByLine}
+ filterLine={this.handleFilterLine}
+ hasSourcesAfter={this.state.hasSourcesAfter}
+ hasSourcesBefore={hasSourcesBefore}
+ highlightedLine={this.state.highlightedLine}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ highlightedLocations={this.props.highlightedLocations}
+ highlightedSymbols={this.state.highlightedSymbols}
+ issueLocationsByLine={this.state.issueLocationsByLine}
+ issuePopup={this.state.issuePopup}
+ issues={this.state.issues}
+ issuesByLine={this.state.issuesByLine}
+ linePopup={this.state.linePopup}
+ loadDuplications={this.loadDuplications}
+ loadSourcesAfter={this.loadSourcesAfter}
+ loadSourcesBefore={this.loadSourcesBefore}
+ loadingSourcesAfter={this.state.loadingSourcesAfter}
+ loadingSourcesBefore={this.state.loadingSourcesBefore}
+ onIssueChange={this.handleIssueChange}
+ onIssuePopupToggle={this.handleIssuePopupToggle}
+ onIssueSelect={this.handleIssueSelect}
+ onIssueUnselect={this.handleIssueUnselect}
+ onIssuesClose={this.handleCloseIssues}
+ onIssuesOpen={this.handleOpenIssues}
+ onLinePopupToggle={this.handleLinePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.handleSymbolClick}
+ openIssuesByLine={this.state.openIssuesByLine}
+ renderDuplicationPopup={this.renderDuplicationPopup}
+ scroll={this.props.scroll}
+ selectedIssue={this.state.selectedIssue}
+ sources={sources}
+ symbolsByLine={this.state.symbolsByLine}
+ />
+ );
+ }
+
+ render() {
+ const { component, loading, sources, notAccessible, sourceRemoved } = this.state;
+
+ if (loading) {
+ return null;
+ }
+
+ if (this.state.notExist) {
+ return (
+ <div className="alert alert-warning spacer-top">
+ {translate('component_viewer.no_component')}
+ </div>
+ );
+ }
+
+ if (notAccessible) {
+ return (
+ <div className="alert alert-warning spacer-top">
+ {translate('code_viewer.no_source_code_displayed_due_to_security')}
+ </div>
+ );
+ }
+
+ if (!component) {
+ return null;
+ }
+
+ const className = classNames('source-viewer', {
+ 'source-duplications-expanded': this.state.displayDuplications
+ });
+
+ return (
+ <div className={className} ref={node => (this.node = node)}>
+ {this.state.component && (
+ <SourceViewerHeader branch={this.props.branch} sourceViewerFile={this.state.component} />
+ )}
+ {sourceRemoved && (
+ <div className="alert alert-warning spacer-top">
+ {translate('code_viewer.no_source_code_displayed_due_to_source_removed')}
+ </div>
+ )}
+ {!sourceRemoved && sources !== undefined && this.renderCode(sources)}
+ </div>
+ );
+ }
+}
+
+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);
+}
+++ /dev/null
-/*
- * 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<Duplication>,
- duplicationsByLine: { [number]: Array<number> },
- duplicatedFiles?: Array<{ key: string }>,
- filterLine?: SourceLine => boolean,
- hasSourcesAfter: boolean,
- hasSourcesBefore: boolean,
- highlightedLine: number | null,
- highlightedLocations?: Array<FlowLocation>,
- highlightedLocationMessage?: { index: number, text: string },
- highlightedSymbols: Array<string>,
- issues: Array<Issue>,
- issuesByLine: { [number]: Array<Issue> },
- issueLocationsByLine: { [number]: Array<LinearIssueLocation> },
- 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<string>) => 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<SourceLine>,
- symbolsByLine: { [number]: Array<string> }
- |};
-*/
-
- getDuplicationsForLine(line /*: SourceLine */) {
- return this.props.duplicationsByLine[line.line] || EMPTY_ARRAY;
- }
-
- getIssuesForLine(line /*: SourceLine */) /*: Array<Issue> */ {
- 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 (
- <Line
- branch={this.props.branch}
- displayAllIssues={this.props.displayAllIssues}
- displayCoverage={displayCoverage}
- displayDuplications={displayDuplications}
- displayIssues={displayIssues}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- duplications={this.getDuplicationsForLine(line)}
- duplicationsCount={duplicationsCount}
- filtered={filtered}
- highlighted={line.line === this.props.highlightedLine}
- highlightedLocationMessage={optimizedLocationMessage}
- highlightedSymbols={optimizedHighlightedSymbols}
- issueLocations={this.getIssueLocationsForLine(line)}
- issues={issuesForLine}
- key={line.line}
- last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
- line={line}
- loadDuplications={this.props.loadDuplications}
- onClick={this.props.onLineClick}
- onCoverageClick={this.props.onCoverageClick}
- onDuplicationClick={this.props.onDuplicationClick}
- onIssueChange={this.props.onIssueChange}
- onIssueSelect={this.props.onIssueSelect}
- onIssueUnselect={this.props.onIssueUnselect}
- onIssuesOpen={this.props.onIssuesOpen}
- onIssuesClose={this.props.onIssuesClose}
- onLocationSelect={this.props.onLocationSelect}
- onSCMClick={this.props.onSCMClick}
- onSymbolClick={this.props.onSymbolClick}
- openIssues={this.props.openIssuesByLine[line.line] || false}
- onPopupToggle={this.props.onPopupToggle}
- openPopup={this.props.openPopup}
- previousLine={index > 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 (
- <div>
- {this.props.hasSourcesBefore && (
- <div className="source-viewer-more-code">
- {this.props.loadingSourcesBefore ? (
- <div className="js-component-viewer-loading-before">
- <i className="spinner" />
- <span className="note spacer-left">
- {translate('source_viewer.loading_more_code')}
- </span>
- </div>
- ) : (
- <button
- className="js-component-viewer-source-before"
- onClick={this.props.loadSourcesBefore}>
- {translate('source_viewer.load_more_code')}
- </button>
- )}
- </div>
- )}
-
- <table className="source-table">
- <tbody>
- {hasFileIssues &&
- this.renderLine(ZERO_LINE, -1, hasCoverage, hasDuplications, hasIssues)}
- {sources.map((line, index) =>
- this.renderLine(line, index, hasCoverage, hasDuplications, hasIssues)
- )}
- </tbody>
- </table>
-
- {this.props.hasSourcesAfter && (
- <div className="source-viewer-more-code">
- {this.props.loadingSourcesAfter ? (
- <div className="js-component-viewer-loading-after">
- <i className="spinner" />
- <span className="note spacer-left">
- {translate('source_viewer.loading_more_code')}
- </span>
- </div>
- ) : (
- <button
- className="js-component-viewer-source-after"
- onClick={this.props.loadSourcesAfter}>
- {translate('source_viewer.load_more_code')}
- </button>
- )}
- </div>
- )}
- </div>
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ 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 (
+ <Line
+ branch={this.props.branch}
+ componentKey={this.props.componentKey}
+ displayAllIssues={this.props.displayAllIssues}
+ displayCoverage={displayCoverage}
+ displayDuplications={displayDuplications}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ displayIssues={displayIssues}
+ displayLocationMarkers={this.props.displayLocationMarkers}
+ duplications={this.getDuplicationsForLine(line)}
+ duplicationsCount={duplicationsCount}
+ filtered={filtered}
+ highlighted={line.line === this.props.highlightedLine}
+ highlightedLocationMessage={optimizedLocationMessage}
+ highlightedSymbols={optimizedHighlightedSymbols}
+ issueLocations={this.getIssueLocationsForLine(line)}
+ issuePopup={this.props.issuePopup}
+ issues={issuesForLine}
+ key={line.line}
+ last={index === this.props.sources.length - 1 && !this.props.hasSourcesAfter}
+ line={line}
+ linePopup={this.props.linePopup}
+ loadDuplications={this.props.loadDuplications}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ onIssueSelect={this.props.onIssueSelect}
+ onIssueUnselect={this.props.onIssueUnselect}
+ onIssuesClose={this.props.onIssuesClose}
+ onIssuesOpen={this.props.onIssuesOpen}
+ onLinePopupToggle={this.props.onLinePopupToggle}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.props.onSymbolClick}
+ openIssues={this.props.openIssuesByLine[line.line] || false}
+ previousLine={index > 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 (
+ <div>
+ {this.props.hasSourcesBefore && (
+ <div className="source-viewer-more-code">
+ {this.props.loadingSourcesBefore ? (
+ <div className="js-component-viewer-loading-before">
+ <i className="spinner" />
+ <span className="note spacer-left">
+ {translate('source_viewer.loading_more_code')}
+ </span>
+ </div>
+ ) : (
+ <Button
+ className="js-component-viewer-source-before"
+ onClick={this.props.loadSourcesBefore}>
+ {translate('source_viewer.load_more_code')}
+ </Button>
+ )}
+ </div>
+ )}
+
+ <table className="source-table">
+ <tbody>
+ {hasFileIssues &&
+ this.renderLine({
+ line: ZERO_LINE,
+ index: -1,
+ displayCoverage,
+ displayDuplications,
+ displayIssues
+ })}
+ {sources.map((line, index) =>
+ this.renderLine({ line, index, displayCoverage, displayDuplications, displayIssues })
+ )}
+ </tbody>
+ </table>
+
+ {this.props.hasSourcesAfter && (
+ <div className="source-viewer-more-code">
+ {this.props.loadingSourcesAfter ? (
+ <div className="js-component-viewer-loading-after">
+ <i className="spinner" />
+ <span className="note spacer-left">
+ {translate('source_viewer.loading_more_code')}
+ </span>
+ </div>
+ ) : (
+ <Button
+ className="js-component-viewer-source-after"
+ onClick={this.props.loadSourcesAfter}>
+ {translate('source_viewer.load_more_code')}
+ </Button>
+ )}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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<Props, State> {
+ 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<HTMLAnchorElement>) => {
+ 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 (
+ <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
+ <div className="bubble-popup-title">
+ {translate('source_viewer.covered')}
+ {!!line.conditions && (
+ <div>
+ {'('}
+ {line.coveredConditions || '0'}
+ {' of '}
+ {line.conditions} {translate('source_viewer.conditions')}
+ {')'}
+ </div>
+ )}
+ </div>
+ {this.state.loading ? (
+ <i className="spinner" />
+ ) : (
+ <>
+ {testFiles.length === 0 &&
+ translate('source_viewer.tooltip.no_information_about_tests')}
+ {testFiles.map(testFile => (
+ <div className="bubble-popup-section" key={testFile.file.key}>
+ <a
+ data-key={testFile.file.key}
+ href="#"
+ onClick={this.handleTestClick}
+ title={testFile.file.longName}>
+ <span>{collapsePath(testFile.file.longName)}</span>
+ </a>
+ <ul className="bubble-popup-list">
+ {testFile.tests.map(testCase => (
+ <li
+ className="component-viewer-popup-test"
+ key={testCase.id}
+ title={testCase.name}>
+ <TestStatusIcon className="spacer-right" status={testCase.status} />
+ {testCase.name}
+ {testCase.status !== 'SKIPPED' && (
+ <span className="spacer-left note">{testCase.durationInMs}ms</span>
+ )}
+ </li>
+ ))}
+ </ul>
+ </div>
+ ))}
+ </>
+ )}
+ </BubblePopup>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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<Props> {
+ 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<HTMLAnchorElement>) => {
+ 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 (
+ <BubblePopup customClass="source-viewer-bubble-popup" position={this.props.popupPosition}>
+ <div className="bubble-popup-container">
+ {this.props.inRemovedComponent && (
+ <div className="alert alert-warning">
+ {translate('duplications.dups_found_on_deleted_resource')}
+ </div>
+ )}
+ {duplications.length > 0 && (
+ <>
+ <div className="bubble-popup-title">
+ {translate('component_viewer.transition.duplication')}
+ </div>
+ {duplications.map(duplication => (
+ <div className="bubble-popup-section" key={duplication.file.key}>
+ <div className="component-name">
+ {this.isDifferentComponent(duplication.file, this.props.sourceViewerFile) && (
+ <>
+ <div className="component-name-parent">
+ <QualifierIcon className="little-spacer-right" qualifier="TRK" />
+ <Link to={getProjectUrl(duplication.file.project)}>
+ {duplication.file.projectName}
+ </Link>
+ </div>
+ {duplication.file.subProject &&
+ duplication.file.subProjectName && (
+ <div className="component-name-parent">
+ <QualifierIcon className="little-spacer-right" qualifier="BRC" />
+ <Link to={getProjectUrl(duplication.file.subProject)}>
+ {duplication.file.subProjectName}
+ </Link>
+ </div>
+ )}
+ </>
+ )}
+
+ {duplication.file.key !== this.props.sourceViewerFile.key && (
+ <div className="component-name-path">
+ <a
+ className="link-action"
+ data-key={duplication.file.key}
+ href="#"
+ onClick={this.handleFileClick}
+ title={duplication.file.name}>
+ <span>{collapsedDirFromPath(duplication.file.name)}</span>
+ <span className="component-name-file">
+ {fileFromPath(duplication.file.name)}
+ </span>
+ </a>
+ </div>
+ )}
+
+ <div className="component-name-path">
+ {'Lines: '}
+ {duplication.blocks.map((block, index) => (
+ <React.Fragment key={index}>
+ <a
+ data-key={duplication.file.key}
+ data-line={block.from}
+ href="#"
+ onClick={this.handleFileClick}>
+ {block.from}
+ {' – '}
+ {block.from + block.size - 1}
+ </a>
+ {index < duplication.blocks.length - 1 && ', '}
+ </React.Fragment>
+ ))}
+ </div>
+ </div>
+ </div>
+ ))}
+ </>
+ )}
+ </div>
+ </BubblePopup>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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<number>,
- duplicationsCount: number,
- filtered: boolean | null,
- highlighted: boolean,
- highlightedLocationMessage?: { index: number, text: string },
- highlightedSymbols?: Array<string>,
- issueLocations: Array<LinearIssueLocation>,
- issues: Array<Issue>,
- 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<string>) => 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 (
- <tr className={className} data-line-number={line.line}>
- <LineNumber line={line} onClick={this.props.onClick} />
-
- <LineSCM
- line={line}
- onClick={this.props.onSCMClick}
- previousLine={this.props.previousLine}
- />
-
- {this.props.displayCoverage && (
- <LineCoverage line={line} onClick={this.props.onCoverageClick} />
- )}
-
- {this.props.displayDuplications && (
- <LineDuplications line={line} onClick={this.props.loadDuplications} />
- )}
-
- {times(duplicationsCount).map(index => (
- <LineDuplicationBlock
- duplicated={duplications.includes(index)}
- index={index}
- key={index}
- line={this.props.line}
- onClick={this.props.onDuplicationClick}
- />
- ))}
-
- {this.props.displayIssues &&
- !this.props.displayAllIssues && (
- <LineIssuesIndicator
- issues={this.props.issues}
- line={line}
- onClick={this.handleIssuesIndicatorClick}
- />
- )}
-
- <LineCode
- branch={this.props.branch}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- displayLocationMarkers={this.props.displayLocationMarkers}
- highlightedLocationMessage={this.props.highlightedLocationMessage}
- highlightedSymbols={this.props.highlightedSymbols}
- issues={this.props.issues}
- issueLocations={this.props.issueLocations}
- line={line}
- onIssueChange={this.props.onIssueChange}
- onIssueSelect={this.props.onIssueSelect}
- onLocationSelect={this.props.onLocationSelect}
- onSymbolClick={this.props.onSymbolClick}
- onPopupToggle={this.props.onPopupToggle}
- openPopup={this.props.openPopup}
- scroll={this.props.scroll}
- secondaryIssueLocations={this.props.secondaryIssueLocations}
- selectedIssue={this.props.selectedIssue}
- showIssues={this.props.openIssues || this.props.displayAllIssues}
- />
- </tr>
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ 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 (
+ <tr className={className} data-line-number={line.line}>
+ <LineNumber
+ branch={this.props.branch}
+ componentKey={this.props.componentKey}
+ line={line}
+ onPopupToggle={this.props.onLinePopupToggle}
+ popupOpen={this.isPopupOpen('line-number')}
+ />
+
+ <LineSCM
+ line={line}
+ onPopupToggle={this.props.onLinePopupToggle}
+ popupOpen={this.isPopupOpen('scm')}
+ previousLine={this.props.previousLine}
+ />
+
+ {this.props.displayCoverage && (
+ <LineCoverage
+ branch={this.props.branch}
+ componentKey={this.props.componentKey}
+ line={line}
+ onPopupToggle={this.props.onLinePopupToggle}
+ popupOpen={this.isPopupOpen('coverage')}
+ />
+ )}
+
+ {this.props.displayDuplications && (
+ <LineDuplications line={line} onClick={this.props.loadDuplications} />
+ )}
+
+ {times(duplicationsCount).map(index => (
+ <LineDuplicationBlock
+ duplicated={duplications.includes(index)}
+ index={index}
+ key={index}
+ line={this.props.line}
+ onPopupToggle={this.props.onLinePopupToggle}
+ popupOpen={this.isPopupOpen('duplications', index)}
+ renderDuplicationPopup={this.props.renderDuplicationPopup}
+ />
+ ))}
+
+ {this.props.displayIssues &&
+ !this.props.displayAllIssues && (
+ <LineIssuesIndicator
+ issues={this.props.issues}
+ line={line}
+ onClick={this.handleIssuesIndicatorClick}
+ />
+ )}
+
+ <LineCode
+ branch={this.props.branch}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ displayLocationMarkers={this.props.displayLocationMarkers}
+ highlightedLocationMessage={this.props.highlightedLocationMessage}
+ highlightedSymbols={this.props.highlightedSymbols}
+ issueLocations={this.props.issueLocations}
+ issuePopup={issuePopup}
+ issues={this.props.issues}
+ line={line}
+ onIssueChange={this.props.onIssueChange}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ onIssueSelect={this.props.onIssueSelect}
+ onLocationSelect={this.props.onLocationSelect}
+ onSymbolClick={this.props.onSymbolClick}
+ scroll={this.props.scroll}
+ secondaryIssueLocations={this.props.secondaryIssueLocations}
+ selectedIssue={this.props.selectedIssue}
+ showIssues={this.props.openIssues || this.props.displayAllIssues}
+ />
+ </tr>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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<string>,
- issues: Array<Issue>,
- issueLocations: Array<LinearIssueLocation>,
- line: SourceLine,
- onIssueChange: Issue => void,
- onIssueSelect: (issueKey: string) => void,
- onLocationSelect?: number => void,
- onSymbolClick: (Array<string>) => 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<HTMLElement>; */
-
- 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 (
- <LocationIndex
- key={`marker-${index}`}
- leading={leading}
- onClick={onClick}
- selected={message != null}>
- <span href="#" ref={ref}>
- {index + 1}
- </span>
- {message != null && <LocationMessage selected={true}>{message}</LocationMessage>}
- </LocationIndex>
- );
- }
-
- 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(
- <span className={token.className} key={index}>
- {token.text}
- </span>
- );
-
- // keep leadingMarker truthy if previous token has only whitespaces
- leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
- });
-
- return (
- <td className={className} data-line-number={line.line}>
- <div className="source-line-code-inner">
- <pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre>
- </div>
- {showIssues &&
- issues.length > 0 && (
- <LineIssuesList
- branch={this.props.branch}
- displayIssueLocationsCount={this.props.displayIssueLocationsCount}
- displayIssueLocationsLink={this.props.displayIssueLocationsLink}
- issues={issues}
- onIssueChange={this.props.onIssueChange}
- onIssueClick={onIssueSelect}
- onPopupToggle={this.props.onPopupToggle}
- openPopup={this.props.openPopup}
- selectedIssue={selectedIssue}
- />
- )}
- </td>
- );
- }
-}
--- /dev/null
+/*
+ * 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<string>) => 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<Props, State> {
+ activeMarkerNode?: HTMLElement | null;
+ codeNode?: HTMLElement | null;
+ symbols?: NodeListOf<HTMLElement>;
+
+ 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 (
+ <LocationIndex
+ key={`marker-${index}`}
+ leading={leading}
+ onClick={onClick}
+ selected={message != null}>
+ <span ref={ref}>{index + 1}</span>
+ {message != null && <LocationMessage selected={true}>{message}</LocationMessage>}
+ </LocationIndex>
+ );
+ }
+
+ 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(
+ <span className={token.className} key={index}>
+ {token.text}
+ </span>
+ );
+
+ // keep leadingMarker truthy if previous token has only whitespaces
+ leadingMarker = (index === 0 ? true : leadingMarker) && !token.text.trim().length;
+ });
+
+ return (
+ <td className={className} data-line-number={line.line}>
+ <div className="source-line-code-inner">
+ <pre ref={node => (this.codeNode = node)}>{renderedTokens}</pre>
+ </div>
+ {showIssues &&
+ issues.length > 0 && (
+ <LineIssuesList
+ branch={this.props.branch}
+ displayIssueLocationsCount={this.props.displayIssueLocationsCount}
+ displayIssueLocationsLink={this.props.displayIssueLocationsLink}
+ issuePopup={this.props.issuePopup}
+ issues={issues}
+ onIssueChange={this.props.onIssueChange}
+ onIssueClick={onIssueSelect}
+ onIssuePopupToggle={this.props.onIssuePopupToggle}
+ selectedIssue={selectedIssue}
+ />
+ )}
+ </td>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 = (
- <td
- className={className}
- data-line-number={line.line}
- role={hasPopup ? 'button' : undefined}
- tabIndex={hasPopup ? 0 : undefined}
- onClick={hasPopup ? this.handleClick : undefined}>
- <div className="source-line-bar" />
- </td>
- );
-
- return line.coverageStatus != null ? (
- <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right">
- {cell}
- </Tooltip>
- ) : (
- cell
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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 ? (
+ <Tooltip overlay={translate('source_viewer.tooltip', line.coverageStatus)} placement="right">
+ <div className="source-line-bar" />
+ </Tooltip>
+ ) : (
+ <div className="source-line-bar" />
+ );
+
+ if (hasPopup) {
+ return (
+ <td
+ className={className}
+ data-line-number={line.line}
+ onClick={this.handleClick}
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+ role="button"
+ tabIndex={0}>
+ {cell}
+ <BubblePopupHelper
+ isOpen={popupOpen}
+ popup={
+ <CoveragePopup
+ branch={branch}
+ componentKey={componentKey}
+ line={line}
+ onClose={this.closePopup}
+ />
+ }
+ position="bottomright"
+ togglePopup={this.handleTogglePopup}
+ />
+ </td>
+ );
+ }
+
+ return (
+ <td className={className} data-line-number={line.line}>
+ {cell}
+ </td>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 = (
- <td
- key={index}
- className={className}
- data-line-number={line.line}
- data-index={index}
- role={duplicated ? 'button' : undefined}
- tabIndex={duplicated ? '0' : undefined}
- onClick={duplicated ? this.handleClick : undefined}>
- <div className="source-line-bar" />
- </td>
- );
-
- return duplicated ? (
- <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
- {cell}
- </Tooltip>
- ) : (
- cell
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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 = <div className="source-line-bar" />;
+
+ return duplicated ? (
+ <td
+ className={className}
+ data-index={index}
+ data-line-number={line.line}
+ onClick={this.handleClick}
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+ role="button"
+ tabIndex={0}>
+ <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
+ {cell}
+ </Tooltip>
+ <BubblePopupHelper
+ isOpen={popupOpen}
+ popup={this.props.renderDuplicationPopup(index, line.line)}
+ position="bottomright"
+ togglePopup={this.handleTogglePopup}
+ />
+ </td>
+ ) : (
+ <td className={className} data-index={index} data-line-number={line.line}>
+ {cell}
+ </td>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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 = (
- <td
- className={className}
- role={line.duplicated ? 'button' : undefined}
- tabIndex={line.duplicated ? 0 : undefined}
- onClick={line.duplicated ? this.handleClick : undefined}>
- <div className="source-line-bar" />
- </td>
- );
-
- return line.duplicated ? (
- <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
- {cell}
- </Tooltip>
- ) : (
- cell
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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 = (
+ <td
+ className={className}
+ onClick={line.duplicated ? this.handleClick : undefined}
+ role={line.duplicated ? 'button' : undefined}
+ tabIndex={line.duplicated ? 0 : undefined}>
+ <div className="source-line-bar" />
+ </td>
+ );
+
+ return line.duplicated ? (
+ <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
+ {cell}
+ </Tooltip>
+ ) : (
+ cell
+ );
+ }
+}
+++ /dev/null
-/*
- * 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<Issue>,
- line: SourceLine,
- onClick: () => void
-};
-*/
-
-export default class LineIssuesIndicator extends React.PureComponent {
- /*:: props: Props; */
-
- handleClick = (e /*: SyntheticInputEvent */) => {
- e.preventDefault();
- this.props.onClick();
- };
-
- render() {
- const { issues, line } = this.props;
- const hasIssues = issues.length > 0;
- const className = classNames('source-meta', 'source-line-issues', {
- 'source-line-with-issues': hasIssues
- });
- const mostImportantIssue = hasIssues ? sortBySeverity(issues)[0] : null;
-
- return (
- <td
- className={className}
- data-line-number={line.line}
- role={hasIssues ? 'button' : undefined}
- tabIndex={hasIssues ? '0' : undefined}
- onClick={hasIssues ? this.handleClick : undefined}>
- {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
- {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
- </td>
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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 (
+ <td
+ className={className}
+ data-line-number={line.line}
+ onClick={hasIssues ? this.handleClick : undefined}
+ role={hasIssues ? 'button' : undefined}
+ tabIndex={hasIssues ? 0 : undefined}>
+ {mostImportantIssue != null && <SeverityIcon severity={mostImportantIssue.severity} />}
+ {issues.length > 1 && <span className="source-line-issues-counter">{issues.length}</span>}
+ </td>
+ );
+ }
+}
+++ /dev/null
-/*
- * 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<IssueType>,
- 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 (
- <div className="issue-list">
- {issues.map(issue => (
- <Issue
- branch={branch}
- displayLocationsCount={this.props.displayIssueLocationsCount}
- displayLocationsLink={this.props.displayIssueLocationsLink}
- issue={issue}
- key={issue.key}
- onChange={this.props.onIssueChange}
- onClick={onIssueClick}
- onPopupToggle={this.props.onPopupToggle}
- openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : null}
- selected={selectedIssue === issue.key}
- />
- ))}
- </div>
- );
- }
-}
--- /dev/null
+/*
+ * 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 (
+ <div className="issue-list">
+ {props.issues.map(issue => (
+ <Issue
+ branch={props.branch}
+ displayLocationsCount={props.displayIssueLocationsCount}
+ displayLocationsLink={props.displayIssueLocationsLink}
+ issue={issue}
+ key={issue.key}
+ onChange={props.onIssueChange}
+ onClick={props.onIssueClick}
+ onPopupToggle={props.onIssuePopupToggle}
+ openPopup={issuePopup && issuePopup.issue === issue.key ? issuePopup.name : undefined}
+ selected={props.selectedIssue === issue.key}
+ />
+ ))}
+ </div>
+ );
+}
+++ /dev/null
-/*
- * 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 (
- <td
- className="source-meta source-line-number"
- /* don't display 0 */
- data-line-number={line ? line : undefined}
- role={line ? 'button' : undefined}
- tabIndex={line ? 0 : undefined}
- onClick={line ? this.handleClick : undefined}
- />
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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 ? (
+ <td
+ className="source-meta source-line-number"
+ data-line-number={lineNumber}
+ onClick={this.handleClick}
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+ role="button"
+ tabIndex={0}>
+ <BubblePopupHelper
+ isOpen={popupOpen}
+ popup={<LineOptionsPopup branch={branch} componentKey={componentKey} line={line} />}
+ position="bottomright"
+ togglePopup={this.handleTogglePopup}
+ />
+ </td>
+ ) : (
+ <td className="source-meta source-line-number" />
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 (
+ <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
+ <div className="bubble-popup-section">
+ <Link className="js-get-permalink" to={permalink}>
+ {translate('component_viewer.get_permalink')}
+ </Link>
+ </div>
+ </BubblePopup>
+ );
+}
+++ /dev/null
-/*
- * 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 (
- <td
- className="source-meta source-line-scm"
- data-line-number={line.line}
- role={clickable ? 'button' : undefined}
- tabIndex={clickable ? 0 : undefined}
- onClick={clickable ? this.handleClick : undefined}>
- {this.isSCMChanged(line, previousLine) && (
- <div className="source-line-scm-inner" data-author={line.scmAuthor} />
- )}
- </td>
- );
- }
-}
--- /dev/null
+/*
+ * 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<Props> {
+ handleClick = (event: React.MouseEvent<HTMLElement>) => {
+ 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) && (
+ <div className="source-line-scm-inner" data-author={line.scmAuthor} />
+ );
+ return hasPopup ? (
+ <td
+ className="source-meta source-line-scm"
+ data-line-number={line.line}
+ onClick={this.handleClick}
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
+ role="button"
+ tabIndex={0}>
+ {cell}
+ <BubblePopupHelper
+ isOpen={popupOpen}
+ popup={<SCMPopup line={line} />}
+ position="bottomright"
+ togglePopup={this.handleTogglePopup}
+ />
+ </td>
+ ) : (
+ <td className="source-meta source-line-scm" data-line-number={line.line}>
+ {cell}
+ </td>
+ );
+ }
+}
+
+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;
+}
--- /dev/null
+/*
+ * 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 (
+ <BubblePopup customClass="source-viewer-bubble-popup" position={popupPosition}>
+ <div className="bubble-popup-section">{line.scmAuthor}</div>
+ {line.scmDate && (
+ <div className="bubble-popup-section">
+ <DateFormatter date={line.scmDate} />
+ </div>
+ )}
+ {line.scmRevision && <div className="bubble-popup-section">{line.scmRevision}</div>}
+ </BubblePopup>
+ );
+}
+++ /dev/null
-/*
- * 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: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
- };
- const issueLocations = [{ from: 0, to: 5, line: 3 }];
- const wrapper = shallow(
- <LineCode
- highlightedSymbols={['sym1']}
- issues={[{ key: 'issue-1' }, { key: 'issue-2' }]}
- issueLocations={issueLocations}
- line={line}
- onIssueSelect={jest.fn()}
- onSelectLocation={jest.fn()}
- onSymbolClick={jest.fn()}
- onPopupToggle={jest.fn()}
- openPopup={null}
- selectedIssue="issue-1"
- showIssues={true}
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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: '<span class="k">class</span> <span class="sym sym-1">Foo</span> {'
+ };
+ const issueLocations = [{ from: 0, to: 5, line: 3 }];
+ const wrapper = shallow(
+ <LineCode
+ branch="feature"
+ highlightedLocationMessage={undefined}
+ highlightedSymbols={['sym1']}
+ issueLocations={issueLocations}
+ issuePopup={undefined}
+ issues={[{ ...issueBase, key: 'issue-1' }, { ...issueBase, key: 'issue-2' }]}
+ line={line}
+ onIssueChange={jest.fn()}
+ onIssuePopupToggle={jest.fn()}
+ onIssueSelect={jest.fn()}
+ onLocationSelect={jest.fn()}
+ onSymbolClick={jest.fn()}
+ secondaryIssueLocations={[]}
+ selectedIssue="issue-1"
+ showIssues={true}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<LineCoverage line={line} onClick={onClick} />);
- 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(<LineCoverage line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('render line with unknown coverage', () => {
- const line = { line: 3 };
- const onClick = jest.fn();
- const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <LineCoverage
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper.find('[tabIndex]'));
+});
+
+it('render uncovered line', () => {
+ const line: SourceLine = { line: 3, coverageStatus: 'uncovered' };
+ const wrapper = shallow(
+ <LineCoverage
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('render line with unknown coverage', () => {
+ const line: SourceLine = { line: 3 };
+ const wrapper = shallow(
+ <LineCoverage
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should open coverage popup', () => {
+ const line: SourceLine = { line: 3, coverageStatus: 'covered' };
+ const onPopupToggle = jest.fn();
+ const wrapper = shallow(
+ <LineCoverage
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={onPopupToggle}
+ popupOpen={false}
+ />
+ );
+ click(wrapper.find('[role="button"]'));
+ expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'coverage' });
+});
+++ /dev/null
-/*
- * 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(
- <LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} />
- );
- 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(
- <LineDuplicationBlock index={1} duplicated={false} line={line} onClick={onClick} />
- );
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <LineDuplicationBlock
+ duplicated={true}
+ index={1}
+ line={line}
+ onPopupToggle={onPopupToggle}
+ popupOpen={false}
+ renderDuplicationPopup={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper.find('[tabIndex]'));
+ expect(onPopupToggle).toHaveBeenCalled();
+});
+
+it('render not duplicated line', () => {
+ const line = { line: 3, duplicated: false };
+ const wrapper = shallow(
+ <LineDuplicationBlock
+ duplicated={false}
+ index={1}
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ renderDuplicationPopup={jest.fn()}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<LineDuplications line={line} onClick={onClick} />);
- 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(<LineDuplications line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(<LineDuplications line={line} onClick={onClick} />);
+ 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(<LineDuplications line={line} onClick={onClick} />);
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-
- click(wrapper);
- expect(onClick).toHaveBeenCalled();
-
- const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
- wrapper.setProps({ issues: nextIssues });
- expect(wrapper).toMatchSnapshot();
-});
-
-it('no issues', () => {
- const line = { line: 3 };
- const issues = [];
- const onClick = jest.fn();
- const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
+ expect(wrapper).toMatchSnapshot();
+
+ click(wrapper);
+ expect(onClick).toHaveBeenCalled();
+
+ const nextIssues = [{ severity: 'MINOR' }, { severity: 'INFO' }];
+ wrapper.setProps({ issues: nextIssues });
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('no issues', () => {
+ const line = { line: 3 };
+ const issues: Issue[] = [];
+ const onClick = jest.fn();
+ const wrapper = shallow(<LineIssuesIndicator issues={issues} line={line} onClick={onClick} />);
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(
- <LineIssuesList
- issues={issues}
- line={line}
- onIssueClick={onIssueClick}
- onPopupToggle={jest.fn()}
- openPopup={null}
- selectedIssue="foo"
- />
- );
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <LineIssuesList
+ branch={undefined}
+ issuePopup={undefined}
+ issues={issues}
+ onIssueChange={jest.fn()}
+ onIssueClick={onIssueClick}
+ onIssuePopupToggle={jest.fn()}
+ selectedIssue="foo"
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<LineNumber line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
- click(wrapper);
- expect(onClick).toHaveBeenCalled();
-});
-
-it('render line 0', () => {
- const line = { line: 0 };
- const onClick = jest.fn();
- const wrapper = shallow(<LineNumber line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <LineNumber
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+ click(wrapper);
+});
+
+it('render line 0', () => {
+ const line = { line: 0 };
+ const wrapper = shallow(
+ <LineNumber
+ branch={undefined}
+ componentKey="foo"
+ line={line}
+ onPopupToggle={jest.fn()}
+ popupOpen={false}
+ />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
--- /dev/null
+/*
+ * 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(<LineOptionsPopup branch="feature" componentKey="foo" line={line} />);
+ expect(wrapper).toMatchSnapshot();
+});
+++ /dev/null
-/*
- * 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(<LineSCM line={line} onClick={onClick} previousLine={previousLine} />);
- expect(wrapper).toMatchSnapshot();
- click(wrapper);
- expect(onClick).toHaveBeenCalled();
-});
-
-it('render scm details for the first line', () => {
- const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
- const onClick = jest.fn();
- const wrapper = shallow(<LineSCM line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('does not render scm details', () => {
- const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
- const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' };
- const onClick = jest.fn();
- const wrapper = shallow(<LineSCM line={line} onClick={onClick} previousLine={previousLine} />);
- expect(wrapper).toMatchSnapshot();
-});
-
-it('does not allow to click', () => {
- const line = { scmAuthor: 'foo', scmDate: '2017-01-01' };
- const onClick = jest.fn();
- const wrapper = shallow(<LineSCM line={line} onClick={onClick} />);
- expect(wrapper).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * 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(
+ <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('render scm details for the first line', () => {
+ const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+ const wrapper = shallow(
+ <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
+ );
+ 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(
+ <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+ );
+ expect(wrapper).toMatchSnapshot();
+});
+
+it('should open popup', () => {
+ const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+ const onPopupToggle = jest.fn();
+ const wrapper = shallow(
+ <LineSCM line={line} onPopupToggle={onPopupToggle} popupOpen={false} previousLine={undefined} />
+ );
+ click(wrapper.find('[role="button"]'));
+ expect(onPopupToggle).toBeCalledWith({ line: 3, name: 'scm' });
+});
--- /dev/null
+/*
+ * 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(<SCMPopup line={line} />)).toMatchSnapshot();
+});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render code 1`] = `
-<td
- className="source-line-code code has-issues"
- data-line-number={3}
->
- <div
- className="source-line-code-inner"
- >
- <pre>
- <span
- className="k source-line-code-issue"
- key="0"
- >
- class
- </span>
- <span
- className=""
- key="1"
- >
-
- </span>
- <span
- className="sym sym-1"
- key="2"
- >
- Foo
- </span>
- <span
- className=""
- key="3"
- >
- {
- </span>
- </pre>
- </div>
- <LineIssuesList
- issues={
- Array [
- Object {
- "key": "issue-1",
- },
- Object {
- "key": "issue-2",
- },
- ]
- }
- onIssueClick={[MockFunction]}
- onPopupToggle={[MockFunction]}
- openPopup={null}
- selectedIssue="issue-1"
- />
-</td>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render code 1`] = `
+<td
+ className="source-line-code code has-issues"
+ data-line-number={3}
+>
+ <div
+ className="source-line-code-inner"
+ >
+ <pre>
+ <span
+ className="k source-line-code-issue"
+ key="0"
+ >
+ class
+ </span>
+ <span
+ className=""
+ key="1"
+ >
+
+ </span>
+ <span
+ className="sym sym-1"
+ key="2"
+ >
+ Foo
+ </span>
+ <span
+ className=""
+ key="3"
+ >
+ {
+ </span>
+ </pre>
+ </div>
+ <LineIssuesList
+ branch="feature"
+ issues={
+ Array [
+ Object {
+ "component": "",
+ "componentLongName": "",
+ "componentQualifier": "",
+ "componentUuid": "",
+ "creationDate": "",
+ "flows": Array [],
+ "key": "issue-1",
+ "message": "",
+ "organization": "",
+ "project": "",
+ "projectName": "",
+ "projectOrganization": "",
+ "projectUuid": "",
+ "rule": "",
+ "ruleName": "",
+ "secondaryLocations": Array [],
+ "severity": "",
+ "status": "",
+ "type": "",
+ },
+ Object {
+ "component": "",
+ "componentLongName": "",
+ "componentQualifier": "",
+ "componentUuid": "",
+ "creationDate": "",
+ "flows": Array [],
+ "key": "issue-2",
+ "message": "",
+ "organization": "",
+ "project": "",
+ "projectName": "",
+ "projectOrganization": "",
+ "projectUuid": "",
+ "rule": "",
+ "ruleName": "",
+ "secondaryLocations": Array [],
+ "severity": "",
+ "status": "",
+ "type": "",
+ },
+ ]
+ }
+ onIssueChange={[MockFunction]}
+ onIssueClick={[MockFunction]}
+ onIssuePopupToggle={[MockFunction]}
+ selectedIssue="issue-1"
+ />
+</td>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render covered line 1`] = `
-<Tooltip
- overlay="source_viewer.tooltip.covered"
- placement="right"
->
- <td
- className="source-meta source-line-coverage source-line-covered"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
- >
- <div
- className="source-line-bar"
- />
- </td>
-</Tooltip>
-`;
-
-exports[`render line with unknown coverage 1`] = `
-<td
- className="source-meta source-line-coverage"
- data-line-number={3}
->
- <div
- className="source-line-bar"
- />
-</td>
-`;
-
-exports[`render uncovered line 1`] = `
-<Tooltip
- overlay="source_viewer.tooltip.uncovered"
- placement="right"
->
- <td
- className="source-meta source-line-coverage source-line-uncovered"
- data-line-number={3}
- >
- <div
- className="source-line-bar"
- />
- </td>
-</Tooltip>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render covered line 1`] = `
+<td
+ className="source-meta source-line-coverage source-line-covered"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <Tooltip
+ overlay="source_viewer.tooltip.covered"
+ placement="right"
+ >
+ <div
+ className="source-line-bar"
+ />
+ </Tooltip>
+ <BubblePopupHelper
+ isOpen={false}
+ popup={
+ <CoveragePopup
+ branch={undefined}
+ componentKey="foo"
+ line={
+ Object {
+ "coverageStatus": "covered",
+ "line": 3,
+ }
+ }
+ onClose={[Function]}
+ />
+ }
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
+
+exports[`render line with unknown coverage 1`] = `
+<td
+ className="source-meta source-line-coverage"
+ data-line-number={3}
+>
+ <div
+ className="source-line-bar"
+ />
+</td>
+`;
+
+exports[`render uncovered line 1`] = `
+<td
+ className="source-meta source-line-coverage source-line-uncovered"
+ data-line-number={3}
+>
+ <Tooltip
+ overlay="source_viewer.tooltip.uncovered"
+ placement="right"
+ >
+ <div
+ className="source-line-bar"
+ />
+ </Tooltip>
+</td>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
- overlay="source_viewer.tooltip.duplicated_block"
- placement="right"
->
- <td
- className="source-meta source-line-duplications-extra source-line-duplicated"
- data-index={1}
- data-line-number={3}
- key="1"
- onClick={[Function]}
- role="button"
- tabIndex="0"
- >
- <div
- className="source-line-bar"
- />
- </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
- className="source-meta source-line-duplications-extra"
- data-index={1}
- data-line-number={3}
- key="1"
->
- <div
- className="source-line-bar"
- />
-</td>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render duplicated line 1`] = `
+<td
+ className="source-meta source-line-duplications-extra source-line-duplicated"
+ data-index={1}
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <Tooltip
+ overlay="source_viewer.tooltip.duplicated_block"
+ placement="right"
+ >
+ <div
+ className="source-line-bar"
+ />
+ </Tooltip>
+ <BubblePopupHelper
+ isOpen={false}
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
+
+exports[`render not duplicated line 1`] = `
+<td
+ className="source-meta source-line-duplications-extra"
+ data-index={1}
+ data-line-number={3}
+>
+ <div
+ className="source-line-bar"
+ />
+</td>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render duplicated line 1`] = `
-<Tooltip
- overlay="source_viewer.tooltip.duplicated_line"
- placement="right"
->
- <td
- className="source-meta source-line-duplications source-line-duplicated"
- onClick={[Function]}
- role="button"
- tabIndex={0}
- >
- <div
- className="source-line-bar"
- />
- </td>
-</Tooltip>
-`;
-
-exports[`render not duplicated line 1`] = `
-<td
- className="source-meta source-line-duplications"
->
- <div
- className="source-line-bar"
- />
-</td>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render duplicated line 1`] = `
+<Tooltip
+ overlay="source_viewer.tooltip.duplicated_line"
+ placement="right"
+>
+ <td
+ className="source-meta source-line-duplications source-line-duplicated"
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+ >
+ <div
+ className="source-line-bar"
+ />
+ </td>
+</Tooltip>
+`;
+
+exports[`render not duplicated line 1`] = `
+<td
+ className="source-meta source-line-duplications"
+>
+ <div
+ className="source-line-bar"
+ />
+</td>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`no issues 1`] = `
-<td
- className="source-meta source-line-issues"
- data-line-number={3}
-/>
-`;
-
-exports[`render highest severity 1`] = `
-<td
- className="source-meta source-line-issues source-line-with-issues"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex="0"
->
- <SeverityIcon
- severity="CRITICAL"
- />
- <span
- className="source-line-issues-counter"
- >
- 2
- </span>
-</td>
-`;
-
-exports[`render highest severity 2`] = `
-<td
- className="source-meta source-line-issues source-line-with-issues"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex="0"
->
- <SeverityIcon
- severity="MINOR"
- />
- <span
- className="source-line-issues-counter"
- >
- 2
- </span>
-</td>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`no issues 1`] = `
+<td
+ className="source-meta source-line-issues"
+ data-line-number={3}
+/>
+`;
+
+exports[`render highest severity 1`] = `
+<td
+ className="source-meta source-line-issues source-line-with-issues"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <SeverityIcon
+ severity="CRITICAL"
+ />
+ <span
+ className="source-line-issues-counter"
+ >
+ 2
+ </span>
+</td>
+`;
+
+exports[`render highest severity 2`] = `
+<td
+ className="source-meta source-line-issues source-line-with-issues"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <SeverityIcon
+ severity="MINOR"
+ />
+ <span
+ className="source-line-issues-counter"
+ >
+ 2
+ </span>
+</td>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render issues list 1`] = `
-<div
- className="issue-list"
->
- <Issue
- displayLocationsCount={true}
- displayLocationsLink={true}
- issue={
- Object {
- "key": "foo",
- }
- }
- key="foo"
- onClick={[MockFunction]}
- onPopupToggle={[MockFunction]}
- openPopup={null}
- selected={true}
- />
- <Issue
- displayLocationsCount={true}
- displayLocationsLink={true}
- issue={
- Object {
- "key": "bar",
- }
- }
- key="bar"
- onClick={[MockFunction]}
- onPopupToggle={[MockFunction]}
- openPopup={null}
- selected={false}
- />
-</div>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render issues list 1`] = `
+<div
+ className="issue-list"
+>
+ <Issue
+ displayLocationsCount={true}
+ displayLocationsLink={true}
+ issue={
+ Object {
+ "component": "",
+ "componentLongName": "",
+ "componentQualifier": "",
+ "componentUuid": "",
+ "creationDate": "",
+ "flows": Array [],
+ "key": "foo",
+ "message": "",
+ "organization": "",
+ "project": "",
+ "projectName": "",
+ "projectOrganization": "",
+ "projectUuid": "",
+ "rule": "",
+ "ruleName": "",
+ "secondaryLocations": Array [],
+ "severity": "",
+ "status": "",
+ "type": "",
+ }
+ }
+ key="foo"
+ onChange={[MockFunction]}
+ onClick={[MockFunction]}
+ onPopupToggle={[MockFunction]}
+ selected={true}
+ />
+ <Issue
+ displayLocationsCount={true}
+ displayLocationsLink={true}
+ issue={
+ Object {
+ "component": "",
+ "componentLongName": "",
+ "componentQualifier": "",
+ "componentUuid": "",
+ "creationDate": "",
+ "flows": Array [],
+ "key": "bar",
+ "message": "",
+ "organization": "",
+ "project": "",
+ "projectName": "",
+ "projectOrganization": "",
+ "projectUuid": "",
+ "rule": "",
+ "ruleName": "",
+ "secondaryLocations": Array [],
+ "severity": "",
+ "status": "",
+ "type": "",
+ }
+ }
+ key="bar"
+ onChange={[MockFunction]}
+ onClick={[MockFunction]}
+ onPopupToggle={[MockFunction]}
+ selected={false}
+ />
+</div>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`render line 0 1`] = `
-<td
- className="source-meta source-line-number"
-/>
-`;
-
-exports[`render line 3 1`] = `
-<td
- className="source-meta source-line-number"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
-/>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`render line 0 1`] = `
+<td
+ className="source-meta source-line-number"
+/>
+`;
+
+exports[`render line 3 1`] = `
+<td
+ className="source-meta source-line-number"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <BubblePopupHelper
+ isOpen={false}
+ popup={
+ <LineOptionsPopup
+ branch={undefined}
+ componentKey="foo"
+ line={
+ Object {
+ "line": 3,
+ }
+ }
+ />
+ }
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<BubblePopup
+ customClass="source-viewer-bubble-popup"
+>
+ <div
+ className="bubble-popup-section"
+ >
+ <Link
+ className="js-get-permalink"
+ onlyActiveOnIndex={false}
+ style={Object {}}
+ to={
+ Object {
+ "pathname": "/component",
+ "query": Object {
+ "branch": "feature",
+ "id": "foo",
+ "line": 3,
+ },
+ }
+ }
+ >
+ component_viewer.get_permalink
+ </Link>
+ </div>
+</BubblePopup>
+`;
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`does not allow to click 1`] = `
-<td
- className="source-meta source-line-scm"
->
- <div
- className="source-line-scm-inner"
- data-author="foo"
- />
-</td>
-`;
-
-exports[`does not render scm details 1`] = `
-<td
- className="source-meta source-line-scm"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
-/>
-`;
-
-exports[`render scm details 1`] = `
-<td
- className="source-meta source-line-scm"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
->
- <div
- className="source-line-scm-inner"
- data-author="foo"
- />
-</td>
-`;
-
-exports[`render scm details for the first line 1`] = `
-<td
- className="source-meta source-line-scm"
- data-line-number={3}
- onClick={[Function]}
- role="button"
- tabIndex={0}
->
- <div
- className="source-line-scm-inner"
- data-author="foo"
- />
-</td>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`does not render scm details 1`] = `
+<td
+ className="source-meta source-line-scm"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <BubblePopupHelper
+ isOpen={false}
+ popup={
+ <SCMPopup
+ line={
+ Object {
+ "line": 3,
+ "scmAuthor": "foo",
+ "scmDate": "2017-01-01",
+ }
+ }
+ />
+ }
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
+
+exports[`render scm details 1`] = `
+<td
+ className="source-meta source-line-scm"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <div
+ className="source-line-scm-inner"
+ data-author="foo"
+ />
+ <BubblePopupHelper
+ isOpen={false}
+ popup={
+ <SCMPopup
+ line={
+ Object {
+ "line": 3,
+ "scmAuthor": "foo",
+ "scmDate": "2017-01-01",
+ }
+ }
+ />
+ }
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
+
+exports[`render scm details for the first line 1`] = `
+<td
+ className="source-meta source-line-scm"
+ data-line-number={3}
+ onClick={[Function]}
+ role="button"
+ tabIndex={0}
+>
+ <div
+ className="source-line-scm-inner"
+ data-author="foo"
+ />
+ <BubblePopupHelper
+ isOpen={false}
+ popup={
+ <SCMPopup
+ line={
+ Object {
+ "line": 3,
+ "scmAuthor": "foo",
+ "scmDate": "2017-01-01",
+ }
+ }
+ />
+ }
+ position="bottomright"
+ togglePopup={[Function]}
+ />
+</td>
+`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render 1`] = `
+<BubblePopup
+ customClass="source-viewer-bubble-popup"
+>
+ <div
+ className="bubble-popup-section"
+ >
+ foo
+ </div>
+ <div
+ className="bubble-popup-section"
+ >
+ <DateFormatter
+ date="2017-01-01"
+ />
+ </div>
+</BubblePopup>
+`;
+++ /dev/null
-/*
- * 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' }
- ]);
- });
-});
--- /dev/null
+/*
+ * 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' }
+ ]);
+ });
+});
+++ /dev/null
-/*
- * 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: '<span class="sym-54 sym"><span class="sym-56 sym">foo</span></span>' },
- { line: 2, code: '<span class="sym-56 sym">bar</span>' },
- { line: 3, code: '<span class="k">qux</span>' }
- ];
- expect(symbolsByLine(lines)).toEqual({
- 1: ['sym-54', 'sym-56'],
- 2: ['sym-56'],
- 3: []
- });
- });
-});
--- /dev/null
+/*
+ * 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: '<span class="sym-54 sym"><span class="sym-56 sym">foo</span></span>' },
+ { line: 2, code: '<span class="sym-56 sym">bar</span>' },
+ { line: 3, code: '<span class="k">qux</span>' }
+ ];
+ expect(symbolsByLine(lines)).toEqual({
+ 1: ['sym-54', 'sym-56'],
+ 2: ['sym-56'],
+ 3: []
+ });
+ });
+});
+++ /dev/null
-/*
- * 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;
-}
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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<number>, text: string };
-*/
-/*::
-export type Tokens = Array<Token>; */
-
-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 => `<span class="${token.className}">${escapeHtml(token.text)}</span>`)
- .join('');
-}
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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<Issue> */) => {
- 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<Issue> */
-) /*: { [number]: Array<LinearIssueLocation> } */ {
- 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<SourceLine> */) => {
- 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;
-};
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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<Location> */ {
- 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;
-}
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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<Issues> */ {
- 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<Issues> */ {
- 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<Issues> */ {
- const query = buildQuery(component, branch);
- return new Promise(resolve => {
- loadPageAndNext(query, toLine, 1).then(issues => {
- resolve(issues);
- });
- });
-}
--- /dev/null
+/*
+ * 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<Issue[]> {
+ 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<Issue[]> {
+ 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<Issue[]> {
+ const query = buildQuery(component, branch);
+ return new Promise(resolve => {
+ loadPageAndNext(query, toLine, 1).then(issues => {
+ resolve(issues);
+ });
+ });
+}
+++ /dev/null
-/*
- * 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 };
- }
-});
+++ /dev/null
-/*
- * 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
- };
- }
-});
+++ /dev/null
-/*
- * 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 };
- }
-});
+++ /dev/null
-/*
- * 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
- };
- }
-});
+++ /dev/null
-<div class="bubble-popup-container">
- <div class="bubble-popup-title">
- {{t 'source_viewer.covered'}}
- {{#if row.conditions}}
- ({{default row.coveredConditions 0}} of {{row.conditions}} {{t 'source_viewer.conditions'}})
- {{/if}}
- </div>
- {{#each testFiles}}
- <div class="bubble-popup-section">
- <a class="component-viewer-popup-test-file link-action" data-key="{{file.key}}" title="{{file.longName}}">
- <span>{{collapsePath file.longName}}</span>
- </a>
- <ul class="bubble-popup-list">
- {{#each tests}}
- <li class="component-viewer-popup-test" title="{{name}}">
- <i class="component-viewer-popup-test-status {{testStatusIconClass status}}"></i>
- <span class="component-viewer-popup-test-name">
- <a class="component-viewer-popup-test-file link-action" title="{{name}}"
- data-key="{{../file.key}}" data-method="{{name}}">
- {{name}}
- </a>
- </span>
- <span class="component-viewer-popup-test-duration">{{durationInMs}}ms</span>
- </li>
- {{/each}}
- </ul>
- </div>
- {{else}}
- {{t 'source_viewer.tooltip.no_information_about_tests'}}
- {{/each}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
+++ /dev/null
-<div class="bubble-popup-container">
- {{#if inRemovedComponent}}
- <div class="alert alert-warning spacer-bottom">{{t 'duplications.dups_found_on_deleted_resource'}}</div>
- {{/if}}
- {{#notEmpty duplications}}
- <div class="bubble-popup-title">{{t 'component_viewer.transition.duplication'}}</div>
- {{#each duplications}}
- <div class="bubble-popup-section">
- <div class="component-name">
- {{#notEqComponents file ../component}}
- <div class="component-name-parent">
- <i class="icon-qualifier-trk"></i> <a href="{{dashboardUrl file.project}}">{{file.projectName}}</a>
- </div>
- {{#if file.subProjectName}}
- <div class="component-name-parent">
- <i class="icon-qualifier-trk"></i> <a
- href="{{dashboardUrl file.subProject}}">{{file.subProjectName}}</a>
- </div>
- {{/if}}
- {{/notEqComponents}}
-
- {{#notEq file.key ../component.key}}
- <div class="component-name-path">
- <a class="link-action" data-key="{{file.key}}" title="{{file.name}}">
- <span>{{collapsedDirFromPath file.name}}</span><span
- class="component-name-file">{{fileFromPath file.name}}</span>
- </a>
- </div>
- {{/notEq}}
-
- <div class="component-name-path">
- Lines:
- {{#joinEach blocks ','}}
- <a class="link-action" data-key="{{../file.key}}" data-line="{{this.from}}">
- {{this.from}} – {{sum from size -1}}
- </a>
- {{/joinEach}}
- </div>
- </div>
- </div>
- {{/each}}
- {{/notEmpty}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
+++ /dev/null
-<div class="bubble-popup-container">
- <div class="bubble-popup-section">
- <a href={{permalink}} target="_blank" class="js-get-permalink link-action">{{t 'component_viewer.get_permalink'}}</a>
- </div>
-</div>
-
-<div class="bubble-popup-arrow"></div>
+++ /dev/null
-<div class="bubble-popup-container">
- <div class="bubble-popup-section">
- {{line.scmAuthor}}
- </div>
- <div class="bubble-popup-section">
- {{dt line.scmDate}}
- </div>
- {{#if line.scmRevision}}
- <div class="bubble-popup-section">
- {{line.scmRevision}}
- </div>
- {{/if}}
-</div>
-
-<div class="bubble-popup-arrow"></div>
}
.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);
}
.source-meta {
+ position: relative;
vertical-align: top;
width: 1px;
background-clip: padding-box;
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;
+++ /dev/null
-/*
- * 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
- }>
-};
-*/
children?: React.ReactNode;
isOpen: boolean;
offset?: { vertical: number; horizontal: number };
- popup: React.ReactElement<any>;
+ popup: JSX.Element;
position: 'bottomleft' | 'bottomright';
togglePopup: (show: boolean) => void;
}
return (
<div
className={classNames(this.props.className, 'bubble-popup-helper')}
- ref={container => (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 && (
<div ref={popupContainer => (this.popupContainer = popupContainer)}>
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;
}
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;
+++ /dev/null
-/*
- * 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');
- }
-});
--- /dev/null
+/*
+ * 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<IssueProps> {}
*/
import { flatten, sortBy } from 'lodash';
import { SEVERITIES } from './constants';
+import { Issue } from '../app/types';
interface TextRange {
startLine: number;
textRange?: TextRange;
}
-interface Issue extends IssueBase {}
-
export function sortBySeverity(issues: Issue[]): Issue[] {
return sortBy(issues, issue => SEVERITIES.indexOf(issue.severity));
}
...ensureTextRange(issue),
secondaryLocations,
flows
- };
+ } as Issue;
}
+++ /dev/null
-/*
- * 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<Favorite>,
- notFavorites: Array<Favorite>
-};
-*/
-
-/*::
-type AddFavoriteAction = {
- type: 'ADD_FAVORITE',
- componentKey: string
-};
-*/
-
-/*::
-type RemoveFavoriteAction = {
- type: 'REMOVE_FAVORITE',
- componentKey: string
-};
-*/
-
-/*::
-type Action = ReceiveFavoritesAction | AddFavoriteAction | RemoveFavoriteAction;
-*/
-
-/*::
-type State = Array<string>;
-*/
-
-export const actions = {
- RECEIVE_FAVORITES: 'RECEIVE_FAVORITES',
- ADD_FAVORITE: 'ADD_FAVORITE',
- REMOVE_FAVORITE: 'REMOVE_FAVORITE'
-};
-
-export function receiveFavorites(
- favorites /*: Array<Favorite> */,
- notFavorites /*: Array<Favorite> */ = []
-) /*: 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);
-}
--- /dev/null
+/*
+ * 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<Favorite>;
+ notFavorites: Array<Favorite>;
+}
+
+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);
+}