Skip to content

Commit 243ad0a

Browse files
authored
RHIDP-12113: Support customization aggregated homepage scorecard card (#2609)
* feat(scorecard): support customization aggregated homepage scorecard cards Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): api reports and tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): logic to customize aggregated cards Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): sonarqube issues Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard) e2e tests Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): specify `ScorecardApi` type in hooks Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * fix(scorecard): improve error messages for missing aggregation ID across multiple languages Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): move `getUserEntityRef` utility and improve permission handling Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * feat(scorecard): add aggregation KPI validation and configuration handling Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): update drill-down page and routing for aggregations Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): update metric routing to include drill-down aggregation ID Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): legacy aggregation routes Signed-off-by: Ihor Mykhno <imykhno@redhat.com> * refactor(scorecard): scorecardPage file name Signed-off-by: Ihor Mykhno <imykhno@redhat.com> --------- Signed-off-by: Ihor Mykhno <imykhno@redhat.com>
1 parent 28ca23d commit 243ad0a

95 files changed

Lines changed: 4760 additions & 1212 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor
3+
'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor
4+
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
5+
---
6+
7+
Aggregated scorecards now use **aggregation IDs** and dedicated HTTP routes. The old catalog-aggregations URL still works but is **deprecated** (not removed).
8+
9+
**Backend (`@red-hat-developer-hub/backstage-plugin-scorecard-backend`)**
10+
11+
- **Deprecated:** `GET /metrics/:metricId/catalog/aggregations` — responses are unchanged, but the handler emits [RFC 8594](https://datatracker.ietf.org/doc/html/rfc8594) `Deprecation` and `Link` headers (alternate successor: `GET .../aggregations/:aggregationId`) and logs a warning. Prefer **`GET /aggregations/:aggregationId`** for new integrations.
12+
- **Added:** `GET /aggregations/:aggregationId` for aggregated results using configured aggregation.
13+
- **Added:** `GET /aggregations/:aggregationId/metadata` for KPI titles, descriptions, and aggregation metadata consumed by the UI.
14+
15+
**Common (`@red-hat-developer-hub/backstage-plugin-scorecard-common`)**
16+
17+
- Types and constants aligned with the aggregation config and new API shapes.
18+
19+
**Frontend (`@red-hat-developer-hub/backstage-plugin-scorecard`)**
20+
21+
- Homepage and aggregated flows resolve cards via **`aggregationId`**, fetch metadata from the new endpoint, and keep localized threshold and error strings where translation keys exist.
22+
23+
**Action for adopters:** Configure aggregated scorecards with `aggregationId` values that match backend aggregation config, replace direct calls to `GET /metrics/:metricId/catalog/aggregations` with `GET /aggregations/:aggregationId` (and metadata if you need the same labels as the plugin UI).

workspaces/scorecard/app-config.local.EXAMPLE.yaml

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,81 @@ permission:
22
enabled: false
33

44
auth:
5-
environment: production
5+
environment: development
66
providers:
77
github:
8-
production:
9-
clientId: TODO
10-
clientSecret: TODO
8+
development:
9+
clientId: ${GITHUB_CLIENT_ID}
10+
clientSecret: ${GITHUB_CLIENT_SECRET}
1111
signIn:
1212
resolvers:
1313
- resolver: usernameMatchingUserEntityName
1414

1515
catalog:
1616
locations:
1717
- type: file
18-
target: ../../examples/all-scorecards.yaml
18+
target: ../../examples/all-scorecards-location.yaml
1919
rules:
20-
- allow:
21-
[Component, System, API, Resource, Location, Template, User, Group]
20+
- allow: [Component]
21+
- type: file
22+
target: ../../examples/orgs/guest.yaml
23+
rules:
24+
- allow: [User, Group]
2225
# TODO: Additional catalog entities for your user
2326

24-
proxy:
25-
'/jira/api':
26-
target: ${JIRA_URL}
27-
headers:
28-
Authorization: ${JIRA_TOKEN}
29-
Accept: 'application/json'
30-
Content-Type: 'application/json'
31-
X-Atlassian-Token: 'nocheck'
32-
User-Agent: 'MY-UA-STRING'
27+
# ToDo: uncomment this when Jira Proxy is needed
28+
# proxy:
29+
# '/jira/api':
30+
# target: ${JIRA_URL}
31+
# headers:
32+
# Authorization: ${JIRA_TOKEN}
33+
# Accept: 'application/json'
34+
# Content-Type: 'application/json'
35+
# X-Atlassian-Token: 'nocheck'
36+
# User-Agent: 'MY-UA-STRING'
3337

3438
integrations:
3539
github:
3640
- host: github.com
37-
token: TODO
41+
token: ${GITHUB_TOKEN}
3842

3943
jira:
40-
proxyPath: /jira/api
44+
# ToDo: uncomment this when Jira Proxy is needed
45+
# proxyPath: /jira/api
46+
4147
product: cloud
4248
baseUrl: ${JIRA_URL}
4349
token: ${JIRA_TOKEN}
4450

51+
# ToDo: uncomment this when Jira Data Center is needed
52+
# product: datacenter
53+
# baseUrl: ${JIRA_DATA_CENTER_URL}
54+
# token: ${JIRA_DATA_CENTER_TOKEN}
55+
56+
# Optional Scorecard overrides (aggregationKPIs defaults are in app-config.yaml).
4557
scorecard:
4658
plugins:
4759
jira:
4860
open_issues:
49-
options:
50-
mandatoryFilter: Resolution = Unresolved
61+
schedule:
62+
frequency: { days: 1 }
63+
timeout: { minutes: 2 }
64+
initialDelay: { minutes: 1 }
65+
# Optional — uncomment to narrow which Jira issues are counted (otherwise all open issues match the provider rules).
66+
# options:
67+
# mandatoryFilter: type = Story
68+
# customFilter: priority = High
69+
github:
70+
open_prs:
71+
# Example threshold overrides; remove to use provider defaults.
72+
thresholds:
73+
rules:
74+
- key: success
75+
expression: '<=20'
76+
color: 'success.main'
77+
- key: warning
78+
expression: '20-50'
79+
color: '#FFFF00'
80+
- key: error
81+
expression: '>50'
82+
color: 'rgb(255, 0, 0)'

workspaces/scorecard/app-config.production.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,18 @@ catalog:
5353
target: ./examples/org.yaml
5454
rules:
5555
- allow: [User, Group]
56+
# Scorecard (uncomment and adjust for production — requires scorecard backend + metric modules)
57+
# scorecard:
58+
# aggregationKPIs:
59+
# openPrsKpi:
60+
# title: GitHub open PRs
61+
# description: Open PRs across owned entities, grouped by status.
62+
# type: statusGrouped
63+
# metricId: github.open_prs
64+
# plugins:
65+
# github:
66+
# open_prs:
67+
# schedule:
68+
# frequency: { hours: 1 }
69+
# timeout: { minutes: 15 }
70+
# initialDelay: { minutes: 1 }

workspaces/scorecard/app-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ catalog:
138138
target: ../../examples/all-scorecards-location.yaml
139139
rules:
140140
- allow: [Component]
141+
- type: file
142+
target: ../../examples/orgs/guest.yaml
143+
rules:
144+
- allow: [User, Group]
141145

142146
- type: url
143147
target: https://github.com/redhat-developer/rhdh/blob/main/catalog-entities/components/showcase.yaml
@@ -161,6 +165,17 @@ permission:
161165

162166
# Scorecard development configuration
163167
scorecard:
168+
aggregationKPIs:
169+
openPrsKpi:
170+
title: GitHub Open PRs KPI
171+
type: statusGrouped
172+
description: This KPI is provide information about GitHub open PRs grouped by status.
173+
metricId: github.open_prs
174+
openIssuesKpi:
175+
title: Jira Open Issues KPI
176+
type: statusGrouped
177+
description: This KPI is provide information about Jira open issues grouped by status.
178+
metricId: jira.open_issues
164179
plugins:
165180
jira:
166181
open_issues:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export const AGGREGATED_CARDS_METRIC_IDS = {
18+
withDeprecatedMetricId: 'jira.open_issues',
19+
withDefaultAggregation: 'github.open_prs',
20+
withGithubOpenPrs: 'openPrsKpi',
21+
withJiraOpenIssuesKpi: 'openIssuesKpi',
22+
} as const;
23+
24+
export const AGGREGATED_CARDS_WIDGET_TITLES = {
25+
/** Must match `title` in App.tsx homepage widget config (Add widget picker). */
26+
withDeprecatedMetricId: 'Scorecard: With deprecated metricId property (Jira)',
27+
withDefaultAggregation: 'Scorecard: With default aggregation config (GitHub)',
28+
withGithubOpenPrs: 'Scorecard: GitHub open PRs',
29+
withJiraOpenIssuesKpi: 'Scorecard: Jira open blocking tickets',
30+
} as const;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export const ScorecardRoutes = {
18+
SCORECARD_API_ROUTE:
19+
'**/api/scorecard/metrics/catalog/Component/default/red-hat-developer-hub*',
20+
OPEN_PRS_KPI_METADATA_ROUTE:
21+
'**/api/scorecard/aggregations/openPrsKpi/metadata',
22+
OPEN_ISSUES_KPI_METADATA_ROUTE:
23+
'**/api/scorecard/aggregations/openIssuesKpi/metadata',
24+
OPEN_PRS_KPI_AGGREGATION_ROUTE: '**/api/scorecard/aggregations/openPrsKpi',
25+
OPEN_ISSUES_KPI_AGGREGATION_ROUTE:
26+
'**/api/scorecard/aggregations/openIssuesKpi',
27+
/** Default aggregation when aggregationId is the metric id (no KPI entry). */
28+
JIRA_OPEN_ISSUES_METRIC_AGGREGATION_ROUTE:
29+
'**/api/scorecard/aggregations/jira.open_issues',
30+
GITHUB_OPEN_PRS_METRIC_AGGREGATION_ROUTE:
31+
'**/api/scorecard/aggregations/github.open_prs',
32+
} as const;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class CatalogPage {
4646
}
4747

4848
async openCatalog() {
49-
await this.page.getByRole('link', { name: 'Catalog', exact: true }).click();
49+
await this.page.goto('/catalog'); // Resolves the issue when "My Groups" sidebar covers the catalog toolbar
5050
await this.page.getByTestId('user-picker-all').getByText('All').click();
5151
}
5252

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

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
getEntitiesLabel,
2121
getEntityCount,
2222
getLastUpdatedLabel,
23-
getMetricTitleEn,
2423
} from '../utils/translationUtils';
2524

