Skip to content

Commit 942a9ae

Browse files
authored
RHDHBUGS-3045 Scorecard: Display correct legend for average aggregation (#3147)
* fix(scorecard): tooltip functionality and translations for average card Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * feat(scorecard): support status translation for `average` aggregation Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): e2e tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> --------- Signed-off-by: Ihor Mykhno <imykhno@redhat.com>
1 parent 6a5a855 commit 942a9ae

19 files changed

Lines changed: 186 additions & 132 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard': patch
3+
---
4+
5+
Fix average score card donut tooltip behavior and align pie chart tooltips

workspaces/scorecard/packages/app-legacy/e2e-tests/pages/HomePage.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ export class HomePage {
131131
}
132132

133133
/**
134-
* Clicks the homepage KPI drill-down link (healthy/total subheader). Mock data uses 10/10.
134+
* Clicks the homepage KPI drill-down link (healthy/total subheader).
135+
* Defaults to 10/10 (plain “10 entities” link). Pass overrides when mock `result.total` differs.
135136
*/
136137
async clickDrillDownLink(options?: { healthy?: string; total?: string }) {
137138
const healthy = options?.healthy ?? '10';

workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ import {
6868
import {
6969
expectAverageCardCenterPercent,
7070
verifyAverageDonutCenterTooltip,
71-
verifyAverageLegendTooltipForStatus,
71+
verifyAverageCenterTooltipBreakdownRows,
7272
} from './utils/averageCardAssertions';
7373
import { runAccessibilityTests } from './utils/accessibility';
7474
import { ScorecardRoutes } from './constants/routes';
@@ -1111,20 +1111,19 @@ test.describe('Scorecard Plugin Tests', () => {
11111111
const card = homePage.getCard(
11121112
AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi,
11131113
);
1114-
await expectAverageCardCenterPercent(card, '50%');
1114+
await expectAverageCardCenterPercent(card, '51.5%');
11151115
await verifyAverageDonutCenterTooltip(
11161116
page,
11171117
card,
11181118
translations,
1119-
500,
1119+
515,
11201120
1000,
11211121
);
1122-
await verifyAverageLegendTooltipForStatus(
1122+
await verifyAverageCenterTooltipBreakdownRows(
11231123
page,
11241124
card,
11251125
translations,
11261126
currentLocale,
1127-
'success',
11281127
);
11291128
});
11301129

@@ -1188,7 +1187,13 @@ test.describe('Scorecard Plugin Tests', () => {
11881187
);
11891188
await homePage.saveChanges();
11901189

1191-
await homePage.clickDrillDownLink();
1190+
const weightedEntityTotal = String(
1191+
openPrsWeightedAggregatedResponse.result.total,
1192+
);
1193+
await homePage.clickDrillDownLink({
1194+
healthy: weightedEntityTotal,
1195+
total: weightedEntityTotal,
1196+
});
11921197
await scorecardDrillDownPage.expectOnPage('github.open_prs', {
11931198
aggregationId: AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi,
11941199
});
@@ -1203,7 +1208,7 @@ test.describe('Scorecard Plugin Tests', () => {
12031208
aggregationId: AGGREGATED_CARDS_METRIC_IDS.withOpenPrsWeightedKpi,
12041209
},
12051210
);
1206-
await expectAverageCardCenterPercent(drillCard, '50%');
1211+
await expectAverageCardCenterPercent(drillCard, '51.5%');
12071212
await scorecardDrillDownPage.expectTableHeadersVisible();
12081213
await scorecardDrillDownPage.expectEntityNamesVisible([
12091214
'all-scorecards-service',

workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,40 @@ function interpolate(template: string, vars: Record<string, string>): string {
3333
);
3434
}
3535

36-
function averageLegendTooltipEntitiesEachTemplateKey(
36+
function averageCenterTooltipBreakdownTemplateKey(
3737
locale: string,
38-
countStr: string,
38+
count: number,
3939
):
40-
| 'averageLegendTooltipEntitiesEach_one'
41-
| 'averageLegendTooltipEntitiesEach_other' {
42-
const n = Number.parseInt(countStr, 10);
43-
if (Number.isNaN(n)) {
44-
return 'averageLegendTooltipEntitiesEach_other';
40+
| 'averageCenterTooltipBreakdownRow_one'
41+
| 'averageCenterTooltipBreakdownRow_other' {
42+
if (Number.isNaN(count)) {
43+
return 'averageCenterTooltipBreakdownRow_other';
4544
}
46-
// Align with `getEntityCount` / i18next-style pluralization used in the app.
47-
if (locale.startsWith('fr') && n === 0) {
48-
return 'averageLegendTooltipEntitiesEach_one';
45+
// Align with i18next-style pluralization for `metric.averageCenterTooltipBreakdownRow`.
46+
if (locale.startsWith('fr') && count === 0) {
47+
return 'averageCenterTooltipBreakdownRow_one';
4948
}
50-
if (n === 1) {
51-
return 'averageLegendTooltipEntitiesEach_one';
49+
if (count === 1) {
50+
return 'averageCenterTooltipBreakdownRow_one';
5251
}
53-
return 'averageLegendTooltipEntitiesEach_other';
52+
return 'averageCenterTooltipBreakdownRow_other';
53+
}
54+
55+
function expectedAverageCenterTooltipBreakdownLine(
56+
translations: ScorecardMessages,
57+
locale: string,
58+
statusKey: string,
59+
count: string,
60+
score: string,
61+
): string {
62+
const n = Number.parseInt(count, 10);
63+
const templateKey = averageCenterTooltipBreakdownTemplateKey(locale, n);
64+
const template = metricCopy(translations, templateKey);
65+
const status =
66+
statusKey in translations.thresholds
67+
? translations.thresholds[statusKey]
68+
: statusKey.charAt(0).toUpperCase() + statusKey.slice(1);
69+
return interpolate(template, { status, count, score });
5470
}
5571

5672
export async function expectAverageCardCenterPercent(
@@ -88,31 +104,36 @@ export async function verifyAverageDonutCenterTooltip(
88104
).toBeVisible();
89105
}
90106

91-
const AVERAGE_LEGEND_EXPECTED: Record<
92-
'success' | 'warning' | 'error',
93-
{ count: string; score: string }
94-
> = {
95-
success: { count: '3', score: '100' },
96-
warning: { count: '5', score: '40' },
97-
error: { count: '2', score: '0' },
98-
};
107+
/** Matches `openPrsWeightedAggregatedResponse.result.values` in scorecardResponseUtils.ts */
108+
const OPEN_PRS_WEIGHTED_MOCK_BREAKDOWN: Array<{
109+
statusKey: 'success' | 'warning' | 'error' | 'critical';
110+
count: string;
111+
score: string;
112+
}> = [
113+
{ statusKey: 'success', count: '3', score: '100' },
114+
{ statusKey: 'warning', count: '5', score: '40' },
115+
{ statusKey: 'error', count: '1', score: '15' },
116+
{ statusKey: 'critical', count: '1', score: '0' },
117+
];
99118

100-
export async function verifyAverageLegendTooltipForStatus(
119+
/**
120+
* Per-status lines under total/max in the center donut tooltip (replaces old side-legend tooltips).
121+
*/
122+
export async function verifyAverageCenterTooltipBreakdownRows(
101123
page: Page,
102124
card: Locator,
103125
translations: ScorecardMessages,
104126
locale: string,
105-
statusKey: 'success' | 'warning' | 'error',
106127
): Promise<void> {
107-
await card.getByTestId(`legend-colorbox-${statusKey}`).hover();
108-
const { count, score } = AVERAGE_LEGEND_EXPECTED[statusKey];
109-
const templateKey = averageLegendTooltipEntitiesEachTemplateKey(
110-
locale,
111-
count,
112-
);
113-
const entitiesLabel = interpolate(metricCopy(translations, templateKey), {
114-
count,
115-
score,
116-
});
117-
await expect(page.getByText(entitiesLabel)).toBeVisible();
128+
await card.getByTestId('average-card-center-percent-hit-area').hover();
129+
for (const row of OPEN_PRS_WEIGHTED_MOCK_BREAKDOWN) {
130+
const line = expectedAverageCenterTooltipBreakdownLine(
131+
translations,
132+
locale,
133+
row.statusKey,
134+
row.count,
135+
row.score,
136+
);
137+
await expect(page.getByText(line)).toBeVisible();
138+
}
118139
}

workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ export const openPrsWeightedKpiMetadataResponse = {
214214
};
215215

216216
/**
217-
* Average KPI: 3×100 + 5×40 + 2×0 = 500 weighted sum; max 100×10 entities → 50% score.
217+
* Average KPI: 3×100 + 5×40 + 1×15 + 1×0 = 515 weighted sum; max 100×10 entities → 51.5% score.
218+
* Includes `critical` as a non-threshold status name (no `thresholds.critical` copy).
218219
* Colors align with aggregation KPI `options.thresholds` warning band (30–79%) in app-config.
219220
*/
220221
export const openPrsWeightedAggregatedResponse = {
@@ -227,13 +228,14 @@ export const openPrsWeightedAggregatedResponse = {
227228
values: [
228229
{ count: 3, name: 'success', score: 100 },
229230
{ count: 5, name: 'warning', score: 40 },
230-
{ count: 2, name: 'error', score: 0 },
231+
{ count: 1, name: 'error', score: 15 },
232+
{ count: 1, name: 'critical', score: 0 },
231233
],
232234
total: 10,
233235
timestamp: '2026-01-24T14:10:32.858Z',
234236
thresholds: DEFAULT_NUMBER_THRESHOLDS,
235-
averageScore: 50,
236-
averageWeightedSum: 500,
237+
averageScore: 51.5,
238+
averageWeightedSum: 515,
237239
averageMaxPossible: 1000,
238240
aggregationChartDisplayColor: 'rgb(224, 189, 108)',
239241
},

workspaces/scorecard/plugins/scorecard/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ Define KPI ids and optional labels under **`scorecard.aggregationKPIs`** so each
345345

346346
**`type: average`** KPIs require **`options.statusScores`** (weights per threshold rule key). Optionally set **`options.thresholds`** so the API returns **`aggregationChartDisplayColor`** for the headline percentage. Behavior, validation, and drill-down notes are described in [aggregation.md](../scorecard-backend/docs/aggregation.md).
347347

348+
For **`type: average`**, the homepage card shows a **centered donut** with the headline percentage. Hovering the **center** opens a tooltip with **total score**, **max possible score**, and a **per-status breakdown** (from aggregation **`result.values`**). There is **no side status legend**; **`statusGrouped`** cards use a multi-slice pie with a legend instead.
349+
348350
#### Card props
349351

350352
The supported model is **a single `aggregationId` string** whose value is either:

workspaces/scorecard/plugins/scorecard/report-alpha.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ export const scorecardTranslationRef: TranslationRef<
200200
readonly 'metric.someEntitiesNotReportingValues': string;
201201
readonly 'metric.averageCenterTooltipTotalLabel': string;
202202
readonly 'metric.averageCenterTooltipMaxLabel': string;
203+
readonly 'metric.averageCenterTooltipBreakdownRow_one': string;
204+
readonly 'metric.averageCenterTooltipBreakdownRow_other': string;
203205
readonly 'metric.averageLegendTooltipEntitiesEach_one': string;
204206
readonly 'metric.averageLegendTooltipEntitiesEach_other': string;
205207
readonly 'metric.averageLegendTooltipRowTotal': string;

workspaces/scorecard/plugins/scorecard/report.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ export const scorecardTranslationRef: TranslationRef<
100100
readonly 'metric.someEntitiesNotReportingValues': string;
101101
readonly 'metric.averageCenterTooltipTotalLabel': string;
102102
readonly 'metric.averageCenterTooltipMaxLabel': string;
103+
readonly 'metric.averageCenterTooltipBreakdownRow_one': string;
104+
readonly 'metric.averageCenterTooltipBreakdownRow_other': string;
103105
readonly 'metric.averageLegendTooltipEntitiesEach_one': string;
104106
readonly 'metric.averageLegendTooltipEntitiesEach_other': string;
105107
readonly 'metric.averageLegendTooltipRowTotal': string;

workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/AverageCardComponent.tsx

Lines changed: 2 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,14 @@ import { useTheme } from '@mui/material/styles';
2121

2222
import { CardWrapper } from '../../Common/CardWrapper';
2323
import type { PieData } from '../../types';
24-
import {
25-
getThresholdRuleColor,
26-
resolveStatusColor,
27-
SCORECARD_ERROR_STATE_COLOR,
28-
} from '../../../utils';
24+
import { resolveStatusColor } from '../../../utils';
2925
import { ResponsivePieChart } from '../../ScorecardHomepageSection/ResponsivePieChart';
3026
import { CardInfoButton } from '../components/CardInfoButton';
3127
import { CardSubheader } from '../components/CardSubheader';
3228
import { CardChartContainer } from '../components/CardChartContainer';
3329
import { CardTooltip } from '../components/CardTooltip';
34-
import { LegendTooltipContent } from './LegendTooltipContent';
3530
import { DonutChartTooltipContent } from './DonutChartTooltipContent';
3631
import type { AverageCardComponentProps, TooltipPosition } from './types';
37-
import { CardLegendContent } from '../components/CardLegendContent';
3832
import { AverageCardPieCenterLabel } from './AverageCardPieCenterLabel';
3933
import { formatPercentage } from '../../../utils/formatPercentage';
4034

@@ -60,9 +54,6 @@ export const AverageCardComponent = ({
6054
}: AverageCardComponentProps) => {
6155
const theme = useTheme();
6256

63-
const [activeIndex, setActiveIndex] = useState<number | null>(null);
64-
const [tooltipPosition, setTooltipPosition] =
65-
useState<TooltipPosition | null>(null);
6657
const [centerTooltipPosition, setCenterTooltipPosition] =
6758
useState<TooltipPosition | null>(null);
6859

@@ -99,18 +90,6 @@ export const AverageCardComponent = ({
9990
},
10091
];
10192

102-
const statusPieData: PieData[] =
103-
scorecard.result.values?.map(value => ({
104-
name: value.name,
105-
value: value.count,
106-
score: value.score,
107-
color: resolveStatusColor(
108-
theme,
109-
getThresholdRuleColor(scorecard.result.thresholds.rules, value.name) ??
110-
SCORECARD_ERROR_STATE_COLOR,
111-
),
112-
})) ?? [];
113-
11493
const subheader = showSubheader ? (
11594
<CardSubheader
11695
aggregationId={aggregationId}
@@ -141,21 +120,10 @@ export const AverageCardComponent = ({
141120
{...props}
142121
centerPercentLabel={centerPercentLabel}
143122
arcResolvedColor={arcResolvedColor}
144-
setActiveIndex={setActiveIndex}
145-
setTooltipPosition={setTooltipPosition}
146123
updateCenterTooltipPosition={updateCenterTooltipPosition}
147124
setCenterTooltipPosition={setCenterTooltipPosition}
148125
/>
149126
)}
150-
legendContent={props => (
151-
<CardLegendContent
152-
{...props}
153-
activeIndex={activeIndex}
154-
setActiveIndex={setActiveIndex}
155-
setTooltipPosition={setTooltipPosition}
156-
pieData={statusPieData}
157-
/>
158-
)}
159127
/>
160128

161129
{centerTooltipPosition && (
@@ -173,32 +141,11 @@ export const AverageCardComponent = ({
173141
<DonutChartTooltipContent
174142
weightedSum={scorecard.result.averageWeightedSum}
175143
maxPossible={scorecard.result.averageMaxPossible}
144+
statusValues={scorecard.result.values}
176145
/>
177146
}
178147
/>
179148
)}
180-
181-
{activeIndex !== null &&
182-
tooltipPosition &&
183-
statusPieData[activeIndex] && (
184-
<CardTooltip
185-
tooltipPosition={tooltipPosition}
186-
pieData={statusPieData}
187-
payload={[
188-
{
189-
name: statusPieData[activeIndex].name,
190-
value: statusPieData[activeIndex].value || 1,
191-
payload: statusPieData[activeIndex],
192-
},
193-
]}
194-
customContent={
195-
<LegendTooltipContent
196-
row={statusPieData[activeIndex]}
197-
maxPossible={scorecard.result.averageMaxPossible}
198-
/>
199-
}
200-
/>
201-
)}
202149
</CardChartContainer>
203150
</CardWrapper>
204151
);

workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/AverageCardPieCenterLabel.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,12 @@
1515
*/
1616

1717
import { PieLabelRenderProps } from 'recharts';
18+
1819
import { TooltipPosition } from './types';
1920

2021
type AverageCardPieCenterLabelProps = PieLabelRenderProps & {
2122
centerPercentLabel: string;
2223
arcResolvedColor: string;
23-
setActiveIndex: (index: number | null) => void;
24-
setTooltipPosition: (position: TooltipPosition | null) => void;
2524
updateCenterTooltipPosition: (e: React.MouseEvent<SVGCircleElement>) => void;
2625
setCenterTooltipPosition: (position: TooltipPosition | null) => void;
2726
};
@@ -32,8 +31,6 @@ export function AverageCardPieCenterLabel({
3231
index,
3332
centerPercentLabel,
3433
arcResolvedColor,
35-
setActiveIndex,
36-
setTooltipPosition,
3734
updateCenterTooltipPosition,
3835
setCenterTooltipPosition,
3936
}: AverageCardPieCenterLabelProps) {
@@ -60,8 +57,6 @@ export function AverageCardPieCenterLabel({
6057
pointerEvents="all"
6158
data-testid="average-card-center-percent-hit-area"
6259
onMouseEnter={e => {
63-
setActiveIndex(null);
64-
setTooltipPosition(null);
6560
updateCenterTooltipPosition(e);
6661
}}
6762
onMouseMove={updateCenterTooltipPosition}

0 commit comments

Comments
 (0)