Browse Source

SONAR-9689 Replaced file navigation button with j/k shortcut in measures page

tags/6.6-RC1
Grégoire Aubert 6 years ago
parent
commit
544f801417

+ 11
- 20
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js View File

@@ -20,6 +20,7 @@
// @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';
@@ -27,14 +28,12 @@ import MeasureHeader from './MeasureHeader';
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'; */
@@ -223,24 +222,17 @@ export default class MeasureContent extends React.PureComponent {
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>
);
}
@@ -322,6 +314,7 @@ export default class MeasureContent extends React.PureComponent {
loading={this.props.loading}
isFile={isFile}
paging={this.state.paging}
totalLoadedComponents={this.state.components.length}
view={view}
/>
</div>
@@ -337,11 +330,9 @@ export default class MeasureContent extends React.PureComponent {
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>

+ 60
- 115
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js View File

@@ -27,8 +27,7 @@ import LanguageDistributionContainer from '../../../components/charts/LanguageDi
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'; */
@@ -39,124 +38,70 @@ import { isDiffMetric } from '../../../helpers/measures';
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}>&lt;</button>}
{hasNext && <button onClick={this.handleSelectNext}>&gt;</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>
);
}

+ 11
- 5
server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js View File

@@ -29,22 +29,27 @@ import { translate } from '../../../helpers/l10n';
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>
@@ -73,8 +78,9 @@ function renderFileShortcuts() {
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>
);

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js View File

@@ -85,7 +85,7 @@ it('should display secondary measure too', () => {
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}

+ 25
- 3
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js View File

@@ -22,15 +22,24 @@ import { shallow } from 'enzyme';
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', () => {
@@ -41,6 +50,19 @@ 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 }}
/>
)

+ 2
- 43
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap View File

@@ -1,32 +1,9 @@
// 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]}
>
&lt;
</button>
<button
onClick={[Function]}
>
&gt;
</button>
</div>
</div>
<LeakPeriodLegend
className="spacer-left"
component={
@@ -47,28 +24,10 @@ exports[`shohuld display correctly for open file 1`] = `
</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]}
>
&lt;
</button>
</div>
</div>
<LeakPeriodLegend
className="spacer-left"
component={

+ 71
- 2
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap View File

@@ -1,6 +1,25 @@
// 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"
>
@@ -11,9 +30,14 @@ exports[`should display correctly for a file 1`] = `
<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
@@ -27,6 +51,10 @@ exports[`should display correctly for a file 1`] = `
className="spinner-placeholder"
/>
</DeferredSpinner>
<FilesCounter
className="spacer-left"
total={10}
/>
</div>
</div>
`;
@@ -106,6 +134,47 @@ exports[`should display the total of files 1`] = `
</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"

+ 103
- 0
server/sonar-web/src/main/js/apps/component-measures/drilldown/CodeView.js View File

@@ -0,0 +1,103 @@
/*
* 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} />;
}
}

+ 14
- 9
server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js View File

@@ -52,18 +52,14 @@ export default class ListView extends React.PureComponent {

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();
}
}

@@ -114,6 +110,15 @@ export default class ListView extends React.PureComponent {
}
};

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)}>

+ 1
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2965,7 +2965,7 @@ component_measures.no_history=There is no historical data.
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

Loading…
Cancel
Save