2625
type ThresholdState = 'success' | 'warning' | 'error';
@@ -74,26 +73,16 @@ export class HomePage {
7473
await this.page.getByRole('button', { name: 'Save' }).click();
7574
}
7675

77-
async expectCardVisible(metricId: 'github.open_prs' | 'jira.open_issues') {
78-
await expect(
79-
this.page.getByText(this.translations.metric[metricId].title),
80-
).toBeVisible();
76+
async expectCardVisible(instanceId: string) {
77+
await expect(this.getCard(instanceId)).toBeVisible();
8178
}
8279

83-
async expectCardNotVisible(metricId: 'github.open_prs' | 'jira.open_issues') {
84-
await expect(
85-
this.page.getByText(this.translations.metric[metricId].title),
86-
).not.toBeVisible();
80+
async expectCardNotVisible(instanceId: string) {
81+
await expect(this.getCard(instanceId)).not.toBeVisible();
8782
}
8883

89-
getCard(metricId: 'github.open_prs' | 'jira.open_issues'): Locator {
90-
const translatedTitle = this.translations.metric[metricId].title;
91-
const enTitle = getMetricTitleEn(metricId);
92-
const pattern =
93-
translatedTitle === enTitle
94-
? translatedTitle
95-
: new RegExp(`${escapeRegex(translatedTitle)}|${escapeRegex(enTitle)}`);
96-
return this.page.locator('article').filter({ hasText: pattern });
84+
getCard(instanceId: string): Locator {
85+
return this.page.getByTestId(`scorecard-homepage-card-${instanceId}`);
9786
}
9887

9988
async verifyThresholdTooltip(
@@ -103,7 +92,7 @@ export class HomePage {
10392
percentage: string,
10493
) {
10594
const stateLabel = this.translations.thresholds[state];
106-
await card.getByText(stateLabel, { exact: true }).hover();
95+
await card.getByText(stateLabel, { exact: true }).first().hover();
10796
await expect(
10897
this.page.getByText(
10998
getEntityCount(this.translations, this.locale, entityCount),
@@ -115,25 +104,21 @@ export class HomePage {
115104
).toBeVisible();
116105
}
117106

118-
async expectCardHasMissingPermission(
119-
metricId: 'github.open_prs' | 'jira.open_issues',
120-
) {
121-
const card = this.getCard(metricId);
107+
async expectCardHasMissingPermission(instanceId: string) {
108+
const card = this.getCard(instanceId);
122109
await expect(card).toContainText(
123110
this.translations.errors.missingPermission,
124111
);
125112
}
126113

127-
async expectCardHasNoDataFound(
128-
metricId: 'github.open_prs' | 'jira.open_issues',
129-
) {
130-
const card = this.getCard(metricId);
114+
async expectCardHasNoDataFound(instanceId: string) {
115+
const card = this.getCard(instanceId);
131116
await expect(card).toContainText(this.translations.errors.noDataFound);
132117
}
133118

134119
async verifyLastUpdatedTooltip(card: Locator, formattedTimestamp: string) {
135120
const label = getLastUpdatedLabel(this.translations, formattedTimestamp);
136-
const infoIcon = card.locator('[data-testid="InfoOutlinedIcon"]');
121+
const infoIcon = card.getByTestId('scorecard-homepage-card-info');
137122
await expect(infoIcon).toBeVisible();
138123
await infoIcon.hover();
139124
await expect(this.page.getByText(label)).toBeVisible();

0 commit comments

Comments
 (0)