@@ -20,7 +20,7 @@ | |||
// @flow | |||
import React from 'react'; | |||
import Helmet from 'react-helmet'; | |||
import MeasureContent from './MeasureContent'; | |||
import MeasureContentContainer from './MeasureContentContainer'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import { parseQuery, serializeQuery } from '../utils'; | |||
import { translate } from '../../../helpers/l10n'; | |||
@@ -153,8 +153,8 @@ export default class App extends React.PureComponent { | |||
</div> | |||
{metric != null && | |||
<MeasureContent | |||
className="layout-page-main-inner" | |||
<MeasureContentContainer | |||
className="layout-page-main" | |||
currentUser={this.props.currentUser} | |||
rootComponent={this.props.component} | |||
fetchMeasures={this.props.fetchMeasures} |
@@ -23,13 +23,12 @@ import Breadcrumb from './Breadcrumb'; | |||
import { getBreadcrumbs } from '../../../api/components'; | |||
import type { Component } from '../types'; | |||
type Props = { | |||
type Props = {| | |||
className?: string, | |||
component: Component, | |||
handleSelect: Component => void, | |||
rootComponent: Component, | |||
view: string | |||
}; | |||
rootComponent: Component | |||
|}; | |||
type State = { | |||
breadcrumbs: Array<Component> | |||
@@ -57,9 +56,9 @@ export default class Breadcrumbs extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchBreadcrumbs = ({ component, rootComponent, view }: Props) => { | |||
fetchBreadcrumbs = ({ component, rootComponent }: Props) => { | |||
const isRoot = component.key === rootComponent.key; | |||
if (isRoot || view === 'list') { | |||
if (isRoot) { | |||
if (this.mounted) { | |||
this.setState({ breadcrumbs: isRoot ? [component] : [rootComponent, component] }); | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
* 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 { translate } from '../../../helpers/l10n'; | |||
import { formatMeasure } from '../../../helpers/measures'; | |||
type Props = { | |||
className?: string, | |||
current: ?number, | |||
total: number | |||
}; | |||
export default function FilesCounter({ className, current, total }: Props) { | |||
return ( | |||
<span className={className}> | |||
<strong> | |||
{current != null && | |||
<span> | |||
{formatMeasure(current, 'INT')} | |||
{' / '} | |||
</span>} | |||
{formatMeasure(total, 'INT')} | |||
</strong>{' '} | |||
{translate('component_measures.files')} | |||
</span> | |||
); | |||
} |
@@ -22,69 +22,60 @@ import React from 'react'; | |||
import moment from 'moment'; | |||
import Breadcrumbs from './Breadcrumbs'; | |||
import Favorite from '../../../components/controls/Favorite'; | |||
import ListView from './drilldown/ListView'; | |||
import FilesView from './drilldown/FilesView'; | |||
import MeasureHeader from './MeasureHeader'; | |||
import MeasureViewSelect from './MeasureViewSelect'; | |||
import MetricNotFound from './MetricNotFound'; | |||
import PageActions from './PageActions'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import { getComponentTree } from '../../../api/components'; | |||
import { complementary } from '../config/complementary'; | |||
import { enhanceComponent, isFileType } from '../utils'; | |||
import { isDiffMetric } from '../../../helpers/measures'; | |||
import type { Component, Period, Query } from '../types'; | |||
import type { Component, ComponentEnhanced, Paging, Period } from '../types'; | |||
import type { MeasureEnhanced } from '../../../components/measure/types'; | |||
import type { Metric } from '../../../store/metrics/actions'; | |||
type Props = { | |||
className?: string, | |||
component: Component, | |||
currentUser: { isLoggedIn: boolean }, | |||
rootComponent: Component, | |||
fetchMeasures: ( | |||
Component, | |||
Array<string> | |||
) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, | |||
loading: boolean, | |||
leakPeriod?: Period, | |||
measure: ?MeasureEnhanced, | |||
metric: Metric, | |||
metrics: { [string]: Metric }, | |||
selected: ?string, | |||
updateQuery: Query => void, | |||
rootComponent: Component, | |||
secondaryMeasure: ?MeasureEnhanced, | |||
updateLoading: ({ [string]: boolean }) => void, | |||
updateSelected: Component => void, | |||
updateView: string => void, | |||
view: string | |||
}; | |||
type State = { | |||
component: ?Component, | |||
loading: { | |||
measure: boolean, | |||
components: boolean | |||
}, | |||
measure: ?MeasureEnhanced, | |||
secondaryMeasure: ?MeasureEnhanced | |||
components: Array<ComponentEnhanced>, | |||
metric: ?Metric, | |||
paging?: Paging | |||
}; | |||
export default class MeasureContent extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
component: null, | |||
loading: { | |||
measure: false, | |||
components: false | |||
}, | |||
measure: null, | |||
secondaryMeasure: null | |||
components: [], | |||
metric: null, | |||
paging: null | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchMeasure(this.props); | |||
this.fetchComponents(this.props); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
const { component } = this.state; | |||
const componentChanged = | |||
!component || | |||
nextProps.rootComponent.key !== component.key || | |||
nextProps.selected !== component.key; | |||
if (componentChanged || nextProps.metric !== this.props.metric) { | |||
this.fetchMeasure(nextProps); | |||
if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { | |||
this.fetchComponents(nextProps); | |||
} | |||
} | |||
@@ -92,56 +83,89 @@ export default class MeasureContent extends React.PureComponent { | |||
this.mounted = false; | |||
} | |||
fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected }: Props) => { | |||
this.updateLoading({ measure: true }); | |||
getComponentRequestParams = (metric: Metric, options: Object = {}) => { | |||
const metricKeys = [metric.key, ...(complementary[metric.key] || [])]; | |||
let opts: Object = { | |||
asc: metric.direction === 1, | |||
ps: 100, | |||
metricSortFilter: 'withMeasuresOnly', | |||
metricSort: metric.key | |||
}; | |||
if (isDiffMetric(metric.key)) { | |||
opts = { | |||
...opts, | |||
s: 'metricPeriod,name', | |||
metricPeriodSort: 1 | |||
}; | |||
} else { | |||
opts = { | |||
...opts, | |||
s: 'metric,name' | |||
}; | |||
} | |||
return { metricKeys, opts: { ...opts, ...options } }; | |||
}; | |||
const metricKeys = [metric.key]; | |||
if (metric.key === 'ncloc') { | |||
metricKeys.push('ncloc_language_distribution'); | |||
} else if (metric.key === 'function_complexity') { | |||
metricKeys.push('function_complexity_distribution'); | |||
} else if (metric.key === 'file_complexity') { | |||
metricKeys.push('file_complexity_distribution'); | |||
fetchComponents = ({ component, metric, view }: Props) => { | |||
if (isFileType(component)) { | |||
return this.setState({ components: [], metric: null, paging: null }); | |||
} | |||
fetchMeasures(selected || rootComponent.key, metricKeys).then( | |||
({ component, measures }) => { | |||
const strategy = view === 'list' ? 'leaves' : 'children'; | |||
const { metricKeys, opts } = this.getComponentRequestParams(metric); | |||
this.props.updateLoading({ components: true }); | |||
getComponentTree(strategy, component.key, metricKeys, opts).then( | |||
r => { | |||
if (this.mounted) { | |||
const measure = measures.find(measure => measure.metric.key === metric.key); | |||
const secondaryMeasure = measures.find(measure => measure.metric.key !== metric.key); | |||
this.setState({ component, measure, secondaryMeasure }); | |||
this.updateLoading({ measure: false }); | |||
this.setState({ | |||
components: r.components.map(component => enhanceComponent(component, metric)), | |||
metric, | |||
paging: r.paging | |||
}); | |||
} | |||
this.props.updateLoading({ components: false }); | |||
}, | |||
() => this.updateLoading({ measure: false }) | |||
() => this.props.updateLoading({ components: false }) | |||
); | |||
}; | |||
handleSelect = (component: Component) => | |||
this.props.updateQuery({ | |||
selected: component.key !== this.props.rootComponent.key ? component.key : null | |||
}); | |||
updateLoading = (loading: { [string]: boolean }) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ loading: { ...state.loading, ...loading } })); | |||
fetchMoreComponents = () => { | |||
const { component, metric, view } = this.props; | |||
const { paging } = this.state; | |||
if (!paging) { | |||
return; | |||
} | |||
const strategy = view === 'list' ? 'leaves' : 'children'; | |||
const { metricKeys, opts } = this.getComponentRequestParams(metric, { | |||
p: paging.pageIndex + 1 | |||
}); | |||
this.props.updateLoading({ components: true }); | |||
getComponentTree(strategy, component.key, metricKeys, opts).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
components: [ | |||
...state.components, | |||
...r.components.map(component => enhanceComponent(component, metric)) | |||
], | |||
metric, | |||
paging: r.paging | |||
})); | |||
} | |||
this.props.updateLoading({ components: false }); | |||
}, | |||
() => this.props.updateLoading({ components: false }) | |||
); | |||
}; | |||
updateView = (view: string) => this.props.updateQuery({ view }); | |||
renderContent() { | |||
const { component } = this.state; | |||
if (!component) { | |||
return null; | |||
} | |||
const { leakPeriod, metric, rootComponent, view } = this.props; | |||
const isFile = component.key !== rootComponent.key && component.qualifier === 'FIL'; | |||
const { component, leakPeriod, view } = this.props; | |||
if (isFile) { | |||
if (isFileType(component)) { | |||
const leakPeriodDate = | |||
isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null; | |||
isDiffMetric(this.props.metric.key) && leakPeriod != null | |||
? moment(leakPeriod.date).toDate() | |||
: null; | |||
let filterLine; | |||
if (leakPeriodDate != null) { | |||
@@ -161,38 +185,40 @@ export default class MeasureContent extends React.PureComponent { | |||
); | |||
} | |||
if (view === 'list') { | |||
const { metric } = this.state; | |||
if (metric == null) { | |||
return null; | |||
} | |||
if (['list', 'tree'].includes(view)) { | |||
return ( | |||
<ListView | |||
component={component} | |||
handleSelect={this.handleSelect} | |||
<FilesView | |||
components={this.state.components} | |||
fetchMore={this.fetchMoreComponents} | |||
handleSelect={this.props.updateSelected} | |||
metric={metric} | |||
metrics={this.props.metrics} | |||
updateLoading={this.updateLoading} | |||
paging={this.state.paging} | |||
/> | |||
); | |||
} | |||
} | |||
render() { | |||
const { currentUser, metric, rootComponent, view } = this.props; | |||
const { component, loading, measure } = this.state; | |||
const { component, currentUser, measure, metric, rootComponent, view } = this.props; | |||
const isLoggedIn = currentUser && currentUser.isLoggedIn; | |||
return ( | |||
<div className="layout-page-main"> | |||
<div className={this.props.className}> | |||
<div className="layout-page-header-panel layout-page-main-header issues-main-header"> | |||
<div className="layout-page-header-panel-inner layout-page-main-header-inner"> | |||
<div className="layout-page-main-inner clearfix"> | |||
{component && | |||
<Breadcrumbs | |||
className="measure-breadcrumbs spacer-right text-ellipsis" | |||
component={component} | |||
handleSelect={this.handleSelect} | |||
rootComponent={rootComponent} | |||
view={view} | |||
/>} | |||
{component && | |||
component.key !== rootComponent.key && | |||
<Breadcrumbs | |||
className="measure-breadcrumbs spacer-right text-ellipsis" | |||
component={component} | |||
handleSelect={this.props.updateSelected} | |||
rootComponent={rootComponent} | |||
/> | |||
{component.key !== rootComponent.key && | |||
isLoggedIn && | |||
<Favorite | |||
favorite={component.isFavorite === true} | |||
@@ -201,13 +227,15 @@ export default class MeasureContent extends React.PureComponent { | |||
/>} | |||
<MeasureViewSelect | |||
className="measure-view-select" | |||
metric={this.props.metric} | |||
handleViewChange={this.updateView} | |||
metric={metric} | |||
handleViewChange={this.props.updateView} | |||
view={view} | |||
/> | |||
<PageActions | |||
loading={loading.measure || loading.components} | |||
isFile={component && component.qualifier === 'FIL'} | |||
current={this.state.components.length} | |||
loading={this.props.loading} | |||
isFile={isFileType(component)} | |||
paging={this.state.paging} | |||
view={view} | |||
/> | |||
</div> | |||
@@ -217,14 +245,12 @@ export default class MeasureContent extends React.PureComponent { | |||
{metric != null && | |||
measure != null && | |||
<div className="layout-page-main-inner"> | |||
{component && | |||
<MeasureHeader | |||
component={component} | |||
leakPeriod={this.props.leakPeriod} | |||
measure={measure} | |||
secondaryMeasure={this.state.secondaryMeasure} | |||
updateQuery={this.props.updateQuery} | |||
/>} | |||
<MeasureHeader | |||
component={component} | |||
leakPeriod={this.props.leakPeriod} | |||
measure={measure} | |||
secondaryMeasure={this.props.secondaryMeasure} | |||
/> | |||
{this.renderContent()} | |||
</div>} | |||
</div> |
@@ -0,0 +1,148 @@ | |||
/* | |||
* 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 MeasureContent from './MeasureContent'; | |||
import type { Component, Period, Query } from '../types'; | |||
import type { MeasureEnhanced } from '../../../components/measure/types'; | |||
import type { Metric } from '../../../store/metrics/actions'; | |||
type Props = { | |||
className?: string, | |||
currentUser: { isLoggedIn: boolean }, | |||
rootComponent: Component, | |||
fetchMeasures: ( | |||
Component, | |||
Array<string> | |||
) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>, | |||
leakPeriod?: Period, | |||
metric: Metric, | |||
metrics: { [string]: Metric }, | |||
selected: ?string, | |||
updateQuery: Query => void, | |||
view: string | |||
}; | |||
type State = { | |||
component: ?Component, | |||
loading: { | |||
measure: boolean, | |||
components: boolean | |||
}, | |||
measure: ?MeasureEnhanced, | |||
secondaryMeasure: ?MeasureEnhanced | |||
}; | |||
export default class MeasureContentContainer extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
component: null, | |||
loading: { | |||
measure: false, | |||
components: false | |||
}, | |||
measure: null, | |||
secondaryMeasure: null | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchMeasure(this.props); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
const { component } = this.state; | |||
const componentChanged = | |||
!component || | |||
nextProps.rootComponent.key !== component.key || | |||
nextProps.selected !== component.key; | |||
if (componentChanged || nextProps.metric !== this.props.metric) { | |||
this.fetchMeasure(nextProps); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchMeasure = ({ rootComponent, fetchMeasures, metric, selected }: Props) => { | |||
this.updateLoading({ measure: true }); | |||
const metricKeys = [metric.key]; | |||
if (metric.key === 'ncloc') { | |||
metricKeys.push('ncloc_language_distribution'); | |||
} else if (metric.key === 'function_complexity') { | |||
metricKeys.push('function_complexity_distribution'); | |||
} else if (metric.key === 'file_complexity') { | |||
metricKeys.push('file_complexity_distribution'); | |||
} | |||
fetchMeasures(selected || rootComponent.key, metricKeys).then( | |||
({ component, measures }) => { | |||
if (this.mounted) { | |||
const measure = measures.find(measure => measure.metric.key === metric.key); | |||
const secondaryMeasure = measures.find(measure => measure.metric.key !== metric.key); | |||
this.setState({ component, measure, secondaryMeasure }); | |||
this.updateLoading({ measure: false }); | |||
} | |||
}, | |||
() => this.updateLoading({ measure: false }) | |||
); | |||
}; | |||
updateLoading = (loading: { [string]: boolean }) => { | |||
if (this.mounted) { | |||
this.setState(state => ({ loading: { ...state.loading, ...loading } })); | |||
} | |||
}; | |||
updateSelected = (component: Component) => | |||
this.props.updateQuery({ | |||
selected: component.key !== this.props.rootComponent.key ? component.key : null | |||
}); | |||
updateView = (view: string) => this.props.updateQuery({ view }); | |||
render() { | |||
if (!this.state.component) { | |||
return null; | |||
} | |||
return ( | |||
<MeasureContent | |||
className={this.props.className} | |||
component={this.state.component} | |||
currentUser={this.props.currentUser} | |||
loading={this.state.loading.measure || this.state.loading.components} | |||
leakPeriod={this.props.leakPeriod} | |||
measure={this.state.measure} | |||
metric={this.props.metric} | |||
metrics={this.props.metrics} | |||
rootComponent={this.props.rootComponent} | |||
secondaryMeasure={this.state.secondaryMeasure} | |||
updateLoading={this.updateLoading} | |||
updateSelected={this.updateSelected} | |||
updateView={this.updateView} | |||
view={this.props.view} | |||
/> | |||
); | |||
} | |||
} |
@@ -32,12 +32,12 @@ import { isDiffMetric } from '../../../helpers/measures'; | |||
import type { Component, Period } from '../types'; | |||
import type { MeasureEnhanced } from '../../../components/measure/types'; | |||
type Props = { | |||
type Props = {| | |||
component: Component, | |||
leakPeriod?: Period, | |||
measure: MeasureEnhanced, | |||
secondaryMeasure: ?MeasureEnhanced | |||
}; | |||
|}; | |||
export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) { | |||
const metric = measure.metric; |
@@ -20,11 +20,15 @@ | |||
// @flow | |||
import React from 'react'; | |||
import DeferredSpinner from '../../../components/common/DeferredSpinner'; | |||
import FilesCounter from './FilesCounter'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import type { Paging } from '../types'; | |||
type Props = {| | |||
current: ?number, | |||
loading: boolean, | |||
isFile: ?boolean, | |||
paging: ?Paging, | |||
view: string | |||
|}; | |||
@@ -61,14 +65,20 @@ export default class PageActions extends React.PureComponent { | |||
} | |||
render() { | |||
const { isFile, view } = this.props; | |||
const { isFile, paging, view } = this.props; | |||
const showShortcuts = ['list', 'tree'].includes(view); | |||
return ( | |||
<div className="pull-right"> | |||
{!isFile && showShortcuts && this.renderShortcuts()} | |||
{isFile && this.renderFileShortcuts()} | |||
<div className="measure-details-page-spinner"> | |||
<DeferredSpinner className="pull-right" loading={this.props.loading} /> | |||
<div className="measure-details-page-actions"> | |||
<DeferredSpinner loading={this.props.loading} /> | |||
{paging != null && | |||
<FilesCounter | |||
className="spacer-left" | |||
current={this.props.current} | |||
total={paging.total} | |||
/>} | |||
</div> | |||
</div> | |||
); |
@@ -0,0 +1,30 @@ | |||
/* | |||
* 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. | |||
*/ | |||
import React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import FilesCounter from '../FilesCounter'; | |||
it('should display x files on y total', () => { | |||
expect(shallow(<FilesCounter current={12} total={123455} />)).toMatchSnapshot(); | |||
}); | |||
it('should display only total of files', () => { | |||
expect(shallow(<FilesCounter current={null} total={123455} />)).toMatchSnapshot(); | |||
}); |
@@ -32,3 +32,17 @@ it('should display correctly for a file', () => { | |||
it('should not display shortcuts for treemap', () => { | |||
expect(shallow(<PageActions loading={true} isFile={false} view="treemap" />)).toMatchSnapshot(); | |||
}); | |||
it('should display the total of files', () => { | |||
expect( | |||
shallow( | |||
<PageActions | |||
current={12} | |||
loading={true} | |||
isFile={false} | |||
view="treemap" | |||
paging={{ total: 120 }} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); |
@@ -35,8 +35,8 @@ exports[`should render correctly 1`] = ` | |||
</div> | |||
</div> | |||
</div> | |||
<MeasureContent | |||
className="layout-page-main-inner" | |||
<MeasureContentContainer | |||
className="layout-page-main" | |||
fetchMeasures={[Function]} | |||
leakPeriod={null} | |||
metric={ |
@@ -0,0 +1,25 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display only total of files 1`] = ` | |||
<span> | |||
<strong> | |||
123,455 | |||
</strong> | |||
component_measures.files | |||
</span> | |||
`; | |||
exports[`should display x files on y total 1`] = ` | |||
<span> | |||
<strong> | |||
<span> | |||
12 | |||
/ | |||
</span> | |||
123,455 | |||
</strong> | |||
component_measures.files | |||
</span> | |||
`; |
@@ -17,10 +17,9 @@ exports[`should display correctly for a file 1`] = ` | |||
</span> | |||
</span> | |||
<div | |||
className="measure-details-page-spinner" | |||
className="measure-details-page-actions" | |||
> | |||
<DeferredSpinner | |||
className="pull-right" | |||
loading={false} | |||
timeout={100} | |||
/> | |||
@@ -65,10 +64,9 @@ exports[`should display correctly for a project 1`] = ` | |||
</span> | |||
</span> | |||
<div | |||
className="measure-details-page-spinner" | |||
className="measure-details-page-actions" | |||
> | |||
<DeferredSpinner | |||
className="pull-right" | |||
loading={true} | |||
timeout={100} | |||
/> | |||
@@ -76,15 +74,34 @@ exports[`should display correctly for a project 1`] = ` | |||
</div> | |||
`; | |||
exports[`should display the total of files 1`] = ` | |||
<div | |||
className="pull-right" | |||
> | |||
<div | |||
className="measure-details-page-actions" | |||
> | |||
<DeferredSpinner | |||
loading={true} | |||
timeout={100} | |||
/> | |||
<FilesCounter | |||
className="spacer-left" | |||
current={12} | |||
total={120} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should not display shortcuts for treemap 1`] = ` | |||
<div | |||
className="pull-right" | |||
> | |||
<div | |||
className="measure-details-page-spinner" | |||
className="measure-details-page-actions" | |||
> | |||
<DeferredSpinner | |||
className="pull-right" | |||
loading={true} | |||
timeout={100} | |||
/> |
@@ -0,0 +1,53 @@ | |||
/* | |||
* 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 ComponentsList from './ComponentsList'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
import type { Component, ComponentEnhanced, Paging } from '../../types'; | |||
import type { Metric } from '../../../../store/metrics/actions'; | |||
type Props = { | |||
components: Array<ComponentEnhanced>, | |||
fetchMore: () => void, | |||
handleSelect: Component => void, | |||
metric: Metric, | |||
metrics: { [string]: Metric }, | |||
paging: ?Paging | |||
}; | |||
export default function ListView(props: Props) { | |||
return ( | |||
<div> | |||
<ComponentsList | |||
components={props.components} | |||
metrics={props.metrics} | |||
metric={props.metric} | |||
onClick={props.handleSelect} | |||
/> | |||
{props.paging && | |||
<ListFooter | |||
count={props.components.length} | |||
total={props.paging.total} | |||
loadMore={props.fetchMore} | |||
/>} | |||
</div> | |||
); | |||
} |
@@ -1,161 +0,0 @@ | |||
/* | |||
* 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 ComponentsList from './ComponentsList'; | |||
import ListFooter from '../../../../components/controls/ListFooter'; | |||
import { getComponentTree } from '../../../../api/components'; | |||
import { complementary } from '../../config/complementary'; | |||
import { enhanceComponent } from '../../utils'; | |||
import { isDiffMetric } from '../../../../helpers/measures'; | |||
import type { Component, ComponentEnhanced, Paging } from '../../types'; | |||
import type { Metric } from '../../../../store/metrics/actions'; | |||
type Props = { | |||
component: Component, | |||
handleSelect: Component => void, | |||
metric: Metric, | |||
metrics: { [string]: Metric }, | |||
updateLoading: ({ [string]: boolean }) => void | |||
}; | |||
type State = { | |||
components: Array<ComponentEnhanced>, | |||
metric: ?Metric, | |||
paging?: Paging | |||
}; | |||
export default class ListView extends React.PureComponent { | |||
mounted: boolean; | |||
props: Props; | |||
state: State = { | |||
components: [], | |||
metric: null, | |||
paging: null | |||
}; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchComponents(this.props); | |||
} | |||
componentWillReceiveProps(nextProps: Props) { | |||
if (nextProps.component !== this.props.component || nextProps.metric !== this.props.metric) { | |||
this.fetchComponents(nextProps); | |||
} | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
getComponentRequestParams = (metric: Metric, options: Object = {}) => { | |||
const metricKeys = [metric.key, ...(complementary[metric.key] || [])]; | |||
let opts: Object = { | |||
asc: metric.direction === 1, | |||
ps: 100, | |||
metricSortFilter: 'withMeasuresOnly', | |||
metricSort: metric.key | |||
}; | |||
if (isDiffMetric(metric.key)) { | |||
opts = { | |||
...opts, | |||
s: 'metricPeriod,name', | |||
metricPeriodSort: 1 | |||
}; | |||
} else { | |||
opts = { | |||
...opts, | |||
s: 'metric,name' | |||
}; | |||
} | |||
return { metricKeys, opts: { ...opts, ...options } }; | |||
}; | |||
fetchComponents = ({ component, metric }: Props) => { | |||
const { metricKeys, opts } = this.getComponentRequestParams(metric); | |||
this.props.updateLoading({ components: true }); | |||
getComponentTree('leaves', component.key, metricKeys, opts).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState({ | |||
components: r.components.map(component => enhanceComponent(component, metric)), | |||
metric, | |||
paging: r.paging | |||
}); | |||
} | |||
this.props.updateLoading({ components: false }); | |||
}, | |||
() => this.props.updateLoading({ components: false }) | |||
); | |||
}; | |||
fetchMoreComponents = () => { | |||
const { component, metric } = this.props; | |||
const { paging } = this.state; | |||
if (!paging) { | |||
return; | |||
} | |||
const { metricKeys, opts } = this.getComponentRequestParams(metric, { | |||
p: paging.pageIndex + 1 | |||
}); | |||
this.props.updateLoading({ components: true }); | |||
getComponentTree('leaves', component.key, metricKeys, opts).then( | |||
r => { | |||
if (this.mounted) { | |||
this.setState(state => ({ | |||
components: [ | |||
...state.components, | |||
...r.components.map(component => enhanceComponent(component, metric)) | |||
], | |||
metric, | |||
paging: r.paging | |||
})); | |||
} | |||
this.props.updateLoading({ components: false }); | |||
}, | |||
() => this.props.updateLoading({ components: false }) | |||
); | |||
}; | |||
render() { | |||
const { components, metric, paging } = this.state; | |||
if (metric == null) { | |||
return null; | |||
} | |||
return ( | |||
<div> | |||
<ComponentsList | |||
components={components} | |||
metrics={this.props.metrics} | |||
metric={metric} | |||
onClick={this.props.handleSelect} | |||
/> | |||
{paging && | |||
<ListFooter | |||
count={components.length} | |||
total={paging.total} | |||
loadMore={this.fetchMoreComponents} | |||
/>} | |||
</div> | |||
); | |||
} | |||
} |
@@ -16,10 +16,13 @@ | |||
white-space: nowrap; | |||
} | |||
.measure-details-page-spinner { | |||
.measure-details-page-actions { | |||
display: inline-block; | |||
min-width: 20px; | |||
min-width: 80px; | |||
text-align: right; | |||
} | |||
.measure-details-page-actions .spinner { | |||
vertical-align: text-bottom; | |||
} | |||
@@ -80,6 +80,9 @@ export const enhanceComponent = (component: Component, metric: Metric): Componen | |||
return { ...component, value, leak, measures: enhancedMeasures }; | |||
}; | |||
export const isFileType = (component: Component): boolean => | |||
['FIL', 'UTS'].includes(component.qualifier); | |||
export const groupByDomains = memoize((measures: Array<MeasureEnhanced>): Array<{ | |||
name: string, | |||
measures: Array<MeasureEnhanced> |
@@ -2884,6 +2884,7 @@ code.open_component_page=Open Component's Page | |||
component_measures.all_measures=All Measures | |||
component_measures.domain_measures={0} Measures | |||
component_measures.back_to_list=Back to List | |||
component_measures.files=files | |||
component_measures.show_metric_history=Show history of this metric | |||
component_measures.tab.tree=Tree | |||
component_measures.tab.list=List |