Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions workspaces/scorecard/.changeset/busy-mice-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@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-node': minor
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
---

Introduces custom threshold rule keys and colors that can be configured in `app-config.yaml`.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DEFAULT_NUMBER_THRESHOLDS } from '@red-hat-developer-hub/backstage-plugin-scorecard-common';

export const customScorecardResponse = [
{
id: 'github.open_prs',
Expand Down Expand Up @@ -189,6 +191,7 @@ export const githubAggregatedResponse = {
],
total: 15,
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
},
};

Expand All @@ -210,6 +213,7 @@ export const jiraAggregatedResponse = {
],
total: 10,
timestamp: '2026-01-24T14:10:32.776Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
},
};

Expand All @@ -231,6 +235,7 @@ export const emptyJiraAggregatedResponse = {
{ count: 0, name: 'error' },
],
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
},
};

Expand All @@ -252,5 +257,6 @@ export const emptyGithubAggregatedResponse = {
{ count: 0, name: 'error' },
],
timestamp: '2026-01-24T14:10:32.858Z',
thresholds: DEFAULT_NUMBER_THRESHOLDS,
},
};
2 changes: 1 addition & 1 deletion workspaces/scorecard/plugins/scorecard-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ To use these providers, install the corresponding backend modules:

## Thresholds

Thresholds define conditions that determine which category a metric value belongs to (`error`, `warning`, or `success`). The Scorecard plugin provides multiple ways to configure thresholds:
Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). The Scorecard plugin provides multiple ways to configure thresholds:

