@@ -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> |
@@ -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}><</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> | |||
); | |||
} |
@@ -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> | |||
); |
@@ -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} |
@@ -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 }} | |||
/> | |||
) |
@@ -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]} | |||
> | |||
< | |||
</button> | |||
<button | |||
onClick={[Function]} | |||
> | |||
> | |||
</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]} | |||
> | |||
< | |||
</button> | |||
</div> | |||
</div> | |||
<LeakPeriodLegend | |||
className="spacer-left" | |||
component={ |
@@ -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" |
@@ -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} />; | |||
} | |||
} |
@@ -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)}> |
@@ -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 |