Skip to content

feat(scorecard): Custom threshold keys and colors - #2378

Merged
dzemanov merged 23 commits into
redhat-developer:mainfrom
dzemanov:scorecard-threshold-keys-colors
Mar 4, 2026
Merged

feat(scorecard): Custom threshold keys and colors#2378
dzemanov merged 23 commits into
redhat-developer:mainfrom
dzemanov:scorecard-threshold-keys-colors

Conversation

@dzemanov

@dzemanov dzemanov commented Feb 23, 2026

Copy link
Copy Markdown
Member

User description

Hey, I just made a Pull Request!

Introduces support for custom threshold keys and colors.
Users can specify colors as RGB or HEX.
Example config of custom threshold keys and colors:

scorecard:
  plugins:
    github:
      open_prs:
        thresholds:
          rules:
            - key: success
              expression: '<=2'
            - key: warning
              expression: '<=35'
            - key: critical
              expression: '>35'
              color: '#0000FF'  # or 'error.main', rgb(255,255,0)
  • out of scope: icon handling, to be worked in following PR, right now default icon is set to CheckCircleOutlineIcon

Demo

Screenshot 2026-02-24 at 16 37 08 Screenshot 2026-02-24 at 16 37 23

✔️ Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

Fixes

Fixes https://issues.redhat.com/browse/RHIDP-11915


PR Type

Enhancement


Description

  • Support custom threshold keys with optional color configuration

  • Refactor aggregated metric structure to use dynamic status counts

  • Add color validation for hex, RGB/RGBA, and theme palette references

  • Unify color resolution logic across scorecard components

  • Expand test coverage for threshold validation and color handling


Diagram Walkthrough

flowchart LR
  A["Threshold Config"] -->|custom keys + colors| B["Validation"]
  B -->|validate color format| C["Color Constants"]
  A -->|rules with colors| D["Database"]
  D -->|statusCounts| E["Aggregation Mapper"]
  E -->|fill missing keys| F["UI Components"]
  F -->|resolveStatusColor| G["Theme Palette"]
  G -->|render colors| H["Scorecard Display"]
Loading

File Walkthrough

Relevant files
Configuration changes
3 files
config.d.ts
Allow custom threshold keys and optional colors                   
+6/-1     
knexfile.js
Add knex configuration for database migrations                     
+31/-0   
20260206115752_remove_status_check_constraint.js
Remove status enum constraint for custom keys                       
+92/-0   
Enhancement
15 files
types.ts
Change status to string and refactor aggregated metric     
+6/-7     
DatabaseMetricValues.ts
Refactor aggregation to use dynamic status counts               
+33/-39 
CatalogMetricService.ts
Remove hardcoded aggregated metrics type definition           
+0/-5     
mappers.ts
Map aggregated metrics with threshold-ordered status counts
+17/-9   
router.ts
Pass thresholds to aggregation mapper                                       
+8/-2     
validateThresholds.ts
Validate custom threshold keys and color formats                 
+95/-14 
threshold.ts
Add optional color property to threshold rules                     
+22/-0   
Metric.ts
Update aggregated metric types with thresholds                     
+5/-3     
utils.ts
Add color resolution and threshold rule color utilities   
+79/-13 
CustomLegend.tsx
Use threshold rule colors in legend display                           
+9/-9     
EntityScorecardContent.tsx
Pass threshold rules to status config                                       
+1/-0     
Scorecard.tsx
Simplify color resolution using new utility                           
+6/-25   
CustomLegend.tsx
Add test ID to legend color boxes                                               
+4/-1     
EmptyStatePanel.tsx
Use new color resolution utility                                                 
+4/-20   
ScorecardHomepageCardComponent.tsx
Apply threshold rule colors to pie chart segments               
+6/-6     
Tests
13 files
DatabaseMetricValues.test.ts
Expand tests for aggregated metric functionality                 
+295/-37
CatalogMetricService.test.ts
Update tests for new aggregation structure                             
+5/-9     
mappers.test.ts
Add comprehensive mapper tests for custom thresholds         
+246/-0 
router.test.ts
Update router tests for threshold parameter                           
+27/-9   
validateThresholds.test.ts
Add tests for custom keys and color validation                     
+145/-23
CustomLegend.test.tsx
Add tests for custom legend color rendering                           
Scorecard.test.tsx
Update scorecard tests for color resolution                           
+81/-10 
EntityScorecardContent.test.tsx
Update entity scorecard tests for threshold rules               
+4/-0     
CustomLegend.test.tsx
Add color display tests for legend items                                 
+31/-3   
ScorecardHomepageCard.test.tsx
Add tests for pie segment colors                                                 
+37/-1   
ScorecardHomepageSection.test.tsx
Update test fixtures with threshold data                                 
+5/-1     
utils.test.tsx
Add comprehensive tests for color utilities                           
+246/-100
aggregatedScorecardData.ts
Add threshold data to mock aggregated scorecard                   
+5/-1     
Documentation
3 files
thresholds.md
Document custom threshold keys and color configuration     
+68/-1   
report.api.md
Update API report with new color constants                             
+19/-2   
busy-mice-call.md
Add changeset for custom thresholds feature                           
+8/-0     
Additional files
1 files
CustomLegend.test.tsx +98/-0   

