Skip to content
Open
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
5 changes: 5 additions & 0 deletions workspaces/scorecard/.changeset/dora-sparkline-frontend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
---

Add sparkline chart support for entity-page metrics whose `defaultVisualization` is `sparkline`. Renders a 30-day time-series area chart via `GET /metrics/catalog/:kind/:namespace/:name/time-series` and a "View data sources" dialog backed by `GET /metrics/:metricId/collectors`. Includes shared `SparklineChart` component, threshold legend, chart view-model utilities, and i18n for collector labels.
110 changes: 107 additions & 3 deletions workspaces/scorecard/plugins/scorecard/dev/legacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import type {
Metric,
EntityMetricDetailResponse,
AggregationMetadata,
MetricTimeSeriesResponse,
AggregatedMetricTimeSeriesResponse,
} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';
import { CatalogEntityPage } from '@backstage/plugin-catalog';

Expand All @@ -54,7 +56,12 @@ import {
} from '../src/plugin';
import { scorecardTranslations } from '../src/translations';
import { scorecardApiRef } from '../src/api';
import type { ScorecardApi } from '../src/api/types';
import type {
GetAggregationTimeSeriesOptions,
GetMetricTimeSeriesOptions,
ScorecardApi,
ScorecardOptions,
} from '../src/api/types';
import type { GetAggregatedScorecardEntitiesOptions } from '../src/components/types';
import {
mockAggregatedScorecardData,
Expand All @@ -63,7 +70,6 @@ import {
} from '../__fixtures__/scorecardData';
import { mockAggregatedScorecardEntitiesData } from '../__fixtures__/aggregatedScorecardEntitiesData';
import { mockCatalogApi } from './mocks';
import { ScorecardOptions } from '../src/api/types';

const mockComponentEntity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
Expand Down Expand Up @@ -95,8 +101,24 @@ class MockScorecardApi implements ScorecardApi {
}

async getAggregationMetadata(
_aggregationId: string,
aggregationId: string,
): Promise<AggregationMetadata> {
if (
aggregationId === 'avgDeploymentFrequency' ||
aggregationId.startsWith('dora.')
) {
return {
title: 'Average Deployment Frequency',
description:
'This KPI provides average weekly production deploys over a 30-day window per entity.',
type: 'number',
unit: '/week',
history: true,
visualization: 'sparkline',
aggregationType: 'average',
};
}

return {
title: 'GitHub open issues',
description: 'GitHub open issues',
Expand All @@ -105,6 +127,51 @@ class MockScorecardApi implements ScorecardApi {
};
}

async getAggregationTimeSeries({
aggregationId,
}: GetAggregationTimeSeriesOptions): Promise<AggregatedMetricTimeSeriesResponse> {
return {
id: aggregationId,
metricId: 'dora.deploymentFrequency',
metadata: {
title: 'Average Deployment Frequency',
description:
'This KPI provides average weekly production deploys over a 30-day window per entity.',
type: 'number',
unit: '/week',
history: true,
visualization: 'sparkline',
aggregationType: 'average',
},
points: [
{
value: 10,
successCount: 5,
errorCount: 0,
total: 5,
status: 'success',
timestamp: '2026-08-23T00:00:00.000Z',
},
{
value: 6.8,
successCount: 4,
errorCount: 3,
total: 7,
status: 'success',
timestamp: '2026-08-24T00:00:00.000Z',
},
],
thresholds: {
rules: [
{ key: 'elite', expression: '>=7', color: 'success.main' },
{ key: 'medium', expression: '1-7', color: 'warning.main' },
{ key: 'error', expression: '<1', color: 'error.main' },
],
},
aggregationChartDisplayColor: 'warning.main',
};
}

async getMetrics(_options: {
metricIds: string[];
}): Promise<{ metrics: Metric[] }> {
Expand All @@ -131,6 +198,43 @@ class MockScorecardApi implements ScorecardApi {
options.pageSize ?? 10,
) as EntityMetricDetailResponse;
}

async getMetricTimeSeries({
entity,
metricId,
}: GetMetricTimeSeriesOptions): Promise<MetricTimeSeriesResponse> {
return {
metricId,
entityRef: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`,
points: [
{ value: 8, timestamp: '2026-04-27T23:10:00.000Z' },
{ value: 7, timestamp: '2026-04-28T22:55:00.000Z' },
],
metadata: {
title: metricId,
description: '',
type: 'number',
history: true,
defaultVisualization: 'sparkline',
},
};
}

async getMetricCollectors(metricId: string) {
if (metricId.startsWith('dora.')) {
return [
{
id: 'github:deploymentWorkflowRuns',
description: 'Collects deployments from GitHub Actions.',
},
{
id: 'jira:incidents',
description: 'Collects Jira incidents.',
},
];
}
return [];
}
}

const ScorecardWrapper = ({ children }: { children: ReactNode }) => (
Expand Down
109 changes: 107 additions & 2 deletions workspaces/scorecard/plugins/scorecard/dev/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
type Metric,
type EntityMetricDetailResponse,
type AggregationMetadata,
type MetricTimeSeriesResponse,
type AggregatedMetricTimeSeriesResponse,
aggregationTypes,
} from '@red-hat-developer-hub/backstage-plugin-scorecard-common';

Expand All @@ -33,7 +35,12 @@ import {
mockScorecardSuccessData,
} from '../__fixtures__/scorecardData';
import { mockAggregatedScorecardEntitiesData } from '../__fixtures__/aggregatedScorecardEntitiesData';
import { ScorecardApi, ScorecardOptions } from '../src/api/types';
import {
ScorecardApi,
ScorecardOptions,
GetMetricTimeSeriesOptions,
GetAggregationTimeSeriesOptions,
} from '../src/api/types';

/** mock catalog entity so the Catalog shows one entity and the Scorecard tab can be opened. */
export const mockComponentEntity: Entity = {
Expand Down Expand Up @@ -97,8 +104,24 @@ export class MockScorecardApi implements ScorecardApi {
}

async getAggregationMetadata(
_aggregationId: string,
aggregationId: string,
): Promise<AggregationMetadata> {
if (
aggregationId === 'avgDeploymentFrequency' ||
aggregationId.startsWith('dora.')
) {
return {
title: 'Average Deployment Frequency',
description:
'This KPI provides average weekly production deploys over a 30-day window per entity.',
type: 'number',
unit: '/week',
history: true,
visualization: 'sparkline',
aggregationType: aggregationTypes.average,
};
}

return {
title: 'GitHub open issues',
description: 'GitHub open issues',
Expand All @@ -107,4 +130,86 @@ export class MockScorecardApi implements ScorecardApi {
aggregationType: aggregationTypes.statusGrouped,
};
}

async getAggregationTimeSeries({
aggregationId,
}: GetAggregationTimeSeriesOptions): Promise<AggregatedMetricTimeSeriesResponse> {
return {
id: aggregationId,
metricId: 'dora.deploymentFrequency',
metadata: {
title: 'Average Deployment Frequency',
description:
'This KPI provides average weekly production deploys over a 30-day window per entity.',
type: 'number',
unit: '/week',
history: true,
visualization: 'sparkline',
aggregationType: aggregationTypes.average,
},
points: [
{
value: 10,
successCount: 5,
errorCount: 0,
total: 5,
status: 'success',
timestamp: '2026-08-23T00:00:00.000Z',
},
{
value: 6.8,
successCount: 4,
errorCount: 3,
total: 7,
status: 'success',
timestamp: '2026-08-24T00:00:00.000Z',
},
],
thresholds: {
rules: [
{ key: 'elite', expression: '>=7', color: 'success.main' },
{ key: 'medium', expression: '1-7', color: 'warning.main' },
{ key: 'error', expression: '<1', color: 'error.main' },
],
},
aggregationChartDisplayColor: 'warning.main',
};
}

async getMetricTimeSeries({
entity,
metricId,
}: GetMetricTimeSeriesOptions): Promise<MetricTimeSeriesResponse> {
return {
metricId,
entityRef: `${entity.kind}:${entity.metadata.namespace}/${entity.metadata.name}`,
points: [
{ value: 8, timestamp: '2026-04-27T23:10:00.000Z' },
{ value: 7, timestamp: '2026-04-28T22:55:00.000Z' },
],
metadata: {
title: metricId,
description: '',
type: 'number',
history: true,
defaultVisualization: 'sparkline',
},
};
}

async getMetricCollectors(metricId: string) {
if (metricId.startsWith('dora.')) {
return [
{
id: 'github:deploymentWorkflowRuns',
description: 'Collects deployments from GitHub Actions.',
},
{
id: 'jira:incidents',
description: 'Collects Jira incidents.',
},
];
}
return [];
}
}
5 changes: 5 additions & 0 deletions workspaces/scorecard/plugins/scorecard/report-alpha.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ export const scorecardTranslationRef: TranslationRef<
readonly 'dataSourcesDialog.close': string;
readonly 'dataSourcesDialog.unknownPlugin': string;
readonly 'dataSourcesDialog.statusTooltip': string;
readonly 'dataSourcesDialog.collectorStatusTooltip': string;
readonly 'dataSourcesDialog.collectorEmptyValue': string;
readonly 'dataSourcesDialog.collectorUnavailableStatus': string;
readonly 'dataSourcesDialog.pluginGithub': string;
readonly 'dataSourcesDialog.pluginJira': string;
readonly 'dataSourcesDialog.columns.plugin': string;
readonly 'dataSourcesDialog.columns.check': string;
readonly 'dataSourcesDialog.columns.value': string;
Expand Down
5 changes: 5 additions & 0 deletions workspaces/scorecard/plugins/scorecard/report-legacy.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ export const scorecardTranslationRef: TranslationRef<
readonly 'dataSourcesDialog.close': string;
readonly 'dataSourcesDialog.unknownPlugin': string;
readonly 'dataSourcesDialog.statusTooltip': string;
readonly 'dataSourcesDialog.collectorStatusTooltip': string;
readonly 'dataSourcesDialog.collectorEmptyValue': string;
readonly 'dataSourcesDialog.collectorUnavailableStatus': string;
readonly 'dataSourcesDialog.pluginGithub': string;
readonly 'dataSourcesDialog.pluginJira': string;
readonly 'dataSourcesDialog.columns.plugin': string;
readonly 'dataSourcesDialog.columns.check': string;
readonly 'dataSourcesDialog.columns.value': string;
Expand Down
Loading
Loading