Skip to content

Commit 91e724f

Browse files
authored
fix(scorecard): entity calculation health on aggregation and drill-down APIs (#2865)
* fix(scorecard): entity calculation health on aggregation and drill-down APIs Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): update generated reports Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): QODO review suggestions and sonarqube fixes Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): align homepage entity-health UX with Figma and error semantics Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): fix generated reports Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): fix issues with e2e tests Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): some updates after rebase Signed-off-by: Patrick Knight <pknight@redhat.com> * fix(scorecard): some more fixes after the rebase Signed-off-by: Patrick Knight <pknight@redhat.com> --------- Signed-off-by: Patrick Knight <pknight@redhat.com>
1 parent ce99f2e commit 91e724f

51 files changed

Lines changed: 955 additions & 110 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard-common': patch
3+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': patch
4+
'@red-hat-developer-hub/backstage-plugin-scorecard': patch
5+
---
6+
7+
Expose scorecard entity calculation health on drill-down and aggregation APIs, and align the drill-down warning plus homepage subheader with those counts.

workspaces/scorecard/app-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ auth:
162162
# see https://backstage.io/docs/auth/ to learn about auth providers
163163
providers:
164164
# See https://backstage.io/docs/auth/guest/provider
165-
guest: {}
165+
guest:
166+
userEntityRef: user:development/guest
166167

