@@ -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> | |||
); |
@@ -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) | |||
}); |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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" |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); | |||
}); |
@@ -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, |
@@ -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(); | |||
}); |
@@ -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(); | |||
}); |
@@ -82,6 +82,7 @@ exports[`should render correctly 1`] = ` | |||
} | |||
selected="" | |||
updateQuery={[Function]} | |||
view="list" | |||
/> | |||
</div> | |||
`; |
@@ -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> | |||
`; |
@@ -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", | |||
}, | |||
], | |||
} | |||
`; |
@@ -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={ |
@@ -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]} | |||
/> | |||
`; |
@@ -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> | |||
`; |
@@ -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} |
@@ -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 |
@@ -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)} |
@@ -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 ( |
@@ -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" | |||
> |
@@ -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; | |||
} |
@@ -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 => ({ |
@@ -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> |
@@ -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 |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} |
@@ -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 } | |||
}); | |||
}); | |||
}); |
@@ -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 ''; | |||
} |
@@ -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 } }; | |||
} | |||
/** |
@@ -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 | |||
#------------------------------------------------------------------------------ | |||
# |