diff --git a/workspaces/scorecard/.changeset/stupid-knives-wonder.md b/workspaces/scorecard/.changeset/stupid-knives-wonder.md new file mode 100644 index 0000000000..84f344415f --- /dev/null +++ b/workspaces/scorecard/.changeset/stupid-knives-wonder.md @@ -0,0 +1,20 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scorecard-backend': minor +'@red-hat-developer-hub/backstage-plugin-scorecard-common': minor +'@red-hat-developer-hub/backstage-plugin-scorecard': minor +--- + +Rename aggregation KPI type `average` to `weightedStatusScore`. + +**Breaking changes** + +### App config + +- `scorecard.aggregationKPIs.*.type`: `average` → `weightedStatusScore` + +### `GET /aggregations/:aggregationId` API + +- `metadata.aggregationType`: `average` → `weightedStatusScore` +- `result.averageScore` → `result.weightedStatusScore` +- `result.averageWeightedSum` → `result.weightedStatusSum` +- `result.averageMaxPossible` → `result.weightedStatusMaxPossible` diff --git a/workspaces/scorecard/README.md b/workspaces/scorecard/README.md index 612a39207e..daa6518dc9 100644 --- a/workspaces/scorecard/README.md +++ b/workspaces/scorecard/README.md @@ -15,10 +15,10 @@ yarn install ## Documentation -| Topic | Location | -| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -| Aggregation KPIs (`statusGrouped`, `average`), API, ownership | [plugins/scorecard-backend/docs/aggregation.md](plugins/scorecard-backend/docs/aggregation.md) | -| Backend installation and RBAC, **`scorecard.aggregationKPIs`** examples | [plugins/scorecard-backend/README.md](plugins/scorecard-backend/README.md) | -| Drill-down (entity list for a metric) | [plugins/scorecard-backend/docs/drill-down.md](plugins/scorecard-backend/docs/drill-down.md) | -| Metric thresholds, annotations, **average KPI result colors** | [plugins/scorecard-backend/docs/thresholds.md](plugins/scorecard-backend/docs/thresholds.md) | -| Frontend (homepage cards, NFS) | [plugins/scorecard/README.md](plugins/scorecard/README.md) | +| Topic | Location | +| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Aggregation KPIs (`statusGrouped`, `weightedStatusScore`), API, ownership | [plugins/scorecard-backend/docs/aggregation.md](plugins/scorecard-backend/docs/aggregation.md) | +| Backend installation and RBAC, **`scorecard.aggregationKPIs`** examples | [plugins/scorecard-backend/README.md](plugins/scorecard-backend/README.md) | +| Drill-down (entity list for a metric) | [plugins/scorecard-backend/docs/drill-down.md](plugins/scorecard-backend/docs/drill-down.md) | +| Metric thresholds, annotations, **weightedStatusScore KPI result colors** | [plugins/scorecard-backend/docs/thresholds.md](plugins/scorecard-backend/docs/thresholds.md) | +| Frontend (homepage cards, NFS) | [plugins/scorecard/README.md](plugins/scorecard/README.md) | diff --git a/workspaces/scorecard/app-config.local.EXAMPLE.yaml b/workspaces/scorecard/app-config.local.EXAMPLE.yaml index 0d8b54811f..fb123f5d8b 100644 --- a/workspaces/scorecard/app-config.local.EXAMPLE.yaml +++ b/workspaces/scorecard/app-config.local.EXAMPLE.yaml @@ -57,8 +57,8 @@ jira: scorecard: openPrsWeightedKpi: title: GitHub Open PRs (weighted health) - type: average - description: Weighted health average for open PRs by threshold status across your entities. + type: weightedStatusScore + description: Weighted health score for open PRs by threshold status across your entities. metricId: github.open_prs options: statusScores: diff --git a/workspaces/scorecard/app-config.yaml b/workspaces/scorecard/app-config.yaml index a4dfd4a1a3..52d44e9b1c 100644 --- a/workspaces/scorecard/app-config.yaml +++ b/workspaces/scorecard/app-config.yaml @@ -235,8 +235,8 @@ scorecard: metricId: github.open_prs openPrsWeightedKpi: title: GitHub Open PRs (weighted health) - type: average - description: Weighted health average for open PRs by threshold status across your entities. + type: weightedStatusScore + description: Weighted health score for open PRs by threshold status across your entities. metricId: github.open_prs options: statusScores: diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts index 0539acdd02..a76c5fb164 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/scorecard.test.ts @@ -59,7 +59,7 @@ import { getTranslations, getEntityCount, getStatusGroupedCardSnapshot, - getAverageCardSnapshot, + getWeightedStatusScoreCardSnapshot, getTableFooterSnapshot, getEntitiesTableFooterRowsLabel, } from './utils/translationUtils'; @@ -73,10 +73,10 @@ import { setupHomepageAllCardsNoData, } from './utils/homepageWidgetUtils'; import { - expectAverageCardCenterPercent, - verifyAverageDonutCenterTooltip, - verifyAverageCenterTooltipBreakdownRows, -} from './utils/averageCardAssertions'; + expectWeightedStatusScoreCardCenterPercent, + verifyWeightedStatusScoreDonutCenterTooltip, + verifyWeightedStatusScoreCenterTooltipBreakdownRows, +} from './utils/weightedStatusScoreCardAssertions'; import { runAccessibilityTests } from './utils/accessibility'; import { ScorecardRoutes } from './constants/routes'; import { @@ -714,7 +714,7 @@ test.describe('Scorecard Plugin Tests', () => { }); }); - test.describe('Configured aggregation KPI - "average" type', () => { + test.describe('Configured aggregation KPI - "weightedStatusScore" type', () => { const aggregationMetadata = AGGREGATED_CARDS_METADATA.githubOpenPrsWeightedKpi; @@ -726,7 +726,7 @@ test.describe('Scorecard Plugin Tests', () => { }); }); - test.describe('Validate "average" type card content', () => { + test.describe('Validate "weightedStatusScore" type card content', () => { let card: Locator; test.beforeAll(async () => { @@ -755,19 +755,19 @@ test.describe('Scorecard Plugin Tests', () => { test('Verify center score percentage', async () => { await expect(card).toBeVisible(); - await expectAverageCardCenterPercent(card, '51.5%'); + await expectWeightedStatusScoreCardCenterPercent(card, '51.5%'); }); test('Verify center tooltip', async () => { await expect(card).toBeVisible(); - await verifyAverageDonutCenterTooltip( + await verifyWeightedStatusScoreDonutCenterTooltip( page, card, translations, - openPrsWeightedAggregatedResponse.result.averageWeightedSum, - openPrsWeightedAggregatedResponse.result.averageMaxPossible, + openPrsWeightedAggregatedResponse.result.weightedStatusSum, + openPrsWeightedAggregatedResponse.result.weightedStatusMaxPossible, ); - await verifyAverageCenterTooltipBreakdownRows( + await verifyWeightedStatusScoreCenterTooltipBreakdownRows( page, card, translations, @@ -814,12 +814,12 @@ test.describe('Scorecard Plugin Tests', () => { await expect(card).toBeVisible(); await expect(card).toMatchAriaSnapshot( - getAverageCardSnapshot(translations, { + getWeightedStatusScoreCardSnapshot(translations, { drillDownMetricId: aggregationMetadata.metricId, drillDownAggregationId: aggregationMetadata.id, cardTitle: partialResponse.metadata.title, cardDescription: partialResponse.metadata.description, - averageScoreLabel: `${partialResponse.result.averageScore}%`, + weightedStatusScoreLabel: `${partialResponse.result.weightedStatusScore}%`, homepageCalculationHealth: { healthy: String(entitiesConsidered - calculationErrorCount), total: String(entitiesConsidered), diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts index 78fc1e1edf..a5460b2b94 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/apiUtils.ts @@ -69,7 +69,8 @@ export function waitForAggregationResponse( const result = json?.result; return ( - result?.averageScore !== undefined || result?.total !== undefined + result?.weightedStatusScore !== undefined || + result?.total !== undefined ); } catch { return false; diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts index 8dac563eca..0105e8ffd9 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/scorecardResponseUtils.ts @@ -207,14 +207,14 @@ export const openIssuesKpiMetadataResponse = { export const openPrsWeightedKpiMetadataResponse = { title: 'GitHub Open PRs (weighted health)', description: - 'Weighted health average for open PRs by threshold status across your entities.', + 'Weighted health score for open PRs by threshold status across your entities.', type: 'number', history: true, - aggregationType: aggregationTypes.average, + aggregationType: aggregationTypes.weightedStatusScore, }; /** - * Average KPI: 3×100 + 5×40 + 1×15 + 1×0 = 515 weighted sum; max 100×10 entities → 51.5% score. + * WeightedStatusScore KPI: 3×100 + 5×40 + 1×15 + 1×0 = 515 weighted sum; max 100×10 entities → 51.5% score. * Includes `critical` as a non-threshold status name (no `thresholds.critical` copy). * Colors align with aggregation KPI `options.thresholds` warning band (30–79%) in app-config. */ @@ -236,9 +236,9 @@ export const openPrsWeightedAggregatedResponse = { calculationErrorCount: 0, timestamp: '2026-01-24T14:10:32.858Z', thresholds: DEFAULT_NUMBER_THRESHOLDS, - averageScore: 51.5, - averageWeightedSum: 515, - averageMaxPossible: 1000, + weightedStatusScore: 51.5, + weightedStatusSum: 515, + weightedStatusMaxPossible: 1000, aggregationChartDisplayColor: 'rgb(224, 189, 108)', }, }; @@ -259,8 +259,8 @@ export const gitHubWeightedPartiallyAggregatedResponse = { total: 8, entitiesConsidered: 6, calculationErrorCount: 2, - averageScore: 46.7, - averageWeightedSum: 466.67, + weightedStatusScore: 46.7, + weightedStatusSum: 466.67, }, }; @@ -279,9 +279,9 @@ export const emptyOpenPrsWeightedAggregatedResponse = { ], timestamp: '2026-01-24T14:10:32.858Z', thresholds: DEFAULT_NUMBER_THRESHOLDS, - averageScore: 0, - averageWeightedSum: 0, - averageMaxPossible: 0, + weightedStatusScore: 0, + weightedStatusSum: 0, + weightedStatusMaxPossible: 0, aggregationChartDisplayColor: '#6bb300', }, }; diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts index fb994bcc37..6e6dd6d83c 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/translationUtils.ts @@ -193,7 +193,7 @@ export function getSomeEntitiesNotReportingTooltip( ); } -/** Flat metric-namespace string by key (e.g. averageCenterTooltipTotalLabel). */ +/** Flat metric-namespace string by key (e.g. weightedStatusScoreCenterTooltipTotalLabel). */ export function getMetricTranslation( translations: ScorecardMessages, key: string, @@ -451,8 +451,8 @@ export function getStatusGroupedCardSnapshot( `; } -/** Snapshot for average-type homepage KPI cards (donut gauge, no threshold legend). */ -export function getAverageCardSnapshot( +/** Snapshot for weightedStatusScore-type homepage KPI cards (donut gauge, no threshold legend). */ +export function getWeightedStatusScoreCardSnapshot( translations: ScorecardMessages, options: { drillDownMetricId: 'jira.open_issues' | 'github.open_prs'; @@ -460,7 +460,7 @@ export function getAverageCardSnapshot( homepageCalculationHealth?: { healthy: string; total: string }; cardTitle: string; cardDescription: string; - averageScoreLabel: string; + weightedStatusScoreLabel: string; }, ): string { const { @@ -468,7 +468,7 @@ export function getAverageCardSnapshot( drillDownAggregationId, cardTitle, cardDescription, - averageScoreLabel, + weightedStatusScoreLabel, } = options; const aggregationSegment = drillDownAggregationId ?? drillDownMetricId; const { healthy, total } = options.homepageCalculationHealth ?? { @@ -488,7 +488,7 @@ export function getAverageCardSnapshot( - button - separator - paragraph: ${cardDescription} - - application: ${averageScoreLabel} + - application: ${weightedStatusScoreLabel} `; } diff --git a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/weightedStatusScoreCardAssertions.ts similarity index 69% rename from workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts rename to workspaces/scorecard/packages/app-legacy/e2e-tests/utils/weightedStatusScoreCardAssertions.ts index 4782f77d80..2590f04ac3 100644 --- a/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/averageCardAssertions.ts +++ b/workspaces/scorecard/packages/app-legacy/e2e-tests/utils/weightedStatusScoreCardAssertions.ts @@ -28,22 +28,22 @@ function interpolate(template: string, vars: Record): string { ); } -function averageCenterTooltipBreakdownTemplateKey( +function weightedStatusScoreCenterTooltipBreakdownTemplateKey( locale: string, count: number, ): - | 'averageCenterTooltipBreakdownRow_one' - | 'averageCenterTooltipBreakdownRow_other' { + | 'weightedStatusScoreCenterTooltipBreakdownRow_one' + | 'weightedStatusScoreCenterTooltipBreakdownRow_other' { if (Number.isNaN(count)) { - return 'averageCenterTooltipBreakdownRow_other'; + return 'weightedStatusScoreCenterTooltipBreakdownRow_other'; } const category = new Intl.PluralRules(locale).select(count); return category === 'one' - ? 'averageCenterTooltipBreakdownRow_one' - : 'averageCenterTooltipBreakdownRow_other'; + ? 'weightedStatusScoreCenterTooltipBreakdownRow_one' + : 'weightedStatusScoreCenterTooltipBreakdownRow_other'; } -function expectedAverageCenterTooltipBreakdownLine( +function expectedWeightedStatusScoreCenterTooltipBreakdownLine( translations: ScorecardMessages, locale: string, statusKey: string, @@ -51,7 +51,10 @@ function expectedAverageCenterTooltipBreakdownLine( score: string, ): string { const n = Number.parseInt(count, 10); - const templateKey = averageCenterTooltipBreakdownTemplateKey(locale, n); + const templateKey = weightedStatusScoreCenterTooltipBreakdownTemplateKey( + locale, + n, + ); const template = getMetricTranslation(translations, templateKey); const status = statusKey in translations.thresholds @@ -60,32 +63,40 @@ function expectedAverageCenterTooltipBreakdownLine( return interpolate(template, { status, count, score }); } -export async function expectAverageCardCenterPercent( +export async function expectWeightedStatusScoreCardCenterPercent( card: Locator, percentLabel: string, ): Promise { - await expect(card.getByTestId('average-card-center-percent')).toHaveText( - percentLabel, - ); + await expect( + card.getByTestId('weighted-status-score-card-center-percent'), + ).toHaveText(percentLabel); } -export async function verifyAverageDonutCenterTooltip( +export async function verifyWeightedStatusScoreDonutCenterTooltip( page: Page, card: Locator, translations: ScorecardMessages, weightedSum: number, maxPossible: number, ): Promise { - await card.getByTestId('average-card-center-percent-hit-area').hover(); + await card + .getByTestId('weighted-status-score-card-center-percent-hit-area') + .hover(); await expect( page.getByText( - getMetricTranslation(translations, 'averageCenterTooltipTotalLabel'), + getMetricTranslation( + translations, + 'weightedStatusScoreCenterTooltipTotalLabel', + ), { exact: true }, ), ).toBeVisible(); await expect( page.getByText( - getMetricTranslation(translations, 'averageCenterTooltipMaxLabel'), + getMetricTranslation( + translations, + 'weightedStatusScoreCenterTooltipMaxLabel', + ), { exact: true }, ), ).toBeVisible(); @@ -112,15 +123,17 @@ const OPEN_PRS_WEIGHTED_MOCK_BREAKDOWN: Array<{ /** * Per-status lines under total/max in the center donut tooltip (replaces old side-legend tooltips). */ -export async function verifyAverageCenterTooltipBreakdownRows( +export async function verifyWeightedStatusScoreCenterTooltipBreakdownRows( page: Page, card: Locator, translations: ScorecardMessages, locale: string, ): Promise { - await card.getByTestId('average-card-center-percent-hit-area').hover(); + await card + .getByTestId('weighted-status-score-card-center-percent-hit-area') + .hover(); for (const row of OPEN_PRS_WEIGHTED_MOCK_BREAKDOWN) { - const line = expectedAverageCenterTooltipBreakdownLine( + const line = expectedWeightedStatusScoreCenterTooltipBreakdownLine( translations, locale, row.statusKey, diff --git a/workspaces/scorecard/plugins/scorecard-backend/README.md b/workspaces/scorecard/plugins/scorecard-backend/README.md index 37ae4b2123..5f36b62891 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/README.md +++ b/workspaces/scorecard/plugins/scorecard-backend/README.md @@ -116,9 +116,9 @@ Thresholds define conditions to assign metric values to specific visual categori - **App Configuration**: Override defaults through `app-config.yaml` - **Entity Annotations**: Override specific thresholds per entity using catalog annotations -Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`). For **number** metrics, configurations loaded through validated paths must cover the **entire real line** when two or more rules are defined (no gaps between intervals); **`average`** KPI **`options.thresholds`** follow the same rule. +Thresholds are evaluated in order, and the first matching rule determines the category. The plugin supports various operators for number metrics (`>`, `>=`, `<`, `<=`, `==`, `!=`, `-` (range)) and boolean metrics (`==`, `!=`). For **number** metrics, configurations loaded through validated paths must cover the **entire real line** when two or more rules are defined (no gaps between intervals); **`weightedStatusScore`** KPI **`options.thresholds`** follow the same rule. -For comprehensive threshold configuration guide, examples, best practices, interval validation, and **aggregation KPI result thresholds** for **`type: average`**, see [thresholds.md](./docs/thresholds.md). +For comprehensive threshold configuration guide, examples, best practices, interval validation, and **aggregation KPI result thresholds** for **`type: weightedStatusScore`**, see [thresholds.md](./docs/thresholds.md). ## Aggregation KPIs (homepage and `GET /aggregations`) @@ -140,14 +140,14 @@ scorecard: openPrsWeightedKpi: title: 'GitHub open PRs (weighted health)' description: 'Weighted health from status counts using configurable scores.' - type: average + type: weightedStatusScore metricId: github.open_prs options: statusScores: success: 100 warning: 50 error: 0 - # Optional: colors for the average-score donut (expressions apply to percentage 0–100) + # Optional: colors for the weightedStatusScore donut (expressions apply to percentage 0–100) thresholds: rules: - key: success @@ -161,17 +161,17 @@ scorecard: color: error.main ``` -| Field | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `title` | Display title for this aggregation (returned in API metadata). | -| `description` | Display description for this aggregation. | -| `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `average` (normalized weighted score). | -| `metricId` | Metric provider id used to load thresholds and compute counts. | -| `options` | Optional for `statusGrouped`. **Required** for `average`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-average-type)); evaluated on **`averageScore`** (**0–100** portfolio percentage, **one decimal**); first match sets **`aggregationChartDisplayColor`**. The API includes **`averageScore`**, **`averageWeightedSum`**, **`averageMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). | +| Field | Description | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `title` | Display title for this aggregation (returned in API metadata). | +| `description` | Display description for this aggregation. | +| `type` | Aggregation algorithm: `statusGrouped` (counts per threshold status) or `weightedStatusScore` (normalized weighted score). | +| `metricId` | Metric provider id used to load thresholds and compute counts. | +| `options` | Optional for `statusGrouped`. **Required** for `weightedStatusScore`: must include **`options.statusScores`** — map status keys to numeric weights (typically one entry per **metric threshold rule key**). Optionally **`options.thresholds`** (same shape as metric thresholds; see [thresholds.md — Aggregation KPI result thresholds](./docs/thresholds.md#4-aggregation-kpi-result-thresholds-weighted-status-score-type)); evaluated on **`weightedStatusScore`** (**0–100** portfolio percentage, **one decimal**); first match sets **`aggregationChartDisplayColor`**. The API includes **`weightedStatusScore`**, **`weightedStatusSum`**, **`weightedStatusMaxPossible`**, and **`aggregationChartDisplayColor`** (from configured or default result thresholds). | - **Path**: `scorecard.aggregationKPIs.`. - If **`aggregationKPIs` is omitted** or a given id is not listed, **`GET /aggregations/:aggregationId`** still works when **`aggregationId` equals the metric id** (e.g. `github.open_prs`): the backend uses that metric with the default `statusGrouped` aggregation and metric-defined title/description. -- **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`average`** KPIs without **`options.statusScores`**, bad expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation). +- **Startup validation**: the backend validates every **`scorecard.aggregationKPIs`** entry when the plugin loads. Invalid configuration (including **`weightedStatusScore`** KPIs without **`options.statusScores`**, bad expressions, or unregistered **`metricId`**) causes the backend to **fail to start** with a clear error. At runtime, some edge cases may still be logged (for example skipping a KPI with unusable weights); prefer correcting app-config. See [aggregation.md](./docs/aggregation.md#configuration-validation). **Homepage cards** are configured in the app (for example Dynamic Home Page mount points). They should pass **`aggregationId`** matching a key in `aggregationKPIs` or the metric id for the default case. See the [Scorecard frontend plugin README](../scorecard/README.md#homepage-scorecard-cards). diff --git a/workspaces/scorecard/plugins/scorecard-backend/config.d.ts b/workspaces/scorecard/plugins/scorecard-backend/config.d.ts index a88ff66541..723486a303 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/config.d.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/config.d.ts @@ -38,13 +38,13 @@ export interface Config { metricId: string; /** Type-specific settings */ options?: { - /** Required under `options` when `type` is `average` */ + /** Required under `options` when `type` is `weightedStatusScore` */ statusScores?: { [thresholdRuleKey: string]: number; }; /** * Optional: threshold rules for coloring the KPI headline value from the aggregation result - * (e.g. average percentage 0–100 for `average` KPIs). + * (e.g. weighted status score percentage 0–100 for `weightedStatusScore` KPIs). */ thresholds?: { rules: AggregationThresholdRule[]; diff --git a/workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md b/workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md index 945d8050d1..19ea6e074f 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md +++ b/workspaces/scorecard/plugins/scorecard-backend/docs/aggregation.md @@ -34,26 +34,26 @@ KPIs under **`scorecard.aggregationKPIs`** declare a **`type`** that selects an **`statusGrouped`** loads each owned entity’s metric status, buckets entities by status key (success, warning, error, etc.), and returns **counts per status** summed across the portfolio. Use it when you want a breakdown of how many entities are in each state (for example a status pie chart). -**`average`** rolls up each owned entity’s metric into status keys, applies **`options.statusScores`** (weights per status key), and returns **one normalized score** as a **percentage** in \[0, 100\] (one decimal), scaled against the metric’s threshold rules. Use it when you want a single “portfolio health” number (for example a donut gauge on the homepage). +**`weightedStatusScore`** rolls up each owned entity’s metric into status keys, applies **`options.statusScores`** (weights per status key), and returns **one normalized score** as a **percentage** in \[0, 100\] (one decimal), scaled against the metric’s threshold rules. Use it when you want a single “portfolio health” number (for example a donut gauge on the homepage). -| Type | Output | Typical use | -| ------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------- | -| **`statusGrouped`** | Counts per status key across owned entities | “How many entities are green vs red” style pie. | -| **`average`** | **`averageScore`** in \[0, 100\] (percent, one decimal) from weighted counts via **`statusScores`** | Portfolio health gauge from one headline score. | +| Type | Output | Typical use | +| ------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| **`statusGrouped`** | Counts per status key across owned entities | “How many entities are green vs red” style pie. | +| **`weightedStatusScore`** | **`weightedStatusScore`** in \[0, 100\] (percent, one decimal) from weighted counts via **`statusScores`** | Portfolio health gauge from one headline score. | -For **`average`**: +For **`weightedStatusScore`**: 1. The backend loads **status-grouped counts** for the configured **`metricId`** across the same **owned entities** scope as other aggregation KPIs. 2. **Weighted sum:** For each **status key** present in the aggregated counts, the contribution is **`count × weight`**. If a key appears in the data but has **no** entry in **`statusScores`**, the backend **warns** and uses weight **0** for that key. Any key present in **`statusScores`** can contribute to the sum if it appears in the stored counts (you should align keys with your metric’s threshold rules and **`statusScores`** to avoid surprising totals). -3. **Denominator and percentage:** Let **`maxWeight`** be the maximum of **`options.statusScores[rule.key]`** over each **`rule.key`** in the metric’s **merged threshold rules** (missing map entries are treated as **0** here). **`averageMaxPossible`** = **`maxWeight × total entities`**. If **`total`** is 0 or **`averageMaxPossible`** is 0, **`averageScore`** is **0**; otherwise **`averageScore`** = **`100 × (weighted sum / averageMaxPossible)`** rounded to **one decimal place** (the API and UI use this **0–100** headline directly). The value can exceed **100** if **`statusScores`** assigns a weight above **`maxWeight`** to a status that still appears in the aggregated counts; keep **`statusScores`** aligned with your metric rules to avoid that. +3. **Denominator and percentage:** Let **`maxWeight`** be the maximum of **`options.statusScores[rule.key]`** over each **`rule.key`** in the metric’s **merged threshold rules** (missing map entries are treated as **0** here). **`weightedStatusMaxPossible`** = **`maxWeight × total entities`**. If **`total`** is 0 or **`weightedStatusMaxPossible`** is 0, **`weightedStatusScore`** is **0**; otherwise **`weightedStatusScore`** = **`100 × (weighted sum / weightedStatusMaxPossible)`** rounded to **one decimal place** (the API and UI use this **0–100** headline directly). The value can exceed **100** if **`statusScores`** assigns a weight above **`maxWeight`** to a status that still appears in the aggregated counts; keep **`statusScores`** aligned with your metric rules to avoid that. -**`options.thresholds`:** **number**-style rules (same shape as metric thresholds) evaluated against **`averageScore`** on the **0–100** scale (higher = better for typical setups). The first matching rule supplies **`result.aggregationChartDisplayColor`**. If omitted from **`scorecard.aggregationKPIs`**, it stays unset in the built KPI config; **`AverageAggregationStrategy`** then applies **`DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS`** from [`src/constants/aggregationKPIs.ts`](../src/constants/aggregationKPIs.ts) when each aggregation runs and logs **info** that the default 0–100% health scale is used: **<30%** = error, **30–79%** = warning, **≥80%** = success. Full detail: [thresholds.md — Aggregation KPI result thresholds](./thresholds.md#4-aggregation-kpi-result-thresholds-average-type) and [backend README — Aggregation KPIs](../README.md#aggregation-kpis-homepage-and-get-aggregations). +**`options.thresholds`:** **number**-style rules (same shape as metric thresholds) evaluated against **`weightedStatusScore`** on the **0–100** scale (higher = better for typical setups). The first matching rule supplies **`result.aggregationChartDisplayColor`**. If omitted from **`scorecard.aggregationKPIs`**, it stays unset in the built KPI config; **`WeightedStatusScoreAggregationStrategy`** then applies **`DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS`** from [`src/constants/aggregationKPIs.ts`](../src/constants/aggregationKPIs.ts) when each aggregation runs and logs **info** that the default 0–100% health scale is used: **<30%** = error, **30–79%** = warning, **≥80%** = success. Full detail: [thresholds.md — Aggregation KPI result thresholds](./thresholds.md#4-aggregation-kpi-result-thresholds-weighted-status-score-type) and [backend README — Aggregation KPIs](../README.md#aggregation-kpis-homepage-and-get-aggregations). ## Configuration validation -**`scorecard.aggregationKPIs`** is validated when the backend plugin starts. Invalid entries (unknown **`type`**, missing **`options`** for **`average`**, empty **`statusScores`**, unknown **`metricId`**, invalid threshold expressions, etc.) cause startup to **fail with an error** so misconfiguration is caught early. Fix app-config and redeploy. +**`scorecard.aggregationKPIs`** is validated when the backend plugin starts. Invalid entries (unknown **`type`**, missing **`options`** for **`weightedStatusScore`**, empty **`statusScores`**, unknown **`metricId`**, invalid threshold expressions, etc.) cause startup to **fail with an error** so misconfiguration is caught early. Fix app-config and redeploy. -For **`type: average`**, optional **`options.thresholds`** must satisfy the same **number interval / gap** rules as metric thresholds when multiple rules apply (union must cover the full real line with no gaps). Errors mention an approximate **first uncovered region**. See [Joint coverage (number metrics)](./thresholds.md#joint-coverage-number-metrics). +For **`type: weightedStatusScore`**, optional **`options.thresholds`** must satisfy the same **number interval / gap** rules as metric thresholds when multiple rules apply (union must cover the full real line with no gaps). Errors mention an approximate **first uncovered region**. See [Joint coverage (number metrics)](./thresholds.md#joint-coverage-number-metrics). Schema reference for config discovery (IDE / `backstage-cli config:schema`): see **`config.d.ts`** on the backend package (`aggregationKPIs` and nested **`options`**). @@ -63,10 +63,10 @@ Schema reference for config discovery (IDE / `backstage-cli config:schema`): see Use this endpoint for all new integrations. -- **`aggregationId`** may be a key under **`scorecard.aggregationKPIs`** in app-config (see the [backend README](../README.md#aggregation-kpis-homepage-and-get-aggregations)), which supplies **title**, **description**, **type**, **metricId**, and for **`type: average`** the **`options.statusScores`** map (threshold rule key → weight), with room for more **`options`** fields per type later. -- If there is **no** `scorecard.aggregationKPIs.` block, the backend still responds successfully: it treats **`aggregationId` as the `metricId`** and uses the default **statusGrouped** strategy (same as calling **`/aggregations/`** with a metric id). A **warning** is logged on the server so missing KPI config is visible in operator logs. To get a custom **title**, **`average`** type, or other KPI options, you must add that block; a typo in the id falls through to this default and can look like “wrong” aggregation behavior in the UI, so check logs and app-config. +- **`aggregationId`** may be a key under **`scorecard.aggregationKPIs`** in app-config (see the [backend README](../README.md#aggregation-kpis-homepage-and-get-aggregations)), which supplies **title**, **description**, **type**, **metricId**, and for **`type: weightedStatusScore`** the **`options.statusScores`** map (threshold rule key → weight), with room for more **`options`** fields per type later. +- If there is **no** `scorecard.aggregationKPIs.` block, the backend still responds successfully: it treats **`aggregationId` as the `metricId`** and uses the default **statusGrouped** strategy (same as calling **`/aggregations/`** with a metric id). A **warning** is logged on the server so missing KPI config is visible in operator logs. To get a custom **title**, **`weightedStatusScore`** type, or other KPI options, you must add that block; a typo in the id falls through to this default and can look like “wrong” aggregation behavior in the UI, so check logs and app-config. -The response shape includes **`id`**, **`status`**, **`metadata`** (title, description, type, aggregation type), and **`result`** (counts per threshold rule, total, thresholds). The **`result`** object also includes **`entitiesConsidered`** (count of in-scope owned entities that have **at least one** latest `metric_values` row for this metric) and **`calculationErrorCount`** (how many of those latest rows are metric calculation failures: `error_message` set and `value` null), so the homepage ratio matches the population behind the drill-down table rather than the raw number of owned catalog refs. For **`average`**, **`result`** also includes **`averageScore`** (portfolio percentage in \[0, 100\], one decimal), **`averageWeightedSum`**, and **`averageMaxPossible`** (see backend README). The homepage card shows a donut gauge for this type instead of a multi-slice status pie. +The response shape includes **`id`**, **`status`**, **`metadata`** (title, description, type, aggregation type), and **`result`** (counts per threshold rule, total, thresholds). The **`result`** object also includes **`entitiesConsidered`** (count of in-scope owned entities that have **at least one** latest `metric_values` row for this metric) and **`calculationErrorCount`** (how many of those latest rows are metric calculation failures: `error_message` set and `value` null), so the homepage ratio matches the population behind the drill-down table rather than the raw number of owned catalog refs. For **`weightedStatusScore`**, **`result`** also includes **`weightedStatusScore`** (portfolio percentage in \[0, 100\], one decimal), **`weightedStatusSum`**, and **`weightedStatusMaxPossible`** (see backend README). The homepage card shows a donut gauge for this type instead of a multi-slice status pie. **“Without calculation errors” on the homepage:** `healthy = entitiesConsidered - calculationErrorCount` counts only among entities that already have a latest stored row for this metric. Owned entities with **no** row yet are omitted from **`entitiesConsidered`** (same as omitting them from the drill-down list until data exists). @@ -90,7 +90,7 @@ When the user owns no relevant entities, the API returns an aggregation with **z ### Drill-down vs aggregation id -The aggregation API uses **`aggregationId`** (KPI key or metric id). **Entity drill-down** remains **metric-scoped**: use **`GET /metrics/:metricId/catalog/aggregations/entities`** with the KPI’s **`metricId`**, not the KPI key. That applies to both **`statusGrouped`** and **`average`** KPIs. See [drill-down.md](./drill-down.md). +The aggregation API uses **`aggregationId`** (KPI key or metric id). **Entity drill-down** remains **metric-scoped**: use **`GET /metrics/:metricId/catalog/aggregations/entities`** with the KPI’s **`metricId`**, not the KPI key. That applies to both **`statusGrouped`** and **`weightedStatusScore`** KPIs. See [drill-down.md](./drill-down.md). ### **Deprecated API:** `GET /metrics/:metricId/catalog/aggregations` @@ -175,6 +175,6 @@ If the user doesn't have access to the specified metric: 5. **Metric access**: Aggregation routes enforce **`scorecard.metric.read`** for the underlying metric and **`catalog.entity.read`** for each included entity; expect **`403 Forbidden`** when either check fails. -For RBAC, scheduling, full endpoint reference, and **app-config examples** for **`average`** KPIs (including **`thresholds`**), see the [Scorecard backend README](../README.md). +For RBAC, scheduling, full endpoint reference, and **app-config examples** for **`weightedStatusScore`** KPIs (including **`thresholds`**), see the [Scorecard backend README](../README.md). -For **per-entity threshold overrides** (annotations), **average KPI result thresholds**, and expression reference, see [thresholds.md](./thresholds.md). +For **per-entity threshold overrides** (annotations), **weightedStatusScore KPI result thresholds**, and expression reference, see [thresholds.md](./thresholds.md). diff --git a/workspaces/scorecard/plugins/scorecard-backend/docs/drill-down.md b/workspaces/scorecard/plugins/scorecard-backend/docs/drill-down.md index d7d9fc1433..8fdb76d93c 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/docs/drill-down.md +++ b/workspaces/scorecard/plugins/scorecard-backend/docs/drill-down.md @@ -6,7 +6,7 @@ The Scorecard plugin provides a drill-down endpoint that returns detailed entity High-level aggregation for homepage KPIs uses **`GET /aggregations/:aggregationId`** (see [aggregation.md](./aggregation.md)). Drill-down is **metric-scoped**: the endpoint **`/metrics/:metricId/catalog/aggregations/entities`** lists entities and values for a single **metric id** (not a KPI id). -**Note:** If the homepage card uses a KPI key (for example **`openPrsWeightedKpi`**) with **`type: average`**, drill-down still uses the KPI’s configured **`metricId`** (e.g. **`github.open_prs`**) in this path—not the KPI id. +**Note:** If the homepage card uses a KPI key (for example **`openPrsWeightedKpi`**) with **`type: weightedStatusScore`**, drill-down still uses the KPI’s configured **`metricId`** (e.g. **`github.open_prs`**) in this path—not the KPI id. The drill-down endpoint provides a detailed view of entities and their metric values. It allows managers and platform engineers to: diff --git a/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md b/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md index fb93aac7a6..fa970a6a02 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md +++ b/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md @@ -152,19 +152,19 @@ For **number** metrics, each overridden expression is validated in isolation fir **Counterexample:** Provider rules partition the line (`'<10'`, `'10-20'`, `'>20'`). Overriding only warning to `'11-20'` leaves **`10`** and **`(10, 11)`** uncovered on the merged set—fix the override or adjacent rules so the union again covers **(-∞, +∞)**. -### 4. Aggregation KPI result thresholds (`average` type) +### 4. Aggregation KPI result thresholds (`weightedStatusScore` type) -These thresholds are **not** per-entity metric rules. They apply only to homepage aggregation KPIs where **`scorecard.aggregationKPIs..type`** is **`average`**. +These thresholds are **not** per-entity metric rules. They apply only to homepage aggregation KPIs where **`scorecard.aggregationKPIs..type`** is **`weightedStatusScore`**. **Configuration path:** `scorecard.aggregationKPIs..options.thresholds` -**YAML shape:** Same as metric thresholds — a **`rules`** array of **`key`**, **`expression`**, and optional **`color`** (and optional **`icon`**, though icons are not used for the average KPI donut). Expressions are **number**-style and are evaluated against **`averageScore`**, the backend’s portfolio **percentage** in **`[0, 100]`** (one decimal; see [Entity Aggregation](./aggregation.md)). The **first** matching rule wins; its **`color`** is returned on the API as **`result.aggregationChartDisplayColor`**. +**YAML shape:** Same as metric thresholds — a **`rules`** array of **`key`**, **`expression`**, and optional **`color`** (and optional **`icon`**, though icons are not used for the weightedStatusScore KPI donut). Expressions are **number**-style and are evaluated against **`weightedStatusScore`**, the backend’s portfolio **percentage** in **`[0, 100]`** (one decimal; see [Entity Aggregation](./aggregation.md)). The **first** matching rule wins; its **`color`** is returned on the API as **`result.aggregationChartDisplayColor`**. -**Defaults:** If **`thresholds`** is omitted from app-config under **`options`**, it is not injected at config-parse time. **`AverageAggregationStrategy`** applies **`DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS`** from [`src/constants/aggregationKPIs.ts`](../src/constants/aggregationKPIs.ts) when serving an aggregation: **`<30`** → error, **`30-79`** → warning, **`>=80`** → success (higher percentage = better). When that default path is used, the strategy logs at **info** that the built-in 0–100% scale is in effect. +**Defaults:** If **`thresholds`** is omitted from app-config under **`options`**, it is not injected at config-parse time. **`WeightedStatusScoreAggregationStrategy`** applies **`DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS`** from [`src/constants/aggregationKPIs.ts`](../src/constants/aggregationKPIs.ts) when serving an aggregation: **`<30`** → error, **`30-79`** → warning, **`>=80`** → success (higher percentage = better). When that default path is used, the strategy logs at **info** that the built-in 0–100% scale is in effect. -**Startup validation:** Invalid rules or expressions are caught when the backend plugin loads, together with the rest of **`scorecard.aggregationKPIs`**. Average KPI **`options.thresholds`** must also satisfy **joint full-line coverage** for number expressions (see [Joint coverage (number metrics)](#joint-coverage-number-metrics)), for example ensure ranges and comparison rules meet at boundaries (**`10-75`** with **`>=75`** and **`<10`**, not **`10-74`** with **`>=75`**, which would leave **`(74, 75)`** uncovered). See [aggregation.md — Configuration validation](./aggregation.md#configuration-validation). +**Startup validation:** Invalid rules or expressions are caught when the backend plugin loads, together with the rest of **`scorecard.aggregationKPIs`**. WeightedStatusScore KPI **`options.thresholds`** must also satisfy **joint full-line coverage** for number expressions (see [Joint coverage (number metrics)](#joint-coverage-number-metrics)), for example ensure ranges and comparison rules meet at boundaries (**`10-75`** with **`>=75`** and **`<10`**, not **`10-74`** with **`>=75`**, which would leave **`(74, 75)`** uncovered). See [aggregation.md — Configuration validation](./aggregation.md#configuration-validation). -**Further reading:** [Entity Aggregation](./aggregation.md) (`average` algorithm, API, drill-down); [Scorecard backend README — Aggregation KPIs](../README.md#aggregation-kpis-homepage-and-get-aggregations) (full **`aggregationKPIs`** example including **`statusScores`**). +**Further reading:** [Entity Aggregation](./aggregation.md) (`weightedStatusScore` algorithm, API, drill-down); [Scorecard backend README — Aggregation KPIs](../README.md#aggregation-kpis-homepage-and-get-aggregations) (full **`aggregationKPIs`** example including **`statusScores`**). ## Threshold Priority Order @@ -429,6 +429,6 @@ rules: ## Related documentation -- [Entity Aggregation](./aggregation.md) — ownership, **`GET /aggregations/:aggregationId`**, **`statusGrouped`** vs **`average`** +- [Entity Aggregation](./aggregation.md) — ownership, **`GET /aggregations/:aggregationId`**, **`statusGrouped`** vs **`weightedStatusScore`** - [Drill-down](./drill-down.md) — entity list for a metric (`metricId`, not KPI id) - [Scorecard backend README](../README.md) — install, RBAC, **`aggregationKPIs`** examples diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/constants/aggregationKPIs.ts b/workspaces/scorecard/plugins/scorecard-backend/src/constants/aggregationKPIs.ts index 65c9fb9eb7..1c1961a348 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/constants/aggregationKPIs.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/constants/aggregationKPIs.ts @@ -23,25 +23,26 @@ export const AGGREGATION_KPIS_CONFIG_PATH = 'scorecard.aggregationKPIs' as const; /** - * Default applied by `AverageAggregationStrategy` when `options.thresholds` is omitted + * Default applied by `WeightedStatusScoreAggregationStrategy` when `options.thresholds` is omitted * from app-config. Higher headline percentage (0–100) = better. Evaluated in order; first match wins. */ -export const DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS: ThresholdConfig = { - rules: [ - { - key: 'success', - expression: '>=80', - color: ScorecardThresholdRuleColors.SUCCESS, - }, - { - key: 'warning', - expression: '30-80', - color: ScorecardThresholdRuleColors.WARNING, - }, - { - key: 'error', - expression: '<30', - color: ScorecardThresholdRuleColors.ERROR, - }, - ], -}; +export const DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS: ThresholdConfig = + { + rules: [ + { + key: 'success', + expression: '>=80', + color: ScorecardThresholdRuleColors.SUCCESS, + }, + { + key: 'warning', + expression: '30-80', + color: ScorecardThresholdRuleColors.WARNING, + }, + { + key: 'error', + expression: '<30', + color: ScorecardThresholdRuleColors.ERROR, + }, + ], + }; diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.test.ts index 38fd55c34f..ca47ec11eb 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/AggregationsService.test.ts @@ -18,12 +18,12 @@ import { ConfigReader } from '@backstage/config'; import { mockServices } from '@backstage/backend-test-utils'; import { aggregationTypes, - type AggregatedMetricAverageResult, + type WeightedStatusScoreAggregationResult, Metric, ThresholdConfig, type AggregationConfig, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../constants/aggregationKPIs'; +import { DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS } from '../../constants/aggregationKPIs'; import { AggregationsService } from './AggregationService'; import type { DatabaseMetricValues } from '../../database/DatabaseMetricValues'; import type { DbAggregatedMetric } from '../../database/types'; @@ -91,7 +91,7 @@ describe('AggregationsService', () => { ); }); - it('getAggregatedMetricByEntityRefs uses average strategy when configured', async () => { + it('getAggregatedMetricByEntityRefs uses weightedStatusScore strategy when configured', async () => { const dbRow: DbAggregatedMetric = { metric_id: metric.id, total: 3, @@ -113,14 +113,14 @@ describe('AggregationsService', () => { entityRefs: ['component:default/a'], thresholds, aggregationConfig: { - id: 'avgKpi', - title: 'Average KPI', - description: 'Average KPI description', + id: 'weightedKpi', + title: 'Weighted health KPI', + description: 'Weighted health score across statuses', metricId: metric.id, - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, options: { statusScores: { error: 0, warning: 50, success: 100 }, - thresholds: DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS, + thresholds: DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS, }, } as AggregationConfig, } as AggregationOptions); @@ -130,12 +130,15 @@ describe('AggregationsService', () => { metric.id, ); - const aggregationResult = result.result as AggregatedMetricAverageResult; + const aggregationResult = + result.result as WeightedStatusScoreAggregationResult; - expect(result.metadata?.aggregationType).toBe(aggregationTypes.average); - expect(aggregationResult.averageScore).toBe(50); - expect(aggregationResult.averageWeightedSum).toBe(150); - expect(aggregationResult.averageMaxPossible).toBe(300); + expect(result.metadata?.aggregationType).toBe( + aggregationTypes.weightedStatusScore, + ); + expect(aggregationResult.weightedStatusScore).toBe(50); + expect(aggregationResult.weightedStatusSum).toBe(150); + expect(aggregationResult.weightedStatusMaxPossible).toBe(300); }); it('getAggregatedMetricByEntityRefs throws when aggregation type is not registered', async () => { @@ -185,7 +188,7 @@ describe('AggregationsService', () => { myKpi: { title: 'KPI title', description: 'KPI desc', - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, metricId: 'github.open_prs', options: { statusScores: { error: 0, warning: 50, success: 100 }, @@ -204,7 +207,7 @@ describe('AggregationsService', () => { const cfg = service.getAggregationConfig('myKpi'); expect(cfg.metricId).toBe('github.open_prs'); - expect(cfg.type).toBe(aggregationTypes.average); + expect(cfg.type).toBe(aggregationTypes.weightedStatusScore); expect(cfg.title).toBe('KPI title'); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/AverageAggregationStrategy.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.ts similarity index 76% rename from workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/AverageAggregationStrategy.ts rename to workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.ts index 13524cb62e..3684a549a0 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/AverageAggregationStrategy.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/WeightedStatusScoreAggregationStrategy.ts @@ -16,13 +16,13 @@ import { type AggregatedMetric, - type AggregatedMetricAverageResult, + type WeightedStatusScoreAggregationResult, type AggregatedMetricResult, type ThresholdConfig, ThresholdRule, type AggregationConfigOptions, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs'; +import { DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs'; import { AggregatedMetricMapper } from '../../mappers'; import type { AggregatedMetricLoader } from '../AggregatedMetricLoader'; import type { AggregationOptions } from '../types'; @@ -30,7 +30,9 @@ import type { AggregationStrategy } from './types'; import { LoggerService } from '@backstage/backend-plugin-api'; import { ThresholdEvaluator } from '../../../threshold/ThresholdEvaluator'; -export class AverageAggregationStrategy implements AggregationStrategy { +export class WeightedStatusScoreAggregationStrategy + implements AggregationStrategy +{ constructor( private readonly loader: AggregatedMetricLoader, private readonly logger: LoggerService, @@ -46,19 +48,19 @@ export class AverageAggregationStrategy implements AggregationStrategy { if (!options?.statusScores) { throw new Error( - `The "scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores" is required for average aggregation`, + `The "scorecard.aggregationKPIs.${aggregationConfig.id}.options.statusScores" is required for weightedStatusScore aggregation`, ); } if (!options.thresholds) { this.logger.info( - `The "scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds" is not configured for average aggregation; ` + + `The "scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds" is not configured for weightedStatusScore aggregation; ` + 'using the default 0–100% health scale (higher is better).', ); } const headlineThresholds = - options.thresholds ?? DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS; + options.thresholds ?? DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS; const aggregatedMetric = await this.loader.loadStatusGroupedMetricByEntityRefs( @@ -72,21 +74,22 @@ export class AverageAggregationStrategy implements AggregationStrategy { metric.id, ); - const { averageScore, maxPossibleScore } = this.prepareScoreValues( - aggregatedMetric.total, - options.statusScores, - thresholds.rules, - weightedSum, - ); + const { weightedStatusScore, maxPossibleScore } = + this.prepareWeightedStatusScoreValues( + aggregatedMetric.total, + options.statusScores, + thresholds.rules, + weightedSum, + ); const aggregationChartDisplayColor = this.getAggregationChartDisplayColor( - averageScore, + weightedStatusScore, headlineThresholds, ); if (!aggregationChartDisplayColor) { throw new Error( - `The color for percentage '${averageScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`, + `The color for percentage '${weightedStatusScore}' metric '${metric.id}' is not configured. Check the 'scorecard.aggregationKPIs.${aggregationConfig.id}.options.thresholds' configuration.`, ); } @@ -101,11 +104,11 @@ export class AverageAggregationStrategy implements AggregationStrategy { score: options.statusScores[rule.key] ?? 0, })), thresholds, - averageScore, - averageWeightedSum: weightedSum, - averageMaxPossible: maxPossibleScore, + weightedStatusScore, + weightedStatusSum: weightedSum, + weightedStatusMaxPossible: maxPossibleScore, aggregationChartDisplayColor, - } as AggregatedMetricAverageResult; + } as WeightedStatusScoreAggregationResult; return AggregatedMetricMapper.toAggregatedMetricResult( metric, @@ -125,7 +128,7 @@ export class AverageAggregationStrategy implements AggregationStrategy { if (score === undefined) { this.logger.warn( - `The status "${status}" is not in the statusScores for average aggregation of metric "${metricId}"`, + `The status "${status}" is not in the statusScores for weightedStatusScore aggregation of metric "${metricId}"`, ); } weightedSum += count * (score ?? 0); @@ -148,23 +151,23 @@ export class AverageAggregationStrategy implements AggregationStrategy { return thresholds.rules.find(r => r.key === matchedThresholdKey)?.color; } - private prepareScoreValues( + private prepareWeightedStatusScoreValues( numberOfEntities: Pick['total'], statusScores: AggregationConfigOptions['statusScores'], rules: ThresholdRule[], weightedSum: number, - ): { averageScore: number; maxPossibleScore: number } { + ): { weightedStatusScore: number; maxPossibleScore: number } { const statusScoresValues = rules.map(r => statusScores[r.key] ?? 0); const maxScore = Math.max(0, ...statusScoresValues); const maxPossibleScore = maxScore * numberOfEntities; - const averageScore = + const weightedStatusScore = numberOfEntities > 0 && maxPossibleScore > 0 ? Math.round((weightedSum / maxPossibleScore) * 1000) / 10 : 0; - return { averageScore, maxPossibleScore }; + return { weightedStatusScore, maxPossibleScore }; } } diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.test.ts index 0d6202bd9f..3766c848d6 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.test.ts @@ -18,11 +18,11 @@ import { mockServices } from '@backstage/backend-test-utils'; import { aggregationTypes } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; import { AggregatedMetricLoader } from '../AggregatedMetricLoader'; import { createAggregationStrategyRegistry } from './registerStrategies'; -import { AverageAggregationStrategy } from './AverageAggregationStrategy'; +import { WeightedStatusScoreAggregationStrategy } from './WeightedStatusScoreAggregationStrategy'; import { StatusGroupedAggregationStrategy } from './StatusGroupedAggregationStrategy'; describe('createAggregationStrategyRegistry', () => { - it('registers statusGrouped and average strategies', () => { + it('registers statusGrouped and weightedStatusScore strategies', () => { const loader = {} as AggregatedMetricLoader; const logger = mockServices.logger.mock(); @@ -31,8 +31,8 @@ describe('createAggregationStrategyRegistry', () => { expect(registry.get(aggregationTypes.statusGrouped)).toBeInstanceOf( StatusGroupedAggregationStrategy, ); - expect(registry.get(aggregationTypes.average)).toBeInstanceOf( - AverageAggregationStrategy, + expect(registry.get(aggregationTypes.weightedStatusScore)).toBeInstanceOf( + WeightedStatusScoreAggregationStrategy, ); expect(registry.size).toBe(2); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.ts index 8cb7c6db37..6172e05606 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/registerStrategies.ts @@ -21,7 +21,7 @@ import { import type { AggregatedMetricLoader } from '../AggregatedMetricLoader'; import type { AggregationStrategy } from './types'; import { StatusGroupedAggregationStrategy } from './StatusGroupedAggregationStrategy'; -import { AverageAggregationStrategy } from './AverageAggregationStrategy'; +import { WeightedStatusScoreAggregationStrategy } from './WeightedStatusScoreAggregationStrategy'; import { LoggerService } from '@backstage/backend-plugin-api'; export function createAggregationStrategyRegistry( @@ -33,6 +33,9 @@ export function createAggregationStrategyRegistry( aggregationTypes.statusGrouped, new StatusGroupedAggregationStrategy(loader), ], - [aggregationTypes.average, new AverageAggregationStrategy(loader, logger)], + [ + aggregationTypes.weightedStatusScore, + new WeightedStatusScoreAggregationStrategy(loader, logger), + ], ]); } diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/averageAggregationStrategy.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/weightedStatusScoreAggregationStrategy.test.ts similarity index 76% rename from workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/averageAggregationStrategy.test.ts rename to workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/weightedStatusScoreAggregationStrategy.test.ts index 72629f1a47..18009ba06e 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/averageAggregationStrategy.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/aggregations/strategies/weightedStatusScoreAggregationStrategy.test.ts @@ -20,11 +20,11 @@ import { Metric, ThresholdConfig, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; -import { DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs'; +import { DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS } from '../../../constants/aggregationKPIs'; import { AggregatedMetricLoader } from '../AggregatedMetricLoader'; -import { AverageAggregationStrategy } from './AverageAggregationStrategy'; +import { WeightedStatusScoreAggregationStrategy } from './WeightedStatusScoreAggregationStrategy'; -describe('AverageAggregationStrategy', () => { +describe('WeightedStatusScoreAggregationStrategy', () => { const metric = { id: 'github.open_prs', title: 'Open PRs', @@ -40,7 +40,7 @@ describe('AverageAggregationStrategy', () => { ], }; - it('computes weighted average fields from loader output', async () => { + it('computes weighted status score fields from loader output', async () => { const loadStatusGroupedMetricByEntityRefs = jest.fn().mockResolvedValue({ values: { error: 1, warning: 1, success: 1 }, total: 3, @@ -54,14 +54,14 @@ describe('AverageAggregationStrategy', () => { } as unknown as AggregatedMetricLoader; const logger = mockServices.logger.mock(); - const strategy = new AverageAggregationStrategy(loader, logger); + const strategy = new WeightedStatusScoreAggregationStrategy(loader, logger); const aggregationConfig = { - id: 'avgKpi', + id: 'weightedKpi', metricId: metric.id, - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, options: { statusScores: { error: 0, warning: 50, success: 100 }, - thresholds: DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS, + thresholds: DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS, }, } as const; @@ -77,9 +77,9 @@ describe('AverageAggregationStrategy', () => { total: 3, entitiesConsidered: 5, calculationErrorCount: 2, - averageWeightedSum: 150, - averageMaxPossible: 300, - averageScore: 50, + weightedStatusSum: 150, + weightedStatusMaxPossible: 300, + weightedStatusScore: 50, aggregationChartDisplayColor: 'warning.main', }), ); @@ -101,16 +101,16 @@ describe('AverageAggregationStrategy', () => { } as unknown as AggregatedMetricLoader; const logger = mockServices.logger.mock(); - const strategy = new AverageAggregationStrategy(loader, logger); + const strategy = new WeightedStatusScoreAggregationStrategy(loader, logger); const out = await strategy.aggregate({ metric, entityRefs: ['component:default/a'], thresholds, aggregationConfig: { - id: 'avgKpi', + id: 'weightedKpi', metricId: metric.id, - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, options: { statusScores: { error: 0, warning: 50, success: 100 }, }, @@ -119,7 +119,7 @@ describe('AverageAggregationStrategy', () => { expect(logger.info).toHaveBeenCalledWith( expect.stringContaining( - 'options.thresholds" is not configured for average aggregation', + 'options.thresholds" is not configured for weightedStatusScore aggregation', ), ); expect(out.result).toEqual( @@ -141,7 +141,7 @@ describe('AverageAggregationStrategy', () => { } as unknown as AggregatedMetricLoader; const logger = mockServices.logger.mock(); - const strategy = new AverageAggregationStrategy(loader, logger); + const strategy = new WeightedStatusScoreAggregationStrategy(loader, logger); await expect( strategy.aggregate({ @@ -149,12 +149,14 @@ describe('AverageAggregationStrategy', () => { entityRefs: ['component:default/a'], thresholds, aggregationConfig: { - id: 'avgKpi', + id: 'weightedKpi', metricId: metric.id, - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, } as any, }), - ).rejects.toThrow(/statusScores.*required for average aggregation/); + ).rejects.toThrow( + /statusScores.*required for weightedStatusScore aggregation/, + ); }); it('warns and ignores when loader returns a status not in the metric threshold rules', async () => { @@ -171,15 +173,15 @@ describe('AverageAggregationStrategy', () => { } as unknown as AggregatedMetricLoader; const logger = mockServices.logger.mock(); - const strategy = new AverageAggregationStrategy(loader, logger); + const strategy = new WeightedStatusScoreAggregationStrategy(loader, logger); const aggregationConfig = { - id: 'avgKpi', + id: 'weightedKpi', metricId: metric.id, - type: aggregationTypes.average, + type: aggregationTypes.weightedStatusScore, options: { statusScores: { error: 0, warning: 50, success: 100 }, - thresholds: DEFAULT_AVERAGE_KPI_RESULT_THRESHOLDS, + thresholds: DEFAULT_WEIGHTED_STATUS_SCORE_KPI_RESULT_THRESHOLDS, }, } as const; @@ -195,9 +197,9 @@ describe('AverageAggregationStrategy', () => { expect.objectContaining({ entitiesConsidered: 4, calculationErrorCount: 1, - averageWeightedSum: 100, - averageMaxPossible: 300, - averageScore: 33.3, + weightedStatusSum: 100, + weightedStatusMaxPossible: 300, + weightedStatusScore: 33.3, }), ); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.test.ts index 5d0a6ef941..29b302d1c1 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.test.ts @@ -182,12 +182,12 @@ describe('AggregatedMetricMapper', () => { }); }); - it('should wrap a average-shaped result and aggregationType from config', () => { + it('should wrap a weightedStatusScore-shaped result and aggregationType from config', () => { const aggregationConfig: AggregationConfig = { id: 'avg.kpi', - type: aggregationTypes.average, - title: 'Avg KPI', - description: 'Average KPI', + type: aggregationTypes.weightedStatusScore, + title: 'Weighted Status Score KPI', + description: 'Weighted status score KPI', metricId: 'test.metric', } as AggregationConfig; const result = AggregatedMetricMapper.toAggregatedMetricResult( @@ -201,16 +201,18 @@ describe('AggregatedMetricMapper', () => { { name: 'error', count: 2, score: 0 }, ], thresholds, - averageScore: 50, - averageWeightedSum: 500, - averageMaxPossible: 1000, + weightedStatusScore: 50, + weightedStatusSum: 500, + weightedStatusMaxPossible: 1000, aggregationChartDisplayColor: 'warning.main', } as any, aggregationConfig, ); - expect(result.metadata.aggregationType).toBe(aggregationTypes.average); - expect((result.result as any).averageScore).toBe(50); + expect(result.metadata.aggregationType).toBe( + aggregationTypes.weightedStatusScore, + ); + expect((result.result as any).weightedStatusScore).toBe(50); }); }); }); diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts index 3c2750b8e3..5338c2b5d7 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/service/router.test.ts @@ -1187,14 +1187,14 @@ describe('createRouter', () => { ); }); - it('should use KPI type average when configured', async () => { + it('should use KPI type weightedStatusScore when configured', async () => { const kpiConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Weighted health KPI', - description: 'Weighted average', - type: 'average', + description: 'Weighted status score', + type: 'weightedStatusScore', metricId: 'github.open_prs', options: { statusScores: { @@ -1245,7 +1245,7 @@ describe('createRouter', () => { kpiApp.use(router); kpiApp.use(mockErrorHandler()); - await request(kpiApp).get('/aggregations/avgKpi'); + await request(kpiApp).get('/aggregations/weightedKpi'); expect(getSpy).toHaveBeenCalledWith( ['component:default/my-service', 'component:default/my-other-service'], diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.test.ts index 5b977cb335..fff3fa2921 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.test.ts @@ -38,11 +38,11 @@ describe('buildAggregationConfig', () => { }); }); - it('maps average KPI config including statusScores', () => { + it('maps weightedStatusScore KPI config including statusScores', () => { const config = new ConfigReader({ title: 'Weighted health', - description: 'Average across statuses', - type: aggregationTypes.average, + description: 'Weighted health score across statuses', + type: aggregationTypes.weightedStatusScore, metricId: 'github.open_prs', options: { statusScores: { @@ -53,13 +53,13 @@ describe('buildAggregationConfig', () => { }, }); - const result = buildAggregationConfig('avgKpi', { config }); + const result = buildAggregationConfig('weightedKpi', { config }); expect(result).toEqual({ - id: 'avgKpi', + id: 'weightedKpi', title: 'Weighted health', - description: 'Average across statuses', - type: aggregationTypes.average, + description: 'Weighted health score across statuses', + type: aggregationTypes.weightedStatusScore, metricId: 'github.open_prs', options: { statusScores: { error: 0, warning: 50, success: 100 }, @@ -68,11 +68,11 @@ describe('buildAggregationConfig', () => { expect(result.options?.thresholds).toBeUndefined(); }); - it('maps optional thresholds for average KPIs', () => { + it('maps optional thresholds for weightedStatusScore KPIs', () => { const config = new ConfigReader({ title: 'Weighted health', - description: 'Average across statuses', - type: aggregationTypes.average, + description: 'Weighted health score across statuses', + type: aggregationTypes.weightedStatusScore, metricId: 'github.open_prs', options: { statusScores: { success: 100, warning: 50, error: 0 }, @@ -86,7 +86,7 @@ describe('buildAggregationConfig', () => { }, }); - const result = buildAggregationConfig('avgKpi', { config }); + const result = buildAggregationConfig('weightedKpi', { config }); expect(result.options?.thresholds?.rules).toEqual([ { key: 'success', expression: '>=75', color: 'success.main' }, diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.ts b/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.ts index 1cad8142a8..ff4e09aa68 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/utils/buildAggregationConfig.ts @@ -70,7 +70,7 @@ export function buildAggregationConfig( description: config.getString('description'), } as AggregationConfig; - if (aggregationConfig.type === aggregationTypes.average) { + if (aggregationConfig.type === aggregationTypes.weightedStatusScore) { aggregationConfig.options = { statusScores: buildStatusScores(config), thresholds: buildAggregationThresholdsConfig(config), diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/validation/schemas/aggregationConfigSchemas.ts b/workspaces/scorecard/plugins/scorecard-backend/src/validation/schemas/aggregationConfigSchemas.ts index 20eec7d0c3..f8761667b9 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/validation/schemas/aggregationConfigSchemas.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/validation/schemas/aggregationConfigSchemas.ts @@ -29,9 +29,9 @@ const statusGroupedAggregationConfigSchema = z.object({ type: z.literal(aggregationTypes.statusGrouped), }); -const averageAggregationConfigSchema = z.object({ +const weightedStatusScoreAggregationConfigSchema = z.object({ ...baseAggregationConfigSchema.shape, - type: z.literal(aggregationTypes.average), + type: z.literal(aggregationTypes.weightedStatusScore), options: z.strictObject({ statusScores: z .record(z.string(), z.number().finite()) @@ -54,5 +54,5 @@ const averageAggregationConfigSchema = z.object({ export const aggregationConfigSchema = z.discriminatedUnion('type', [ statusGroupedAggregationConfigSchema, - averageAggregationConfigSchema, + weightedStatusScoreAggregationConfigSchema, ]); diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.test.ts index 951c39d3a1..158afa5548 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.test.ts @@ -122,7 +122,7 @@ describe('validateAggregationConfig', () => { ); }); - it('should not throw when average KPI has options.statusScores (app-config shape)', () => { + it('should not throw when weightedStatusScore KPI has options.statusScores (app-config shape)', () => { const registry = new MetricProvidersRegistry(); registry.register(new MockNumberProvider('github.open_prs', 'github')); @@ -131,8 +131,8 @@ describe('validateAggregationConfig', () => { aggregationKPIs: { openPrsWeightedKpi: { title: 'GitHub Open PRs (weighted health)', - type: aggregationTypes.average, - description: 'Weighted health average for open PRs.', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score for open PRs.', metricId: 'github.open_prs', options: { statusScores: { @@ -151,17 +151,17 @@ describe('validateAggregationConfig', () => { ).not.toThrow(); }); - it('should throw when type is average but required options block is missing', () => { + it('should throw when type is weightedStatusScore but required options block is missing', () => { const registry = new MetricProvidersRegistry(); registry.register(new MockNumberProvider('github.open_prs', 'github')); const rootConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Avg KPI', - type: aggregationTypes.average, - description: 'Weighted health', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score', metricId: 'github.open_prs', }, }, @@ -173,17 +173,17 @@ describe('validateAggregationConfig', () => { ); }); - it('should throw InputError when type is average but options.statusScores is empty', () => { + it('should throw InputError when type is weightedStatusScore but options.statusScores is empty', () => { const registry = new MetricProvidersRegistry(); registry.register(new MockNumberProvider('github.open_prs', 'github')); const rootConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Avg KPI', - type: aggregationTypes.average, - description: 'Weighted health', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score', metricId: 'github.open_prs', options: { statusScores: {} }, }, @@ -196,17 +196,17 @@ describe('validateAggregationConfig', () => { ); }); - it('should not throw when average KPI includes optional thresholds', () => { + it('should not throw when weightedStatusScore KPI includes optional thresholds', () => { const registry = new MetricProvidersRegistry(); registry.register(new MockNumberProvider('github.open_prs', 'github')); const rootConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Avg KPI', - type: aggregationTypes.average, - description: 'Weighted health', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score', metricId: 'github.open_prs', options: { statusScores: { success: 100, warning: 50, error: 0 }, @@ -243,10 +243,10 @@ describe('validateAggregationConfig', () => { const rootConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Avg KPI', - type: aggregationTypes.average, - description: 'Weighted health', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score', metricId: 'github.open_prs', options: { statusScores: { success: 100, warning: 50, error: 0 }, @@ -270,17 +270,17 @@ describe('validateAggregationConfig', () => { ); }); - it('should throw when average KPI thresholds leave a gap on the number line', () => { + it('should throw when weightedStatusScore KPI thresholds leave a gap on the number line', () => { const registry = new MetricProvidersRegistry(); registry.register(new MockNumberProvider('github.open_prs', 'github')); const rootConfig = new ConfigReader({ scorecard: { aggregationKPIs: { - avgKpi: { + weightedKpi: { title: 'Avg KPI', - type: aggregationTypes.average, - description: 'Weighted health', + type: aggregationTypes.weightedStatusScore, + description: 'Weighted health score', metricId: 'github.open_prs', options: { statusScores: { success: 100, warning: 50, error: 0 }, diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.ts b/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.ts index 1aa223e987..7665228786 100644 --- a/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.ts +++ b/workspaces/scorecard/plugins/scorecard-backend/src/validation/validateAggregationConfig.ts @@ -38,7 +38,7 @@ function parseAggregationConfig(config: unknown): AggregationConfig { } if ( - parsed.data?.type === aggregationTypes.average && + parsed.data?.type === aggregationTypes.weightedStatusScore && parsed.data.options?.thresholds ) { validateThresholdsForAggregation(parsed.data.options.thresholds, 'number'); diff --git a/workspaces/scorecard/plugins/scorecard-common/report.api.md b/workspaces/scorecard/plugins/scorecard-common/report.api.md index 213f62c1bd..c34eb8680a 100644 --- a/workspaces/scorecard/plugins/scorecard-common/report.api.md +++ b/workspaces/scorecard/plugins/scorecard-common/report.api.md @@ -14,14 +14,6 @@ export type AggregatedMetric = { calculationErrorCount: number; }; -// @public (undocumented) -export type AggregatedMetricAverageResult = StatusGroupedAggregationResult & { - averageScore: number; - averageWeightedSum: number; - averageMaxPossible: number; - aggregationChartDisplayColor: string; -}; - // @public (undocumented) export type AggregatedMetricResult = { id: string; @@ -65,7 +57,7 @@ export type AggregationMetadata = { // @public (undocumented) export type AggregationResultByType = | StatusGroupedAggregationResult - | AggregatedMetricAverageResult; + | WeightedStatusScoreAggregationResult; // @public export type AggregationThresholdRule = Pick< @@ -80,7 +72,7 @@ export type AggregationType = // @public export const aggregationTypes: Readonly<{ statusGrouped: 'statusGrouped'; - average: 'average'; + weightedStatusScore: 'weightedStatusScore'; }>; // @public @@ -219,5 +211,14 @@ export type ThresholdRule = { icon?: string; }; +// @public (undocumented) +export type WeightedStatusScoreAggregationResult = + StatusGroupedAggregationResult & { + weightedStatusScore: number; + weightedStatusSum: number; + weightedStatusMaxPossible: number; + aggregationChartDisplayColor: string; + }; + // (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/scorecard/plugins/scorecard-common/src/constants/aggregations.ts b/workspaces/scorecard/plugins/scorecard-common/src/constants/aggregations.ts index 33d713619e..99be2b042b 100644 --- a/workspaces/scorecard/plugins/scorecard-common/src/constants/aggregations.ts +++ b/workspaces/scorecard/plugins/scorecard-common/src/constants/aggregations.ts @@ -15,7 +15,7 @@ */ const STATUS_GROUPED = 'statusGrouped' as const; -const AVERAGE = 'average' as const; +const WEIGHTED_STATUS_SCORE = 'weightedStatusScore' as const; /** * Supported aggregation types @@ -23,5 +23,5 @@ const AVERAGE = 'average' as const; */ export const aggregationTypes = Object.freeze({ statusGrouped: STATUS_GROUPED, - average: AVERAGE, + weightedStatusScore: WEIGHTED_STATUS_SCORE, }); diff --git a/workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts b/workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts index df1f26a3dc..a44efb9c11 100644 --- a/workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts +++ b/workspaces/scorecard/plugins/scorecard-common/src/types/aggregation.ts @@ -30,7 +30,7 @@ export type AggregationType = export type AggregatedMetricValue = { count: number; name: string; - /** Present when the API includes per-status weights (e.g. average aggregation). */ + /** Present when the API includes per-status weights (e.g. weightedStatusScore aggregation). */ score?: number; }; @@ -70,19 +70,20 @@ export type StatusGroupedAggregationResult = Omit< 'values' > & { values: AggregatedMetricValue[]; thresholds: ThresholdConfig }; -export type AggregatedMetricAverageResult = StatusGroupedAggregationResult & { - averageScore: number; - averageWeightedSum: number; - averageMaxPossible: number; - aggregationChartDisplayColor: string; -}; +export type WeightedStatusScoreAggregationResult = + StatusGroupedAggregationResult & { + weightedStatusScore: number; + weightedStatusSum: number; + weightedStatusMaxPossible: number; + aggregationChartDisplayColor: string; + }; /** * @public */ export type AggregationResultByType = | StatusGroupedAggregationResult - | AggregatedMetricAverageResult; + | WeightedStatusScoreAggregationResult; /** * @public diff --git a/workspaces/scorecard/plugins/scorecard/README.md b/workspaces/scorecard/plugins/scorecard/README.md index 5356935f61..887635a0e9 100644 --- a/workspaces/scorecard/plugins/scorecard/README.md +++ b/workspaces/scorecard/plugins/scorecard/README.md @@ -6,7 +6,7 @@ The plugin supports both the **legacy** Backstage frontend and the **New Fronten **Features:** - **Entity scorecard tab** — View scorecard metrics on catalog entity pages (components, websites, etc.). -- **Scorecard homepage card** — Show aggregated KPIs on the home page (e.g. GitHub open PRs, Jira open issues). Supports **`statusGrouped`** (multi-slice pie) and **`average`** (weighted health donut) KPI types configured under **`scorecard.aggregationKPIs`**. +- **Scorecard homepage card** — Show aggregated KPIs on the home page (e.g. GitHub open PRs, Jira open issues). Supports **`statusGrouped`** (multi-slice pie) and **`weightedStatusScore`** (weighted health donut) KPI types configured under **`scorecard.aggregationKPIs`**. - **Scorecard Entities page** — Drill down from an aggregated metric to see the list of entities contributing to that metric, with entity-level values and status, so you can identify services impacting the KPI and investigate issues. ## Getting started @@ -235,7 +235,7 @@ The following modules and extensions are available from `@red-hat-developer-hub/ - `home-page-widget:home/scorecard-github-open-prs` — Homepage widget showing GitHub open PRs. - `home-page-widget:home/scorecard-github-filecheck-license` - Homepage widget showing file check "License". - `home-page-widget:home/scorecard-github-filecheck-codeowners` - Homepage widget showing file check "Codeowners". -- `home-page-widget:home/scorecard-github-open-prs-weighted` - Homepage widget showing average GitHub open PRs. +- `home-page-widget:home/scorecard-github-open-prs-weighted` - Homepage widget showing weighted status score for GitHub open PRs. #### Legacy app @@ -343,9 +343,9 @@ The plugin exports **`ScorecardHomepageCard`** (see [`plugin.ts`](./src/plugin.t Define KPI ids and optional labels under **`scorecard.aggregationKPIs`** so each card can call **`GET /aggregations/`** with a stable id. See [Scorecard backend README — Aggregation KPIs](../scorecard-backend/README.md#aggregation-kpis-homepage-and-get-aggregations). If you omit a KPI entry, use the **metric id** as `aggregationId` (default status-grouped aggregation). -**`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). +**`type: weightedStatusScore`** 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). -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. +For **`type: weightedStatusScore`**, 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. #### Card props diff --git a/workspaces/scorecard/plugins/scorecard/__fixtures__/scorecardData.ts b/workspaces/scorecard/plugins/scorecard/__fixtures__/scorecardData.ts index f33f6da963..8643895a21 100644 --- a/workspaces/scorecard/plugins/scorecard/__fixtures__/scorecardData.ts +++ b/workspaces/scorecard/plugins/scorecard/__fixtures__/scorecardData.ts @@ -158,15 +158,15 @@ export const mockAggregatedScorecardData = { calculationErrorCount: 0, }, } as AggregatedMetricResult, - [aggregationTypes.average]: { + [aggregationTypes.weightedStatusScore]: { id: 'github.open_prs', status: 'success', metadata: { title: 'GitHub open PRs', - description: 'Weighted health average for the Generative AI API group.', + description: 'Weighted health score for the Generative AI API group.', type: 'number', history: true, - aggregationType: aggregationTypes.average, + aggregationType: aggregationTypes.weightedStatusScore, }, result: { values: [ @@ -177,9 +177,9 @@ export const mockAggregatedScorecardData = { total: 8, timestamp: '2024-01-15T10:30:00Z', thresholds: DEFAULT_NUMBER_THRESHOLDS, - averageScore: 75, - averageWeightedSum: 18, - averageMaxPossible: 24, + weightedStatusScore: 75, + weightedStatusSum: 18, + weightedStatusMaxPossible: 24, }, } as AggregatedMetricResult, }; diff --git a/workspaces/scorecard/plugins/scorecard/report-alpha.api.md b/workspaces/scorecard/plugins/scorecard/report-alpha.api.md index 297dde95a2..21f58654a9 100644 --- a/workspaces/scorecard/plugins/scorecard/report-alpha.api.md +++ b/workspaces/scorecard/plugins/scorecard/report-alpha.api.md @@ -198,13 +198,13 @@ export const scorecardTranslationRef: TranslationRef< readonly 'metric.lastUpdated': string; readonly 'metric.lastUpdatedNotAvailable': string; readonly 'metric.someEntitiesNotReportingValues': string; - readonly 'metric.averageCenterTooltipTotalLabel': string; - readonly 'metric.averageCenterTooltipMaxLabel': string; - readonly 'metric.averageCenterTooltipBreakdownRow_one': string; - readonly 'metric.averageCenterTooltipBreakdownRow_other': string; - readonly 'metric.averageLegendTooltipEntitiesEach_one': string; - readonly 'metric.averageLegendTooltipEntitiesEach_other': string; - readonly 'metric.averageLegendTooltipRowTotal': string; + readonly 'metric.weightedStatusScoreCenterTooltipTotalLabel': string; + readonly 'metric.weightedStatusScoreCenterTooltipMaxLabel': string; + readonly 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': string; + readonly 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': string; + readonly 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': string; + readonly 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': string; + readonly 'metric.weightedStatusScoreLegendTooltipRowTotal': string; readonly 'metric.drillDownCalculationFailures': string; readonly 'metric.homepageEntityHealthRatio': string; readonly 'metric.homepageEntityCalculationHealth': string; diff --git a/workspaces/scorecard/plugins/scorecard/report.api.md b/workspaces/scorecard/plugins/scorecard/report.api.md index 31909b82b4..072812e365 100644 --- a/workspaces/scorecard/plugins/scorecard/report.api.md +++ b/workspaces/scorecard/plugins/scorecard/report.api.md @@ -98,13 +98,13 @@ export const scorecardTranslationRef: TranslationRef< readonly 'metric.lastUpdated': string; readonly 'metric.lastUpdatedNotAvailable': string; readonly 'metric.someEntitiesNotReportingValues': string; - readonly 'metric.averageCenterTooltipTotalLabel': string; - readonly 'metric.averageCenterTooltipMaxLabel': string; - readonly 'metric.averageCenterTooltipBreakdownRow_one': string; - readonly 'metric.averageCenterTooltipBreakdownRow_other': string; - readonly 'metric.averageLegendTooltipEntitiesEach_one': string; - readonly 'metric.averageLegendTooltipEntitiesEach_other': string; - readonly 'metric.averageLegendTooltipRowTotal': string; + readonly 'metric.weightedStatusScoreCenterTooltipTotalLabel': string; + readonly 'metric.weightedStatusScoreCenterTooltipMaxLabel': string; + readonly 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': string; + readonly 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': string; + readonly 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': string; + readonly 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': string; + readonly 'metric.weightedStatusScoreLegendTooltipRowTotal': string; readonly 'metric.drillDownCalculationFailures': string; readonly 'metric.homepageEntityHealthRatio': string; readonly 'metric.homepageEntityCalculationHealth': string; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AggregatedMetricCard.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AggregatedMetricCard.tsx index 208ad6406f..abd6bb2e6d 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AggregatedMetricCard.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AggregatedMetricCard.tsx @@ -14,35 +14,48 @@ * limitations under the License. */ -import { aggregationTypes } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; +import { + aggregationTypes, + AggregatedMetricResult, +} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; import { StatusGroupedCardComponent } from './StatusGroupedCard/StatusGroupedCardComponent'; -import { AverageCardComponent } from './AverageCard/AverageCardComponent'; -import type { AverageCardComponentProps } from './AverageCard/types'; -import type { StatusGroupedCardComponentProps } from './StatusGroupedCard/types'; +import { WeightedStatusScoreCardComponent } from './WeightedStatusScoreCard/WeightedStatusScoreCardComponent'; import { UnsupportedAggregationType } from './UnsupportedAggregationType'; -export type AggregatedMetricCardProps = - | StatusGroupedCardComponentProps - | AverageCardComponentProps; +import { WeightedStatusScoreCardComponentProps } from './WeightedStatusScoreCard/types'; +import { StatusGroupedCardComponentProps } from './StatusGroupedCard/types'; +import { AggregatedMetricCardBaseProps } from './types'; + +type AggregatedMetricCardProps = AggregatedMetricCardBaseProps & { + scorecard: AggregatedMetricResult; +}; + +const isStatusGroupedCardProps = ( + props: AggregatedMetricCardProps, +): props is StatusGroupedCardComponentProps => + props.scorecard.metadata.aggregationType === aggregationTypes.statusGrouped; + +const isWeightedStatusScoreCardProps = ( + props: AggregatedMetricCardProps, +): props is WeightedStatusScoreCardComponentProps => + props.scorecard.metadata.aggregationType === + aggregationTypes.weightedStatusScore; export const AggregatedMetricCard = (props: AggregatedMetricCardProps) => { - switch (props.scorecard.metadata.aggregationType) { - case aggregationTypes.statusGrouped: - return ( - - ); - case aggregationTypes.average: - return ; - default: - return ( - - ); + const { cardTitle, description, dataTestId, scorecard } = props; + + if (isStatusGroupedCardProps(props)) { + return ; + } + if (isWeightedStatusScoreCardProps(props)) { + return ; } + return ( + + ); }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/StatusGroupedCard/types.ts b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/StatusGroupedCard/types.ts index e708c02b9c..9c79280af2 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/StatusGroupedCard/types.ts +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/StatusGroupedCard/types.ts @@ -16,17 +16,17 @@ import { AggregatedMetricResult, + AggregationMetadata, + aggregationTypes, StatusGroupedAggregationResult, } from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; +import { AggregatedMetricCardBaseProps } from '../types'; -export type StatusGroupedCardComponentProps = { - scorecard: Omit & { +export type StatusGroupedCardComponentProps = AggregatedMetricCardBaseProps & { + scorecard: Omit & { + metadata: AggregationMetadata & { + aggregationType: typeof aggregationTypes.statusGrouped; + }; result: StatusGroupedAggregationResult; }; - cardTitle: string; - description: string; - aggregationId: string; - showSubheader?: boolean; - showInfo?: boolean; - dataTestId?: string; }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/DonutChartTooltipContent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/DonutChartTooltipContent.tsx similarity index 91% rename from workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/DonutChartTooltipContent.tsx rename to workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/DonutChartTooltipContent.tsx index 20429d96d0..05cf41fe27 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/DonutChartTooltipContent.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/DonutChartTooltipContent.tsx @@ -38,11 +38,11 @@ export const DonutChartTooltipContent = ({ @@ -61,7 +61,7 @@ export const DonutChartTooltipContent = ({ variant="body2" sx={{ color: 'text.primary', fontWeight: 500 }} > - {t('metric.averageCenterTooltipBreakdownRow', { + {t('metric.weightedStatusScoreCenterTooltipBreakdownRow', { count: row.count, status: getTranslatedStatus(row.name, t), score: formatAggregationScoreDetail(row.score ?? 0), diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/LegendTooltipContent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/LegendTooltipContent.tsx similarity index 91% rename from workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/LegendTooltipContent.tsx rename to workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/LegendTooltipContent.tsx index bc60416ad2..8ee9d7d576 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/LegendTooltipContent.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/LegendTooltipContent.tsx @@ -39,13 +39,13 @@ export const LegendTooltipContent = ({ return ( { +}: WeightedStatusScoreCardComponentProps) => { const theme = useTheme(); const [centerTooltipPosition, setCenterTooltipPosition] = @@ -65,26 +68,26 @@ export const AverageCardComponent = ({ }); }; - const averageScorePercent = scorecard.result.averageScore; + const weightedStatusScorePercent = scorecard.result.weightedStatusScore; const { fill: chartFillPercent, remainder: chartRemainderPercent } = - clampPercentForDonut(averageScorePercent); + clampPercentForDonut(weightedStatusScorePercent); - const centerPercentLabel = `${formatPercentage(averageScorePercent)}%`; + const centerPercentLabel = `${formatPercentage(weightedStatusScorePercent)}%`; const arcResolvedColor = resolveStatusColor( theme, scorecard.result.aggregationChartDisplayColor, ); - const averagePieData: PieData[] = [ + const weightedStatusScorePieData: PieData[] = [ { - name: AVERAGE_SCORE_SLICE, + name: WEIGHTED_STATUS_SCORE_SLICE, value: chartFillPercent, color: arcResolvedColor, }, { - name: AVERAGE_REMAINDER_SLICE, + name: WEIGHTED_STATUS_SCORE_REMAINDER_SLICE, value: chartRemainderPercent, color: theme.palette.grey[300], }, @@ -114,9 +117,9 @@ export const AverageCardComponent = ({ > ( - } diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/AverageCardPieCenterLabel.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/WeightedStatusScoreCardPieCenterLabel.tsx similarity index 85% rename from workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/AverageCardPieCenterLabel.tsx rename to workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/WeightedStatusScoreCardPieCenterLabel.tsx index 44235d31cb..5f157112e3 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/AverageCardPieCenterLabel.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/WeightedStatusScoreCardPieCenterLabel.tsx @@ -18,14 +18,14 @@ import { PieLabelRenderProps } from 'recharts'; import { TooltipPosition } from './types'; -type AverageCardPieCenterLabelProps = PieLabelRenderProps & { +type WeightedStatusScoreCardPieCenterLabelProps = PieLabelRenderProps & { centerPercentLabel: string; arcResolvedColor: string; updateCenterTooltipPosition: (e: React.MouseEvent) => void; setCenterTooltipPosition: (position: TooltipPosition | null) => void; }; -export function AverageCardPieCenterLabel({ +export function WeightedStatusScoreCardPieCenterLabel({ cx, cy, index, @@ -33,7 +33,7 @@ export function AverageCardPieCenterLabel({ arcResolvedColor, updateCenterTooltipPosition, setCenterTooltipPosition, -}: AverageCardPieCenterLabelProps) { +}: WeightedStatusScoreCardPieCenterLabelProps) { if ( cx === undefined || cx === null || @@ -55,7 +55,7 @@ export function AverageCardPieCenterLabel({ fill="transparent" stroke="none" pointerEvents="all" - data-testid="average-card-center-percent-hit-area" + data-testid="weighted-status-score-card-center-percent-hit-area" onMouseEnter={e => { updateCenterTooltipPosition(e); }} @@ -71,7 +71,7 @@ export function AverageCardPieCenterLabel({ fontSize={24} fontWeight={500} pointerEvents="none" - data-testid="average-card-center-percent" + data-testid="weighted-status-score-card-center-percent" > {centerPercentLabel} diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/types.ts b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/types.ts new file mode 100644 index 0000000000..1ad8dcdb59 --- /dev/null +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/WeightedStatusScoreCard/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + WeightedStatusScoreAggregationResult, + AggregatedMetricResult, + AggregationMetadata, + aggregationTypes, +} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; +import { AggregatedMetricCardBaseProps } from '../types'; + +export type WeightedStatusScoreCardComponentProps = + AggregatedMetricCardBaseProps & { + scorecard: Omit & { + metadata: AggregationMetadata & { + aggregationType: typeof aggregationTypes.weightedStatusScore; + }; + result: WeightedStatusScoreAggregationResult; + }; + }; + +export type TooltipPosition = { left: number; top: number }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardLegendContent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardLegendContent.tsx index 4eaf833a82..681b9b059b 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardLegendContent.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardLegendContent.tsx @@ -17,7 +17,7 @@ import CustomLegend from '../../ScorecardHomepageSection/CustomLegend'; import type { PieData } from '../../types'; import type { PieLegendContentProps } from '../../ScorecardHomepageSection/ResponsivePieChart'; -import type { TooltipPosition } from '../AverageCard/types'; +import type { TooltipPosition } from '../WeightedStatusScoreCard/types'; export type CardLegendContentProps = PieLegendContentProps & { activeIndex: number | null; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardTooltip.tsx b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardTooltip.tsx index 52144b1062..525119dcc4 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardTooltip.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/components/CardTooltip.tsx @@ -21,7 +21,7 @@ import { import Box from '@mui/material/Box'; import Portal from '@mui/material/Portal'; import type { PieData } from '../../types'; -import { TooltipPosition } from '../AverageCard/types'; +import { TooltipPosition } from '../WeightedStatusScoreCard/types'; type CardTooltipProps = { tooltipPosition: TooltipPosition; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/types.ts b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/types.ts similarity index 68% rename from workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/types.ts rename to workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/types.ts index 48169ffe8c..b8a6a210a3 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/AverageCard/types.ts +++ b/workspaces/scorecard/plugins/scorecard/src/components/AggregatedMetricCards/types.ts @@ -14,15 +14,7 @@ * limitations under the License. */ -import { - AggregatedMetricAverageResult, - AggregatedMetricResult, -} from '@red-hat-developer-hub/backstage-plugin-scorecard-common'; - -export type AverageCardComponentProps = { - scorecard: Omit & { - result: AggregatedMetricAverageResult; - }; +export type AggregatedMetricCardBaseProps = { cardTitle: string; description: string; aggregationId: string; @@ -30,5 +22,3 @@ export type AverageCardComponentProps = { showInfo?: boolean; dataTestId?: string; }; - -export type TooltipPosition = { left: number; top: number }; diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx index fd80b6b16d..d484ab119f 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/CustomLegend.test.tsx @@ -146,16 +146,16 @@ describe('CustomLegend', () => { ); }); - it('should render two legend items for average donut segment names with translation key fallback', () => { - const averagePieData = [ - { name: 'averageScoreFill', value: 75, color: '#F0AB00' }, - { name: 'averageScoreRemainder', value: 25, color: '#e0e0e0' }, + it('should render two legend items for weightedStatusScore donut segment names with translation key fallback', () => { + const weightedStatusScorePieData = [ + { name: 'weightedStatusScoreFill', value: 75, color: '#F0AB00' }, + { name: 'weightedStatusScoreRemainder', value: 25, color: '#e0e0e0' }, ]; render(
{
, ); - expect(screen.getByText('AverageScoreFill')).toBeInTheDocument(); - expect(screen.getByText('AverageScoreRemainder')).toBeInTheDocument(); - expect(screen.getByTestId('legend-colorbox-averageScoreFill')).toHaveStyle( - 'background-color: #F0AB00', - ); + expect(screen.getByText('WeightedStatusScoreFill')).toBeInTheDocument(); + expect( + screen.getByText('WeightedStatusScoreRemainder'), + ).toBeInTheDocument(); + expect( + screen.getByTestId('legend-colorbox-weightedStatusScoreFill'), + ).toHaveStyle('background-color: #F0AB00'); }); }); diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx index cdc730ef66..4d56d0a195 100644 --- a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx +++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardHomepageSection/__tests__/ScorecardHomepageCard.test.tsx @@ -85,7 +85,7 @@ jest.mock('../ResponsivePieChart', () => ({ {data.name}: {data.value} ))} -
+
{typeof LabelContent === 'function' ? ( @@ -166,17 +166,17 @@ const mockScorecard: AggregatedMetricResult = { }, }; -const mockAverageScorecard: AggregatedMetricResult = { +const mockWeightedStatusScoreScorecard: AggregatedMetricResult = { ...mockScorecard, metadata: { ...mockScorecard.metadata, - aggregationType: aggregationTypes.average, + aggregationType: aggregationTypes.weightedStatusScore, }, result: { ...mockScorecard.result, - averageScore: 75, - averageWeightedSum: 18, - averageMaxPossible: 24, + weightedStatusScore: 75, + weightedStatusSum: 18, + weightedStatusMaxPossible: 24, aggregationChartDisplayColor: 'warning.main', }, }; @@ -422,27 +422,27 @@ describe('AggregatedMetricCard (homepage scorecard)', () => { expect(screen.getByTestId('pie-data-length')).toHaveTextContent('0'); }); - it('should render two donut slices and center percent for average aggregation', () => { + it('should render two donut slices and center percent for weightedStatusScore aggregation', () => { render( , { wrapper: TestWrapper }, ); expect(screen.getByTestId('pie-data-length')).toHaveTextContent('2'); expect( - screen.getByTestId('pie-segment-averageScoreFill'), + screen.getByTestId('pie-segment-weightedStatusScoreFill'), ).toBeInTheDocument(); expect( - screen.getByTestId('pie-segment-averageScoreRemainder'), + screen.getByTestId('pie-segment-weightedStatusScoreRemainder'), ).toBeInTheDocument(); - expect(screen.getByTestId('average-card-center-percent')).toHaveTextContent( - '75%', - ); + expect( + screen.getByTestId('weighted-status-score-card-center-percent'), + ).toHaveTextContent('75%'); }); it('should render error panel when aggregation type is not supported', () => { diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/de.ts b/workspaces/scorecard/plugins/scorecard/src/translations/de.ts index db35af36e4..e3f4b2e015 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/de.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/de.ts @@ -71,17 +71,19 @@ const scorecardTranslationDe = createTranslationMessages({ 'Diese Scorecard verwendet einen Aggregationstyp, der von dieser Version des Plugins nicht unterstützt wird.', 'errors.userNotFoundInCatalogMessage': 'Benutzerentität im Katalog nicht gefunden.', - 'metric.averageCenterTooltipMaxLabel': 'Maximal erreichbare Punktzahl', - 'metric.averageCenterTooltipTotalLabel': 'Gesamtpunktzahl', - 'metric.averageCenterTooltipBreakdownRow_one': + 'metric.weightedStatusScoreCenterTooltipMaxLabel': + 'Maximal erreichbare Punktzahl', + 'metric.weightedStatusScoreCenterTooltipTotalLabel': 'Gesamtpunktzahl', + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': '{{status}}: {{count}} entity, score: {{score}}', - 'metric.averageCenterTooltipBreakdownRow_other': + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': '{{status}}: {{count}} entities, score: {{score}}', - 'metric.averageLegendTooltipEntitiesEach_one': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': '{{count}} Entitäten, jede {{score}}', - 'metric.averageLegendTooltipEntitiesEach_other': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': '{{count}} Entitäten, jeweils {{score}}', - 'metric.averageLegendTooltipRowTotal': 'Gesamtpunktzahl {{total}}', + 'metric.weightedStatusScoreLegendTooltipRowTotal': + 'Gesamtpunktzahl {{total}}', 'metric.drillDownCalculationFailures': 'Bei der Berechnung dieser Kennzahl ist ein oder mehrere Fehler aufgetreten.', 'metric.filecheck.description': diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/es.ts b/workspaces/scorecard/plugins/scorecard/src/translations/es.ts index a809361ae5..7f2616028e 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/es.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/es.ts @@ -72,17 +72,19 @@ const scorecardTranslationEs = createTranslationMessages({ 'Esta tarjeta de puntuación utiliza un tipo de agregación que no es compatible con esta versión del complemento.', 'errors.userNotFoundInCatalogMessage': 'No se encontró la entidad de usuario en el catálogo.', - 'metric.averageCenterTooltipMaxLabel': 'Puntuación máxima posible', - 'metric.averageCenterTooltipTotalLabel': 'Puntuación total', - 'metric.averageCenterTooltipBreakdownRow_one': + 'metric.weightedStatusScoreCenterTooltipMaxLabel': + 'Puntuación máxima posible', + 'metric.weightedStatusScoreCenterTooltipTotalLabel': 'Puntuación total', + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': '{{status}}: {{count}} entity, score: {{score}}', - 'metric.averageCenterTooltipBreakdownRow_other': + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': '{{status}}: {{count}} entities, score: {{score}}', - 'metric.averageLegendTooltipEntitiesEach_one': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': '{{count}} entidad, cada una con {{score}}', - 'metric.averageLegendTooltipEntitiesEach_other': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': '{{count}} entidades, cada una con {{score}}', - 'metric.averageLegendTooltipRowTotal': 'Puntuación total {{total}}', + 'metric.weightedStatusScoreLegendTooltipRowTotal': + 'Puntuación total {{total}}', 'metric.drillDownCalculationFailures': 'No se pudieron validar una o más entidades cuando se calculó esta métrica.', 'metric.filecheck.description': diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts b/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts index 7e94ac5e8a..fe9ce24e8c 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/fr.ts @@ -73,17 +73,17 @@ const scorecardTranslationFr = createTranslationMessages({ "Ce tableau de bord utilise un type d'agrégation qui n'est pas pris en charge par cette version du plugin.", 'errors.userNotFoundInCatalogMessage': 'Entité utilisateur introuvable dans le catalogue.', - 'metric.averageCenterTooltipMaxLabel': 'Score maximal possible', - 'metric.averageCenterTooltipTotalLabel': 'Score total', - 'metric.averageCenterTooltipBreakdownRow_one': + 'metric.weightedStatusScoreCenterTooltipMaxLabel': 'Score maximal possible', + 'metric.weightedStatusScoreCenterTooltipTotalLabel': 'Score total', + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': '{{status}}: {{count}} entity, score: {{score}}', - 'metric.averageCenterTooltipBreakdownRow_other': + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': '{{status}}: {{count}} entities, score: {{score}}', - 'metric.averageLegendTooltipEntitiesEach_one': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': 'Entité {{count}}, chaque {{score}}', - 'metric.averageLegendTooltipEntitiesEach_other': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': '{{count}} entités, chacune {{score}}', - 'metric.averageLegendTooltipRowTotal': 'Score total {{total}}', + 'metric.weightedStatusScoreLegendTooltipRowTotal': 'Score total {{total}}', 'metric.drillDownCalculationFailures': 'Une ou plusieurs entités ont rencontré une erreur lors du calcul de cette métrique.', 'metric.filecheck.description': diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/it.ts b/workspaces/scorecard/plugins/scorecard/src/translations/it.ts index 242ab68d98..6e1440bc21 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/it.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/it.ts @@ -73,17 +73,19 @@ const scorecardTranslationIt = createTranslationMessages({ 'Questa scorecard utilizza un tipo di aggregazione non supportato da questa versione del plugin.', 'errors.userNotFoundInCatalogMessage': 'Entità utente non trovata nel catalogo.', - 'metric.averageCenterTooltipMaxLabel': 'Punteggio massimo possibile', - 'metric.averageCenterTooltipTotalLabel': 'Punteggio totale', - 'metric.averageCenterTooltipBreakdownRow_one': + 'metric.weightedStatusScoreCenterTooltipMaxLabel': + 'Punteggio massimo possibile', + 'metric.weightedStatusScoreCenterTooltipTotalLabel': 'Punteggio totale', + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': '{{status}}: {{count}} entity, score: {{score}}', - 'metric.averageCenterTooltipBreakdownRow_other': + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': '{{status}}: {{count}} entities, score: {{score}}', - 'metric.averageLegendTooltipEntitiesEach_one': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': '{{count}} entità, ciascuna {{score}}', - 'metric.averageLegendTooltipEntitiesEach_other': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': '{{count}} entità, ciascuna {{score}}', - 'metric.averageLegendTooltipRowTotal': 'Punteggio totale {{total}}', + 'metric.weightedStatusScoreLegendTooltipRowTotal': + 'Punteggio totale {{total}}', 'metric.drillDownCalculationFailures': 'Si è verificato un errore durante il calcolo di questa metrica da parte di una o più entità.', 'metric.filecheck.description': diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts b/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts index aaf9571662..5d407e5186 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/ja.ts @@ -72,17 +72,17 @@ const scorecardTranslationJa = createTranslationMessages({ 'このスコアカードは、このバージョンのプラグインでサポートされていない集計タイプを使用しています。', 'errors.userNotFoundInCatalogMessage': 'カタログにユーザーエンティティーが見つかりません。', - 'metric.averageCenterTooltipMaxLabel': '最高スコア', - 'metric.averageCenterTooltipTotalLabel': '合計スコア', - 'metric.averageCenterTooltipBreakdownRow_one': + 'metric.weightedStatusScoreCenterTooltipMaxLabel': '最高スコア', + 'metric.weightedStatusScoreCenterTooltipTotalLabel': '合計スコア', + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_one': '{{status}}: {{count}} entity, score: {{score}}', - 'metric.averageCenterTooltipBreakdownRow_other': + 'metric.weightedStatusScoreCenterTooltipBreakdownRow_other': '{{status}}: {{count}} entities, score: {{score}}', - 'metric.averageLegendTooltipEntitiesEach_one': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_one': '{{count}} 個のエンティティー、各 {{score}}', - 'metric.averageLegendTooltipEntitiesEach_other': + 'metric.weightedStatusScoreLegendTooltipEntitiesEach_other': '{{count}} 個のエンティティー、各 {{score}}', - 'metric.averageLegendTooltipRowTotal': '合計スコア {{total}}', + 'metric.weightedStatusScoreLegendTooltipRowTotal': '合計スコア {{total}}', 'metric.drillDownCalculationFailures': 'このメトリクスの計算中に 1 つ以上のエンティティーが失敗しました。', 'metric.filecheck.description': diff --git a/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts b/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts index d76080342c..5f6256cffd 100644 --- a/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts +++ b/workspaces/scorecard/plugins/scorecard/src/translations/ref.ts @@ -148,16 +148,17 @@ export const scorecardMessages = { lastUpdatedNotAvailable: 'Last updated: Not available', someEntitiesNotReportingValues: 'Some entities are not reporting values related to this metric.', - averageCenterTooltipTotalLabel: 'Total score', - averageCenterTooltipMaxLabel: 'Max possible score', - averageCenterTooltipBreakdownRow_one: + weightedStatusScoreCenterTooltipTotalLabel: 'Total score', + weightedStatusScoreCenterTooltipMaxLabel: 'Max possible score', + weightedStatusScoreCenterTooltipBreakdownRow_one: '{{status}}: {{count}} entity, score: {{score}}', - averageCenterTooltipBreakdownRow_other: + weightedStatusScoreCenterTooltipBreakdownRow_other: '{{status}}: {{count}} entities, score: {{score}}', - averageLegendTooltipEntitiesEach_one: '{{count}} entity, each {{score}}', - averageLegendTooltipEntitiesEach_other: + weightedStatusScoreLegendTooltipEntitiesEach_one: + '{{count}} entity, each {{score}}', + weightedStatusScoreLegendTooltipEntitiesEach_other: '{{count}} entities, each {{score}}', - averageLegendTooltipRowTotal: 'Total score {{total}}', + weightedStatusScoreLegendTooltipRowTotal: 'Total score {{total}}', drillDownCalculationFailures: 'One or more entities failed while calculating this metric.', homepageEntityHealthRatio: '{{healthy}}/{{total}} entities',