diff --git a/workspaces/scorecard/.changeset/fancy-rice-rush.md b/workspaces/scorecard/.changeset/fancy-rice-rush.md
new file mode 100644
index 00000000000..7b9e72a57b3
--- /dev/null
+++ b/workspaces/scorecard/.changeset/fancy-rice-rush.md
@@ -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 icons that can be configured in `app-config.yaml`.
diff --git a/workspaces/scorecard/packages/app-legacy/src/App.tsx b/workspaces/scorecard/packages/app-legacy/src/App.tsx
index 5a2896d6d84..83fe15bba90 100644
--- a/workspaces/scorecard/packages/app-legacy/src/App.tsx
+++ b/workspaces/scorecard/packages/app-legacy/src/App.tsx
@@ -57,6 +57,9 @@ import { getThemes } from '@red-hat-developer-hub/backstage-plugin-theme';
import {
ScorecardHomepageCard,
ScorecardPage,
+ ScorecardErrorStatusIcon,
+ ScorecardSuccessStatusIcon,
+ ScorecardWarningStatusIcon,
} from '@red-hat-developer-hub/backstage-plugin-scorecard';
import { ScalprumContext, ScalprumState } from '@scalprum/react-core';
@@ -274,6 +277,11 @@ const scalprumState: ScalprumState = {
const app = createApp({
apis,
+ icons: {
+ scorecardSuccessStatusIcon: ScorecardSuccessStatusIcon,
+ scorecardWarningStatusIcon: ScorecardWarningStatusIcon,
+ scorecardErrorStatusIcon: ScorecardErrorStatusIcon,
+ },
themes: getThemes(),
__experimentalTranslations: {
availableLanguages: ['en', 'de', 'es', 'fr', 'it', 'ja'],
diff --git a/workspaces/scorecard/packages/app/src/App.tsx b/workspaces/scorecard/packages/app/src/App.tsx
index b2ea4008298..347638fd853 100644
--- a/workspaces/scorecard/packages/app/src/App.tsx
+++ b/workspaces/scorecard/packages/app/src/App.tsx
@@ -23,6 +23,7 @@ import {
} from '@red-hat-developer-hub/backstage-plugin-scorecard/alpha';
import { signInModule } from './modules/signIn';
import { navModule } from './modules/nav';
+import { iconsModule } from './modules/icons';
/*
* app: Backstage app using the New Frontend System (NFS).
@@ -32,6 +33,7 @@ const app = createApp({
rhdhThemeModule,
scorecardCatalogModule,
scorecardTranslationsModule,
+ iconsModule,
signInModule,
navModule,
],
diff --git a/workspaces/scorecard/packages/app/src/modules/icons/index.ts b/workspaces/scorecard/packages/app/src/modules/icons/index.ts
new file mode 100644
index 00000000000..271499229ca
--- /dev/null
+++ b/workspaces/scorecard/packages/app/src/modules/icons/index.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { createFrontendModule } from '@backstage/frontend-plugin-api';
+import { IconBundleBlueprint } from '@backstage/plugin-app-react';
+
+import {
+ ScorecardErrorStatusIcon,
+ ScorecardSuccessStatusIcon,
+ ScorecardWarningStatusIcon,
+} from '@red-hat-developer-hub/backstage-plugin-scorecard';
+
+export const iconsModule = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ IconBundleBlueprint.make({
+ params: {
+ icons: {
+ scorecardSuccessStatusIcon: ScorecardSuccessStatusIcon,
+ scorecardWarningStatusIcon: ScorecardWarningStatusIcon,
+ scorecardErrorStatusIcon: ScorecardErrorStatusIcon,
+ },
+ },
+ }),
+ ],
+});
diff --git a/workspaces/scorecard/plugins/scorecard-backend/config.d.ts b/workspaces/scorecard/plugins/scorecard-backend/config.d.ts
index 2f899aefbf6..e9a5bceb5ba 100644
--- a/workspaces/scorecard/plugins/scorecard-backend/config.d.ts
+++ b/workspaces/scorecard/plugins/scorecard-backend/config.d.ts
@@ -43,17 +43,58 @@ export interface Config {
[metricName: string]: {
/** Threshold configuration for the metric */
thresholds?: {
+ /**
+ * Rules describe how metric values are categorized and how that category is presented in the UI.
+ * They are evaluated in order and the first matching rule is applied.
+ */
rules?: Array<{
+ /**
+ * Threshold category key that a metric value is assigned to when this rule
+ * matches (for example `success`, `warning`, `error`, or a custom key).
+ */
key: string;
- /** Threshold expression - supports: >=, <=, >, <, ==, !=, - (range) */
+ /**
+ * Threshold expression that determines whether a metric value matches this
+ * rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
+ *
+ * @example `<= 10` - Metric value must be less than or equal to 10.
+ * @example `10-60` - Metric value must be between 10 and 60 (inclusive).
+ */
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 configuration - supports multiple formats:
+ * - theme palette reference (`success.main` / `warning.main` / `error.main`)
+ * - HEX code (e.g. '#FFA500')
+ * - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
+ *
+ * Threshold rules 'success', 'warning' and 'error' have default colors.
*/
color?: string;
+ /**
+ * Icon configuration - supports multiple formats:
+ * - Backstage system icons: 'kind:component', 'kind:api', etc.
+ * - Material Design icons: 'settings', 'home', 'build', etc.
+ * - SVG strings: ''
+ * - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
+ * - Data URIs: 'data:image/svg+xml;base64,...'
+ *
+ * Threshold rules 'success', 'warning' and 'error' have default icons.
+ */
+ icon?: string;
}>;
};
+ /**
+ * Schedule for collecting this metric. If not set, the default hourly schedule is used.
+ *
+ * Default schedule:
+ * ```ts
+ * {
+ * frequency: { hours: 1 },
+ * timeout: { minutes: 15 },
+ * initialDelay: { minutes: 1 },
+ * }
+ * ```
+ */
schedule?: SchedulerServiceTaskScheduleDefinitionConfig;
};
};
diff --git a/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md b/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md
index 052486778bd..cb74cfc99a3 100644
--- a/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md
+++ b/workspaces/scorecard/plugins/scorecard-backend/docs/thresholds.md
@@ -9,6 +9,7 @@ Thresholds are evaluated in order and the **first matching** threshold rule is a
- **`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))
+- **`icon`** (optional): The icon to display for this threshold in the UI (see [Threshold Icons](#threshold-icons))
## Threshold Configuration Options
@@ -287,6 +288,70 @@ If no color is specified for a threshold rule, frontend will use these default c
**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.
+## Threshold Icons
+
+You can customize the icon displayed for each threshold rule in the scorecard UI by adding an `icon` property.
+
+### Icon Configuration
+
+Add an `icon` property to any threshold rule:
+
+```yaml
+scorecard:
+ plugins:
+ myDatasource:
+ myMetric:
+ thresholds:
+ rules:
+ - key: success
+ expression: '<10'
+ icon: scorecardSuccessStatusIcon
+ - key: warning
+ expression: '10-50'
+ icon: ''
+ - key: error
+ expression: '>50'
+ icon: 'customIcon'
+```
+
+### Supported Icon Formats
+
+The `icon` value is a string and can be one of:
+
+- **Backstage System Icon**: `kind:component`, `kind:api`, your `customIcon` registered with Backstage
+- **Material Design Icon**: `settings`, `home`, `build`
+- **SVG String**: ``
+- **Image URL**: `http(s)://...`, `/assets/icon.svg`
+- **Data URI**: `data:image/svg+xml;base64,...`
+
+> [!NOTE]
+> SVG String, Image URL, and Data URI icons are treated as images and do not inherit the threshold status color. You must define the color within the icon itself.
+
+For information on registering custom icons with Backstage, see [Adding Icons](https://backstage.io/docs/conf/user-interface/icons/#adding-icons).
+To use your custom Backstage System icons in RHDH for Scorecard, you will need to add `appIcons` key to the plugin that is exporting them:
+
+```yaml
+dynamicPlugins:
+ rootDirectory: dynamic-plugins-root
+ frontend:
+ example.plugin-custom:
+ appIcons:
+ - name: customIcon
+ importName: CustomIcon
+```
+
+### Default icons
+
+If no icon is specified for a threshold rule, the frontend uses default icons for standard threshold rule keys:
+
+| Rule Key | Default icon constant | Material Design Icon |
+| -------- | ---------------------------- | -------------------- |
+| success | `scorecardSuccessStatusIcon` | CheckCircleOutline |
+| warning | `scorecardWarningStatusIcon` | WarningAmber |
+| error | `scorecardErrorStatusIcon` | DangerousOutlined |
+
+**Important:** Custom threshold keys (not `success`, `warning`, or `error`) **must** specify an `icon` property. The configuration will fail validation if a custom key is used without an icon. 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.
diff --git a/workspaces/scorecard/plugins/scorecard-backend/src/utils/mergeEntityAndProviderThresholds.test.ts b/workspaces/scorecard/plugins/scorecard-backend/src/utils/mergeEntityAndProviderThresholds.test.ts
index 487d2817a65..cd354a3e5c5 100644
--- a/workspaces/scorecard/plugins/scorecard-backend/src/utils/mergeEntityAndProviderThresholds.test.ts
+++ b/workspaces/scorecard/plugins/scorecard-backend/src/utils/mergeEntityAndProviderThresholds.test.ts
@@ -215,7 +215,12 @@ describe('mergeEntityAndProviderThresholds', () => {
return {
rules: [
{ key: 'low', expression: '<10', color: 'success.main' },
- { key: 'high', expression: '10-20', color: '#FF0000' },
+ {
+ key: 'high',
+ expression: '10-20',
+ color: '#FF0000',
+ icon: 'scorecardWarningStatusIcon',
+ },
{ key: 'error', expression: '>20' },
],
};
@@ -237,7 +242,12 @@ describe('mergeEntityAndProviderThresholds', () => {
expect(result).toEqual({
rules: [
{ key: 'low', expression: '<10', color: 'success.main' },
- { key: 'high', expression: '10-60', color: '#FF0000' },
+ {
+ key: 'high',
+ expression: '10-60',
+ color: '#FF0000',
+ icon: 'scorecardWarningStatusIcon',
+ },
{ key: 'error', expression: '>60' },
],
});
diff --git a/workspaces/scorecard/plugins/scorecard-common/report.api.md b/workspaces/scorecard/plugins/scorecard-common/report.api.md
index 3e9e1a00780..dee33af7960 100644
--- a/workspaces/scorecard/plugins/scorecard-common/report.api.md
+++ b/workspaces/scorecard/plugins/scorecard-common/report.api.md
@@ -150,6 +150,7 @@ export type ThresholdRule = {
key: string;
expression: string;
color?: string;
+ icon?: string;
};
// (No @packageDocumentation comment for this package)
diff --git a/workspaces/scorecard/plugins/scorecard-common/src/types/threshold.ts b/workspaces/scorecard/plugins/scorecard-common/src/types/threshold.ts
index 314a727e765..30e020eeb61 100644
--- a/workspaces/scorecard/plugins/scorecard-common/src/types/threshold.ts
+++ b/workspaces/scorecard/plugins/scorecard-common/src/types/threshold.ts
@@ -19,9 +19,39 @@
* @public
*/
export type ThresholdRule = {
+ /**
+ * Threshold category key that a metric value is assigned to when this rule
+ * matches (for example `success`, `warning`, `error`, or a custom key).
+ */
key: string;
+ /**
+ * Threshold expression that determines whether a metric value matches this
+ * rule. Supports:`>=`, `<=`, `>`, `<`, `==`, `!=`, `-` (range).
+ *
+ * @example `<= 10` - Metric value must be less than or equal to 10.
+ * @example `10-60` - Metric value must be between 10 and 60 (inclusive).
+ */
expression: string;
+ /**
+ * Color configuration - supports multiple formats:
+ * - theme palette reference (`success.main` / `warning.main` / `error.main`)
+ * - HEX code (e.g. '#FFA500')
+ * - RGB/RGBA (e.g. 'rgb(255, 0, 0)')
+ *
+ * Threshold rules 'success', 'warning' and 'error' have default colors.
+ */
color?: string;
+ /**
+ * Icon configuration - supports multiple formats:
+ * - Backstage system icons: 'kind:component', 'kind:api', etc.
+ * - Material Design icons: 'settings', 'home', 'build', etc.
+ * - SVG strings: ''
+ * - URLs: 'https://example.com/icon.png', '/assets/icon.svg'
+ * - Data URIs: 'data:image/svg+xml;base64,...'
+ *
+ * Threshold rules 'success', 'warning' and 'error' have default icons.
+ */
+ icon?: string;
};
/**
@@ -29,6 +59,10 @@ export type ThresholdRule = {
* @public
*/
export type ThresholdConfig = {
+ /**
+ * Rules describe how metric values are categorized and how that category is presented in the UI.
+ * They are evaluated in order and the first matching rule is applied.
+ */
rules: ThresholdRule[];
};
diff --git a/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.test.ts b/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.test.ts
index a3cf9366ce2..328b8a4234a 100644
--- a/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.test.ts
+++ b/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.test.ts
@@ -61,13 +61,33 @@ describe('validateThresholds', () => {
expect(() => validateThresholds(validConfig, 'number')).not.toThrow();
});
- it('should validate config with custom threshold keys and colors', () => {
+ it('should validate config with custom threshold keys, colors and icons', () => {
const validConfig = {
rules: [
- { key: 'critical', expression: '>80', color: '#d32f2f' },
- { key: 'high', expression: '60-79', color: '#ff9800' },
- { key: 'medium', expression: '40-59', color: '#ffc107' },
- { key: 'low', expression: '20-39', color: '#4caf50' },
+ {
+ key: 'critical',
+ expression: '>80',
+ color: '#d32f2f',
+ icon: 'scorecardErrorStatusIcon',
+ },
+ {
+ key: 'high',
+ expression: '60-79',
+ color: '#ff9800',
+ icon: '',
+ },
+ {
+ key: 'medium',
+ expression: '40-59',
+ color: '#ffc107',
+ icon: 'kind:component',
+ },
+ {
+ key: 'low',
+ expression: '20-39',
+ color: '#4caf50',
+ icon: 'https://raw.githubusercontent.com/redhat-developer/example/main/icons/scorecard-icon.svg',
+ },
{ key: 'success', expression: '<20' },
],
};
@@ -236,13 +256,64 @@ describe('validateThresholds', () => {
const config = {
rules: [
{ key: 'success', expression: '<20' },
- { key: 'critical', expression: '>=20' },
+ {
+ key: 'critical',
+ expression: '>=20',
+ icon: 'scorecardErrorStatusIcon',
+ },
+ ],
+ };
+
+ expect(() => validateThresholds(config, 'number')).toThrow(
+ new ThresholdConfigFormatError(
+ "Custom threshold key \"critical\" must specify a color or icon property. Only standard keys ('success', 'warning', 'error') have default colors and icons.",
+ ),
+ );
+ });
+
+ it('should throw error for custom threshold key without icon', () => {
+ const config = {
+ rules: [
+ { key: 'success', expression: '<20' },
+ {
+ key: 'critical',
+ expression: '>=20',
+ color: '#FF0000',
+ },
+ ],
+ };
+
+ expect(() => validateThresholds(config, 'number')).toThrow(
+ new ThresholdConfigFormatError(
+ "Custom threshold key \"critical\" must specify a color or icon property. Only standard keys ('success', 'warning', 'error') have default colors and icons.",
+ ),
+ );
+ });
+
+ it('should throw error for invalid icon type', () => {
+ const config = {
+ rules: [
+ { key: 'critical', expression: '>=20', color: '#FF0000', icon: 123 },
+ ],
+ };
+
+ expect(() => validateThresholds(config, 'number')).toThrow(
+ new ThresholdConfigFormatError(
+ 'Invalid icon format for rule "critical": icon must be a non-empty string',
+ ),
+ );
+ });
+
+ it('should throw error for empty icon string', () => {
+ const config = {
+ rules: [
+ { key: 'critical', expression: '>=20', color: '#FF0000', icon: '' },
],
};
expect(() => validateThresholds(config, 'number')).toThrow(
new ThresholdConfigFormatError(
- "Custom threshold key \"critical\" must specify a color property. Only standard keys ('success', 'warning', 'error') have default colors.",
+ 'Invalid icon format for rule "critical": icon must be a non-empty string',
),
);
});
@@ -251,50 +322,52 @@ describe('validateThresholds', () => {
{
description: 'missing # in hex color',
config: {
- rules: [{ key: 'custom', expression: '<5', color: 'FF5733' }],
+ rules: [{ key: 'success', expression: '<5', color: 'FF5733' }],
},
expectedError:
- 'Invalid color format for rule "custom": "FF5733" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
+ 'Invalid color format for rule "success": "FF5733" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
},
{
description: 'invalid hex characters',
config: {
- rules: [{ key: 'custom', expression: '<5', color: '#GGGGGG' }],
+ rules: [{ key: 'success', expression: '<5', color: '#GGGGGG' }],
},
expectedError:
- 'Invalid color format for rule "custom": "#GGGGGG" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
+ 'Invalid color format for rule "success": "#GGGGGG" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
},
{
description: 'invalid predefined constant',
config: {
- rules: [{ key: 'custom', expression: '<5', color: 'invalid.color' }],
+ rules: [{ key: 'success', expression: '<5', color: 'invalid.color' }],
},
expectedError:
- 'Invalid color format for rule "custom": "invalid.color" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
+ 'Invalid color format for rule "success": "invalid.color" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
},
{
description: 'empty color string',
config: {
- rules: [{ key: 'custom', expression: '<5', color: '' }],
+ rules: [{ key: 'success', expression: '<5', color: '' }],
},
expectedError:
- 'Invalid color format for rule "custom": color must be a non-empty string',
+ 'Invalid color format for rule "success": color must be a non-empty string',
},
{
description: 'non-string color',
config: {
- rules: [{ key: 'custom', expression: '<5', color: 123 } as any],
+ rules: [{ key: 'success', expression: '<5', color: 123 }],
},
expectedError:
- 'Invalid color format for rule "custom": color must be a non-empty string',
+ 'Invalid color format for rule "success": color must be a non-empty string',
},
{
description: 'RGB with missing comma',
config: {
- rules: [{ key: 'custom', expression: '<5', color: 'rgb(50, 87 37)' }],
+ rules: [
+ { key: 'success', expression: '<5', color: 'rgb(50, 87 37)' },
+ ],
},
expectedError:
- 'Invalid color format for rule "custom": "rgb(50, 87 37)" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
+ 'Invalid color format for rule "success": "rgb(50, 87 37)" must be either a predefined constant (\'success.main\', \'warning.main\', \'error.main\'), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")',
},
])(
'should throw error for invalid color: $description',
diff --git a/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.ts b/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.ts
index 8c5e9299884..37a14930f85 100644
--- a/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.ts
+++ b/workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.ts
@@ -85,6 +85,21 @@ function validateRuleColor(rule: ThresholdRule): void {
}
}
+/**
+ * Validates the icon format if present in a rule.
+ */
+function validateRuleIcon(rule: ThresholdRule): void {
+ if (!('icon' in rule)) {
+ return;
+ }
+
+ if (typeof rule.icon !== 'string' || rule.icon.trim() === '') {
+ throw new ThresholdConfigFormatError(
+ `Invalid icon format for rule "${rule.key}": icon must be a non-empty string`,
+ );
+ }
+}
+
function isThresholdRule(rule: unknown): asserts rule is ThresholdRule {
if (
typeof rule !== 'object' ||
@@ -127,15 +142,19 @@ export function validateThresholds(
for (const rule of thresholds.rules) {
isThresholdRule(rule);
validateRuleColor(rule);
+ validateRuleIcon(rule);
const standardThresholdRuleKeys = ['success', 'warning', 'error'];
- if (!standardThresholdRuleKeys.includes(rule.key) && !('color' in rule)) {
+ if (
+ !standardThresholdRuleKeys.includes(rule.key) &&
+ (!('color' in rule) || !('icon' in rule))
+ ) {
throw new ThresholdConfigFormatError(
`Custom threshold key "${
rule.key
- }" must specify a color property. Only standard keys (${standardThresholdRuleKeys
+ }" must specify a color or icon property. Only standard keys (${standardThresholdRuleKeys
.map(k => `'${k}'`)
- .join(', ')}) have default colors.`,
+ .join(', ')}) have default colors and icons.`,
);
}
diff --git a/workspaces/scorecard/plugins/scorecard/report.api.md b/workspaces/scorecard/plugins/scorecard/report.api.md
index 2107b2d4913..7c2b33d65f2 100644
--- a/workspaces/scorecard/plugins/scorecard/report.api.md
+++ b/workspaces/scorecard/plugins/scorecard/report.api.md
@@ -5,12 +5,17 @@
```ts
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react/jsx-runtime';
+import { default as ScorecardErrorStatusIcon } from '@mui/icons-material/DangerousOutlined';
+import { default as ScorecardSuccessStatusIcon } from '@mui/icons-material/CheckCircleOutline';
+import { default as ScorecardWarningStatusIcon } from '@mui/icons-material/WarningAmber';
import { TranslationRef } from '@backstage/frontend-plugin-api';
import { TranslationResource } from '@backstage/frontend-plugin-api';
// @public
export const EntityScorecardContent: () => JSX_2.Element;
+export { ScorecardErrorStatusIcon };
+
// @public
export const ScorecardHomepageCard: ({
metricId,
@@ -28,6 +33,8 @@ export const ScorecardPage: () => JSX_2.Element;
// @public
export const scorecardPlugin: BackstagePlugin<{}, {}, {}>;
+export { ScorecardSuccessStatusIcon };
+
// @public
export const scorecardTranslationRef: TranslationRef<
'plugin.scorecard',
@@ -96,5 +103,7 @@ export const scorecardTranslationRef: TranslationRef<
// @public
export const scorecardTranslations: TranslationResource<'plugin.scorecard'>;
+export { ScorecardWarningStatusIcon };
+
// (No @packageDocumentation comment for this package)
```
diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx
index 282b1222594..a0394f02493 100644
--- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx
+++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/EntityScorecardContent.tsx
@@ -98,7 +98,7 @@ export const EntityScorecardContent = () => {
cardTitle={finalTitle}
description={finalDescription}
statusColor={statusConfig.color}
- StatusIcon={statusConfig.icon ?? (() => null)}
+ statusIcon={statusConfig.icon ?? ''}
value={metric.result?.value}
thresholds={metric.result?.thresholdResult}
isMetricDataError={isMetricDataError}
diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx
index 373b1ef40d7..e1701898276 100644
--- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx
+++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/Scorecard.tsx
@@ -35,6 +35,7 @@ import { useTheme } from '@mui/material/styles';
import { useTranslation } from '../../hooks/useTranslation';
import { CardWrapper } from '../Common/CardWrapper';
import CustomLegend from './CustomLegend';
+import { ScorecardIcon } from '../ScorecardIcon/ScorecardIcon';
import {
getHeightForCenterLabel,
getYOffsetForCenterLabel,
@@ -47,7 +48,7 @@ interface ScorecardProps {
cardTitle: string;
description: string;
statusColor: string;
- StatusIcon: React.ElementType;
+ statusIcon: string;
value: MetricValue | null;
thresholds?: ThresholdResult;
isMetricDataError?: boolean;
@@ -59,7 +60,7 @@ interface ScorecardProps {
const ScorecardCenterLabel = ({
cx,
cy,
- StatusIcon,
+ statusIcon,
value,
isErrorState,
errorLabel,
@@ -69,7 +70,7 @@ const ScorecardCenterLabel = ({
}: {
cx: number;
cy: number;
- StatusIcon: React.ElementType;
+ statusIcon: string;
value: MetricValue | null;
isErrorState: boolean;
errorLabel: string;
@@ -104,7 +105,9 @@ const ScorecardCenterLabel = ({
return (
- {
value,
loading,
statusColor,
- StatusIcon,
+ statusIcon,
thresholds,
isThresholdError,
thresholdError,
@@ -63,7 +63,7 @@ jest.mock('../Scorecard', () => {
{description}
Value: {value}
Status: {statusColor}
- {StatusIcon && Status Icon}
+ {statusIcon && Status Icon}
{thresholds && Thresholds}
{isThresholdError && (
diff --git a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx
index 2d9d42b1784..891f5e5ab05 100644
--- a/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx
+++ b/workspaces/scorecard/plugins/scorecard/src/components/Scorecard/__tests__/Scorecard.test.tsx
@@ -16,12 +16,20 @@
import { render, screen } from '@testing-library/react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
-import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
-import WarningAmberIcon from '@mui/icons-material/WarningAmber';
-import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined';
import Scorecard from '../Scorecard';
+jest.mock('@backstage/core-plugin-api', () => ({
+ ...jest.requireActual('@backstage/core-plugin-api'),
+ useApp: () => ({
+ getSystemIcon: (key: string) => {
+ return function MockSystemIcon() {
+ return ;
+ };
+ },
+ }),
+}));
+
jest.mock('recharts', () => ({
ResponsiveContainer: ({ children }: any) => (
{children}
@@ -78,7 +86,7 @@ describe('Scorecard Component', () => {
description:
'Current count of open Pull Requests for a given GitHub repository.',
statusColor: 'success.main',
- StatusIcon: CheckCircleOutlineIcon,
+ statusIcon: 'scorecardSuccessStatusIcon',
value: 8,
thresholds: {
status: 'success' as const,
@@ -137,10 +145,11 @@ describe('Scorecard Component', () => {
,
);
- const iconElement = container.querySelector(
- '[data-testid="CheckCircleOutlineIcon"]',
- );
- expect(iconElement).toBeInTheDocument();
+ expect(
+ container.querySelector(
+ '[data-testid="system-icon-scorecardSuccessStatusIcon"]',
+ ),
+ ).toBeInTheDocument();
});
it('should handle zero value correctly', () => {
@@ -170,7 +179,7 @@ describe('Scorecard Component', () => {
const warningProps = {
...defaultProps,
statusColor: 'warning.main',
- StatusIcon: WarningAmberIcon,
+ statusIcon: 'scorecardWarningStatusIcon',
value: 25,
};
@@ -191,7 +200,7 @@ describe('Scorecard Component', () => {
const errorProps = {
...defaultProps,
statusColor: 'error.main',
- StatusIcon: DangerousOutlinedIcon,
+ statusIcon: 'scorecardErrorStatusIcon',
value: 75,
};
@@ -212,7 +221,7 @@ describe('Scorecard Component', () => {
const customProps = {
...defaultProps,
statusColor: '#FF5733',
- StatusIcon: CheckCircleOutlineIcon,
+ statusIcon: 'scorecardSuccessStatusIcon',
value: 4,
thresholds: {
...defaultProps.thresholds,
diff --git a/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx
new file mode 100644
index 00000000000..106ab438045
--- /dev/null
+++ b/workspaces/scorecard/plugins/scorecard/src/components/ScorecardIcon/ScorecardIcon.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 { useApp } from '@backstage/core-plugin-api';
+import Box from '@mui/material/Box';
+import MuiIcon from '@mui/material/Icon';
+import type { SxProps, Theme } from '@mui/material/styles';
+
+/**
+ * @public
+ */
+export interface ScorecardIconProps {
+ icon: string;
+ size?: 'small' | 'medium' | 'large';
+ sx?: SxProps;
+}
+
+/**
+ * @public
+ */
+export const ScorecardIcon = ({
+ icon,
+ size = 'small',
+ sx,
+}: ScorecardIconProps) => {
+ const app = useApp();
+ if (!icon) {
+ return null;
+ }
+
+ const SystemIcon = app.getSystemIcon(icon);
+ if (SystemIcon) {
+ return (
+
+
+
+ );
+ }
+
+ if (icon.startsWith('