Browse Source

SONAR-9608 SONAR-9635 Make tree view and file view use the same base component

tags/6.6-RC1
Grégoire Aubert 6 years ago
parent
commit
d0d3a549fc
17 changed files with 494 additions and 281 deletions
  1. 3
    3
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  2. 5
    6
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
  3. 45
    0
      server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js
  4. 122
    96
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
  5. 148
    0
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js
  6. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
  7. 13
    3
      server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js
  8. 30
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js
  9. 14
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js
  10. 2
    2
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
  11. 25
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap
  12. 23
    6
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap
  13. 53
    0
      server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js
  14. 0
    161
      server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js
  15. 5
    2
      server/sonar-web/src/main/js/apps/component-measures/style.css
  16. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/utils.js
  17. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 3
server/sonar-web/src/main/js/apps/component-measures/components/App.js View File

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

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

@@ -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] });
}

+ 45
- 0
server/sonar-web/src/main/js/apps/component-measures/components/FilesCounter.js View File

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

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

@@ -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>

+ 148
- 0
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js View File

@@ -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}
/>
);
}
}

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

@@ -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;

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

@@ -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>
);

+ 30
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/FilesCounter-test.js View File

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

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

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

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

@@ -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={

+ 25
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/FilesCounter-test.js.snap View File

@@ -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>
`;

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

@@ -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}
/>

+ 53
- 0
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js View File

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

+ 0
- 161
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js View File

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

+ 5
- 2
server/sonar-web/src/main/js/apps/component-measures/style.css View File

@@ -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;
}


+ 3
- 0
server/sonar-web/src/main/js/apps/component-measures/utils.js View File

@@ -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>

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

@@ -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

Loading…
Cancel
Save