Skip to content

Commit 8eac7f8

Browse files
committed
feat: allow overriding build info
1 parent aa1499a commit 8eac7f8

File tree

9 files changed

+205
-20
lines changed

9 files changed

+205
-20
lines changed

.ibm/pipelines/resources/config_map/app-config-rhdh.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,10 @@ argocd:
176176
token: temp
177177
permission:
178178
enabled: false
179+
180+
buildInfo:
181+
title: 'RHDH Build info'
182+
card:
183+
TechDocs builder: 'local'
184+
Authentication provider: 'Github'
185+
RBAC: disabled

docs/customization.md

+17
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,20 @@ app:
162162
If support is not configured, it would look as below.
163163

164164
![Example Support Not Configured](images/support-not-configured.png)
165+
166+
## Customizing Build info content in the user settings page
167+
168+
To customize the build info content in the user settings page, provide your content to the `buildInfo` field in the `app-config.yaml` file.
169+
170+
Example configurations:
171+
172+
```
173+
buildInfo:
174+
title: <Specify the title you want to display in the build info card.>
175+
card:
176+
<Specify the key-value pairs you want to display in the build info card.>
177+
<!--
178+
TechDocs builder: 'local'
179+
Authentication provider: 'Github'
180+
RBAC: disabled -->
181+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { test } from "@playwright/test";
2+
import { Common } from "../../utils/common";
3+
import { UIhelper } from "../../utils/ui-helper";
4+
import { UI_HELPER_ELEMENTS } from "../../support/pageObjects/global-obj";
5+
6+
test.describe("Test user settings info card", () => {
7+
let uiHelper: UIhelper;
8+
9+
test.beforeEach(async ({ page }) => {
10+
const common = new Common(page);
11+
await common.loginAsGuest();
12+
13+
uiHelper = new UIhelper(page);
14+
});
15+
16+
test("Check if customized build info is rendered", async ({ page }) => {
17+
await uiHelper.openSidebar("Settings");
18+
await uiHelper.verifyTextInSelector(
19+
UI_HELPER_ELEMENTS.MuiCardHeader,
20+
"RHDH Build info",
21+
);
22+
await uiHelper.verifyTextInSelector(
23+
UI_HELPER_ELEMENTS.MuiCard("RHDH Build info"),
24+
"TechDocs builder: local\nAuthentication provider: Github",
25+
);
26+
await page.getByTitle("Show more").click();
27+
await uiHelper.verifyTextInSelector(
28+
UI_HELPER_ELEMENTS.MuiCard("RHDH Build info"),
29+
"TechDocs builder: local\nAuthentication provider: Github\nRBAC: disabled",
30+
);
31+
});
32+
});

packages/app/config.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,10 @@ export interface Config {
187187
* @visibility frontend
188188
*/
189189
includeTransitiveGroupOwnership?: boolean;
190+
191+
/**
192+
* Allows you to customize RHDH Metadata card
193+
* @deepVisibility frontend
194+
*/
195+
buildInfo?: { title: string; card: { [key: string]: string } };
190196
}
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ConfigApi, createApiRef } from '@backstage/core-plugin-api';
2+
3+
export type BuildInfo = {
4+
title: string;
5+
card: { [key: string]: string };
6+
};
7+
8+
export interface BuildInfoApi {
9+
getBuildInfo(): Promise<BuildInfo>;
10+
}
11+
12+
export const buildInfoApiRef = createApiRef<BuildInfoApi>({
13+
id: 'internal.buildinfo',
14+
});
15+
16+
export type Options = {
17+
configApi: ConfigApi;
18+
};
19+
20+
export class BuildInfoApiClient implements BuildInfoApi {
21+
private readonly configApi: ConfigApi;
22+
23+
constructor(options: Options) {
24+
this.configApi = options.configApi;
25+
}
26+
27+
async getBuildInfo() {
28+
return this.configApi.getOptional('buildInfo') as BuildInfo;
29+
}
30+
}

packages/app/src/apis.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
oidcAuthApiRef,
1919
samlAuthApiRef,
2020
} from './api/AuthApiRefs';
21+
import { BuildInfoApiClient, buildInfoApiRef } from './api/BuildInfoApiClient';
2122
import {
2223
LearningPathApiClient,
2324
learningPathApiRef,
@@ -29,6 +30,11 @@ export const apis: AnyApiFactory[] = [
2930
deps: { configApi: configApiRef },
3031
factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi),
3132
}),
33+
createApiFactory({
34+
api: buildInfoApiRef,
35+
deps: { configApi: configApiRef },
36+
factory: ({ configApi }) => new BuildInfoApiClient({ configApi }),
37+
}),
3238
ScmAuth.createDefaultApiFactory(),
3339
createApiFactory({
3440
api: learningPathApiRef,

packages/app/src/build-metadata.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"title": "RHDH Metadata",
3-
"card": [
4-
"RHDH Version: 1.6.0",
5-
"Backstage Version: 1.35.1",
6-
"Last Commit: repo @ 2025-01-31T14:15:06Z"
7-
]
3+
"card": {
4+
"RHDH Version": "1.6.0",
5+
"Backstage Version": "1.35.1",
6+
"Last Commit": "repo @ 2025-01-31T14:15:06Z"
7+
}
88
}
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,76 @@
1-
import { renderInTestApp } from '@backstage/test-utils';
1+
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
22

33
import { userEvent } from '@testing-library/user-event';
44

5+
import { BuildInfo, buildInfoApiRef } from '../../api/BuildInfoApiClient';
56
import { InfoCard } from './InfoCard';
67

8+
class MockBuildInfoApiClient {
9+
async getBuildInfo(): Promise<BuildInfo> {
10+
return {} as BuildInfo;
11+
}
12+
}
13+
14+
const mockBuildInfoApiClient = new MockBuildInfoApiClient();
15+
716
describe('InfoCard', () => {
817
it('should render essential versions by default', async () => {
9-
const renderResult = await renderInTestApp(<InfoCard />);
18+
const renderResult = await renderInTestApp(
19+
<TestApiProvider apis={[[buildInfoApiRef, mockBuildInfoApiClient]]}>
20+
<InfoCard />
21+
</TestApiProvider>,
22+
);
1023
expect(renderResult.getByText(/RHDH Version/)).toBeInTheDocument();
1124
expect(renderResult.getByText(/Backstage Version/)).toBeInTheDocument();
1225
});
1326

1427
it('should hide the build time by default and show it on click', async () => {
15-
const renderResult = await renderInTestApp(<InfoCard />);
28+
const renderResult = await renderInTestApp(
29+
<TestApiProvider apis={[[buildInfoApiRef, mockBuildInfoApiClient]]}>
30+
<InfoCard />
31+
</TestApiProvider>,
32+
);
1633
expect(renderResult.queryByText(/Last Commit/)).toBeNull();
1734
await userEvent.click(renderResult.getByText(/RHDH Version/));
1835
expect(renderResult.getByText(/Last Commit/)).toBeInTheDocument();
1936
});
37+
38+
it('should render the customized values when build info is configured', async () => {
39+
mockBuildInfoApiClient.getBuildInfo = async () => ({
40+
title: 'RHDH Build info',
41+
card: {
42+
'TechDocs builder': 'local',
43+
'Authentication provider': 'Github',
44+
RBAC: 'disabled',
45+
},
46+
});
47+
const renderResult = await renderInTestApp(
48+
<TestApiProvider apis={[[buildInfoApiRef, mockBuildInfoApiClient]]}>
49+
<InfoCard />
50+
</TestApiProvider>,
51+
);
52+
expect(renderResult.queryByText(/TechDocs builder/)).toBeInTheDocument();
53+
expect(
54+
renderResult.queryByText(/Authentication provider/),
55+
).toBeInTheDocument();
56+
await userEvent.click(renderResult.getByText(/TechDocs builder/));
57+
expect(renderResult.getByText(/RBAC/)).toBeInTheDocument();
58+
});
59+
60+
it('should fallback to default json if the customized card value is empty', async () => {
61+
mockBuildInfoApiClient.getBuildInfo = async () => ({
62+
title: 'RHDH Build info',
63+
card: {},
64+
});
65+
const renderResult = await renderInTestApp(
66+
<TestApiProvider apis={[[buildInfoApiRef, mockBuildInfoApiClient]]}>
67+
<InfoCard />
68+
</TestApiProvider>,
69+
);
70+
expect(
71+
renderResult.queryByText(/TechDocs builder/),
72+
).not.toBeInTheDocument();
73+
expect(renderResult.getByText(/RHDH Version/)).toBeInTheDocument();
74+
expect(renderResult.getByText(/Backstage Version/)).toBeInTheDocument();
75+
});
2076
});

packages/app/src/components/UserSettings/InfoCard.tsx

+43-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import React from 'react';
2+
import useAsync from 'react-use/lib/useAsync';
23

34
import {
45
InfoCard as BSInfoCard,
56
CopyTextButton,
67
} from '@backstage/core-components';
8+
import { useApi } from '@backstage/core-plugin-api';
79

810
import UnfoldLessIcon from '@mui/icons-material/UnfoldLess';
911
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';
1012
import IconButton from '@mui/material/IconButton';
1113
import Typography from '@mui/material/Typography';
1214

15+
import { buildInfoApiRef } from '../../api/BuildInfoApiClient';
1316
import buildMetadata from '../../build-metadata.json';
1417

1518
export const InfoCard = () => {
19+
const client = useApi(buildInfoApiRef);
20+
const { value: buildInfo } = useAsync(() => {
21+
return client.getBuildInfo();
22+
});
23+
1624
const [showBuildInformation, setShowBuildInformation] =
1725
React.useState<boolean>(
1826
() =>
@@ -32,24 +40,47 @@ export const InfoCard = () => {
3240
}
3341
};
3442

35-
let clipboardText = buildMetadata.title;
36-
if (buildMetadata.card?.length) {
43+
const buildDetailsTitle = () => {
44+
if (buildInfo?.title && Object.keys(buildInfo?.card ?? {}).length > 0) {
45+
return buildInfo.title;
46+
}
47+
if (!buildInfo?.title && Object.keys(buildInfo?.card ?? {}).length > 0) {
48+
// eslint-disable-next-line no-console
49+
console.warn(
50+
'Set a custom title for your build details to display your preferred title.',
51+
);
52+
return 'Build Metadata';
53+
}
54+
return buildMetadata.title;
55+
};
56+
57+
let clipboardText = buildDetailsTitle();
58+
const buildDetails = Object.entries(
59+
buildInfo?.card && Object.keys(buildInfo.card).length > 0
60+
? buildInfo.card
61+
: buildMetadata?.card || {},
62+
).map(([key, value]) => `${key}: ${value}`);
63+
if (buildDetails?.length) {
3764
clipboardText += '\n\n';
38-
buildMetadata.card.forEach(text => {
65+
buildDetails.forEach(text => {
3966
clipboardText += `${text}\n`;
4067
});
4168
}
4269

43-
const filteredCards = showBuildInformation
44-
? buildMetadata.card
45-
: buildMetadata.card.filter(
46-
text =>
47-
text.startsWith('RHDH Version') ||
48-
text.startsWith('Backstage Version'),
49-
);
70+
const filteredContent = () => {
71+
if (Object.keys(buildInfo?.card ?? {})?.length > 0) {
72+
return buildDetails.slice(0, 2);
73+
}
74+
return buildDetails.filter(
75+
text =>
76+
text.startsWith('RHDH Version') || text.startsWith('Backstage Version'),
77+
);
78+
};
79+
80+
const filteredCards = showBuildInformation ? buildDetails : filteredContent();
5081
// Ensure that we show always some information
5182
const versionInfo =
52-
filteredCards.length > 0 ? filteredCards.join('\n') : buildMetadata.card[0];
83+
filteredCards.length > 0 ? filteredCards.join('\n') : buildDetails[0];
5384

5485
/**
5586
* Show all build information and automatically select them
@@ -80,7 +111,7 @@ export const InfoCard = () => {
80111

81112
return (
82113
<BSInfoCard
83-
title={buildMetadata.title}
114+
title={buildDetailsTitle()}
84115
action={
85116
// This is a workaround to ensure that the buttons doesn't increase the header size.
86117
<div style={{ position: 'relative' }}>

0 commit comments

Comments
 (0)