// @flow
import React from 'react';
import classNames from 'classnames';
+import CodeView from '../drilldown/CodeView';
import Breadcrumbs from './Breadcrumbs';
import FilesView from '../drilldown/FilesView';
import MeasureFavoriteContainer from './MeasureFavoriteContainer';
import MeasureViewSelect from './MeasureViewSelect';
import MetricNotFound from './MetricNotFound';
import PageActions from './PageActions';
-import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import TreeMapView from '../drilldown/TreeMapView';
import { getComponentTree } from '../../../api/components';
import { complementary } from '../config/complementary';
import { enhanceComponent, isFileType, isViewType } from '../utils';
import { getProjectUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
-import { parseDate } from '../../../helpers/dates';
/*:: import type { Component, ComponentEnhanced, Paging, Period } from '../types'; */
/*:: import type { MeasureEnhanced } from '../../../components/measure/types'; */
/*:: import type { Metric } from '../../../store/metrics/actions'; */
onSelectComponent = (componentKey /*: string */) => this.setState({ selected: componentKey });
renderCode() {
- const { branch, component, leakPeriod } = this.props;
- const leakPeriodDate =
- isDiffMetric(this.props.metric.key) && leakPeriod != null ? parseDate(leakPeriod.date) : null;
-
- let filterLine;
- if (leakPeriodDate != null) {
- filterLine = line => {
- if (line.scmDate) {
- const scmDate = parseDate(line.scmDate);
- return scmDate >= leakPeriodDate;
- } else {
- return false;
- }
- };
- }
return (
<div className="measure-details-viewer">
- <SourceViewer branch={branch} component={component.key} filterLine={filterLine} />
+ <CodeView
+ branch={this.props.branch}
+ component={this.props.component}
+ components={this.state.components}
+ leakPeriod={this.props.leakPeriod}
+ metric={this.props.metric}
+ selectedIdx={this.getSelectedIndex()}
+ updateSelected={this.props.updateSelected}
+ />
</div>
);
}
loading={this.props.loading}
isFile={isFile}
paging={this.state.paging}
+ totalLoadedComponents={this.state.components.length}
view={view}
/>
</div>
branch={branch}
component={component}
components={this.state.components}
- handleSelect={this.props.updateSelected}
leakPeriod={this.props.leakPeriod}
measure={measure}
secondaryMeasure={this.props.secondaryMeasure}
- selectedIdx={selectedIdx}
/>
{isFileType(component) ? this.renderCode() : this.renderMeasure()}
</div>
import LeakPeriodLegend from './LeakPeriodLegend';
import Measure from '../../../components/measure/Measure';
import Tooltip from '../../../components/controls/Tooltip';
-import { isFileType } from '../utils';
-import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { getLocalizedMetricName, translate } from '../../../helpers/l10n';
import { getMeasureHistoryUrl } from '../../../helpers/urls';
import { isDiffMetric } from '../../../helpers/measures';
/*:: import type { Component, Period } from '../types'; */
component: Component,
components: Array<Component>,
leakPeriod?: Period,
- handleSelect: string => void,
measure: MeasureEnhanced,
- secondaryMeasure: ?MeasureEnhanced,
- selectedIdx: ?number
+ secondaryMeasure: ?MeasureEnhanced
|}; */
-export default class MeasureHeader extends React.PureComponent {
- /*:: props: Props; */
-
- handleSelectPrevious = (e /*: Event & { target: HTMLElement } */) => {
- e.target.blur();
- if (this.props.selectedIdx != null) {
- const prevComponent = this.props.components[this.props.selectedIdx - 1];
- if (prevComponent) {
- this.props.handleSelect(prevComponent.key);
- }
- }
- };
-
- handleSelectNext = (e /*: Event & { target: HTMLElement } */) => {
- e.target.blur();
- if (this.props.selectedIdx != null) {
- const prevComponent = this.props.components[this.props.selectedIdx + 1];
- if (prevComponent) {
- this.props.handleSelect(prevComponent.key);
- }
- }
- };
-
- renderFileNav() {
- const { components, selectedIdx } = this.props;
- if (selectedIdx == null) {
- return null;
- }
- const hasPrevious = selectedIdx > 0;
- const hasNext = selectedIdx < components.length - 1;
- return (
- <div className="display-inline-block">
- {components.length > 0 && (
- <span className="note spacer-right">
- {translateWithParameters(
- 'component_measures.x_of_y',
- selectedIdx + 1,
- components.length
- )}
+export default function MeasureHeader(props /*: Props*/) {
+ const { branch, component, leakPeriod, measure, secondaryMeasure } = props;
+ const metric = measure.metric;
+ const isDiff = isDiffMetric(metric.key);
+ const hasHistory = !isDiff && ['TRK', 'VW', 'SVW', 'APP'].includes(component.qualifier);
+ return (
+ <div className="measure-details-header big-spacer-bottom">
+ <div className="measure-details-primary">
+ <div className="measure-details-metric">
+ <IssueTypeIcon query={metric.key} className="little-spacer-right text-text-bottom" />
+ {getLocalizedMetricName(metric)}
+ <span className="measure-details-value spacer-left">
+ <strong>
+ {isDiff ? (
+ <Measure className="domain-measures-leak" measure={measure} metric={metric} />
+ ) : (
+ <Measure measure={measure} metric={metric} />
+ )}
+ </strong>
</span>
- )}
- <div className="button-group">
- {hasPrevious && <button onClick={this.handleSelectPrevious}><</button>}
- {hasNext && <button onClick={this.handleSelectNext}>></button>}
+ {hasHistory && (
+ <Tooltip
+ placement="right"
+ overlay={translate('component_measures.show_metric_history')}>
+ <Link
+ className="js-show-history spacer-left button button-small button-compact"
+ to={getMeasureHistoryUrl(component.key, metric.key, branch)}>
+ <HistoryIcon />
+ </Link>
+ </Tooltip>
+ )}
</div>
- </div>
- );
- }
-
- render() {
- const { branch, component, components, leakPeriod, measure, secondaryMeasure } = this.props;
- const metric = measure.metric;
- const isDiff = isDiffMetric(metric.key);
- const hasHistory = !isDiff && ['TRK', 'VW', 'SVW', 'APP'].includes(component.qualifier);
- const hasComponents = components && components.length > 1;
- return (
- <div className="measure-details-header big-spacer-bottom">
- <div className="measure-details-primary">
- <div className="measure-details-metric">
- <IssueTypeIcon query={metric.key} className="little-spacer-right text-text-bottom" />
- {getLocalizedMetricName(metric)}
- <span className="measure-details-value spacer-left">
- <strong>
- {isDiff ? (
- <Measure className="domain-measures-leak" measure={measure} metric={metric} />
- ) : (
- <Measure measure={measure} metric={metric} />
- )}
- </strong>
- </span>
- {hasHistory && (
- <Tooltip
- placement="right"
- overlay={translate('component_measures.show_metric_history')}>
- <Link
- className="js-show-history spacer-left button button-small button-compact"
- to={getMeasureHistoryUrl(component.key, metric.key, branch)}>
- <HistoryIcon />
- </Link>
- </Tooltip>
- )}
- </div>
- <div className="measure-details-primary-actions">
- {hasComponents && isFileType(component) && this.renderFileNav()}
- {leakPeriod != null && (
- <LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
- )}
- </div>
+ <div className="measure-details-primary-actions">
+ {leakPeriod != null && (
+ <LeakPeriodLegend className="spacer-left" component={component} period={leakPeriod} />
+ )}
</div>
- {secondaryMeasure &&
- secondaryMeasure.metric.key === 'ncloc_language_distribution' && (
- <div className="measure-details-secondary">
- <LanguageDistributionContainer
- alignTicks={true}
- distribution={secondaryMeasure.value}
- width={260}
- />
- </div>
- )}
- {secondaryMeasure &&
- secondaryMeasure.metric.key === 'function_complexity_distribution' && (
- <div className="measure-details-secondary">
- <ComplexityDistribution distribution={secondaryMeasure.value} of="function" />
- </div>
- )}
- {secondaryMeasure &&
- secondaryMeasure.metric.key === 'file_complexity_distribution' && (
- <div className="measure-details-secondary">
- <ComplexityDistribution distribution={secondaryMeasure.value} of="file" />
- </div>
- )}
</div>
- );
- }
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'ncloc_language_distribution' && (
+ <div className="measure-details-secondary">
+ <LanguageDistributionContainer
+ alignTicks={true}
+ distribution={secondaryMeasure.value}
+ width={260}
+ />
+ </div>
+ )}
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'function_complexity_distribution' && (
+ <div className="measure-details-secondary">
+ <ComplexityDistribution distribution={secondaryMeasure.value} of="function" />
+ </div>
+ )}
+ {secondaryMeasure &&
+ secondaryMeasure.metric.key === 'file_complexity_distribution' && (
+ <div className="measure-details-secondary">
+ <ComplexityDistribution distribution={secondaryMeasure.value} of="file" />
+ </div>
+ )}
+ </div>
+ );
}
loading: boolean,
isFile: ?boolean,
paging: ?Paging,
+ totalLoadedComponents?: number,
view?: string
|}; */
export default function PageActions(props /*: Props */) {
- const { isFile, paging } = props;
+ const { isFile, paging, totalLoadedComponents } = props;
const showShortcuts = ['list', 'tree'].includes(props.view);
return (
<div className="pull-right">
{!isFile && showShortcuts && renderShortcuts()}
- {isFile && renderFileShortcuts()}
+ {isFile && paging && renderFileShortcuts()}
<div className="measure-details-page-actions">
<DeferredSpinner loading={props.loading}>
<i className="spinner-placeholder" />
</DeferredSpinner>
{paging != null && (
- <FilesCounter className="spacer-left" current={props.current} total={paging.total} />
+ <FilesCounter
+ className="spacer-left"
+ current={props.current}
+ total={isFile && totalLoadedComponents != null ? totalLoadedComponents : paging.total}
+ />
)}
</div>
</div>
return (
<span className="note spacer-right">
<span>
- <span className="shortcut-button little-spacer-right">←</span>
- {translate('component_measures.to_navigate_back')}
+ <span className="shortcut-button little-spacer-right">j</span>
+ <span className="shortcut-button little-spacer-right">k</span>
+ {translate('component_measures.to_navigate_files')}
</span>
</span>
);
expect(wrapper.find('Connect(LanguageDistribution)')).toHaveLength(1);
});
-it('shohuld display correctly for open file', () => {
+it('should display correctly for open file', () => {
const wrapper = shallow(
<MeasureHeader
{...PROPS}
import PageActions from '../PageActions';
it('should display correctly for a project', () => {
- expect(shallow(<PageActions loading={true} isFile={false} view="list" />)).toMatchSnapshot();
+ expect(
+ shallow(<PageActions loading={true} isFile={false} view="list" totalLoadedComponents={20} />)
+ ).toMatchSnapshot();
});
it('should display correctly for a file', () => {
- expect(shallow(<PageActions loading={false} isFile={true} view="tree" />)).toMatchSnapshot();
+ const wrapper = shallow(
+ <PageActions loading={false} isFile={true} view="tree" totalLoadedComponents={10} />
+ );
+ expect(wrapper).toMatchSnapshot();
+ wrapper.setProps({ paging: { total: 100 } });
+ expect(wrapper).toMatchSnapshot();
});
it('should not display shortcuts for treemap', () => {
- expect(shallow(<PageActions loading={true} isFile={false} view="treemap" />)).toMatchSnapshot();
+ expect(
+ shallow(<PageActions loading={true} isFile={false} view="treemap" totalLoadedComponents={20} />)
+ ).toMatchSnapshot();
});
it('should display the total of files', () => {
loading={true}
isFile={false}
view="treemap"
+ totalLoadedComponents={20}
+ paging={{ total: 120 }}
+ />
+ )
+ ).toMatchSnapshot();
+ expect(
+ shallow(
+ <PageActions
+ current={12}
+ loading={false}
+ isFile={true}
+ view="list"
+ totalLoadedComponents={20}
paging={{ total: 120 }}
/>
)
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`shohuld display correctly for open file 1`] = `
+exports[`should display correctly for open file 1`] = `
<div
className="measure-details-primary-actions"
>
- <div
- className="display-inline-block"
- >
- <span
- className="note spacer-right"
- >
- component_measures.x_of_y.2.3
- </span>
- <div
- className="button-group"
- >
- <button
- onClick={[Function]}
- >
- <
- </button>
- <button
- onClick={[Function]}
- >
- >
- </button>
- </div>
- </div>
<LeakPeriodLegend
className="spacer-left"
component={
</div>
`;
-exports[`shohuld display correctly for open file 2`] = `
+exports[`should display correctly for open file 2`] = `
<div
className="measure-details-primary-actions"
>
- <div
- className="display-inline-block"
- >
- <span
- className="note spacer-right"
- >
- component_measures.x_of_y.2.2
- </span>
- <div
- className="button-group"
- >
- <button
- onClick={[Function]}
- >
- <
- </button>
- </div>
- </div>
<LeakPeriodLegend
className="spacer-left"
component={
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should display correctly for a file 1`] = `
+<div
+ className="pull-right"
+>
+ <div
+ className="measure-details-page-actions"
+ >
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <i
+ className="spinner-placeholder"
+ />
+ </DeferredSpinner>
+ </div>
+</div>
+`;
+
+exports[`should display correctly for a file 2`] = `
<div
className="pull-right"
>
<span
className="shortcut-button little-spacer-right"
>
- ←
+ j
+ </span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ k
</span>
- component_measures.to_navigate_back
+ component_measures.to_navigate_files
</span>
</span>
<div
className="spinner-placeholder"
/>
</DeferredSpinner>
+ <FilesCounter
+ className="spacer-left"
+ total={10}
+ />
</div>
</div>
`;
</div>
`;
+exports[`should display the total of files 2`] = `
+<div
+ className="pull-right"
+>
+ <span
+ className="note spacer-right"
+ >
+ <span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ j
+ </span>
+ <span
+ className="shortcut-button little-spacer-right"
+ >
+ k
+ </span>
+ component_measures.to_navigate_files
+ </span>
+ </span>
+ <div
+ className="measure-details-page-actions"
+ >
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <i
+ className="spinner-placeholder"
+ />
+ </DeferredSpinner>
+ <FilesCounter
+ className="spacer-left"
+ current={12}
+ total={20}
+ />
+ </div>
+</div>
+`;
+
exports[`should not display shortcuts for treemap 1`] = `
<div
className="pull-right"
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import key from 'keymaster';
+import SourceViewer from '../../../components/SourceViewer/SourceViewer';
+import { isDiffMetric } from '../../../helpers/measures';
+import { parseDate } from '../../../helpers/dates';
+/*:: import type { ComponentEnhanced, Paging, Period } from '../types'; */
+/*:: import type { Metric } from '../../../store/metrics/actions'; */
+
+/*:: type Props = {|
+ branch?: string,
+ component: ComponentEnhanced,
+ components: Array<ComponentEnhanced>,
+ leakPeriod?: Period,
+ metric: Metric,
+ selectedIdx: ?number,
+ updateSelected: string => void,
+|}; */
+
+export default class CodeView extends React.PureComponent {
+ /*:: props: Props; */
+
+ componentDidMount() {
+ this.attachShortcuts();
+ }
+
+ componentWillUnmount() {
+ this.detachShortcuts();
+ }
+
+ attachShortcuts() {
+ key('j', 'measures-files', () => {
+ this.selectNext();
+ return false;
+ });
+ key('k', 'measures-files', () => {
+ this.selectPrevious();
+ return false;
+ });
+ }
+
+ detachShortcuts() {
+ ['j', 'k'].map(action => key.unbind(action, 'measures-files'));
+ }
+
+ selectPrevious = () => {
+ const { selectedIdx } = this.props;
+ if (selectedIdx != null && selectedIdx > 0) {
+ const prevComponent = this.props.components[selectedIdx - 1];
+ if (prevComponent) {
+ this.props.updateSelected(prevComponent.key);
+ }
+ }
+ };
+
+ selectNext = () => {
+ const { components, selectedIdx } = this.props;
+ if (selectedIdx != null && selectedIdx < components.length - 1) {
+ const nextComponent = components[selectedIdx + 1];
+ if (nextComponent) {
+ this.props.updateSelected(nextComponent.key);
+ }
+ }
+ };
+
+ render() {
+ const { branch, component, leakPeriod } = this.props;
+ const leakPeriodDate =
+ isDiffMetric(this.props.metric.key) && leakPeriod != null ? parseDate(leakPeriod.date) : null;
+
+ let filterLine;
+ if (leakPeriodDate != null) {
+ filterLine = line => {
+ if (line.scmDate) {
+ const scmDate = parseDate(line.scmDate);
+ return scmDate >= leakPeriodDate;
+ } else {
+ return false;
+ }
+ };
+ }
+ return <SourceViewer branch={branch} component={component.key} filterLine={filterLine} />;
+ }
+}
componentDidMount() {
this.attachShortcuts();
+ if (this.props.selectedKey != null) {
+ this.scrollToElement();
+ }
}
componentDidUpdate(prevProps /*: Props */) {
- if (
- this.listContainer &&
- this.props.selectedKey != null &&
- prevProps.selectedKey !== this.props.selectedKey
- ) {
- const elem = this.listContainer.getElementsByClassName('selected')[0];
- if (elem) {
- scrollToElement(elem, { topOffset: 215, bottomOffset: 100 });
- }
+ if (this.props.selectedKey != null && prevProps.selectedKey !== this.props.selectedKey) {
+ this.scrollToElement();
}
}
}
};
+ scrollToElement = () => {
+ if (this.listContainer) {
+ const elem = this.listContainer.getElementsByClassName('selected')[0];
+ if (elem) {
+ scrollToElement(elem, { topOffset: 215, bottomOffset: 100 });
+ }
+ }
+ };
+
render() {
return (
<div ref={elem => (this.listContainer = elem)}>
component_measures.not_found=The requested measure was not found.
component_measures.to_select_files=to select files
component_measures.to_navigate=to navigate
-component_measures.to_navigate_back=to navigate back
+component_measures.to_navigate_files=to next/previous file
component_measures.overview.project_overview.facet=Project Overview
component_measures.overview.project_overview.title=Risk