@rhdh-gh-app

rhdh-gh-app Bot commented Feb 23, 2026

Copy link
Copy Markdown

Important

This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior.

Changed Packages

Package Name Package Path Changeset Bump Current Version
app workspaces/scorecard/packages/app none v0.0.0
@red-hat-developer-hub/backstage-plugin-scorecard-backend workspaces/scorecard/plugins/scorecard-backend minor v2.3.5
@red-hat-developer-hub/backstage-plugin-scorecard-common workspaces/scorecard/plugins/scorecard-common minor v2.3.5
@red-hat-developer-hub/backstage-plugin-scorecard-node workspaces/scorecard/plugins/scorecard-node minor v2.3.5
@red-hat-developer-hub/backstage-plugin-scorecard workspaces/scorecard/plugins/scorecard minor v2.3.5

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 23, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- createMetricValue
- describe: AggregatedMetricMapper
- describe: toAggregatedMetric
- it: should map DbAggregatedMetric to AggregatedMetric
- it: should handle undefined input with defaults
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Missing color validation: resolveStatusColor accepts arbitrary statusColor strings from configuration/UI paths
without validation, which may lead to inconsistent rendering or hard-to-debug failures
when invalid values are provided.

Referred Code
export const resolveStatusColor = (
  theme: Theme,
  statusColor: string,
  isError: boolean = false,
): string => {
  if (isError) {
    return (
      (theme as ThemeConfig).palette?.rhdh?.general?.cardBorderColor ??
      theme.palette.success.main
    );
  }

  // If statusColor contains a dot, treat it as a theme palette reference
  if (statusColor.includes('.')) {
    const parts = statusColor.split('.');
    let value: any = theme.palette;

    for (const part of parts) {
      value = value?.[part];
      if (value === undefined) {
        break;


 ... (clipped 8 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Unvalidated config input: The new thresholds.rules[].color configuration input is accepted as a free-form string and
later used to influence UI styling without explicit validation/sanitization in the diff.

Referred Code
  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;
}>;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Feb 23, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to 9032678

CategorySuggestion                                                                                                                                    Impact
Possible issue
Aggregate using latest timestamps

Modify the latestIdsSubquery to use MAX(timestamp) instead of MAX(id) to ensure
aggregation consistently uses the most recent metric value by time.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [74-78]

 const latestIdsSubquery = this.dbClient(this.tableName)
-  .max('id')
-  .where('metric_id', metric_id)
-  .whereIn('catalog_entity_ref', catalog_entity_refs)
-  .groupBy('catalog_entity_ref');
+  .select(this.dbClient.raw('MAX(id) as id'))
+  .join(
+    this.dbClient(this.tableName)
+      .select('catalog_entity_ref')
+      .max('timestamp as max_ts')
+      .where('metric_id', metric_id)
+      .whereIn('catalog_entity_ref', catalog_entity_refs)
+      .groupBy('catalog_entity_ref')
+      .as('latest'),
+    join => {
+      join
+        .on(`${this.tableName}.catalog_entity_ref`, '=', 'latest.catalog_entity_ref')
+        .andOn(`${this.tableName}.timestamp`, '=', 'latest.max_ts');
+    },
+  )
+  .where(`${this.tableName}.metric_id`, metric_id)
+  .groupBy(`${this.tableName}.catalog_entity_ref`);

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that using MAX(id) is not a reliable way to get the latest record and proposes a more robust solution using MAX(timestamp), which is critical for data correctness.

Medium
Fix incorrect color fallbacks

In getStatusConfig, improve the fallback logic to use default colors for
standard evaluations (success, warning, error) when thresholdRules are not
provided, preventing incorrect color display.

workspaces/scorecard/plugins/scorecard/src/utils/utils.ts [69-99]

 export const getStatusConfig = ({
   evaluation,
   thresholdStatus,
   metricStatus,
   thresholdRules,
 }: {
   evaluation: string | null;
   thresholdStatus?: 'success' | 'error';
   metricStatus?: 'success' | 'error';
   thresholdRules?: ThresholdRule[];
 }): StatusConfig => {
   // If threshold or metric has an error, return error state color
   if (thresholdStatus === 'error' || metricStatus === 'error') {
     return { color: SCORECARD_ERROR_STATE_COLOR };
   }
 
-  let evaluationColor: string | undefined;
-  if (thresholdRules && evaluation) {
-    evaluationColor = getThresholdRuleColor(thresholdRules, evaluation);
-  }
-  const color = evaluationColor ?? SCORECARD_ERROR_STATE_COLOR;
+  const defaultColorForEvaluation = (e: string | null): string => {
+    switch (e) {
+      case 'error':
+        return ScorecardThresholdRuleColors.ERROR;
+      case 'warning':
+        return ScorecardThresholdRuleColors.WARNING;
+      default:
+        return ScorecardThresholdRuleColors.SUCCESS;
+    }
+  };
+
+  const color =
+    (thresholdRules && evaluation
+      ? getThresholdRuleColor(thresholdRules, evaluation)
+      : undefined) ?? defaultColorForEvaluation(evaluation);
 
   switch (evaluation) {
     case 'error':
       return { color, icon: DangerousOutlinedIcon };
     case 'warning':
       return { color, icon: WarningAmberIcon };
     default:
       return { color, icon: CheckCircleOutlineIcon };
   }
 };

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a logic flaw where a successful evaluation could be displayed with an error color if threshold rules are not available, and provides a robust fix.

Medium
Normalize rollback count handling

In the down migration, normalize the count from knex.count() to a number before
comparison to ensure the rollback guard works consistently across different
database drivers.

workspaces/scorecard/plugins/scorecard-backend/migrations/20260206115752_remove_status_check_constraint.js [52-62]

 const incompatibleRows = await knex('metric_values')
   .whereNotIn('status', ['success', 'warning', 'error'])
   .whereNotNull('status')
   .count('* as count')
   .first();
-if (incompatibleRows && incompatibleRows.count > 0) {
+
+const incompatibleCount = Number(incompatibleRows?.count ?? 0);
+if (incompatibleCount > 0) {
   throw new Error(
-    `Cannot rollback migration: Found ${incompatibleRows.count} rows with status values ` +
+    `Cannot rollback migration: Found ${incompatibleCount} rows with status values ` +
       `outside of ['success', 'warning', 'error']. Please migrate or remove these rows before rolling back.`,
   );
 }
  • Apply / Chat
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly points out that database drivers can return counts as strings, and explicitly converting to a number makes the rollback check more robust and cross-database compatible.

Low
  • Update

Previous suggestions

✅ Suggestions up to commit 2e56c05
CategorySuggestion                                                                                                                                    Impact
High-level
Custom threshold keys lack icon mapping

The getStatusConfig function incorrectly assigns a default success icon to all
custom threshold keys that are not 'error' or 'warning'. This creates a
misleading user experience, such as showing a checkmark for a 'critical' status.

Examples:

workspaces/scorecard/plugins/scorecard/src/utils/utils.ts [83-90]
  switch (evaluation) {
    case 'error':
      return { color, icon: DangerousOutlinedIcon };
    case 'warning':
      return { color, icon: WarningAmberIcon };
    default:
      return { color, icon: CheckCircleOutlineIcon };
  }

Solution Walkthrough:

Before:

function getStatusConfig({ evaluation, thresholdRules }) {
  // ... error checks ...

  const color = getThresholdRuleColor(thresholdRules, evaluation) ?? 'success.main';

  switch (evaluation) {
    case 'error':
      return { color, icon: DangerousOutlinedIcon };
    case 'warning':
      return { color, icon: WarningAmberIcon };
    default: // 'success', 'critical', 'high', etc. all fall here
      return { color, icon: CheckCircleOutlineIcon };
  }
}

After:

function getStatusConfig({ evaluation, thresholdRules }) {
  // ... error checks ...

  const color = getThresholdRuleColor(thresholdRules, evaluation) ?? 'success.main';

  // A more robust mapping is needed.
  // This could be based on keywords or a new config property.
  if (['error', 'critical', 'severe'].includes(evaluation)) {
    return { color, icon: DangerousOutlinedIcon };
  }
  if (['warning', 'high'].includes(evaluation)) {
    return { color, icon: WarningAmberIcon };
  }
  // Default to a success or neutral icon
  return { color, icon: CheckCircleOutlineIcon };
}
Suggestion importance[1-10]: 8

__

Why: This is a significant UX design flaw in the new feature, as a 'critical' status showing a success icon is highly misleading, even if noted as out-of-scope.

Medium
General
Validate custom color property in thresholds
Suggestion Impact:The commit added explicit validation for the optional "color" field: it now errors if "color" exists but is not a non-empty string, and additionally validates that the color value matches allowed constants or common CSS color formats. The main validation loop was updated to call this new color validator for each rule.

code diff:

+/**
+ * Validates if a color string is valid
+ * - Predefined constants: {@link ScorecardThresholdRuleColors}
+ * - Hex colors: #RGB, #RRGGBB, #RRGGBBAA
+ * - RGB/RGBA colors: rgb(r, g, b), rgba(r, g, b, a)
+ */
+function isValidColor(color: string): boolean {
+  if (
+    (SCORECARD_THRESHOLD_RULE_COLOR_VALUES as readonly string[]).includes(color)
+  ) {
+    return true;
+  }
+
+  // Check for hex color format: #RGB, #RRGGBB, #RRGGBBAA
+  const hexColorRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
+  if (hexColorRegex.test(color)) {
+    return true;
+  }
+
+  // Check for RGB color format: rgb(r, g, b)
+  const rgbRegex = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
+  if (rgbRegex.test(color)) {
+    return true;
+  }
+
+  // Check for RGBA color format: rgba(r, g, b, a)
+  const rgbaRegex =
+    /^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d+(?:\.\d+)?\s*\)$/;
+  return rgbaRegex.test(color);
+}
+
+/**
+ * Validates the color format if present in a rule
+ */
+function validateRuleColor(rule: ThresholdRule): void {
+  if (!('color' in rule)) {
+    return;
+  }
+
+  if (typeof rule.color !== 'string' || rule.color.trim() === '') {
+    throw new ThresholdConfigFormatError(
+      `Invalid color format for rule "${rule.key}": color must be a non-empty string`,
+    );
+  }
+
+  if (!isValidColor(rule.color)) {
+    throw new ThresholdConfigFormatError(
+      `Invalid color format for rule "${rule.key}": "${
+        rule.color
+      }" must be either a predefined constant (${SCORECARD_THRESHOLD_RULE_COLOR_VALUES.map(
+        v => `'${v}'`,
+      ).join(
+        ', ',
+      )}), a hex color (e.g., "#ADD8E6"), or an RGB/RGBA color (e.g., "rgb(255, 255, 0)")`,
+    );
+  }
+}
+
+function isThresholdRule(rule: unknown): asserts rule is ThresholdRule {
+  if (
+    typeof rule !== 'object' ||
+    rule === null ||
+    !('key' in rule) ||
+    !('expression' in rule) ||
+    typeof rule.key !== 'string' ||
+    typeof rule.expression !== 'string' ||
+    rule.key.trim() === '' ||
+    rule.expression.trim() === ''
+  ) {
+    throw new ThresholdConfigFormatError(
+      `Invalid threshold rule format "${JSON.stringify(
+        rule,
+      )}": must be an object with "key" and "expression" non-empty string properties`,
+    );
+  }
+}
 
 /**
  * Validate thresholds configuration
@@ -43,28 +125,30 @@
 
   const seenKeys = new Set<string>();
   for (const rule of thresholds.rules) {
+    isThresholdRule(rule);
+    validateRuleColor(rule);
+
+    const standard_threshold_rule_keys = ['success', 'warning', 'error'];
     if (
-      typeof rule !== 'object' ||
-      rule === null ||
-      !('key' in rule) ||
-      !('expression' in rule) ||
-      typeof rule.key !== 'string' ||
-      typeof rule.expression !== 'string' ||
-      rule.key.trim() === '' ||
-      rule.expression.trim() === ''
+      !standard_threshold_rule_keys.includes(rule.key) &&
+      !('color' in rule)
     ) {

Add validation to the validateThresholds function to ensure that the optional
color property in threshold rules is a non-empty string if it exists.

workspaces/scorecard/plugins/scorecard-node/src/utils/thresholds/validateThresholds.ts [29-42]

 export function validateThresholds(
   thresholds: JsonValue,
   expectedMetricType: MetricType,
 ): asserts thresholds is ThresholdConfig {
   if (
     typeof thresholds !== 'object' ||
     thresholds === null ||
     !('rules' in thresholds) ||
     !Array.isArray(thresholds.rules)
   ) {
     throw new ThresholdConfigFormatError(
       'Invalid type for ThresholdConfig, must have a rules property that is an array',
     );
   }
-...
 
+  const seenKeys = new Set<string>();
+  for (const rule of thresholds.rules) {
+    if (
+      typeof rule !== 'object' ||
+      rule === null ||
+      !('key' in rule) ||
+      !('expression' in rule) ||
+      typeof rule.key !== 'string' ||
+      typeof rule.expression !== 'string' ||
+      rule.key.trim() === '' ||
+      rule.expression.trim() === '' ||
+      ('color' in rule && (typeof rule.color !== 'string' || rule.color.trim() === ''))
+    ) {
+      throw new ThresholdConfigFormatError(
+        `Invalid threshold rule format "${JSON.stringify(
+          rule,
+        )}": must be an object with "key" and "expression" non-empty string properties. Optional "color" must also be a non-empty string.`,
+      );
+    }
+    if (seenKeys.has(rule.key)) {
+      throw new ThresholdConfigFormatError(
+        `Duplicate key detected for "${rule.key}" with expression "${rule.expression}"`,
+      );
+    }
+    seenKeys.add(rule.key);
+    parseThresholdExpression(rule.expression, expectedMetricType);
+  }
+}
+
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the new color property is not being validated, which could lead to frontend errors if misconfigured, and proposes a robust validation check.

Medium
Use epoch fallback for timestamps

In normalizeTimestamp, use new Date(0) as the fallback for unparsable timestamps
instead of new Date() to prevent incorrect max timestamp calculations.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [96-106]

 const normalizeTimestamp = (timestamp: any): Date => {
   if (timestamp instanceof Date) {
     return timestamp;
   } else if (
     typeof timestamp === 'number' ||
     typeof timestamp === 'string'
   ) {
     return new Date(timestamp);
   }
-  return new Date();
+  return new Date(0);
 };
Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies that falling back to the current time for an unparsable timestamp can lead to incorrect calculations and proposes using a deterministic epoch date instead.

Low
Use a more appropriate fallback color
Suggestion Impact:The commit removed the isError-based success-color fallback and refactored resolveStatusColor to fall back to the scorecard error state color (cardBorderColor) and then theme.palette.error.main, eliminating the misleading success color in error scenarios.

code diff:

 
 /**
  * Resolves a color value from the theme palette or returns a custom color.
- * Supports both theme palette paths (e.g., 'error.main', 'rhdh.general.disabled')
+ * Supports theme palette paths (e.g., 'error.main', 'rhdh.general.cardBorderColor')
  * and direct color values (e.g., '#FF5733', 'blue', 'rgb(255,0,0)').
  *
  * @param theme - The theme configuration object
  * @param statusColor - Either a theme palette path or a direct color value
- * @param isError - If true, returns the theme's card border color for error states, defaults to false
  * @returns The resolved color string
  */
 export const resolveStatusColor = (
   theme: Theme,
   statusColor: string,
-  isError: boolean = false,
 ): string => {
-  if (isError) {
-    return (
-      (theme as ThemeConfig).palette?.rhdh?.general?.cardBorderColor ??
-      theme.palette.success.main
-    );
+  if (!statusColor.includes('.')) {
+    return statusColor;
   }
 
-  // If statusColor contains a dot, treat it as a theme palette reference
-  if (statusColor.includes('.')) {
-    const parts = statusColor.split('.');
-    let value: any = theme.palette;
+  // Resolve theme palette reference
+  const parts = statusColor.split('.');
+  let value: any = theme.palette;
 
-    for (const part of parts) {
-      value = value?.[part];
-      if (value === undefined) {
-        break;
-      }
+  for (const part of parts) {
+    value = value?.[part];
+    if (value === undefined) {
+      break;
     }
-
-    return typeof value === 'string' ? value : theme.palette.success.main;
   }
 
-  return statusColor;
+  if (typeof value === 'string') {
+    return value;
+  }
+
+  // Fallback to error state color, then error.main
+  return (
+    (theme as ThemeConfig).palette?.rhdh?.general?.cardBorderColor ??
+    theme.palette.error.main
+  );

In resolveStatusColor, change the fallback color for error states from
theme.palette.success.main to theme.palette.action.disabled to avoid showing a
success color for an error.

workspaces/scorecard/plugins/scorecard/src/utils/utils.ts [103-113]

 export const resolveStatusColor = (
   theme: Theme,
   statusColor: string,
   isError: boolean = false,
 ): string => {
   if (isError) {
     return (
       (theme as ThemeConfig).palette?.rhdh?.general?.cardBorderColor ??
-      theme.palette.success.main
+      theme.palette.action.disabled
     );
   }
 ...
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that falling back to a success color for an error state is misleading and proposes a more appropriate neutral color, improving the UI's correctness.

Low
Possible issue
Drop non-grouped column in SELECT
Suggestion Impact:The query's SELECT clause was changed to drop 'metric_id', selecting only 'status' while keeping the GROUP BY unchanged.

code diff:

     const statusRows = await this.dbClient(this.tableName)
-      .select('status', 'metric_id')
+      .select('status')

To ensure SQL compatibility, remove the non-aggregated metric_id column from the
SELECT clause, as it is not used and violates strict GROUP BY rules.

workspaces/scorecard/plugins/scorecard-backend/src/database/DatabaseMetricValues.ts [80-87]

 const statusRows = await this.dbClient(this.tableName)
-  .select('status', 'metric_id')
+  .select('status')
   .count('* as count')
   .max('timestamp as max_timestamp')
   .whereIn('id', latestIdsSubquery)
   .whereNotNull('status')
   .whereNotNull('value')
   .groupBy('status');
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a SQL compatibility issue and proposes removing the redundant, non-aggregated metric_id from the SELECT clause, which is a cleaner fix than adding it to GROUP BY.

Medium

@rhdh-qodo-merge

Copy link
Copy Markdown

Persistent suggestions updated to latest commit 9032678

@dzemanov
dzemanov marked this pull request as ready for review February 24, 2026 13:27
@rhdh-qodo-merge

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
🟢
No codebase code duplication found New Components Detected (Top 5):
- createMetricValue
- describe: AggregatedMetricMapper
- describe: toAggregatedMetric
- it: should map DbAggregatedMetric to AggregatedMetric
- it: should handle undefined input with defaults
Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status:
Non-idiomatic variable: The newly introduced variable standard_threshold_rule_keys uses snake_case (non-idiomatic
for TypeScript) reducing readability and consistency with surrounding code.

Referred Code
const standard_threshold_rule_keys = ['success', 'warning', 'error'];
if (
  !standard_threshold_rule_keys.includes(rule.key) &&
  !('color' in rule)
) {
  throw new ThresholdConfigFormatError(
    `Custom threshold key "${
      rule.key
    }" must specify a color property. Only standard keys (${standard_threshold_rule_keys
      .map(k => `'${k}'`)
      .join(', ')}) have default colors.`,

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Color support mismatch: The new validation only permits predefined palette constants, hex, and rgb/rgba formats,
but other newly added docs/types imply named CSS colors may be allowed, creating a gap
where user-provided input may be incorrectly rejected or inconsistently handled across
components.

Referred Code
function isValidColor(color: string): boolean {
  if (
    (SCORECARD_THRESHOLD_RULE_COLOR_VALUES as readonly string[]).includes(color)
  ) {
    return true;
  }

  // Check for hex color format: #RGB, #RRGGBB, #RRGGBBAA
  const hexColorRegex = /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/;
  if (hexColorRegex.test(color)) {
    return true;
  }

  // Check for RGB color format: rgb(r, g, b)
  const rgbRegex = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
  if (rgbRegex.test(color)) {
    return true;
  }

  // Check for RGBA color format: rgba(r, g, b, a)
  const rgbaRegex =


 ... (clipped 30 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
RGB/RGBA bounds unchecked: isValidColor accepts rgb()/rgba() values without enforcing component bounds (0-255) and
alpha bounds (0-1), which can lead to unexpected acceptance of invalid configurations and
inconsistent UI behavior.

Referred Code
// Check for RGB color format: rgb(r, g, b)
const rgbRegex = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
if (rgbRegex.test(color)) {
  return true;
}

// Check for RGBA color format: rgba(r, g, b, a)
const rgbaRegex =
  /^rgba\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d+(?:\.\d+)?\s*\)$/;
return rgbaRegex.test(color);

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@rhdh-qodo-merge

Copy link
Copy Markdown

PR Code Suggestions ✨

No code suggestions found for the PR.

@Eswaraiahsapram Eswaraiahsapram left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @dzemanov, 🎉

I tested it in the local environment and reviewed the frontend changes, everything looks good to me.

/lgtm

Image Image Image

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@dzemanov
dzemanov force-pushed the scorecard-threshold-keys-colors branch from 24069be to b6580a1 Compare February 25, 2026 14:37
@dzemanov

Copy link
Copy Markdown
Member Author

Rebased to resolve conflicts.

Comment thread workspaces/scorecard/plugins/scorecard/src/components/Scorecard/CustomLegend.tsx Outdated
Comment thread workspaces/scorecard/plugins/scorecard-backend/src/service/mappers.ts Outdated
Comment thread workspaces/scorecard/plugins/scorecard/src/components/Scorecard/CustomLegend.tsx Outdated
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Comment thread workspaces/scorecard/plugins/scorecard-backend/knexfile.js
dzemanov added 5 commits March 2, 2026 13:30
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@imykhno

imykhno commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Thank you for this great PR!
I was tested this changes locally and all works as expected. I attach screenshot with data I tested:

Local testing
Local testing homegae

/lgtm

@dzemanov

dzemanov commented Mar 4, 2026

Copy link
Copy Markdown
Member Author

/hold

Fixing usage of custom threshold keys in annotations

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
@openshift-ci openshift-ci Bot removed the lgtm label Mar 4, 2026
@sonarqubecloud

sonarqubecloud Bot commented Mar 4, 2026

Copy link
Copy Markdown

@imykhno

imykhno commented Mar 4, 2026

Copy link
Copy Markdown
Contributor

Thank you. I also tested the latest changes and they fixed the custom threshold keys in annotations
/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Mar 4, 2026
@dzemanov
dzemanov merged commit 7062658 into redhat-developer:main Mar 4, 2026
9 checks passed
rohitratannagar pushed a commit to rohitratannagar/rhdh-plugins that referenced this pull request Mar 12, 2026
)

* Enable custom threshold keys

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Introduce custom colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Introduce SCORECARD_THRESHOLD_RULE_COLOR_VALUES

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Unify scorecard error colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Simplify resolveStatusColor

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use constant for error state color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Unify scorecard error state handling for color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Custom threshold keys must specify color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update docs

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update reports

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix issues

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix e2e

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Rename to fix case

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update docs

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Split utils

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use statusCounts as record

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use error state color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update to include also PG config

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Prefer Number.parseInt

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix rgba colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Simplify DbAggregatedMetric computation

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix overriding custom keys by entity annotations

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

---------

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Co-authored-by: Eswaraiah Sapram <esapram@redhat.com>
asmasarw pushed a commit to asmasarw/rhdh-plugins that referenced this pull request Mar 12, 2026
)

* Enable custom threshold keys

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Introduce custom colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Introduce SCORECARD_THRESHOLD_RULE_COLOR_VALUES

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Unify scorecard error colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Simplify resolveStatusColor

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use constant for error state color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Unify scorecard error state handling for color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Custom threshold keys must specify color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update docs

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update reports

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix issues

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix e2e

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Rename to fix case

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update docs

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Split utils

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use statusCounts as record

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Use error state color

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Update to include also PG config

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Prefer Number.parseInt

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix rgba colors

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Simplify DbAggregatedMetric computation

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

* Fix overriding custom keys by entity annotations

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>

---------

Signed-off-by: Dominika Zemanovicova <dzemanov@redhat.com>
Co-authored-by: Eswaraiah Sapram <esapram@redhat.com>
@dzemanov
dzemanov deleted the scorecard-threshold-keys-colors branch June 16, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants