From: Grégoire Aubert Date: Thu, 3 Aug 2017 13:13:22 +0000 (+0200) Subject: SONAR-9608 SONAR-9636 Add treemap view to the components measures page X-Git-Tag: 6.6-RC1~650 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=4f29eb943e9935587cbd635eadbe31e1ed4fa8d0;p=sonarqube.git SONAR-9608 SONAR-9636 Add treemap view to the components measures page --- 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 e742c486be0..773235ee8ca 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 @@ -35,8 +35,8 @@ type Props = {| currentUser: { isLoggedIn: boolean }, location: { pathname: string, query: RawQuery }, fetchMeasures: ( - Component, - Array + component: string, + metricsKey: Array ) => Promise<{ component: Component, measures: Array, leakPeriod: ?Period }>, fetchMetrics: () => void, metrics: { [string]: Metric }, 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 653380d1ad7..7ed37803274 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 @@ -26,7 +26,6 @@ import { getComponent, getCurrentUser, getMetrics, - getMetricByKey, getMetricsKey } from '../../../store/rootReducer'; import { fetchMetrics } from '../../../store/rootActions'; @@ -57,17 +56,17 @@ const banQualityGate = (component: Component): Array => { return newMeasures; }; -const fetchMeasures = (component: string, metrics: Array) => ( +const fetchMeasures = (component: string, metricsKey: Array) => ( dispatch, getState ): Promise<{ component: Component, measures: Array, leakPeriod: ?Period }> => { - if (metrics.length <= 0) { + if (metricsKey.length <= 0) { return Promise.resolve({ component: {}, measures: [], leakPeriod: null }); } - return getMeasuresAndMeta(component, metrics, { additionalFields: 'periods' }).then(r => { + return getMeasuresAndMeta(component, metricsKey, { additionalFields: 'periods' }).then(r => { const measures: Array = banQualityGate(r.component).map(measure => - enhanceMeasure(measure, getMetricByKey(getState(), measure.metric)) + enhanceMeasure(measure, getMetrics(getState())) ); const newBugs = measures.find(measure => measure.metric.key === 'new_bugs'); 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 index 2be60f7c918..99882ca4a7a 100644 --- 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 @@ -27,7 +27,7 @@ type Props = { canBrowse: boolean, component: Component, isLast: boolean, - handleSelect: Component => void + handleSelect: string => void }; export default class Breadcrumb extends React.PureComponent { @@ -36,7 +36,7 @@ export default class Breadcrumb extends React.PureComponent { handleClick = (e: Event & { target: HTMLElement }) => { e.preventDefault(); e.target.blur(); - this.props.handleSelect(this.props.component); + this.props.handleSelect(this.props.component.key); }; render() { 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 index 78a035729e8..a9b9c3ecbcf 100644 --- 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 @@ -26,7 +26,7 @@ import type { Component } from '../types'; type Props = {| className?: string, component: Component, - handleSelect: Component => void, + handleSelect: string => void, rootComponent: Component |}; 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 cf99ff49311..3f473359707 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 @@ -28,6 +28,7 @@ import MeasureViewSelect from './MeasureViewSelect'; import MetricNotFound from './MetricNotFound'; import PageActions from './PageActions'; import SourceViewer from '../../../components/SourceViewer/SourceViewer'; +import TreeMapView from './drilldown/TreeMapView'; import { getComponentTree } from '../../../api/components'; import { complementary } from '../config/complementary'; import { enhanceComponent, isFileType } from '../utils'; @@ -36,7 +37,7 @@ import type { Component, ComponentEnhanced, Paging, Period } from '../types'; import type { MeasureEnhanced } from '../../../components/measure/types'; import type { Metric } from '../../../store/metrics/actions'; -type Props = { +type Props = {| className?: string, component: Component, currentUser: { isLoggedIn: boolean }, @@ -48,10 +49,10 @@ type Props = { rootComponent: Component, secondaryMeasure: ?MeasureEnhanced, updateLoading: ({ [string]: boolean }) => void, - updateSelected: Component => void, + updateSelected: string => void, updateView: string => void, view: string -}; +|}; type State = { components: Array, @@ -83,42 +84,42 @@ export default class MeasureContent extends React.PureComponent { this.mounted = false; } - getComponentRequestParams = (metric: Metric, options: Object = {}) => { - const metricKeys = [metric.key, ...(complementary[metric.key] || [])]; - let opts: Object = { - asc: metric.direction === 1, - ps: 100, - metricSortFilter: 'withMeasuresOnly', - metricSort: metric.key - }; - if (isDiffMetric(metric.key)) { - opts = { - ...opts, - s: 'metricPeriod,name', - metricPeriodSort: 1 - }; + getComponentRequestParams = (view: string, metric: Metric, options: Object = {}) => { + const strategy = view === 'list' ? 'leaves' : 'children'; + const metricKeys = [metric.key]; + const opts: Object = { metricSortFilter: 'withMeasuresOnly' }; + if (view === 'treemap') { + metricKeys.push('ncloc'); + opts.asc = false; + opts.metricSort = 'ncloc'; + opts.s = 'metric'; } else { - opts = { - ...opts, - s: 'metric,name' - }; + metricKeys.push(...(complementary[metric.key] || [])); + opts.asc = metric.direction === 1; + opts.ps = 100; + opts.metricSort = metric.key; + if (isDiffMetric(metric.key)) { + opts.s = 'metricPeriod,name'; + opts.metricPeriodSort = 1; + } else { + opts.s = 'metric,name'; + } } - return { metricKeys, opts: { ...opts, ...options } }; + return { metricKeys, opts: { ...opts, ...options }, strategy }; }; - fetchComponents = ({ component, metric, view }: Props) => { + fetchComponents = ({ component, metric, metrics, view }: Props) => { if (isFileType(component)) { return this.setState({ components: [], metric: null, paging: null }); } - const strategy = view === 'list' ? 'leaves' : 'children'; - const { metricKeys, opts } = this.getComponentRequestParams(metric); + const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric); this.props.updateLoading({ components: true }); getComponentTree(strategy, component.key, metricKeys, opts).then( r => { if (this.mounted) { this.setState({ - components: r.components.map(component => enhanceComponent(component, metric)), + components: r.components.map(component => enhanceComponent(component, metric, metrics)), metric, paging: r.paging }); @@ -130,13 +131,12 @@ export default class MeasureContent extends React.PureComponent { }; fetchMoreComponents = () => { - const { component, metric, view } = this.props; + const { component, metric, metrics, view } = this.props; const { paging } = this.state; if (!paging) { return; } - const strategy = view === 'list' ? 'leaves' : 'children'; - const { metricKeys, opts } = this.getComponentRequestParams(metric, { + const { metricKeys, opts, strategy } = this.getComponentRequestParams(view, metric, { p: paging.pageIndex + 1 }); this.props.updateLoading({ components: true }); @@ -146,7 +146,7 @@ export default class MeasureContent extends React.PureComponent { this.setState(state => ({ components: [ ...state.components, - ...r.components.map(component => enhanceComponent(component, metric)) + ...r.components.map(component => enhanceComponent(component, metric, metrics)) ], metric, paging: r.paging @@ -202,11 +202,22 @@ export default class MeasureContent extends React.PureComponent { /> ); } + + if (view === 'treemap') { + return ( + + ); + } } render() { const { component, currentUser, measure, metric, rootComponent, view } = this.props; const isLoggedIn = currentUser && currentUser.isLoggedIn; + const isFile = isFileType(component); return (
@@ -225,16 +236,17 @@ export default class MeasureContent extends React.PureComponent { component={component.key} className="measure-favorite spacer-right" />} - + {!isFile && + } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js index 747a7af5c33..08378cccae7 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContentContainer.js @@ -29,8 +29,8 @@ type Props = { currentUser: { isLoggedIn: boolean }, rootComponent: Component, fetchMeasures: ( - Component, - Array + component: string, + metricsKey: Array ) => Promise<{ component: Component, measures: Array }>, leakPeriod?: Period, metric: Metric, @@ -114,9 +114,9 @@ export default class MeasureContentContainer extends React.PureComponent { } }; - updateSelected = (component: Component) => + updateSelected = (component: string) => this.props.updateQuery({ - selected: component.key !== this.props.rootComponent.key ? component.key : null + selected: component !== this.props.rootComponent.key ? component : null }); updateView = (view: string) => this.props.updateQuery({ view }); 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 8d723304ce8..a0294788571 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 @@ -28,7 +28,7 @@ import type { Component } from '../../types'; type Props = { component: Component, isSelected: boolean, - onClick: Component => void + onClick: string => void }; export default class ComponentCell extends React.PureComponent { @@ -40,7 +40,7 @@ export default class ComponentCell extends React.PureComponent { if (isLeftClickEvent && !isModifiedEvent) { e.preventDefault(); - this.props.onClick(); + this.props.onClick(this.props.component.key); } }; diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js index ba8d42d84c9..cdf62ecea70 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsList.js @@ -26,13 +26,13 @@ import { getLocalizedMetricName } from '../../../../helpers/l10n'; import type { Component } from '../../types'; import type { Metric } from '../../../../store/metrics/actions'; -type Props = { +type Props = {| components: Array, - onClick: Component => void, + onClick: string => void, metric: Metric, metrics: { [string]: Metric }, selectedComponent?: ?string -}; +|}; export default function ComponentsList({ components, diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js index deab1a98453..bbb2ca2fe8c 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/ComponentsListRow.js @@ -24,47 +24,37 @@ import MeasureCell from './MeasureCell'; import type { Component } from '../../types'; import type { Metric } from '../../../../store/metrics/actions'; -type Props = { +type Props = {| component: Component, isSelected: boolean, - onClick: Component => void, + onClick: string => void, otherMetrics: Array, metric: Metric -}; +|}; -export default class ComponentsListRow extends React.PureComponent { - props: Props; +export default function ComponentsListRow(props: Props) { + const { component } = props; + const otherMeasures = props.otherMetrics.map(metric => { + const measure = component.measures.find(measure => measure.metric === metric.key); + return { ...measure, metric }; + }); + return ( + + - handleClick = () => this.props.onClick(this.props.component); + - render() { - const { component } = this.props; - const otherMeasures = this.props.otherMetrics.map(metric => { - const measure = component.measures.find(measure => measure.metric === metric.key); - return { ...measure, metric }; - }); - return ( - - + - - - - {otherMeasures.map(measure => - - )} - - ); - } + )} + + ); } diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js index 2e2d12dff72..912a74ea31d 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/FilesView.js @@ -21,17 +21,17 @@ import React from 'react'; import ComponentsList from './ComponentsList'; import ListFooter from '../../../../components/controls/ListFooter'; -import type { Component, ComponentEnhanced, Paging } from '../../types'; +import type { ComponentEnhanced, Paging } from '../../types'; import type { Metric } from '../../../../store/metrics/actions'; -type Props = { +type Props = {| components: Array, fetchMore: () => void, - handleSelect: Component => void, + handleSelect: string => void, metric: Metric, metrics: { [string]: Metric }, paging: ?Paging -}; +|}; export default function ListView(props: Props) { return ( diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/TreeMapView.js b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/TreeMapView.js new file mode 100644 index 00000000000..41fc108a9f8 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/component-measures/components/drilldown/TreeMapView.js @@ -0,0 +1,182 @@ +/* + * 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 { AutoSizer } from 'react-virtualized'; +import { scaleLinear, scaleOrdinal } from 'd3-scale'; +import QualifierIcon from '../../../../components/shared/QualifierIcon'; +import TreeMap from '../../../../components/charts/TreeMap'; +import { + translate, + translateWithParameters, + getLocalizedMetricName +} from '../../../../helpers/l10n'; +import { formatMeasure, isDiffMetric } from '../../../../helpers/measures'; +import { getComponentUrl } from '../../../../helpers/urls'; +import type { Metric } from '../../../../store/metrics/actions'; +import type { ComponentEnhanced } from '../../types'; +import type { TreeMapItem } from '../../../../components/charts/TreeMap'; + +type Props = {| + components: Array, + handleSelect: string => void, + metric: Metric +|}; + +type State = { + treemapItems: Array +}; + +const HEIGHT = 500; + +export default class TreeMapView extends React.PureComponent { + props: Props; + state: State; + + constructor(props: Props) { + super(props); + this.state = { treemapItems: this.getTreemapComponents(props) }; + } + + componentWillReceiveProps(nextProps: Props) { + if (nextProps.components !== this.props.components) { + this.setState({ treemapItems: this.getTreemapComponents(nextProps) }); + } + } + + getTreemapComponents = ({ components, metric }: Props): Array => { + const colorScale = this.getColorScale(metric); + return components + .map(component => { + const colorMeasure = component.measures.find(measure => measure.metric.key === metric.key); + const sizeMeasure = component.measures.find(measure => measure.metric.key !== metric.key); + if (colorMeasure == null || sizeMeasure == null) { + // $FlowFixMe Null values are filtered just after + return null; + } + const colorValue = isDiffMetric(colorMeasure.metric.key) + ? colorMeasure.leak + : colorMeasure.value; + const sizeValue = isDiffMetric(sizeMeasure.metric.key) + ? sizeMeasure.leak + : sizeMeasure.value; + if (sizeValue == null) { + // $FlowFixMe Null values are filtered just after + return null; + } + return { + key: component.key, + size: sizeValue, + color: colorValue != null ? colorScale(colorValue) : '#777', + icon: , + tooltip: this.getTooltip( + component.name, + colorMeasure.metric, + sizeMeasure.metric, + colorValue, + sizeValue + ), + label: component.name, + link: getComponentUrl(component.key) + }; + }) + .filter(component => component != null); + }; + + getLevelColorScale = () => + scaleOrdinal() + .domain(['ERROR', 'WARN', 'OK', 'NONE']) + .range(['#d4333f', '#ed7d20', '#00aa00', '#b4b4b4']); + + getPercentColorScale = (metric: Metric) => { + const color = scaleLinear().domain([0, 25, 50, 75, 100]); + color.range( + metric.direction === 1 + ? ['#d4333f', '#ed7d20', '#eabe06', '#b0d513', '#00aa00'] + : ['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f'] + ); + return color; + }; + + getRatingColorScale = () => + scaleLinear() + .domain([1, 2, 3, 4, 5]) + .range(['#00aa00', '#b0d513', '#eabe06', '#ed7d20', '#d4333f']); + + getColorScale = (metric: Metric) => { + if (metric.type === 'LEVEL') { + return this.getLevelColorScale(); + } + if (metric.type === 'RATING') { + return this.getRatingColorScale(); + } + return this.getPercentColorScale(metric); + }; + + getTooltip = ( + componentName: string, + colorMetric: Metric, + sizeMetric: Metric, + colorValue: ?number, + sizeValue: number + ) => { + const formatted = + colorMetric != null && colorValue != null ? formatMeasure(colorValue, colorMetric.type) : '—'; + return ( +
+ {componentName} +
+ {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} +
+ {`${getLocalizedMetricName(colorMetric)}: ${formatted}`} +
+ ); + }; + + render() { + return ( +
+
    +
  • + {translateWithParameters( + 'component_measures.legend.color_x', + getLocalizedMetricName(this.props.metric) + )} +
  • +
  • + {translateWithParameters( + 'component_measures.legend.size_x', + translate('metric.ncloc.name') + )} +
  • +
+ + {({ width }) => + } + +
+ ); + } +} 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 74ef48f3c63..bf126b8a5b4 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,9 @@ export default class Sidebar extends React.PureComponent { })); }; - changeMetric = (metric: string) => this.props.updateQuery({ metric, selected: null }); + resetSelection = () => ({ selected: null, view: 'list' }); + + changeMetric = (metric: string) => this.props.updateQuery({ metric, ...this.resetSelection() }); render() { return ( 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 74f1c8c4335..5f00fa61bf7 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 @@ -72,8 +72,12 @@ export function sortMeasures( ]); } -export const enhanceComponent = (component: Component, metric: Metric): ComponentEnhanced => { - const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metric)); +export const enhanceComponent = ( + component: Component, + metric: Metric, + metrics: { [string]: Metric } +): ComponentEnhanced => { + const enhancedMeasures = component.measures.map(measure => enhanceMeasure(measure, metrics)); const measure = enhancedMeasures.find(measure => measure.metric.key === metric.key); const value = measure ? measure.value : null; const leak = measure ? measure.leak : null; diff --git a/server/sonar-web/src/main/js/components/charts/TreeMap.js b/server/sonar-web/src/main/js/components/charts/TreeMap.js new file mode 100644 index 00000000000..b5eea12cb97 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/TreeMap.js @@ -0,0 +1,111 @@ +/* + * 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 { treemap as d3Treemap, hierarchy as d3Hierarchy } from 'd3-hierarchy'; +import TreeMapRect from './TreeMapRect'; +import { translate } from '../../helpers/l10n'; + +export type TreeMapItem = { + key: string, + size: number, + color: string, + icon?: React.Element<*>, + tooltip?: string | React.Element<*>, + label: string, + link?: string +}; + +type Props = {| + items: Array, + onRectangleClick?: string => void, + height: number, + width: number +|}; + +export default class TreeMap extends React.PureComponent { + props: Props; + + mostCommitPrefix = (labels: Array) => { + const sortedLabels = labels.slice(0).sort(); + const firstLabel = sortedLabels[0]; + const firstLabelLength = firstLabel.length; + const lastLabel = sortedLabels[sortedLabels.length - 1]; + let i = 0; + while (i < firstLabelLength && firstLabel.charAt(i) === lastLabel.charAt(i)) { + i++; + } + const prefix = firstLabel.substr(0, i); + const prefixTokens = prefix.split(/[\s\\\/]/); + const lastPrefixPart = prefixTokens[prefixTokens.length - 1]; + return prefix.substr(0, prefix.length - lastPrefixPart.length); + }; + + renderNoData() { + return ( +
+
+ {translate('no_data')} +
+
+ ); + } + + render() { + const { items, height, width } = this.props; + if (items.length <= 0) { + return this.renderNoData(); + } + + const hierarchy = d3Hierarchy({ children: items }) + .sum(d => d.size) + .sort((a, b) => b.value - a.value); + + const treemap = d3Treemap().round(true).size([width, height]); + + const nodes = treemap(hierarchy).leaves(); + const prefix = this.mostCommitPrefix(items.map(item => item.label)); + return ( +
+
+ {nodes.map(node => + + )} +
+
+ ); + } +} diff --git a/server/sonar-web/src/main/js/components/charts/TreeMapRect.js b/server/sonar-web/src/main/js/components/charts/TreeMapRect.js new file mode 100644 index 00000000000..c0bc1033a15 --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/TreeMapRect.js @@ -0,0 +1,118 @@ +/* + * 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 classNames from 'classnames'; +import { scaleLinear } from 'd3-scale'; +import Tooltip from '../controls/Tooltip'; + +const SIZE_SCALE = scaleLinear().domain([3, 15]).range([11, 18]).clamp(true); + +type Props = {| + x: number, + y: number, + width: number, + height: number, + fill: string, + label: string, + prefix: string, + icon?: React.Element<*>, + tooltip?: string | React.Element<*>, + itemKey: string, + link?: string, + onClick?: string => void +|}; + +export default class TreeMapRect extends React.PureComponent { + props: Props; + + handleLinkClick = (e: Event) => e.stopPropagation(); + + handleRectClick = () => { + if (this.props.onClick != null) { + this.props.onClick(this.props.itemKey); + } + }; + + renderLink = () => { + const { link, height, width } = this.props; + if (link == null) { + return null; + } + + if (width >= 24 && height >= 24 && (width >= 48 || height >= 50)) { + return ( + + + + ); + } + }; + + renderCell = () => { + const cellStyles = { + left: this.props.x, + top: this.props.y, + width: this.props.width, + height: this.props.height, + backgroundColor: this.props.fill, + fontSize: SIZE_SCALE(this.props.width / this.props.label.length), + lineHeight: `${this.props.height}px`, + cursor: this.props.onClick != null ? 'pointer' : 'default' + }; + const isTextVisible = this.props.width >= 40 && this.props.height >= 45; + const isIconVisible = this.props.width >= 24 && this.props.height >= 26; + + const label = this.props.prefix + ? `${this.props.prefix}
${this.props.label.substr(this.props.prefix.length)}` + : this.props.label; + + return ( +
+
+ {isIconVisible && + + {this.props.icon} + } + {isTextVisible && + } +
+ {this.renderLink()} +
+ ); + }; + + render() { + const { tooltip } = this.props; + if (tooltip != null) { + return ( + + {this.renderCell()} + + ); + } + return this.renderCell(); + } +} diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.js b/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-test.js new file mode 100644 index 00000000000..181f60b082b --- /dev/null +++ b/server/sonar-web/src/main/js/components/charts/__tests__/TreeMap-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 TreeMap from '../TreeMap'; + +it('should display', () => { + const items = [ + { key: '1', size: 10, color: '#777', label: 'SonarQube :: Server' }, + { key: '2', size: 30, color: '#777', label: 'SonarQube :: Web' }, + { key: '3', size: 20, color: '#777', label: 'SonarQube :: Search' } + ]; + const chart = shallow( + {}} /> + ); + expect(chart.find('TreeMapRect')).toHaveLength(3); +}); diff --git a/server/sonar-web/src/main/js/components/charts/__tests__/treemap-test.js b/server/sonar-web/src/main/js/components/charts/__tests__/treemap-test.js deleted file mode 100644 index dea905a78f8..00000000000 --- a/server/sonar-web/src/main/js/components/charts/__tests__/treemap-test.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2017 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import React from 'react'; -import { shallow } from 'enzyme'; -import { Treemap, TreemapRect } from '../treemap'; - -it('should display', () => { - const items = [ - { size: 10, color: '#777', label: 'SonarQube :: Server' }, - { size: 30, color: '#777', label: 'SonarQube :: Web' }, - { size: 20, color: '#777', label: 'SonarQube :: Search' } - ]; - const chart = shallow( - true} /> - ); - expect(chart.find(TreemapRect).length).toBe(3); -}); diff --git a/server/sonar-web/src/main/js/components/measure/utils.js b/server/sonar-web/src/main/js/components/measure/utils.js index 74cc616d680..92fefd654ce 100644 --- a/server/sonar-web/src/main/js/components/measure/utils.js +++ b/server/sonar-web/src/main/js/components/measure/utils.js @@ -29,10 +29,13 @@ import type { Metric } from '../../store/metrics/actions'; const KNOWN_RATINGS = ['sqale_rating', 'reliability_rating', 'security_rating']; -export const enhanceMeasure = (measure: Measure, metric: Metric): MeasureEnhanced => ({ +export const enhanceMeasure = ( + measure: Measure, + metrics: { [string]: Metric } +): MeasureEnhanced => ({ value: measure.value, periods: measure.periods, - metric, + metric: metrics[measure.metric], leak: getLeakValue(measure) }); diff --git a/server/sonar-web/src/main/less/components/graphics.less b/server/sonar-web/src/main/less/components/graphics.less index d3ca9af1283..32648b5b3ff 100644 --- a/server/sonar-web/src/main/less/components/graphics.less +++ b/server/sonar-web/src/main/less/components/graphics.less @@ -31,18 +31,33 @@ border-bottom: 1px solid #fff; box-sizing: border-box; text-align: center; + + &:focus { + outline: none; + } } .sonar-d3 .treemap-inner { - display: inline-block; + display: inline-flex; vertical-align: middle; - line-height: 1.2; - padding: 5px; + align-items: center; + justify-content: center; + flex-wrap: wrap; + padding: 0 4px; box-sizing: border-box; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + line-height: 1.2; + + .treemap-icon { + flex-shrink: 0; + } + + .treemap-text { + flex-shrink: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; + } } .sonar-d3 .treemap-link { @@ -52,6 +67,8 @@ right: 5px; line-height: @iconSmallFontSize; opacity: 0.5; + font-size: 12; + .link-no-underline; &:hover { @@ -66,21 +83,6 @@ line-height: inherit; } -.sonar-d3 .treemap-cell-small { - .treemap-inner { - display: none; - } -} - -.sonar-d3 .treemap-cell-very-small { - .treemap-inner { - display: none; - } - .treemap-link { - display: none; - } -} - .sonar-d3 .treemap-breadcrumbs { margin-top: 10px; padding-top: 7px;