currentUser: { isLoggedIn: boolean },
location: { pathname: string, query: RawQuery },
fetchMeasures: (
- Component,
- Array<string>
+ component: string,
+ metricsKey: Array<string>
) => Promise<{ component: Component, measures: Array<MeasureEnhanced>, leakPeriod: ?Period }>,
fetchMetrics: () => void,
metrics: { [string]: Metric },
getComponent,
getCurrentUser,
getMetrics,
- getMetricByKey,
getMetricsKey
} from '../../../store/rootReducer';
import { fetchMetrics } from '../../../store/rootActions';
return newMeasures;
};
-const fetchMeasures = (component: string, metrics: Array<string>) => (
+const fetchMeasures = (component: string, metricsKey: Array<string>) => (
dispatch,
getState
): Promise<{ component: Component, measures: Array<MeasureEnhanced>, 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<MeasureEnhanced> = 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');
canBrowse: boolean,
component: Component,
isLast: boolean,
- handleSelect: Component => void
+ handleSelect: string => void
};
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() {
type Props = {|
className?: string,
component: Component,
- handleSelect: Component => void,
+ handleSelect: string => void,
rootComponent: Component
|};
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';
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 },
rootComponent: Component,
secondaryMeasure: ?MeasureEnhanced,
updateLoading: ({ [string]: boolean }) => void,
- updateSelected: Component => void,
+ updateSelected: string => void,
updateView: string => void,
view: string
-};
+|};
type State = {
components: Array<ComponentEnhanced>,
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
});
};
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 });
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
/>
);
}
+
+ if (view === 'treemap') {
+ return (
+ <TreeMapView
+ components={this.state.components}
+ handleSelect={this.props.updateSelected}
+ metric={metric}
+ />
+ );
+ }
}
render() {
const { component, currentUser, measure, metric, rootComponent, view } = this.props;
const isLoggedIn = currentUser && currentUser.isLoggedIn;
+ const isFile = isFileType(component);
return (
<div className={this.props.className}>
<div className="layout-page-header-panel layout-page-main-header issues-main-header">
component={component.key}
className="measure-favorite spacer-right"
/>}
- <MeasureViewSelect
- className="measure-view-select"
- metric={metric}
- handleViewChange={this.props.updateView}
- view={view}
- />
+ {!isFile &&
+ <MeasureViewSelect
+ className="measure-view-select"
+ metric={metric}
+ handleViewChange={this.props.updateView}
+ view={view}
+ />}
<PageActions
current={this.state.components.length}
loading={this.props.loading}
- isFile={isFileType(component)}
+ isFile={isFile}
paging={this.state.paging}
view={view}
/>
currentUser: { isLoggedIn: boolean },
rootComponent: Component,
fetchMeasures: (
- Component,
- Array<string>
+ component: string,
+ metricsKey: Array<string>
) => Promise<{ component: Component, measures: Array<MeasureEnhanced> }>,
leakPeriod?: Period,
metric: Metric,
}
};
- 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 });
type Props = {
component: Component,
isSelected: boolean,
- onClick: Component => void
+ onClick: string => void
};
export default class ComponentCell extends React.PureComponent {
if (isLeftClickEvent && !isModifiedEvent) {
e.preventDefault();
- this.props.onClick();
+ this.props.onClick(this.props.component.key);
}
};
import type { Component } from '../../types';
import type { Metric } from '../../../../store/metrics/actions';
-type Props = {
+type Props = {|
components: Array<Component>,
- onClick: Component => void,
+ onClick: string => void,
metric: Metric,
metrics: { [string]: Metric },
selectedComponent?: ?string
-};
+|};
export default function ComponentsList({
components,
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: 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 (
+ <tr>
+ <ComponentCell component={component} isSelected={props.isSelected} onClick={props.onClick} />
- handleClick = () => this.props.onClick(this.props.component);
+ <MeasureCell component={component} metric={props.metric} />
- 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 (
- <tr>
- <ComponentCell
- component={component}
- isSelected={this.props.isSelected}
- onClick={this.handleClick}
+ {otherMeasures.map(measure =>
+ <MeasureCell
+ key={measure.metric.key}
+ component={{
+ ...component,
+ value: measure.value,
+ leak: measure.leak
+ }}
+ metric={measure.metric}
/>
-
- <MeasureCell component={component} metric={this.props.metric} />
-
- {otherMeasures.map(measure =>
- <MeasureCell
- key={measure.metric.key}
- component={{
- ...component,
- value: measure.value,
- leak: measure.leak
- }}
- metric={measure.metric}
- />
- )}
- </tr>
- );
- }
+ )}
+ </tr>
+ );
}
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<ComponentEnhanced>,
fetchMore: () => void,
- handleSelect: Component => void,
+ handleSelect: string => void,
metric: Metric,
metrics: { [string]: Metric },
paging: ?Paging
-};
+|};
export default function ListView(props: Props) {
return (
--- /dev/null
+/*
+ * 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<ComponentEnhanced>,
+ handleSelect: string => void,
+ metric: Metric
+|};
+
+type State = {
+ treemapItems: Array<TreeMapItem>
+};
+
+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<TreeMapItem> => {
+ 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: <QualifierIcon qualifier={component.qualifier} />,
+ 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 (
+ <div className="text-left">
+ {componentName}
+ <br />
+ {`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`}
+ <br />
+ {`${getLocalizedMetricName(colorMetric)}: ${formatted}`}
+ </div>
+ );
+ };
+
+ render() {
+ return (
+ <div className="measure-details-treemap">
+ <ul className="list-inline note spacer-bottom">
+ <li>
+ {translateWithParameters(
+ 'component_measures.legend.color_x',
+ getLocalizedMetricName(this.props.metric)
+ )}
+ </li>
+ <li>
+ {translateWithParameters(
+ 'component_measures.legend.size_x',
+ translate('metric.ncloc.name')
+ )}
+ </li>
+ </ul>
+ <AutoSizer>
+ {({ width }) =>
+ <TreeMap
+ items={this.state.treemapItems}
+ onRectangleClick={this.props.handleSelect}
+ height={HEIGHT}
+ width={width}
+ />}
+ </AutoSizer>
+ </div>
+ );
+ }
+}
}));
};
- 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 (
]);
}
-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;
--- /dev/null
+/*
+ * 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<TreeMapItem>,
+ onRectangleClick?: string => void,
+ height: number,
+ width: number
+|};
+
+export default class TreeMap extends React.PureComponent {
+ props: Props;
+
+ mostCommitPrefix = (labels: Array<string>) => {
+ 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 (
+ <div className="sonar-d3">
+ <div
+ className="treemap-container"
+ style={{ width: this.props.width, height: this.props.height }}>
+ {translate('no_data')}
+ </div>
+ </div>
+ );
+ }
+
+ 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 (
+ <div className="sonar-d3">
+ <div className="treemap-container" style={{ width, height }}>
+ {nodes.map(node =>
+ <TreeMapRect
+ key={node.data.key}
+ x={node.x0}
+ y={node.y0}
+ width={node.x1 - node.x0}
+ height={node.y1 - node.y0}
+ fill={node.data.color}
+ label={node.data.label}
+ prefix={prefix}
+ itemKey={node.data.key}
+ icon={node.data.icon}
+ tooltip={node.data.tooltip}
+ link={node.data.link}
+ onClick={this.props.onRectangleClick}
+ />
+ )}
+ </div>
+ </div>
+ );
+ }
+}
--- /dev/null
+/*
+ * 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 (
+ <a className="treemap-link" href={link} onClick={this.handleLinkClick}>
+ <span className="icon-link" />
+ </a>
+ );
+ }
+ };
+
+ 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}<br>${this.props.label.substr(this.props.prefix.length)}`
+ : this.props.label;
+
+ return (
+ <div
+ className="treemap-cell"
+ style={cellStyles}
+ onClick={this.handleRectClick}
+ role="treeitem"
+ tabIndex={0}>
+ <div className="treemap-inner" style={{ maxWidth: this.props.width }}>
+ {isIconVisible &&
+ <span className={classNames('treemap-icon', { 'spacer-right': isTextVisible })}>
+ {this.props.icon}
+ </span>}
+ {isTextVisible &&
+ <span className="treemap-text" dangerouslySetInnerHTML={{ __html: label }} />}
+ </div>
+ {this.renderLink()}
+ </div>
+ );
+ };
+
+ render() {
+ const { tooltip } = this.props;
+ if (tooltip != null) {
+ return (
+ <Tooltip overlay={tooltip}>
+ {this.renderCell()}
+ </Tooltip>
+ );
+ }
+ return this.renderCell();
+ }
+}
--- /dev/null
+/*
+ * 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(
+ <TreeMap items={items} width={100} height={100} onRectangleClick={() => {}} />
+ );
+ expect(chart.find('TreeMapRect')).toHaveLength(3);
+});
+++ /dev/null
-/*
- * 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(
- <Treemap items={items} width={100} height={100} breadcrumbs={[]} canBeClicked={() => true} />
- );
- expect(chart.find(TreemapRect).length).toBe(3);
-});
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)
});
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 {
right: 5px;
line-height: @iconSmallFontSize;
opacity: 0.5;
+ font-size: 12;
+
.link-no-underline;
&:hover {
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;