Skip to content

Commit 71cfae4

Browse files
feat(scorecard): implement blueprint architecture for grouped layouts (#3282)
* feat(scorecard): implement blueprint architecture for grouped layouts * add changeset * enhanced with review suggestions * fix api reports * fix e2e * removed default config * add removed comments * address review suggestions * addressed fullsend ai review comments
1 parent d196266 commit 71cfae4

17 files changed

Lines changed: 964 additions & 11 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-scorecard': minor
3+
---
4+
5+
Add blueprint architecture for grouped scorecard layouts
6+
7+
Implement blueprint pattern enabling platform engineers to configure grouped scorecard metrics via app-config.yaml. Layout extensions are auto-discovered through Backstage's New Frontend System (NFS).

workspaces/scorecard/app-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ app:
2121
type: website
2222
- kind: api # e.g. any API entity
2323
- type: service # e.g. Component or System with spec.type: service
24+
2425
- home-page-layout:home/dynamic-homepage-layout:
2526
config:
2627
customizable: true
@@ -203,7 +204,6 @@ catalog:
203204
target: ../../examples/orgs/guest.yaml
204205
rules:
205206
- allow: [User, Group]
206-
207207
- type: url
208208
target: https://github.com/redhat-developer/rhdh/blob/main/catalog-entities/components/showcase.yaml
209209

workspaces/scorecard/plugins/scorecard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
"@backstage/core-app-api": "^1.19.6",
8282
"@backstage/dev-utils": "^1.1.21",
8383
"@backstage/frontend-defaults": "^0.5.0",
84+
"@backstage/frontend-test-utils": "^0.4.2",
8485
"@backstage/plugin-catalog": "^2.0.1",
8586
"@backstage/plugin-user-settings": "^0.9.1",
8687
"@backstage/test-utils": "^1.7.16",

workspaces/scorecard/plugins/scorecard/report-alpha.api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ const _default: OverridableFrontendPlugin<
5858
path?: string | undefined;
5959
};
6060
output:
61-
| ExtensionDataRef<string, 'core.routing.path', {}>
6261
| ExtensionDataRef<JSX_2.Element, 'core.reactElement', {}>
62+
| ExtensionDataRef<string, 'core.routing.path', {}>
6363
| ExtensionDataRef<
6464
RouteRef_2<AnyRouteRefParams>,
6565
'core.routing.ref',
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
ScorecardEntityContentLayoutBlueprint,
19+
scorecardLayoutTitleDataRef,
20+
} from './ScorecardLayoutBlueprint';
21+
import {
22+
createExtensionTester,
23+
renderInTestApp,
24+
} from '@backstage/frontend-test-utils';
25+
import { screen, waitFor } from '@testing-library/react';
26+
27+
describe('ScorecardEntityContentLayoutBlueprint', () => {
28+
it('should create an extension with sensible defaults', () => {
29+
expect(
30+
ScorecardEntityContentLayoutBlueprint.make({
31+
params: {
32+
title: 'Grid',
33+
loader: async () => () => <div />,
34+
},
35+
}),
36+
).toMatchInlineSnapshot(`
37+
{
38+
"$$type": "@backstage/ExtensionDefinition",
39+
"T": undefined,
40+
"attachTo": {
41+
"id": "entity-content:catalog/entity-content-scorecard",
42+
"input": "layouts",
43+
},
44+
"configSchema": {
45+
"parse": [Function],
46+
"schema": {
47+
"$schema": "http://json-schema.org/draft-07/schema#",
48+
"additionalProperties": false,
49+
"properties": {
50+
"groups": {
51+
"additionalProperties": {
52+
"additionalProperties": false,
53+
"properties": {
54+
"description": {
55+
"type": "string",
56+
},
57+
"metrics": {
58+
"items": {
59+
"type": "string",
60+
},
61+
"type": "array",
62+
},
63+
"title": {
64+
"type": "string",
65+
},
66+
},
67+
"required": [
68+
"title",
69+
"metrics",
70+
],
71+
"type": "object",
72+
},
73+
"default": {},
74+
"type": "object",
75+
},
76+
},
77+
"type": "object",
78+
},
79+
},
80+
"disabled": false,
81+
"factory": [Function],
82+
"if": undefined,
83+
"inputs": {},
84+
"kind": "scorecard-layout",
85+
"name": undefined,
86+
"output": [
87+
[Function],
88+
[Function],
89+
],
90+
"override": [Function],
91+
"toString": [Function],
92+
"version": "v2",
93+
}
94+
`);
95+
});
96+
97+
it('should create a named extension', () => {
98+
const extension = ScorecardEntityContentLayoutBlueprint.make({
99+
name: 'scorecard-entity-layout-grid',
100+
params: {
101+
title: 'Grid',
102+
loader: async () => () => <div />,
103+
},
104+
});
105+
106+
const extensionData = JSON.parse(JSON.stringify(extension));
107+
108+
expect(extensionData).toBeDefined();
109+
expect(extensionData.kind).toBe('scorecard-layout');
110+
expect(extensionData.name).toBe('scorecard-entity-layout-grid');
111+
expect(extensionData.attachTo).toEqual({
112+
id: 'entity-content:catalog/entity-content-scorecard',
113+
input: 'layouts',
114+
});
115+
});
116+
117+
it('should yield the layout title via scorecardLayoutTitleDataRef', () => {
118+
const extension = ScorecardEntityContentLayoutBlueprint.make({
119+
params: {
120+
title: 'Grid',
121+
loader: async () => () => <div />,
122+
},
123+
});
124+
125+
const tester = createExtensionTester(extension);
126+
const title = tester.get(scorecardLayoutTitleDataRef);
127+
128+
expect(title).toBe('Grid');
129+
});
130+
131+
it('should yield a different layout title for List', () => {
132+
const extension = ScorecardEntityContentLayoutBlueprint.make({
133+
params: {
134+
title: 'List',
135+
loader: async () => () => <div />,
136+
},
137+
});
138+
139+
const tester = createExtensionTester(extension);
140+
const title = tester.get(scorecardLayoutTitleDataRef);
141+
142+
expect(title).toBe('List');
143+
});
144+
145+
it('should render the loaded component as a react element', async () => {
146+
const MockLayout = () => <div data-testid="mock-layout">Mock Grid</div>;
147+
148+
const extension = ScorecardEntityContentLayoutBlueprint.make({
149+
params: {
150+
title: 'Grid',
151+
loader: async () => () => <MockLayout />,
152+
},
153+
});
154+
155+
const tester = createExtensionTester(extension);
156+
157+
renderInTestApp(tester.reactElement());
158+
159+
await waitFor(() => {
160+
expect(screen.getByTestId('mock-layout')).toBeInTheDocument();
161+
});
162+
});
163+
164+
it('should pass config groups to the loaded component', async () => {
165+
const MockLayout = (props: { groups: Record<string, any> }) => (
166+
<div data-testid="mock-layout">{JSON.stringify(props.groups)}</div>
167+
);
168+
169+
const extension = ScorecardEntityContentLayoutBlueprint.make({
170+
params: {
171+
title: 'Grid',
172+
loader: async () => MockLayout,
173+
},
174+
});
175+
176+
const groups = {
177+
quality: {
178+
title: 'Quality',
179+
description: 'Quality metrics',
180+
metrics: ['sonarqube-coverage', 'sonarqube-bugs'],
181+
},
182+
};
183+
184+
const tester = createExtensionTester(extension, { config: { groups } });
185+
186+
renderInTestApp(tester.reactElement());
187+
188+
await waitFor(() => {
189+
expect(screen.getByTestId('mock-layout')).toBeInTheDocument();
190+
expect(screen.getByTestId('mock-layout').textContent).toContain(
191+
'Quality',
192+
);
193+
});
194+
});
195+
196+
it('should default groups to empty object when no config is provided', async () => {
197+
const MockLayout = (props: { groups: Record<string, any> }) => (
198+
<div data-testid="mock-layout">
199+
{Object.keys(props.groups).length === 0 ? 'empty' : 'has-groups'}
200+
</div>
201+
);
202+
203+
const extension = ScorecardEntityContentLayoutBlueprint.make({
204+
params: {
205+
title: 'Grid',
206+
loader: async () => MockLayout,
207+
},
208+
});
209+
210+
const tester = createExtensionTester(extension);
211+
212+
renderInTestApp(tester.reactElement());
213+
214+
await waitFor(() => {
215+
expect(screen.getByTestId('mock-layout')).toHaveTextContent('empty');
216+
});
217+
});
218+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ComponentType, createElement } from 'react';
18+
import {
19+
coreExtensionData,
20+
createExtensionBlueprint,
21+
createExtensionDataRef,
22+
ExtensionBoundary,
23+
} from '@backstage/frontend-plugin-api';
24+
25+
/** @alpha */
26+
export interface ScorecardLayoutProps {
27+
groups: Record<
28+
string,
29+
{ title: string; description?: string; metrics: string[] }
30+
>;
31+
}
32+
33+
/**
34+
* Extension data ref carrying the human-readable layout title
35+
* (e.g. "Grid", "List") so the entity tab can build a toggle.
36+
*
37+
* @alpha
38+
*/
39+
export const scorecardLayoutTitleDataRef =
40+
createExtensionDataRef<string>().with({ id: 'scorecard.layout-title' });
41+
42+
/**
43+
* Blueprint for creating scorecard layout extensions.
44+
*
45+
* Each layout is a separate extension that attaches to the Scorecard entity tab.
46+
* Platform engineers enable/disable individual layouts via app-config.yaml;
47+
* when multiple layouts are enabled the tab renders a toggle to switch between them.
48+
*
49+
* @alpha
50+
*/
51+
export const ScorecardEntityContentLayoutBlueprint = createExtensionBlueprint({
52+
kind: 'scorecard-layout',
53+
attachTo: {
54+
id: 'entity-content:catalog/entity-content-scorecard',
55+
input: 'layouts',
56+
},
57+
output: [coreExtensionData.reactElement, scorecardLayoutTitleDataRef],
58+
dataRefs: {
59+
title: scorecardLayoutTitleDataRef,
60+
},
61+
config: {
62+
schema: {
63+
groups: schema =>
64+
schema
65+
.record(
66+
schema.object({
67+
title: schema.string(),
68+
description: schema.string().optional(),
69+
metrics: schema.array(schema.string()),
70+
}),
71+
)
72+
.optional()
73+
.default({}),
74+
},
75+
},
76+
*factory(
77+
params: {
78+
title: string;
79+
loader: () => Promise<ComponentType<ScorecardLayoutProps>>;
80+
},
81+
{ config, node },
82+
) {
83+
yield scorecardLayoutTitleDataRef(params.title);
84+
yield coreExtensionData.reactElement(
85+
ExtensionBoundary.lazy(node, async () => {
86+
const Component = await params.loader();
87+
return createElement(Component, {
88+
groups: config.groups,
89+
});
90+
}),
91+
);
92+
},
93+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export * from './ScorecardLayoutBlueprint';

0 commit comments

Comments
 (0)