Explorar el Código

SONAR-9608 SONAR-9613 Add the page actions and a select to switch between views

tags/6.6-RC1
Grégoire Aubert hace 6 años
padre
commit
5be60c5d33
Se han modificado 34 ficheros con 1071 adiciones y 91 borrados
  1. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/components/App.js
  2. 2
    0
      server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js
  3. 67
    0
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js
  4. 95
    0
      server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
  5. 99
    26
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
  6. 2
    0
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js
  7. 95
    0
      server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js
  8. 76
    0
      server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js
  9. 56
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js
  10. 70
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js
  11. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js
  12. 30
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js
  13. 34
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js
  14. 1
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap
  15. 23
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap
  16. 93
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap
  17. 2
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap
  18. 81
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap
  19. 93
    0
      server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap
  20. 3
    9
      server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js
  21. 3
    35
      server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js
  22. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js
  23. 1
    1
      server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
  24. 4
    4
      server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap
  25. 34
    0
      server/sonar-web/src/main/js/apps/component-measures/style.css
  26. 3
    0
      server/sonar-web/src/main/js/apps/component-measures/utils.js
  27. 1
    1
      server/sonar-web/src/main/js/apps/issues/components/PageActions.js
  28. 1
    1
      server/sonar-web/src/main/js/components/icons-components/ListIcon.js
  29. 44
    0
      server/sonar-web/src/main/js/components/icons-components/TreeIcon.js
  30. 43
    0
      server/sonar-web/src/main/js/components/icons-components/TreemapIcon.js
  31. 4
    4
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.js
  32. 2
    3
      server/sonar-web/src/main/js/helpers/path.js
  33. 1
    4
      server/sonar-web/src/main/js/helpers/urls.js
  34. 3
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 3
- 0
server/sonar-web/src/main/js/apps/component-measures/components/App.js Ver fichero

@@ -32,6 +32,7 @@ import '../style.css';

