@@ -604,6 +604,16 @@ export const lightTheme = { | |||
'bubble.4': [...COLORS.orange[500], 0.3], | |||
'bubble.5': [...COLORS.red[500], 0.3], | |||
// TreeMap Colors | |||
'treeMap.A': COLORS.green[500], | |||
'treeMap.B': COLORS.yellowGreen[500], | |||
'treeMap.C': COLORS.yellow[500], | |||
'treeMap.D': COLORS.orange[500], | |||
'treeMap.E': COLORS.red[500], | |||
'treeMap.NA1': COLORS.blueGrey[300], | |||
'treeMap.NA2': COLORS.blueGrey[200], | |||
treeMapCellTextColor: COLORS.blueGrey[900], | |||
// new code legend | |||
newCodeLegend: [...COLORS.indigo[300], 0.15], | |||
newCodeLegendBorder: COLORS.indigo[200], |
@@ -120,17 +120,44 @@ describe('rendering', () => { | |||
expect(ui.measuresRows.getAll()).toHaveLength(7); | |||
}); | |||
it('should correctly render a treemap view', async () => { | |||
it('should correctly render a rating treemap view', async () => { | |||
const { ui } = getPageObject(); | |||
renderMeasuresApp('component_measures?id=foo&metric=sqale_rating&view=treemap'); | |||
await ui.appLoaded(); | |||
expect(within(ui.treeMap.get()).getAllByRole('link')).toHaveLength(7); | |||
expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7); | |||
expect(ui.treeMapCell(/folderA .+ Maintainability Rating: C/).get()).toBeInTheDocument(); | |||
expect(ui.treeMapCell(/test1\.js .+ Maintainability Rating: B/).get()).toBeInTheDocument(); | |||
expect(ui.treeMapCell(/index\.tsx .+ Maintainability Rating: A/).get()).toBeInTheDocument(); | |||
}); | |||
it('should correctly render a percent treemap view', async () => { | |||
const { measures } = componentsHandler; | |||
measures['foo:folderA'][MetricKey.coverage] = { | |||
metric: MetricKey.coverage, | |||
value: '74.2', | |||
}; | |||
measures['foo:test1.js'][MetricKey.coverage] = { | |||
metric: MetricKey.coverage, | |||
value: undefined, | |||
}; | |||
measures['foo:index.tsx'][MetricKey.coverage] = { | |||
metric: MetricKey.coverage, | |||
value: '13.1', | |||
}; | |||
const { ui } = getPageObject(); | |||
renderMeasuresApp('component_measures?id=foo&metric=coverage&view=treemap'); | |||
await ui.appLoaded(); | |||
expect(ui.treeMap.byRole('link').getAll()).toHaveLength(7); | |||
expect(ui.treeMapCell(/folderA .+ Coverage: 74.2%/).get()).toBeInTheDocument(); | |||
expect(ui.treeMapCell(/test1\.js .+ Coverage: —/).get()).toBeInTheDocument(); | |||
expect(ui.treeMapCell(/index\.tsx .+ Coverage: 13.1%/).get()).toBeInTheDocument(); | |||
}); | |||
it('should render correctly for an unknown metric', async () => { | |||
const { ui } = getPageObject(); | |||
renderMeasuresApp('component_measures?id=foo&metric=unknown'); |
@@ -19,11 +19,11 @@ | |||
*/ | |||
import { withTheme } from '@emotion/react'; | |||
import styled from '@emotion/styled'; | |||
import { Spinner } from '@sonarsource/echoes-react'; | |||
import { | |||
LargeCenteredLayout, | |||
Note, | |||
PageContentFontWrapper, | |||
Spinner, | |||
themeBorder, | |||
themeColor, | |||
} from 'design-system'; | |||
@@ -45,7 +45,6 @@ import { MeasurePageView } from '../../../types/measures'; | |||
import { MetricKey } from '../../../types/metrics'; | |||
import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types'; | |||
import Sidebar from '../sidebar/Sidebar'; | |||
import '../style.css'; | |||
import { | |||
Query, | |||
banQualityGateMeasure, | |||
@@ -281,7 +280,7 @@ class ComponentMeasuresApp extends React.PureComponent<Props, State> { | |||
<Suggestions suggestions="component_measures" /> | |||
<Helmet defer={false} title={translate('layout.measures')} /> | |||
<PageContentFontWrapper className="sw-body-sm"> | |||
<Spinner className="my-10 sw-flex sw-content-center" loading={this.state.loading} /> | |||
<Spinner className="my-10 sw-flex sw-content-center" isLoading={this.state.loading} /> | |||
{measures.length > 0 ? ( | |||
<div className="sw-grid sw-grid-cols-12 sw-w-full"> |
@@ -430,7 +430,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
secondaryMeasure={secondaryMeasure} | |||
/> | |||
{isFileComponent ? ( | |||
<div className="measure-details-viewer"> | |||
<div> | |||
<SourceViewer | |||
hideHeader | |||
branchLike={branchLike} |
@@ -17,9 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { Note } from 'design-system'; | |||
import * as React from 'react'; | |||
import { translate } from '../../../helpers/l10n'; | |||
export default function EmptyResult() { | |||
return <div className="note">{translate('no_results')}</div>; | |||
return <Note>{translate('no_results')}</Note>; | |||
} |
@@ -18,15 +18,23 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { scaleLinear, scaleOrdinal } from 'd3-scale'; | |||
import { TreeMap, TreeMapItem } from 'design-system'; | |||
import { | |||
CSSColor, | |||
Note, | |||
QualifierIcon, | |||
ThemeColors, | |||
ThemeProp, | |||
TreeMap, | |||
TreeMapItem, | |||
themeColor, | |||
withTheme, | |||
} from 'design-system'; | |||
import { isEmpty } from 'lodash'; | |||
import * as React from 'react'; | |||
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer'; | |||
import { colors } from '../../../app/theme'; | |||
import ColorBoxLegend from '../../../components/charts/ColorBoxLegend'; | |||
import ColorGradientLegend from '../../../components/charts/ColorGradientLegend'; | |||
import QualifierIcon from '../../../components/icons/QualifierIcon'; | |||
import { getComponentMeasureUniqueKey } from '../../../helpers/component'; | |||
import { RATING_COLORS } from '../../../helpers/constants'; | |||
import { getLocalizedMetricName, translate } from '../../../helpers/l10n'; | |||
import { formatMeasure, isDiffMetric } from '../../../helpers/measures'; | |||
import { isDefined } from '../../../helpers/types'; | |||
@@ -34,27 +42,32 @@ import { MetricKey, MetricType } from '../../../types/metrics'; | |||
import { ComponentMeasureEnhanced, ComponentMeasureIntern, Metric } from '../../../types/types'; | |||
import EmptyResult from './EmptyResult'; | |||
interface Props { | |||
interface TreeMapViewProps { | |||
components: ComponentMeasureEnhanced[]; | |||
handleSelect: (component: ComponentMeasureIntern) => void; | |||
metric: Metric; | |||
} | |||
type Props = TreeMapViewProps & ThemeProp; | |||
interface State { | |||
treemapItems: Array<TreeMapItem<ComponentMeasureIntern>>; | |||
} | |||
const PERCENT_SCALE_DOMAIN = [0, 25, 50, 75, 100]; | |||
const RATING_SCALE_DOMAIN = [1, 2, 3, 4, 5]; | |||
const HEIGHT = 500; | |||
const COLORS = RATING_COLORS.map(({ fill }) => fill); | |||
const LEVEL_COLORS = [ | |||
colors.error500, | |||
colors.orange, | |||
colors.success500, | |||
colors.disabledQualityGate, | |||
const NA_COLORS: [ThemeColors, ThemeColors] = ['treeMap.NA1', 'treeMap.NA2']; | |||
const TREEMAP_COLORS: ThemeColors[] = [ | |||
'treeMap.A', | |||
'treeMap.B', | |||
'treeMap.C', | |||
'treeMap.D', | |||
'treeMap.E', | |||
]; | |||
const NA_GRADIENT = `linear-gradient(-45deg, ${colors.gray71} 25%, ${colors.gray60} 25%, ${colors.gray60} 50%, ${colors.gray71} 50%, ${colors.gray71} 75%, ${colors.gray60} 75%, ${colors.gray60} 100%)`; | |||
export default class TreeMapView extends React.PureComponent<Props, State> { | |||
export class TreeMapView extends React.PureComponent<Props, State> { | |||
state: State; | |||
constructor(props: Props) { | |||
@@ -98,9 +111,9 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
return { | |||
key: getComponentMeasureUniqueKey(component) ?? '', | |||
color: colorValue ? (colorScale as Function)(colorValue) : undefined, | |||
gradient: !colorValue ? NA_GRADIENT : undefined, | |||
icon: <QualifierIcon fill={colors.baseFontColor} qualifier={component.qualifier} />, | |||
color: isDefined(colorValue) ? (colorScale as Function)(colorValue) : undefined, | |||
gradient: !isDefined(colorValue) ? this.getNAGradient() : undefined, | |||
icon: <QualifierIcon fill="pageContent" qualifier={component.qualifier} />, | |||
label: [component.name, component.branch].filter((s) => !!s).join(' / '), | |||
size: sizeValue, | |||
measureValue: colorValue, | |||
@@ -118,16 +131,35 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
.filter(isDefined); | |||
}; | |||
getNAGradient = () => { | |||
const { theme } = this.props; | |||
const [shade1, shade2] = NA_COLORS.map((c) => themeColor(c)({ theme })); | |||
return `linear-gradient(-45deg, ${shade1} 25%, ${shade2} 25%, ${shade2} 50%, ${shade1} 50%, ${shade1} 75%, ${shade2} 75%, ${shade2} 100%)`; | |||
}; | |||
getMappedThemeColors = (): string[] => { | |||
const { theme } = this.props; | |||
return TREEMAP_COLORS.map((c) => themeColor(c)({ theme })); | |||
}; | |||
getLevelColorScale = () => | |||
scaleOrdinal<string, string>().domain(['ERROR', 'WARN', 'OK', 'NONE']).range(LEVEL_COLORS); | |||
scaleOrdinal<string, string>() | |||
.domain(['ERROR', 'WARN', 'OK', 'NONE']) | |||
.range(this.getMappedThemeColors()); | |||
getPercentColorScale = (metric: Metric) => { | |||
const color = scaleLinear<string, string>().domain([0, 25, 50, 75, 100]); | |||
color.range(metric.higherValuesAreBetter ? [...COLORS].reverse() : COLORS); | |||
const color = scaleLinear<string, string>().domain(PERCENT_SCALE_DOMAIN); | |||
color.range( | |||
metric.higherValuesAreBetter | |||
? [...this.getMappedThemeColors()].reverse() | |||
: this.getMappedThemeColors(), | |||
); | |||
return color; | |||
}; | |||
getRatingColorScale = () => scaleLinear<string, string>().domain([1, 2, 3, 4, 5]).range(COLORS); | |||
getRatingColorScale = () => | |||
scaleLinear<string, string>().domain(RATING_SCALE_DOMAIN).range(this.getMappedThemeColors()); | |||
getColorScale = (metric: Metric) => { | |||
if (metric.type === MetricType.Level) { | |||
@@ -155,8 +187,8 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
const formatted = | |||
colorMetric && colorValue !== undefined ? formatMeasure(colorValue, colorMetric.type) : '—'; | |||
return ( | |||
<div className="text-left"> | |||
{[component.name, component.branch].filter((s) => !!s).join(' / ')} | |||
<div className="sw-text-left"> | |||
{[component.name, component.branch].filter((s) => !isEmpty(s)).join(' / ')} | |||
<br /> | |||
{`${getLocalizedMetricName(sizeMetric)}: ${formatMeasure(sizeValue, sizeMetric.type)}`} | |||
<br /> | |||
@@ -172,22 +204,16 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
} | |||
renderLegend() { | |||
const { metric } = this.props; | |||
const { metric, theme } = this.props; | |||
const colorScale = this.getColorScale(metric); | |||
if ([MetricType.Level, MetricType.Rating].includes(metric.type as MetricType)) { | |||
return ( | |||
<ColorBoxLegend | |||
className="measure-details-treemap-legend color-box-full" | |||
colorScale={colorScale} | |||
metricType={metric.type} | |||
/> | |||
); | |||
return <ColorBoxLegend colorScale={colorScale} metricType={metric.type} />; | |||
} | |||
return ( | |||
<ColorGradientLegend | |||
className="measure-details-treemap-legend" | |||
showColorNA | |||
colorScale={colorScale} | |||
naColors={NA_COLORS.map((c) => themeColor(c)({ theme })) as [CSSColor, CSSColor]} | |||
height={30} | |||
width={200} | |||
/> | |||
@@ -205,22 +231,22 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
? components[0].measures.find((measure) => measure.metric.key !== metric.key) | |||
: null; | |||
return ( | |||
<div className="measure-details-treemap" data-testid="treemap"> | |||
<div className="display-flex-start note spacer-bottom"> | |||
<div data-testid="treemap"> | |||
<Note as="div" className="sw-flex sw-items-start sw-mb-2"> | |||
<span> | |||
<strong className="sw-mr-1">{translate('component_measures.legend.color')}</strong> | |||
{getLocalizedMetricName(metric)} | |||
</span> | |||
<span className="spacer-left flex-1"> | |||
<span className="sw-ml-2 sw-flex-1"> | |||
<strong className="sw-mr-1">{translate('component_measures.legend.size')}</strong> | |||
{translate( | |||
'metric', | |||
sizeMeasure && sizeMeasure.metric ? sizeMeasure.metric.key : MetricKey.ncloc, | |||
sizeMeasure?.metric ? sizeMeasure.metric.key : MetricKey.ncloc, | |||
'name', | |||
)} | |||
</span> | |||
<span>{this.renderLegend()}</span> | |||
</div> | |||
</Note> | |||
<AutoSizer disableHeight> | |||
{({ width }) => ( | |||
<TreeMap<ComponentMeasureIntern> | |||
@@ -235,3 +261,5 @@ export default class TreeMapView extends React.PureComponent<Props, State> { | |||
); | |||
} | |||
} | |||
export default withTheme(TreeMapView); |
@@ -1,38 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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. | |||
*/ | |||
button.search-navigator-facet { | |||
text-align: start; | |||
} | |||
.measure-details-treemap-legend.color-box-legend { | |||
margin-right: 0; | |||
} | |||
.measure-details-viewer .issue-list { | |||
/* no math, just a good guess */ | |||
min-width: 600px; | |||
width: 800px; | |||
} | |||
@media (max-width: 1320px) { | |||
.measure-details-viewer .issue-list { | |||
width: calc(60vw - 80px); | |||
} | |||
} |
@@ -1,70 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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. | |||
*/ | |||
.color-box-legend { | |||
display: flex; | |||
justify-content: center; | |||
} | |||
.color-box-legend .link-checkbox .color-box-legend-rating { | |||
width: 20px; | |||
height: 20px; | |||
line-height: 20px; | |||
display: inline-block; | |||
border: 1px solid transparent; | |||
border-radius: 20px; | |||
color: var(--blacka87); | |||
} | |||
.color-box-legend .link-checkbox[aria-checked='false'] .color-box-legend-rating { | |||
background-color: transparent !important; | |||
border-color: transparent !important; | |||
} | |||
.color-box-legend > *:not(:first-child) { | |||
margin-left: 24px; | |||
} | |||
.color-box-legend .color-box-legend-rect { | |||
display: inline-block; | |||
margin-top: 1px; | |||
margin-right: 4px; | |||
border: 1px solid; | |||
} | |||
.color-box-legend .color-box-legend-rect-inner { | |||
display: block; | |||
width: 8px; | |||
height: 8px; | |||
opacity: 0.2; | |||
} | |||
.color-box-legend.color-box-full .color-box-legend-rect-inner { | |||
opacity: 1; | |||
} | |||
.color-box-legend a { | |||
color: var(--baseFontColor); | |||
border-bottom: none; | |||
display: block; | |||
} | |||
.color-box-legend a.filtered { | |||
opacity: 0.3; | |||
} |
@@ -17,14 +17,12 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import classNames from 'classnames'; | |||
import styled from '@emotion/styled'; | |||
import { ScaleLinear, ScaleOrdinal } from 'd3-scale'; | |||
import * as React from 'react'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
import './ColorBoxLegend.css'; | |||
interface Props { | |||
className?: string; | |||
colorNA?: string; | |||
colorScale: | |||
| ScaleOrdinal<string, string> // used for LEVEL type | |||
@@ -32,30 +30,40 @@ interface Props { | |||
metricType: string; | |||
} | |||
export default function ColorBoxLegend({ className, colorScale, colorNA, metricType }: Props) { | |||
export default function ColorBoxLegend({ colorScale, colorNA, metricType }: Props) { | |||
const colorDomain: Array<number | string> = colorScale.domain(); | |||
const colorRange = colorScale.range(); | |||
return ( | |||
<div className={classNames('color-box-legend', className)}> | |||
<div className="sw-flex sw-justify-center sw-gap-6"> | |||
{colorDomain.map((value, idx) => ( | |||
<div key={value}> | |||
<span className="color-box-legend-rect" style={{ borderColor: colorRange[idx] }}> | |||
<span | |||
className="color-box-legend-rect-inner" | |||
style={{ backgroundColor: colorRange[idx] }} | |||
/> | |||
</span> | |||
<LegendRect style={{ borderColor: colorRange[idx] }}> | |||
<span style={{ backgroundColor: colorRange[idx] }} /> | |||
</LegendRect> | |||
{formatMeasure(value, metricType)} | |||
</div> | |||
))} | |||
{colorNA && ( | |||
<div> | |||
<span className="color-box-legend-rect" style={{ borderColor: colorNA }}> | |||
<span className="color-box-legend-rect-inner" style={{ backgroundColor: colorNA }} /> | |||
</span> | |||
<LegendRect style={{ borderColor: colorNA }}> | |||
<span style={{ backgroundColor: colorNA }} /> | |||
</LegendRect> | |||
N/A | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
const LegendRect = styled.span` | |||
display: inline-block; | |||
margin-top: 1px; | |||
margin-right: 4px; | |||
border: 1px solid; | |||
& span { | |||
display: block; | |||
width: 8px; | |||
height: 8px; | |||
} | |||
`; |
@@ -1,33 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2024 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. | |||
*/ | |||
.gradient-legend-text, | |||
.gradient-legend-na { | |||
text-anchor: middle; | |||
fill: var(--secondFontColor); | |||
font-size: 10px; | |||
} | |||
.gradient-legend-text:first-of-type { | |||
text-anchor: start; | |||
} | |||
.gradient-legend-text:last-of-type { | |||
text-anchor: end; | |||
} |
@@ -17,16 +17,17 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import styled from '@emotion/styled'; | |||
import { ScaleLinear, ScaleOrdinal } from 'd3-scale'; | |||
import { CSSColor, themeColor } from 'design-system'; | |||
import * as React from 'react'; | |||
import { colors } from '../../app/theme'; | |||
import './ColorGradientLegend.css'; | |||
interface Props { | |||
className?: string; | |||
colorScale: | |||
| ScaleOrdinal<string, string> // used for LEVEL type | |||
| ScaleLinear<string, string | number>; // used for RATING or PERCENT type | |||
naColors?: [CSSColor, CSSColor]; | |||
height: number; | |||
padding?: [number, number, number, number]; | |||
showColorNA?: boolean; | |||
@@ -42,6 +43,7 @@ export default function ColorGradientLegend({ | |||
padding = [12, 24, 0, 0], | |||
height, | |||
showColorNA = false, | |||
naColors = ['rgb(36,36,36)', 'rgb(120,120,120)'], | |||
width, | |||
}: Props) { | |||
const colorRange: Array<string | number> = colorScale.range(); | |||
@@ -74,14 +76,14 @@ export default function ColorGradientLegend({ | |||
y1="0" | |||
x2={i} | |||
y2="30" | |||
style={{ stroke: colors.gray71, strokeWidth: NA_SPACING }} | |||
style={{ stroke: naColors[0], strokeWidth: NA_SPACING }} | |||
/> | |||
<line | |||
x1={i + NA_SPACING} | |||
y1="0" | |||
x2={i + NA_SPACING} | |||
y2="30" | |||
style={{ stroke: colors.gray60, strokeWidth: NA_SPACING }} | |||
style={{ stroke: naColors[1], strokeWidth: NA_SPACING }} | |||
/> | |||
</React.Fragment> | |||
))} | |||
@@ -90,8 +92,7 @@ export default function ColorGradientLegend({ | |||
<g transform={`translate(${padding[3]}, ${padding[0]})`}> | |||
<rect fill="url(#gradient-legend)" height={rectHeight} width={widthNoPadding} x={0} y={0} /> | |||
{colorDomain.map((d, idx) => ( | |||
<text | |||
className="gradient-legend-text" | |||
<GradientLegendText | |||
dy="-2px" | |||
// eslint-disable-next-line react/no-array-index-key | |||
key={idx} | |||
@@ -99,7 +100,7 @@ export default function ColorGradientLegend({ | |||
y={0} | |||
> | |||
{d} | |||
</text> | |||
</GradientLegendText> | |||
))} | |||
</g> | |||
{showColorNA && ( | |||
@@ -111,16 +112,27 @@ export default function ColorGradientLegend({ | |||
x={NA_SPACING} | |||
y={0} | |||
/> | |||
<text | |||
className="gradient-legend-na" | |||
dy="-2px" | |||
x={NA_SPACING + (padding[1] - NA_SPACING) / 2} | |||
y={0} | |||
> | |||
<GradientLegendTextBase dy="-2px" x={NA_SPACING + (padding[1] - NA_SPACING) / 2} y={0}> | |||
N/A | |||
</text> | |||
</GradientLegendTextBase> | |||
</g> | |||
)} | |||
</svg> | |||
); | |||
} | |||
const GradientLegendTextBase = styled.text` | |||
text-anchor: middle; | |||
fill: ${themeColor('pageContent')}; | |||
font-size: 10px; | |||
`; | |||
const GradientLegendText = styled(GradientLegendTextBase)` | |||
&:first-of-type { | |||
text-anchor: start; | |||
} | |||
&:last-of-type { | |||
text-anchor: end; | |||
} | |||
`; |
@@ -17,11 +17,10 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { QualityGateIndicator } from 'design-system'; | |||
import { MetricsLabel, MetricsRatingBadge, QualityGateIndicator } from 'design-system'; | |||
import * as React from 'react'; | |||
import Tooltip from '../../components/controls/Tooltip'; | |||
import Rating from '../../components/ui/Rating'; | |||
import { translateWithParameters } from '../../helpers/l10n'; | |||
import { translate, translateWithParameters } from '../../helpers/l10n'; | |||
import { formatMeasure } from '../../helpers/measures'; | |||
import { MetricType } from '../../types/metrics'; | |||
import { Status } from '../../types/types'; | |||
@@ -76,14 +75,21 @@ export default function Measure({ | |||
} | |||
const tooltip = <RatingTooltipContent metricKey={metricKey} value={value} />; | |||
const rating = ratingComponent ?? <Rating value={value} />; | |||
const rating = ratingComponent ?? ( | |||
<MetricsRatingBadge | |||
size={small ? 'sm' : 'md'} | |||
label={ | |||
value | |||
? translateWithParameters('metric.has_rating_X', formatMeasure(value, MetricType.Rating)) | |||
: translate('metric.no_rating') | |||
} | |||
rating={formatMeasure(value, MetricType.Rating) as MetricsLabel} | |||
/> | |||
); | |||
if (tooltip) { | |||
return ( | |||
<Tooltip overlay={tooltip}> | |||
<span className={className}>{rating}</span> | |||
</Tooltip> | |||
); | |||
} | |||
return rating; | |||
return ( | |||
<Tooltip overlay={tooltip}> | |||
<span className={className}>{rating}</span> | |||
</Tooltip> | |||
); | |||
} |