diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-02 14:25:57 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-14 11:44:44 +0200 |
commit | 5be60c5d3348076336e5a79e6308104db52f27dc (patch) | |
tree | 498f4b70fbbe58d9b84406301b687265814cf267 /server/sonar-web/src/main/js/apps | |
parent | d7b669175e4e40341f6f1553ebe8ed84a9980ce2 (diff) | |
download | sonarqube-5be60c5d3348076336e5a79e6308104db52f27dc.tar.gz sonarqube-5be60c5d3348076336e5a79e6308104db52f27dc.zip |
SONAR-9608 SONAR-9613 Add the page actions and a select to switch between views
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
27 files changed, 973 insertions, 78 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js index 7b9ac879874..2524de80259 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js @@ -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> ); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js index f8979c7eea7..653380d1ad7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/AppContainer.js @@ -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) }); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js new file mode 100644 index 00000000000..2be60f7c918 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumb.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js new file mode 100644 index 00000000000..320a9ea93e9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js index 68bc0bb3eac..a0447e10030 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js index c58748cda20..a29d87081cd 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureHeader.js @@ -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" diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js new file mode 100644 index 00000000000..42b22ac2213 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js @@ -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} + /> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js new file mode 100644 index 00000000000..22df52e5ea9 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/PageActions.js @@ -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> + ); + } +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js new file mode 100644 index 00000000000..fd5da228036 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumb-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js new file mode 100644 index 00000000000..22018da2057 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/Breadcrumbs-test.js @@ -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(); + }); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js index 8914cf0a86e..34ea5cd64d1 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureHeader-test.js @@ -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, diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js new file mode 100644 index 00000000000..607b30330a2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/MeasureViewSelect-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js new file mode 100644 index 00000000000..d3770a5fa0e --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/PageActions-test.js @@ -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(); +}); diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap index 1eeed263913..99335765bdc 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/App-test.js.snap @@ -82,6 +82,7 @@ exports[`should render correctly 1`] = ` } selected="" updateQuery={[Function]} + view="list" /> </div> `; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap new file mode 100644 index 00000000000..e116a3a254c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumb-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap new file mode 100644 index 00000000000..63bab85ba77 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/Breadcrumbs-test.js.snap @@ -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", + }, + ], +} +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap index 3e66e8fa4fc..cd412e31862 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureHeader-test.js.snap @@ -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={ diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap new file mode 100644 index 00000000000..7d51b2dd127 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap @@ -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]} +/> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap new file mode 100644 index 00000000000..26a060b6fb2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/PageActions-test.js.snap @@ -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> +`; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js index a3b54e1955e..8d723304ce8 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentCell.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js index d567f772759..b9f94f2c715 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ListView.js @@ -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 diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js index c7e8dd68b21..5df2b938c07 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.js @@ -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)} diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js index 58c0326c9dc..74ef48f3c63 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js @@ -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 ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap index a55b82c48d7..f883847f9ac 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap +++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.js.snap @@ -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" > diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index 1e3aa63a269..238b36f5be3 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -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; +} diff --git a/server/sonar-web/src/main/js/apps/component-measures/utils.js b/server/sonar-web/src/main/js/apps/component-measures/utils.js index f6fcfdabc3d..7e3a1d07cb5 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/utils.js +++ b/server/sonar-web/src/main/js/apps/component-measures/utils.js @@ -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 => ({ diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js index d8591e0e6a3..467ba68edfc 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js @@ -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> |