type Props = {|
component: Component,
currentUser: { isLoggedIn: boolean },
location: { pathname: string, query: RawQuery },
fetchMeasures: (
Component,
@@ -154,6 +155,7 @@ export default class App extends React.PureComponent {
{metric != null &&
<MeasureContent
className="layout-page-main-inner"
currentUser={this.props.currentUser}
rootComponent={this.props.component}
fetchMeasures={this.props.fetchMeasures}
leakPeriod={this.state.leakPeriod}
@@ -161,6 +163,7 @@ export default class App extends React.PureComponent {
metrics={this.props.metrics}
selected={query.selected}
updateQuery={this.updateQuery}
view={query.view}
/>}
</div>
);

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js Ver fichero

@@ -24,6 +24,7 @@ import App from './App';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import {
getComponent,
getCurrentUser,
getMetrics,
getMetricByKey,
getMetricsKey
@@ -37,6 +38,7 @@ import type { Measure, MeasureEnhanced } from '../../../components/measure/types

const mapStateToProps = (state, ownProps) => ({
component: getComponent(state, ownProps.location.query.id),
currentUser: getCurrentUser(state),
metrics: getMetrics(state),
metricsKey: getMetricsKey(state)
});

+ 67
- 0
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js Ver fichero

@@ -0,0 +1,67 @@
/*
* 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 Tooltip from '../../../components/controls/Tooltip';
import { collapsePath, limitComponentName } from '../../../helpers/path';
import type { Component } from '../types';

type Props = {
canBrowse: boolean,
component: Component,
isLast: boolean,
handleSelect: Component => void
};

export default class Breadcrumb extends React.PureComponent {
props: Props;

handleClick = (e: Event & { target: HTMLElement }) => {
e.preventDefault();
e.target.blur();
this.props.handleSelect(this.props.component);
};

render() {
const { canBrowse, component, isLast } = this.props;
const isPath = component.qualifier === 'DIR';
const componentName = isPath
? collapsePath(component.name, 15)
: limitComponentName(component.name);
const breadcrumbItem = canBrowse
? <a href="#" onClick={this.handleClick}>
{componentName}
</a>
: <span>
{componentName}
</span>;

return (
<span>
{component.name !== componentName
? <Tooltip overlay={component.name} placement="bottom">
{breadcrumbItem}
</Tooltip>
: breadcrumbItem}
{!isLast && <span className="slash-separator" />}
</span>
);
}
}

+ 95
- 0
server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js Ver fichero

@@ -0,0 +1,95 @@
/*
* 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 Breadcrumb from './Breadcrumb';
import { getBreadcrumbs } from '../../../api/components';
import type { Component } from '../types';

type Props = {
className?: string,
component: Component,
handleSelect: Component => void,
rootComponent: Component,
view: string
};

type State = {
breadcrumbs: Array<Component>
};

export default class Breadcrumbs extends React.PureComponent {
mounted: boolean;
props: Props;
state: State = {
breadcrumbs: []
};

componentDidMount() {
this.mounted = true;
this.fetchBreadcrumbs(this.props);
}

componentWillReceiveProps(nextProps: Props) {
if (this.props.component !== nextProps.component) {
this.fetchBreadcrumbs(nextProps);
}
}

componentWillUnmount() {
this.mounted = false;
}

fetchBreadcrumbs = ({ component, rootComponent, view }: Props) => {
const isRoot = component.key === rootComponent.key;
if (isRoot || view === 'list') {
if (this.mounted) {
this.setState({ breadcrumbs: isRoot ? [component] : [rootComponent, component] });
}
return;
}
getBreadcrumbs(component.key).then(breadcrumbs => {
if (this.mounted) {
this.setState({ breadcrumbs });
}
});
};

render() {
const { breadcrumbs } = this.state;
if (breadcrumbs.length <= 0) {
return null;
}
const lastItem = breadcrumbs[breadcrumbs.length - 1];
return (
<div className={this.props.className}>
{breadcrumbs.map(component =>
<Breadcrumb
key={component.key}
canBrowse={component.key !== lastItem.key}
component={component}
isLast={component.key === lastItem.key}
handleSelect={this.props.handleSelect}
/>
)}
</div>
);
}
}

+ 99
- 26
server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js Ver fichero

@@ -19,16 +19,23 @@
*/
// @flow
import React from 'react';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import moment from 'moment';
import Breadcrumbs from './Breadcrumbs';
import Favorite from '../../../components/controls/Favorite';
import ListView from './drilldown/ListView';
import MeasureHeader from './MeasureHeader';
import MeasureViewSelect from './MeasureViewSelect';
import MetricNotFound from './MetricNotFound';
import PageActions from './PageActions';
import SourceViewer from '../../../components/SourceViewer/SourceViewer';
import { isDiffMetric } from '../../../helpers/measures';
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,
@@ -38,7 +45,8 @@ type Props = {
metric: Metric,
metrics: { [string]: Metric },
selected: ?string,
updateQuery: Query => void
updateQuery: Query => void,
view: string
};

type State = {
@@ -109,7 +117,10 @@ export default class MeasureContent extends React.PureComponent {
);
};

handleSelect = (component: Component) => this.props.updateQuery({ selected: component.key });
handleSelect = (component: Component) =>
this.props.updateQuery({
selected: component.key !== this.props.rootComponent.key ? component.key : null
});

updateLoading = (loading: { [string]: boolean }) => {
if (this.mounted) {
@@ -117,43 +128,105 @@ export default class MeasureContent extends React.PureComponent {
}
};

render() {
const { metric } = this.props;
const { loading, measure } = this.state;
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';

if (isFile) {
const leakPeriodDate =
isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null;

let filterLine;
if (leakPeriodDate != null) {
filterLine = line => {
if (line.scmDate) {
const scmDate = moment(line.scmDate).toDate();
return scmDate >= leakPeriodDate;
} else {
return false;
}
};
}
return (
<div className="measure-details-viewer">
<SourceViewer component={component.key} filterLine={filterLine} />
</div>
);
}

if (view === 'list') {
return (
<ListView
component={component}
handleSelect={this.handleSelect}
metric={metric}
metrics={this.props.metrics}
updateLoading={this.updateLoading}
/>
);
}
}

render() {
const { currentUser, metric, rootComponent, view } = this.props;
const { component, loading, measure } = this.state;
const isLoggedIn = currentUser && currentUser.isLoggedIn;
return (
<div className="layout-page-main">
<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">
Page Actions
<DeferredSpinner
className="pull-right"
<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 &&
isLoggedIn &&
<Favorite
favorite={component.isFavorite === true}
component={component.key}
className="measure-favorite spacer-right"
/>}
<MeasureViewSelect
className="measure-view-select"
metric={this.props.metric}
handleViewChange={this.updateView}
view={view}
/>
<PageActions
loading={loading.measure || loading.components}
isFile={component && component.qualifier === 'FIL'}
view={view}
/>
</div>
</div>
</div>
{metric != null && measure != null
? <div className="layout-page-main-inner">
{metric == null && <MetricNotFound className="layout-page-main-inner" />}
{metric != null &&
measure != null &&
<div className="layout-page-main-inner">
{component &&
<MeasureHeader
component={this.state.component}
component={component}
leakPeriod={this.props.leakPeriod}
measure={measure}
secondaryMeasure={this.state.secondaryMeasure}
/>
<ListView
component={this.state.component}
handleSelect={this.handleSelect}
leakPeriod={this.props.leakPeriod}
loading={loading.components}
metric={metric}
metrics={this.props.metrics}
selectedComponent={this.props.selected}
updateLoading={this.updateLoading}
/>
</div>
: <MetricNotFound className="layout-page-main-inner" />}
updateQuery={this.props.updateQuery}
/>}
{this.renderContent()}
</div>}
</div>
);
}

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js Ver fichero

@@ -42,6 +42,7 @@ type Props = {
export default function MeasureHeader({ component, leakPeriod, measure, secondaryMeasure }: Props) {
const metric = measure.metric;
const isDiff = isDiffMetric(metric.key);
const hasHistory = ['TRK', 'VW', 'SVW', 'APP'].includes(component.qualifier);
return (
<div className="measure-details-header big-spacer-bottom">
<div className="measure-details-metric">
@@ -55,6 +56,7 @@ export default function MeasureHeader({ component, leakPeriod, measure, secondar
</strong>
</span>
{!isDiff &&
hasHistory &&
<Tooltip placement="right" overlay={translate('component_measures.show_metric_history')}>
<Link
className="js-show-history spacer-left button button-small button-compact"

+ 95
- 0
server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js Ver fichero

@@ -0,0 +1,95 @@
/*
* 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 Select from 'react-select';
import ListIcon from '../../../components/icons-components/ListIcon';
import TreeIcon from '../../../components/icons-components/TreeIcon';
import TreemapIcon from '../../../components/icons-components/TreemapIcon';
import { hasTreemap } from '../utils';
import { translate } from '../../../helpers/l10n';
import type { Metric } from '../../../store/metrics/actions';

type Props = {
className?: string,
metric: Metric,
handleViewChange: (view: string) => void,
view: string
};

export default class MeasureViewSelect extends React.PureComponent {
props: Props;

getOptions = () => {
const { metric } = this.props;
const options = [];
options.push({
value: 'list',
label: (
<div>
<ListIcon className="little-spacer-right" />
{translate('component_measures.tab.list')}
</div>
),
icon: <ListIcon />
});
options.push({
value: 'tree',
label: (
<div>
<TreeIcon className="little-spacer-right" />
{translate('component_measures.tab.tree')}
</div>
),
icon: <TreeIcon />
});
if (hasTreemap(metric.type)) {
options.push({
value: 'treemap',
label: (
<div>
<TreemapIcon className="little-spacer-right" />
{translate('component_measures.tab.treemap')}
</div>
),
icon: <TreemapIcon />
});
}
return options;
};

handleChange = (option: { value: string }) => this.props.handleViewChange(option.value);

renderValue = (value: { icon: Element<*> }) => value.icon;

render() {
return (
<Select
className={this.props.className}
clearable={false}
searchable={false}
value={this.props.view}
valueRenderer={this.renderValue}
options={this.getOptions()}
onChange={this.handleChange}
/>
);
}
}

+ 76
- 0
server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js Ver fichero

@@ -0,0 +1,76 @@
/*
* 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 DeferredSpinner from '../../../components/common/DeferredSpinner';
import { translate } from '../../../helpers/l10n';

type Props = {|
loading: boolean,
isFile: ?boolean,
view: string
|};

export default class PageActions extends React.PureComponent {
props: Props;

renderShortcuts() {
return (
<span className="note big-spacer-right">
<span className="big-spacer-right">
<span className="shortcut-button little-spacer-right">↑</span>
<span className="shortcut-button little-spacer-right">↓</span>
{translate('component_measures.to_select_files')}
</span>

<span>
<span className="shortcut-button little-spacer-right">←</span>
<span className="shortcut-button little-spacer-right">→</span>
{translate('component_measures.to_navigate')}
</span>
</span>
);
}

renderFileShortcuts() {
return (
<span className="note big-spacer-right">
<span>
<span className="shortcut-button little-spacer-right">←</span>
{translate('component_measures.to_navigate_back')}
</span>
</span>
);
}

render() {
const { isFile, 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>
</div>
);
}
}

+ 56
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js Ver fichero

@@ -0,0 +1,56 @@
/*
* 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 Breadcrumb from '../Breadcrumb';

it('should show the last element without clickable link', () => {
expect(
shallow(
<Breadcrumb
canBrowse={false}
component={{
key: 'foo',
name: 'Foo',
qualifier: 'TRK'
}}
isLast={true}
handleSelect={() => {}}
/>
)
).toMatchSnapshot();
});

it('should correctly show a middle element', () => {
expect(
shallow(
<Breadcrumb
canBrowse={true}
component={{
key: 'foo',
name: 'Foo',
qualifier: 'TRK'
}}
isLast={false}
handleSelect={() => {}}
/>
)
).toMatchSnapshot();
});

+ 70
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js Ver fichero

@@ -0,0 +1,70 @@
/*
* 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 { mount } from 'enzyme';
import Breadcrumbs from '../Breadcrumbs';
import { doAsync } from '../../../../helpers/testUtils';

jest.mock('../../../../api/components', () => ({
getBreadcrumbs: () =>
Promise.resolve([
{ key: 'anc1', name: 'Ancestor1' },
{ key: 'anc2', name: 'Ancestor2' },
{ key: 'bar', name: 'Bar' }
])
}));

it('should display correctly for the list view', () => {
const wrapper = mount(
<Breadcrumbs
component={{ key: 'bar', name: 'Bar' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}
view="list"
/>
);
expect(wrapper).toMatchSnapshot();
});

it('should display only the root component', () => {
const wrapper = mount(
<Breadcrumbs
component={{ key: 'foo', name: 'Foo' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}
view="tree"
/>
);
expect(wrapper.state()).toMatchSnapshot();
});

it.only('should load the breadcrumb from the api', () => {
const wrapper = mount(
<Breadcrumbs
component={{ key: 'bar', name: 'Bar' }}
handleSelect={() => {}}
rootComponent={{ key: 'foo', name: 'Foo' }}
view="tree"
/>
);
return doAsync(() => {
expect(wrapper.state()).toMatchSnapshot();
});
});

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

@@ -53,7 +53,7 @@ const SECONDARY = {
};

const PROPS = {
component: { key: 'foo' },
component: { key: 'foo', qualifier: 'TRK' },
leakPeriod: {
date: '2017-05-16T13:50:02+0200',
index: 1,

+ 30
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js Ver fichero

@@ -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 MeasureViewSelect from '../MeasureViewSelect';

it('should display correctly with treemap option', () => {
expect(
shallow(
<MeasureViewSelect metric={{ type: 'PERCENT' }} handleViewChange={() => {}} view="tree" />
)
).toMatchSnapshot();
});

+ 34
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js Ver fichero

@@ -0,0 +1,34 @@
/*
* 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 PageActions from '../PageActions';

it('should display correctly for a project', () => {
expect(shallow(<PageActions loading={true} isFile={false} view="list" />)).toMatchSnapshot();
});

it('should display correctly for a file', () => {
expect(shallow(<PageActions loading={false} isFile={true} view="tree" />)).toMatchSnapshot();
});

it('should not display shortcuts for treemap', () => {
expect(shallow(<PageActions loading={true} isFile={false} view="treemap" />)).toMatchSnapshot();
});

+ 1
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap Ver fichero

@@ -82,6 +82,7 @@ exports[`should render correctly 1`] = `
}
selected=""
updateQuery={[Function]}
view="list"
/>
</div>
`;

+ 23
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap Ver fichero

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should correctly show a middle element 1`] = `
<span>
<a
href="#"
onClick={[Function]}
>
Foo
</a>
<span
className="slash-separator"
/>
</span>
`;

exports[`should show the last element without clickable link 1`] = `
<span>
<span>
Foo
</span>
</span>
`;

+ 93
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap Ver fichero

@@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly for the list view 1`] = `
<Breadcrumbs
component={
Object {
"key": "bar",
"name": "Bar",
}
}
handleSelect={[Function]}
rootComponent={
Object {
"key": "foo",
"name": "Foo",
}
}
view="list"
>
<div>
<Breadcrumb
canBrowse={true}
component={
Object {
"key": "foo",
"name": "Foo",
}
}
handleSelect={[Function]}
isLast={false}
>
<span>
<a
href="#"
onClick={[Function]}
>
Foo
</a>
<span
className="slash-separator"
/>
</span>
</Breadcrumb>
<Breadcrumb
canBrowse={false}
component={
Object {
"key": "bar",
"name": "Bar",
}
}
handleSelect={[Function]}
isLast={true}
>
<span>
<span>
Bar
</span>
</span>
</Breadcrumb>
</div>
</Breadcrumbs>
`;

exports[`should display only the root component 1`] = `
Object {
"breadcrumbs": Array [
Object {
"key": "foo",
"name": "Foo",
},
],
}
`;

exports[`should load the breadcrumb from the api 1`] = `
Object {
"breadcrumbs": Array [
Object {
"key": "anc1",
"name": "Ancestor1",
},
Object {
"key": "anc2",
"name": "Ancestor2",
},
Object {
"key": "bar",
"name": "Bar",
},
],
}
`;

+ 2
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap Ver fichero

@@ -71,6 +71,7 @@ exports[`should render correctly 1`] = `
component={
Object {
"key": "foo",
"qualifier": "TRK",
}
}
period={
@@ -134,6 +135,7 @@ exports[`should render correctly for leak 1`] = `
component={
Object {
"key": "foo",
"qualifier": "TRK",
}
}
period={

+ 81
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap Ver fichero

@@ -0,0 +1,81 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly with treemap option 1`] = `
<Select
addLabelText="Add \\"{label}\\"?"
arrowRenderer={[Function]}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
clearAllText="Clear all"
clearRenderer={[Function]}
clearValueText="Clear value"
clearable={false}
deleteRemoves={true}
delimiter=","
disabled={false}
escapeClearsValue={true}
filterOptions={[Function]}
ignoreAccents={true}
ignoreCase={true}
inputProps={Object {}}
isLoading={false}
joinValues={false}
labelKey="label"
matchPos="any"
matchProp="any"
menuBuffer={0}
menuRenderer={[Function]}
multi={false}
noResultsText="No results found"
onBlurResetsInput={true}
onChange={[Function]}
onCloseResetsInput={true}
optionComponent={[Function]}
options={
Array [
Object {
"icon": <ListIcon />,
"label": <div>
<ListIcon
className="little-spacer-right"
/>
component_measures.tab.list
</div>,
"value": "list",
},
Object {
"icon": <TreeIcon />,
"label": <div>
<TreeIcon
className="little-spacer-right"
/>
component_measures.tab.tree
</div>,
"value": "tree",
},
Object {
"icon": <TreemapIcon />,
"label": <div>
<TreemapIcon
className="little-spacer-right"
/>
component_measures.tab.treemap
</div>,
"value": "treemap",
},
]
}
pageSize={5}
placeholder="Select..."
required={false}
scrollMenuIntoView={true}
searchable={false}
simpleValue={false}
tabSelectsValue={true}
value="tree"
valueComponent={[Function]}
valueKey="value"
valueRenderer={[Function]}
/>
`;

+ 93
- 0
server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap Ver fichero

@@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should display correctly for a file 1`] = `
<div
className="pull-right"
>
<span
className="note big-spacer-right"
>
<span>
<span
className="shortcut-button little-spacer-right"
>
</span>
component_measures.to_navigate_back
</span>
</span>
<div
className="measure-details-page-spinner"
>
<DeferredSpinner
className="pull-right"
loading={false}
timeout={100}
/>
</div>
</div>
`;

exports[`should display correctly for a project 1`] = `
<div
className="pull-right"
>
<span
className="note big-spacer-right"
>
<span
className="big-spacer-right"
>
<span
className="shortcut-button little-spacer-right"
>
</span>
<span
className="shortcut-button little-spacer-right"
>
</span>
component_measures.to_select_files
</span>
<span>
<span
className="shortcut-button little-spacer-right"
>
</span>
<span
className="shortcut-button little-spacer-right"
>
</span>
component_measures.to_navigate
</span>
</span>
<div
className="measure-details-page-spinner"
>
<DeferredSpinner
className="pull-right"
loading={true}
timeout={100}
/>
</div>
</div>
`;

exports[`should not display shortcuts for treemap 1`] = `
<div
className="pull-right"
>
<div
className="measure-details-page-spinner"
>
<DeferredSpinner
className="pull-right"
loading={true}
timeout={100}
/>
</div>
</div>
`;

+ 3
- 9
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js Ver fichero

@@ -74,15 +74,9 @@ export default class ComponentCell extends React.PureComponent {
});

return (
<td style={{ maxWidth: 0 }}>
<div
style={{
maxWidth: '100%',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{component.refId == null || component.qualifier === 'DEV_PRJ'
<td className="measure-details-component-cell">
<div className="text-ellipsis">
{component.refId == null
? <a
id={'component-measures-component-link-' + component.key}
className={linkClassName}

+ 3
- 35
server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js Ver fichero

@@ -19,25 +19,20 @@
*/
// @flow
import React from 'react';
import moment from 'moment';
import ComponentsList from './ComponentsList';
import ListFooter from '../../../../components/controls/ListFooter';
import SourceViewer from '../../../../components/SourceViewer/SourceViewer';
import { getComponentTree } from '../../../../api/components';
import { complementary } from '../../config/complementary';
import { isDiffMetric } from '../../../../helpers/measures';
import { enhanceComponent } from '../../utils';
import type { Component, ComponentEnhanced, Paging, Period } from '../../types';
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,
leakPeriod?: Period,
loading: boolean,
metric: Metric,
metrics: { [string]: Metric },
selectedComponent: ?string,
updateLoading: ({ [string]: boolean }) => void
};

@@ -94,11 +89,7 @@ export default class ListView extends React.PureComponent {
return { metricKeys, opts: { ...opts, ...options } };
};

fetchComponents = ({ component, metric, selectedComponent }: Props) => {
if (selectedComponent) {
this.setState({ metric });
return;
}
fetchComponents = ({ component, metric }: Props) => {
const { metricKeys, opts } = this.getComponentRequestParams(metric);
this.props.updateLoading({ components: true });
getComponentTree('leaves', component.key, metricKeys, opts).then(
@@ -150,29 +141,6 @@ export default class ListView extends React.PureComponent {
return null;
}

const { leakPeriod, selectedComponent } = this.props;
if (selectedComponent) {
const leakPeriodDate =
isDiffMetric(metric.key) && leakPeriod != null ? moment(leakPeriod.date).toDate() : null;

let filterLine;
if (leakPeriodDate != null) {
filterLine = line => {
if (line.scmDate) {
const scmDate = moment(line.scmDate).toDate();
return scmDate >= leakPeriodDate;
} else {
return false;
}
};
}
return (
<div className="measure-details-viewer">
<SourceViewer component={selectedComponent} filterLine={filterLine} />
</div>
);
}

return (
<div>
<ComponentsList

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js Ver fichero

@@ -63,7 +63,7 @@ export default class DomainFacet extends React.PureComponent {
disabled={false}
key={measure.metric.key}
name={
<Tooltip overlay={getLocalizedMetricName(measure.metric)} mouseEnterDelay={1}>
<Tooltip overlay={getLocalizedMetricName(measure.metric)} mouseEnterDelay={0.5}>
<span id={`measure-${measure.metric.key}-name`}>
<IssueTypeIcon query={measure.metric.key} className="little-spacer-right" />
{getLocalizedMetricName(measure.metric)}

+ 1
- 1
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js Ver fichero

@@ -49,7 +49,7 @@ export default class Sidebar extends React.PureComponent {
}));
};

changeMetric = (metric: string) => this.props.updateQuery({ metric });
changeMetric = (metric: string) => this.props.updateQuery({ metric, selected: null });

render() {
return (

+ 4
- 4
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap Ver fichero

@@ -15,7 +15,7 @@ exports[`should display facet item list 1`] = `
halfWidth={false}
name={
<Tooltip
mouseEnterDelay={1}
mouseEnterDelay={0.5}
overlay="Bugs"
placement="bottom"
>
@@ -61,7 +61,7 @@ exports[`should display facet item list 1`] = `
halfWidth={false}
name={
<Tooltip
mouseEnterDelay={1}
mouseEnterDelay={0.5}
overlay="New Bugs"
placement="bottom"
>
@@ -119,7 +119,7 @@ exports[`should display facet item list with bugs selected 1`] = `
halfWidth={false}
name={
<Tooltip
mouseEnterDelay={1}
mouseEnterDelay={0.5}
overlay="Bugs"
placement="bottom"
>
@@ -165,7 +165,7 @@ exports[`should display facet item list with bugs selected 1`] = `
halfWidth={false}
name={
<Tooltip
mouseEnterDelay={1}
mouseEnterDelay={0.5}
overlay="New Bugs"
placement="bottom"
>

+ 34
- 0
server/sonar-web/src/main/js/apps/component-measures/style.css Ver fichero

@@ -16,6 +16,13 @@
white-space: nowrap;
}

.measure-details-page-spinner {
display: inline-block;
min-width: 20px;
text-align: right;
vertical-align: text-bottom;
}

.measure-details-header {
display: flex;
flex-wrap: nowrap;
@@ -38,6 +45,14 @@
margin-top: -10px;
}

.measure-details-component-cell {
max-width: 0;
}

.measure-details-component-cell > div {
max-width: 100%;
}

.domain-measures-value .rating,
.measure-details-value .rating {
width: 18px;
@@ -47,3 +62,22 @@
margin-bottom: -2px;
font-size: 12px;
}

.measure-view-select {
width: 50px;
}

.measure-view-select .Select-menu-outer {
width: 100px;
border-top-right-radius: 4px;
}

.measure-breadcrumbs {
display: inline-block;
max-width: 60%;
vertical-align: middle;
}

.measure-favorite svg {
vertical-align: middle;
}

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

@@ -99,6 +99,9 @@ export const groupByDomains = memoize((measures: Array<MeasureEnhanced>): Array<
]);
});

export const hasTreemap = (metricType: string): boolean =>
['PERCENT', 'RATING', 'LEVEL'].includes(metricType);

export const hasBubbleChart = (domainName: string): boolean => bubbles[domainName] != null;

export const parseQuery = memoize((urlQuery: RawQuery): Query => ({

+ 1
- 1
server/sonar-web/src/main/js/apps/issues/components/PageActions.js Ver fichero

@@ -61,7 +61,7 @@ export default class PageActions extends React.PureComponent {

<div className="issues-page-actions">
{this.props.loading
? <i className="issues-main-header-spinner spinner" />
? <i className="issues-main-header-spinner spinner spacer-right" />
: <ReloadButton className="spacer-right" onClick={this.props.onReload} />}
{paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
</div>

+ 1
- 1
server/sonar-web/src/main/js/components/icons-components/ListIcon.js Ver fichero

@@ -22,7 +22,7 @@ import React from 'react';

type Props = { className?: string, size?: number };

export default function ListIcon({ className, size = 16 }: Props) {
export default function ListIcon({ className, size = 14 }: Props) {
/* eslint-disable max-len */
return (
<svg

+ 44
- 0
server/sonar-web/src/main/js/components/icons-components/TreeIcon.js Ver fichero

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

type Props = { className?: string, size?: number };

export default function TreeIcon({ className, size = 14 }: Props) {
/* eslint-disable max-len */
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={className}
height={size}
width={size}
viewBox="0 0 16 16"
fillRule="evenodd"
clipRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="1.414">
<path
fill="currentColor"
d="M16 1.785c0-0.315-0.256-0.571-0.571-0.571h-14.857c-0.315 0-0.571 0.256-0.571 0.571v1.143c0 0.315 0.256 0.571 0.571 0.571h14.857c0.315 0 0.571-0.256 0.571-0.571v-1.143zM16 5.214c0-0.315-0.22-0.571-0.49-0.571h-12.735c-0.27 0-0.49 0.256-0.49 0.571v1.143c0 0.315 0.219 0.571 0.49 0.571h12.735c0.27 0 0.49-0.256 0.49-0.571v-1.143zM16 8.642c0-0.315-0.183-0.571-0.408-0.571h-10.612c-0.225 0-0.408 0.256-0.408 0.571v1.143c0 0.315 0.183 0.571 0.408 0.571h10.612c0.225 0 0.408-0.256 0.408-0.571v-1.143zM16 12.072c0-0.315-0.146-0.571-0.326-0.571h-8.49c-0.18 0-0.327 0.256-0.327 0.571v1.143c0 0.315 0.146 0.571 0.327 0.571h8.49c0.18 0 0.326-0.256 0.326-0.571v-1.143z"
/>
</svg>
);
}

+ 43
- 0
server/sonar-web/src/main/js/components/icons-components/TreemapIcon.js Ver fichero

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

type Props = { className?: string, size?: number };

export default function TreemapIcon({ className, size = 14 }: Props) {
/* eslint-disable max-len */
return (
<svg
className={className}
height={size}
width={size}
viewBox="0 0 16 16"
fillRule="evenodd"
clipRule="evenodd"
strokeLinejoin="round"
strokeMiterlimit="1.414">
<path
fill="currentColor"
d="M0 0h8v16h-8zM9.143 0h6.857v9.143h-6.857zM9.143 10.286h6.857v5.714h-6.857z"
/>
</svg>
);
}

+ 4
- 4
server/sonar-web/src/main/js/helpers/__tests__/urls-test.js Ver fichero

@@ -78,15 +78,15 @@ describe('#getComponentIssuesUrl', () => {
describe('#getComponentDrilldownUrl', () => {
it('should return component drilldown url', () => {
expect(getComponentDrilldownUrl(SIMPLE_COMPONENT_KEY, METRIC)).toEqual({
pathname: '/component_measures_old/metric/' + METRIC,
query: { id: SIMPLE_COMPONENT_KEY }
pathname: '/component_measures',
query: { id: SIMPLE_COMPONENT_KEY, metric: METRIC }
});
});

it('should not encode component key', () => {
expect(getComponentDrilldownUrl(COMPLEX_COMPONENT_KEY, METRIC)).toEqual({
pathname: '/component_measures_old/metric/' + METRIC,
query: { id: COMPLEX_COMPONENT_KEY }
pathname: '/component_measures',
query: { id: COMPLEX_COMPONENT_KEY, metric: METRIC }
});
});
});

+ 2
- 3
server/sonar-web/src/main/js/helpers/path.js Ver fichero

@@ -103,10 +103,9 @@ export function splitPath(path) {
}
}

export function limitComponentName(str) {
export function limitComponentName(str, limit = 30) {
if (typeof str === 'string') {
const LIMIT = 30;
return str.length > LIMIT ? str.substr(0, LIMIT) + '...' : str;
return str.length > limit ? str.substr(0, limit) + '...' : str;
} else {
return '';
}

+ 1
- 4
server/sonar-web/src/main/js/helpers/urls.js Ver fichero

@@ -62,10 +62,7 @@ export function getComponentIssuesUrlAsString(componentKey, query) {
* @returns {Object}
*/
export function getComponentDrilldownUrl(componentKey, metric) {
return {
pathname: `/component_measures_old/metric/${metric}`,
query: { id: componentKey }
};
return { pathname: '/component_measures', query: { id: componentKey, metric } };
}

/**

+ 3
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties Ver fichero

@@ -2895,7 +2895,9 @@ component_measures.legend.size_x=Size: {0}
component_measures.x_of_y={0} of {1}
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

#------------------------------------------------------------------------------
#

Cargando…
Cancelar
Guardar