167168
scaffolder:
168169
# see https://backstage.io/docs/features/software-templates/configuration for software template options

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ export class CatalogPage {
3939
async loginAndSetLocale(locale: string) {
4040
await this.page.goto('/');
4141
const enterButton = this.page.getByRole('button', { name: 'Enter' });
42-
await expect(enterButton).toBeVisible();
42+
await expect(enterButton).toBeVisible({ timeout: 30000 });
4343
await enterButton.click();
44-
await expect(this.page.getByText('Welcome back!')).toBeVisible();
44+
// Guest flow copy varies by Backstage / branding; wait for shell instead of "Welcome back!".
45+
await expect(
46+
this.page.getByRole('link', { name: 'Home' }).first(),
47+
).toBeVisible({
48+
timeout: 30000,
49+
});
4550
await this.switchToLocale(locale);
4651
}
4752

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

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import { Locator, Page, expect } from '@playwright/test';
1818
import { AGGREGATED_CARDS_WIDGET_TITLES } from '../constants/homepageWidgetTitles';
1919
import {
2020
ScorecardMessages,
21-
getEntitiesLabel,
2221
getEntityCount,
22+
getHomepageEntityCalculationHealthText,
2323
getLastUpdatedLabel,
2424
} from '../utils/translationUtils';
2525

@@ -130,22 +130,18 @@ export class HomePage {
130130
await expect(this.page.getByText(label)).toBeVisible();
131131
}
132132

133-
async clickDrillDownLink() {
134-
// CardSubheader renders the count as a Link (e.g. "10 entities"). The card
135-
// description can also contain the word "entities" (see API metadata), so
136-
// getByText(entitiesLabel) is ambiguous. MUI Tooltip also sets the link’s
137-
// accessible name to the long tooltip, so getByRole('link', { name }) is
138-
// locale‑fragile. Match only links whose *visible* text is "{{count}} <label>".
139-
const entitiesLabel = getEntitiesLabel(this.translations);
140-
await this.page
141-
.getByRole('link')
142-
.filter({
143-
hasText: new RegExp(
144-
String.raw`^\d+\s*${escapeRegex(entitiesLabel)}$`,
145-
'i',
146-
),
147-
})
148-
.first()
149-
.click();
133+
/**
134+
* Clicks the homepage KPI drill-down link (healthy/total subheader). Mock data uses 10/10.
135+
*/
136+
async clickDrillDownLink(options?: { healthy?: string; total?: string }) {
137+
const healthy = options?.healthy ?? '10';
138+
const total = options?.total ?? '10';
139+
const name = getHomepageEntityCalculationHealthText(
140+
this.translations,
141+
healthy,
142+
total,
143+
);
144+
// Multiple homepage scorecards can share the same health string; target the first match.
145+
await this.page.getByRole('link', { name }).first().click();
150146
}
151147
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,18 @@ export class ScorecardDrillDownPage {
163163
await expect(this.page.locator('tbody')).toContainText(noDataText);
164164
}
165165

166+
/**
167+
* When mocks report no calculation failures, the drill-down must not show the
168+
* calculation-warning icon next to the Entities heading.
169+
*/
170+
async expectNoDrillDownCalculationErrorWarningIcon() {
171+
const heading = this.page.getByRole('heading', {
172+
level: 3,
173+
name: this.translations.entitiesPage.entitiesTable.title,
174+
});
175+
await expect(heading.locator('svg.MuiSvgIcon-colorWarning')).toHaveCount(0);
176+
}
177+
166178
/** Verifies the "some entities not reporting" icon tooltip on the drill-down card. */
167179
async verifySomeEntitiesNotReportingTooltip() {
168180
const icon = this.page.getByTestId('ReportProblemOutlinedIcon');
@@ -190,9 +202,21 @@ export class ScorecardDrillDownPage {
190202
}
191203
}
192204

205+
/**
206+
* Asserts each entity row is present. Uses the catalog entity link `href`
207+
* (…/component/&lt;slug&gt;) so it works when the UI shows `metadata.title`
208+
* (e.g. "Red Hat Developer Hub") instead of `metadata.name` (slug).
209+
*/
193210
async expectEntityNamesVisible(entityNames: string[]) {
211+
const entitiesTable = this.getEntitiesTable();
194212
for (const name of entityNames) {
195-
await expect(this.page.getByText(name, { exact: true })).toBeVisible();
213+
const slug = encodeURIComponent(name);
214+
await expect(
215+
entitiesTable
216+
.locator('tbody')
217+
.locator(`a[href*="/catalog/default/component/${slug}"]`)
218+
.first(),
219+
).toBeVisible({ timeout: 15_000 });
196220
}
197221
}
198222

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

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ test.describe('Scorecard Plugin Tests', () => {
139139
notAllowedAggregationErrorBody,
140140
403,
141141
);
142+
142143
await catalogPage.openCatalog();
143144
await catalogPage.openComponent('Red Hat Developer Hub');
144145
await page.getByText('Scorecard', { exact: true }).click();
@@ -417,11 +418,6 @@ test.describe('Scorecard Plugin Tests', () => {
417418
await addAggregatedScorecardWidgets(homePage);
418419
await page.reload();
419420

420-
const jiraEntityCount = getEntityCount(
421-
translations,
422-
currentLocale,
423-
'10',
424-
);
425421
const card = homePage.getCard(
426422
AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId,
427423
);
@@ -435,7 +431,6 @@ test.describe('Scorecard Plugin Tests', () => {
435431
getThresholdsSnapshot(translations, {
436432
drillDownMetricId:
437433
AGGREGATED_CARDS_METRIC_IDS.withDeprecatedMetricId,
438-
entityCount: jiraEntityCount,
439434
cardTitle: metadata.title,
440435
cardDescription: metadata.description,
441436
}),
@@ -525,11 +520,6 @@ test.describe('Scorecard Plugin Tests', () => {
525520
await addAggregatedScorecardWidgets(homePage);
526521
await page.reload();
527522

528-
const githubEntityCount = getEntityCount(
529-
translations,
530-
currentLocale,
531-
'10',
532-
);
533523
const metadata =
534524
translations.metric[
535525
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation
@@ -543,7 +533,6 @@ test.describe('Scorecard Plugin Tests', () => {
543533
getThresholdsSnapshot(translations, {
544534
drillDownMetricId:
545535
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation,
546-
entityCount: githubEntityCount,
547536
cardTitle: metadata.title,
548537
cardDescription: metadata.description,
549538
}),
@@ -647,11 +636,6 @@ test.describe('Scorecard Plugin Tests', () => {
647636
);
648637
await page.reload();
649638

650-
const githubEntityCount = getEntityCount(
651-
translations,
652-
currentLocale,
653-
'10',
654-
);
655639
const card = homePage.getCard(
656640
AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs,
657641
);
@@ -663,7 +647,6 @@ test.describe('Scorecard Plugin Tests', () => {
663647
AGGREGATED_CARDS_METRIC_IDS.withDefaultAggregation,
664648
drillDownAggregationId:
665649
AGGREGATED_CARDS_METRIC_IDS.withGithubOpenPrs,
666-
entityCount: githubEntityCount,
667650
cardTitle: githubAggregatedResponse.metadata.title,
668651
cardDescription: githubAggregatedResponse.metadata.description,
669652
}),
@@ -798,7 +781,7 @@ test.describe('Scorecard Plugin Tests', () => {
798781
cardDescription: githubAggregatedResponse.metadata.description,
799782
},
800783
);
801-
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
784+
await scorecardDrillDownPage.expectNoDrillDownCalculationErrorWarningIcon();
802785
await scorecardDrillDownPage.expectTableHeadersVisible();
803786
const rows5Label = getEntitiesTableFooterRowsLabel(translations, 5);
804787
await scorecardDrillDownPage.expectTableFooterSnapshot(
@@ -820,7 +803,7 @@ test.describe('Scorecard Plugin Tests', () => {
820803
// First page: only 5 entities (pageSize=5)
821804
await scorecardDrillDownPage.expectEntityNamesVisible([
822805
'all-scorecards-service',
823-
'Red Hat Developer Hub',
806+
'red-hat-developer-hub',
824807
'github-scorecard-only-service',
825808
'all-scorecards-service-different-owner',
826809
'backend-api',
@@ -915,7 +898,7 @@ test.describe('Scorecard Plugin Tests', () => {
915898
cardDescription: jiraAggregatedResponse.metadata.description,
916899
},
917900
);
918-
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
901+
await scorecardDrillDownPage.expectNoDrillDownCalculationErrorWarningIcon();
919902
await scorecardDrillDownPage.expectTableHeadersVisible();
920903
await scorecardDrillDownPage.expectEntityNamesVisible([
921904
'platform-api',
@@ -1111,11 +1094,10 @@ test.describe('Scorecard Plugin Tests', () => {
11111094
},
11121095
);
11131096
await expectAverageCardCenterPercent(drillCard, '50%');
1114-
await scorecardDrillDownPage.verifySomeEntitiesNotReportingTooltip();
11151097
await scorecardDrillDownPage.expectTableHeadersVisible();
11161098
await scorecardDrillDownPage.expectEntityNamesVisible([
11171099
'all-scorecards-service',
1118-
'Red Hat Developer Hub',
1100+
'red-hat-developer-hub',
11191101
'github-scorecard-only-service',
11201102
'all-scorecards-service-different-owner',
11211103
'backend-api',

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ export const githubAggregatedResponse = {
307307
total: 10,
308308
timestamp: '2026-01-24T14:10:32.858Z',
309309
thresholds: DEFAULT_NUMBER_THRESHOLDS,
310+
entitiesConsidered: 10,
311+
calculationErrorCount: 0,
310312
},
311313
};
312314

@@ -330,6 +332,8 @@ export const jiraAggregatedResponse = {
330332
total: 10,
331333
timestamp: '2026-01-24T14:10:32.776Z',
332334
thresholds: DEFAULT_NUMBER_THRESHOLDS,
335+
entitiesConsidered: 10,
336+
calculationErrorCount: 0,
333337
},
334338
};
335339

@@ -353,6 +357,8 @@ export const emptyJiraAggregatedResponse = {
353357
],
354358
timestamp: '2026-01-24T14:10:32.858Z',
355359
thresholds: DEFAULT_NUMBER_THRESHOLDS,
360+
entitiesConsidered: 0,
361+
calculationErrorCount: 0,
356362
},
357363
};
358364

@@ -376,6 +382,8 @@ export const emptyGithubAggregatedResponse = {
376382
],
377383
timestamp: '2026-01-24T14:10:32.858Z',
378384
thresholds: DEFAULT_NUMBER_THRESHOLDS,
385+
entitiesConsidered: 0,
386+
calculationErrorCount: 0,
379387
},
380388
};
381389

@@ -497,6 +505,11 @@ export const githubEntitiesDrillDownResponse = {
497505
totalPages: 1,
498506
isCapped: false,
499507
},
508+
entityHealth: {
509+
totalEntities: 10,
510+
calculationErrorCount: 0,
511+
countsArePartial: false,
512+
},
500513
};
501514

502515
/** Mock response for GET .../api/scorecard/metrics/jira.open_issues/catalog/aggregations/entities (in sync with jiraAggregatedResponse) */
@@ -557,6 +570,11 @@ export const jiraEntitiesDrillDownResponse = {
557570
totalPages: 1,
558571
isCapped: false,
559572
},
573+
entityHealth: {
574+
totalEntities: 4,
575+
calculationErrorCount: 0,
576+
countsArePartial: false,
577+
},
560578
};
561579

562580
/** Mock response for Jira entities drill-down when aggregation has no data (empty list). */
@@ -576,6 +594,11 @@ export const jiraEntitiesDrillDownNoDataResponse = {
576594
totalPages: 0,
577595
isCapped: false,
578596
},
597+
entityHealth: {
598+
totalEntities: 0,
599+
calculationErrorCount: 0,
600+
countsArePartial: false,
601+
},
579602
};
580603

581604
export const fileCheckScorecardResponse = [

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

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -306,20 +306,37 @@ export function getLastUpdatedLabel(
306306
}
307307

308308
/**
309-
* Homepage KPI cards use aggregationIds (e.g. openPrsKpi); labels fall back to API/config
310-
* metadata in English, not `metric.github.open_prs` locale keys. Use ref copy for title
311-
* / description; keep localized errors, thresholds, and entity-count strings.
309+
* Homepage KPI drill-down link text:
310+
* - with calculation errors: healthy/total ratio
311+
* - without calculation errors: plain entity count
312312
*/
313-
function getSomeEntitiesNotReportingLabel(
313+
export function getHomepageEntityCalculationHealthText(
314314
translations: ScorecardMessages,
315+
healthy: string,
316+
total: string,
315317
): string {
316-
const metric = translations.metric as {
317-
someEntitiesNotReportingValues?: string;
318-
};
319-
return (
320-
metric.someEntitiesNotReportingValues ??
321-
scorecardMessages.metric.someEntitiesNotReportingValues
322-
);
318+
if (healthy === total) {
319+
const count = Number(total);
320+
const entitiesTemplate =
321+
count === 1
322+
? translations.thresholds.entities_one
323+
: translations.thresholds.entities_other;
324+
return entitiesTemplate.replaceAll('{{count}}', String(count));
325+
}
326+
327+
const template =
328+
(
329+
translations.metric as {
330+
homepageEntityHealthRatio?: string;
331+
homepageEntityCalculationHealth?: string;
332+
}
333+
).homepageEntityHealthRatio ??
334+
scorecardMessages.metric.homepageEntityHealthRatio ??
335+
scorecardMessages.metric.homepageEntityCalculationHealth;
336+
337+
return template
338+
.replaceAll('{{healthy}}', healthy)
339+
.replaceAll('{{total}}', total);
323340
}
324341

325342
/** Snapshot for the scorecard card on the drill-down page when permission is missing (no entity count in UI). */
@@ -357,26 +374,33 @@ export function getThresholdsSnapshot(
357374
options: {
358375
drillDownMetricId: 'jira.open_issues' | 'github.open_prs';
359376
drillDownAggregationId?: string;
360-
entityCount: string;
377+
/** Interpolation for homepage subheader (mock data uses 10/10). */
378+
homepageCalculationHealth?: { healthy: string; total: string };
361379
cardTitle: string;
362380
cardDescription: string;
363381
},
364382
): string {
365383
const {
366384
drillDownMetricId,
367385
drillDownAggregationId,
368-
entityCount,
369386
cardTitle,
370387
cardDescription,
371388
} = options;
372389
const aggregationSegment = drillDownAggregationId ?? drillDownMetricId;
373-
const drillDownLinkName = getSomeEntitiesNotReportingLabel(translations);
390+
const { healthy, total } = options.homepageCalculationHealth ?? {
391+
healthy: '10',
392+
total: '10',
393+
};
394+
const drillDownLinkText = getHomepageEntityCalculationHealthText(
395+
translations,
396+
healthy,
397+
total,
398+
);
374399
return `
375400
- article:
376401
- text: ${cardTitle}
377-
- link "${drillDownLinkName}":
402+
- link "${drillDownLinkText}":
378403
- /url: /scorecard/aggregations/${aggregationSegment}/metrics/${drillDownMetricId}
379-
- text: ${entityCount}
380404
- button
381405
- separator
382406
- paragraph: ${cardDescription}

0 commit comments

Comments
 (0)