- **Provider Defaults**: Metric providers define default thresholds
- **App Configuration**: Override defaults through `app-config.yaml`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ export interface Config {
/** Threshold configuration for the metric */
thresholds?: {
rules?: Array<{
key: 'error' | 'warning' | 'success';
key: string;
/** Threshold expression - supports: >=, <=, >, <, ==, !=, - (range) */
expression: string;
/**
* Color for this threshold rule. Can be a theme palette path (e.g., 'error.main')
* or a direct color value (e.g., '#ADD8E6', 'blue', 'rgb(255,255,0)')
*/
color?: string;
}>;
};
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Thresholds Configuration

Thresholds define conditions that determine which category a metric value belongs to (`success`, `warning`, or `error`). When a metric value matches a threshold condition, it gets assigned to that threshold's category. The Scorecard plugin provides multiple ways to configure thresholds with a flexible priority system.
Thresholds define conditions to assign metric values to specific visual categories (`success`, `warning`, `error` or any custom category). When a metric value matches a threshold condition, it gets assigned to that threshold's category. The Scorecard plugin provides multiple ways to configure thresholds with a flexible priority system.

## Overview

Thresholds are evaluated in order and the **first matching** threshold rule is applied. Each threshold rule consists of:

- **`key`**: The threshold category (only allowed keys are `success`, `warning`, `error`)
- **`key`**: The threshold category (e.g., `success`, `warning`, `error`, or custom keys)
- **`expression`**: The condition that determines if a metric value matches this threshold
- **`color`** (optional): The color to display for this threshold in the UI (see [Threshold Colors](#threshold-colors))

## Threshold Configuration Options

Expand Down Expand Up @@ -220,6 +221,72 @@ rules:
expression: '!=true'
```

## Threshold Colors

You can customize the color displayed for each threshold rule in the scorecard UI. Colors can be specified using predefined constants (`success.main`, `warning.main`, `error.main`), hex codes, or RGB/RGBA values.

### Color Configuration

Add a `color` property to any threshold rule:

```yaml
scorecard:
plugins:
myDatasource:
myMetric:
thresholds:
rules:
- key: success
expression: '<10'
color: 'success.main'
- key: warning
expression: '10-50'
color: '#FFA500'
- key: error
expression: '>50'
color: 'rgb(255, 0, 0)'
```

### Predefined Color Constants

You can use the following predefined constants in your color configuration:

- **`success.main`** - Maps to `theme.palette.success.main` (green)
- **`warning.main`** - Maps to `theme.palette.warning.main` (orange/yellow)
- **`error.main`** - Maps to `theme.palette.error.main` (red)

Example configuration:

```yaml
scorecard:
plugins:
myDatasource:
myMetric:
thresholds:
rules:
- key: low
expression: '<50'
color: success.main
- key: medium
expression: '50-79'
color: warning.main
- key: high
expression: '80-100'
color: error.main
```

### Default Colors

If no color is specified for a threshold rule, frontend will use these default colors for standard threshold rule keys:

| Rule Key | Color |
| -------- | ------------------------------ |
| success | `success.main` (green) |
| warning | `warning.main` (orange/yellow) |
| error | `error.main` (red) |

**Important:** Custom threshold keys (not `success`, `warning`, or `error`) **must** specify a `color` property. The configuration will fail validation if a custom key is used without a color. This requirement ensures that all thresholds can be properly visualized in the UI.

## ThresholdEvaluator

The `ThresholdEvaluator` service processes threshold rules and determines which threshold a metric value matches.
Expand Down
54 changes: 54 additions & 0 deletions workspaces/scorecard/plugins/scorecard-backend/knexfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.
*/

// To create new migration file use: "yarn knex migrate:make migrations",
// open generated new migration file and edit it to complete code.
//
// This `knexfile.js` exports multiple named environments, so you must specify
// which one to use with `--env` when running migrations.
//
// Examples:
// - sqlite3: "yarn knex --env sqlite3 migrate:latest"
// - pg: "yarn knex --env pg migrate:latest"
//
// To run new migration use: "yarn knex --env sqlite3 migrate:up some_file_name"
// To run latest migration use: "yarn knex --env sqlite3 migrate:latest"
// To rollback concrete migration use: "yarn knex --env sqlite3 migrate:down some_file_name"
// To rollback latest migration batch use: "yarn knex --env sqlite3 migrate:rollback"

module.exports = {
sqlite3: {
client: 'better-sqlite3',
connection: ':memory:',
useNullAsDefault: true,
migrations: {
directory: './migrations',
},
},
pg: {
client: 'pg',
connection: {
host: process.env.POSTGRES_HOST,
port: Number.parseInt(process.env.POSTGRES_PORT, 10),
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: process.env.POSTGRES_DB,
},
migrations: {
directory: './migrations',
},
},
};
Comment thread
imykhno marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.
*/

exports.up = async function up(knex) {
// Remove the status_check constraint that limits status values to 'success', 'warning', 'error'
const client = knex.client.config.client;

if (client === 'sqlite3' || client === 'better-sqlite3') {
await knex.raw(`
ALTER TABLE metric_values
RENAME COLUMN status TO status_old;
`);

await knex.raw(`
ALTER TABLE metric_values
ADD COLUMN status VARCHAR(255) NULL;
`);

await knex.raw(`
UPDATE metric_values
SET status = status_old;
`);

await knex.raw(`
ALTER TABLE metric_values
DROP COLUMN status_old;
`);
} else {
await knex.schema.alterTable('metric_values', table => {
table.dropChecks(['status_check']);
});
}
};

exports.down = async function down(knex) {
// Re-add the status_check constraint

// Fail if any incompatible rows with status values that don't match the constraint
const incompatibleRows = await knex('metric_values')
.whereNotIn('status', ['success', 'warning', 'error'])
.whereNotNull('status')
.count('* as count')
.first();
if (incompatibleRows && incompatibleRows.count > 0) {
throw new Error(
`Cannot rollback migration: Found ${incompatibleRows.count} rows with status values ` +
`outside of ['success', 'warning', 'error']. Please migrate or remove these rows before rolling back.`,
);
}

const client = knex.client.config.client;

if (client === 'sqlite3' || client === 'better-sqlite3') {
await knex.raw(`
ALTER TABLE metric_values
RENAME COLUMN status TO status_old;
`);

await knex.raw(`
ALTER TABLE metric_values
ADD COLUMN status VARCHAR(255) NULL
CHECK (status IN ('success', 'warning', 'error'));
`);

await knex.raw(`
UPDATE metric_values
SET status = status_old;
`);

await knex.raw(`
ALTER TABLE metric_values
DROP COLUMN status_old;
`);
} else {
await knex.raw(
"ALTER TABLE metric_values ADD CONSTRAINT status_check CHECK (status IN ('success', 'warning', 'error'))",
);
}
};
Loading