Skip to content

Commit a94816d

Browse files
authored
Feature/custom link to wiki (#37)
Url for wiki can be now dynamically specified in app.config: ```yaml scorecards: wikiLinkTemplate: https://link-to-wiki/{id} ``` ![image](https://user-images.githubusercontent.com/16001792/191766246-e8489df7-897f-4542-bf9f-77bdf1c2c59f.png) #### ✔️ Checklist - [x] Added tests for new functionality and regression tests for bug fixes - [x] Added changeset (run `yarn changeset` in the root) - [x] Screenshots of before and after attached (for UI changes) - [x] Added or updated documentation (if applicable)
1 parent 201ac64 commit a94816d

File tree

10 files changed

+141
-31
lines changed

10 files changed

+141
-31
lines changed

.changeset/ninety-phones-juggle.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@oriflame/backstage-plugin-score-card': minor
3+
---
4+
5+
New config [wikiLinkTemplate]

app-config.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ scaffolder:
4747

4848
scorecards:
4949
jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/ #this is being served via http-server
50+
wikiLinkTemplate: https://link-to-wiki/{id}
5051

5152
catalog:
5253
rules:

packages/app/cypress/integration/scoreboard.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('score-card', () => {
7777
.should(
7878
'have.attr',
7979
'href',
80-
'https://TBD/XXX/_wiki/wikis/XXX.wiki/2157',
80+
'https://link-to-wiki/2157',
8181
);
8282
});
8383
});

plugins/score-card/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ scorecards:
3333
3434
In the above location it expects data in a format see [scoring data](https://github.com/backstage/backstage/tree/master/plugins/score-card/sample-data).
3535
36+
### Configuration
37+
38+
All configuration options:
39+
40+
- `jsonDataUrl`[optional]: url for the JSON data client, see [ScoringDataJsonClient](#scoringdatajsonclient).
41+
- `wikiLinkTemplate`: the template for the link to the wiki. You may use any existing properties from the `SystemScoreEntry`, e.g. `"https://TBD/XXX/_wiki/wikis/XXX.wiki/{id}"`.
42+
3643
### How to use the plugin
3744

3845
1. Add Score board to `packages/app/src/App.tsx`:

plugins/score-card/config.d.ts

+5
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,10 @@ export interface Config {
2424
* @visibility frontend
2525
*/
2626
jsonDataUrl?: string;
27+
/**
28+
* The template for the link to the wiki, e.g. "https://TBD/XXX/_wiki/wikis/XXX.wiki/{id}"
29+
* @visibility frontend
30+
*/
31+
wikiLinkTemplate?: string;
2732
};
2833
}

plugins/score-card/src/components/ScoreCard/ScoreCard.test.tsx

+11-6
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ import { TestApiProvider } from '@backstage/test-utils';
2020
import { ScoringDataApi, scoringDataApiRef } from '../../api';
2121
import { Entity } from '@backstage/catalog-model';
2222
import { SystemScoreExtended } from '../../api/types';
23-
import { errorApiRef } from '@backstage/core-plugin-api';
23+
import { configApiRef, errorApiRef } from '@backstage/core-plugin-api';
2424
import { lightTheme } from '@backstage/theme';
2525
import { ThemeProvider } from '@material-ui/core';
2626
import { EntityProvider } from '@backstage/plugin-catalog-react';
27+
import { ConfigReader } from '@backstage/core-app-api';
28+
29+
const sharedConfigApiMock = new ConfigReader({
30+
scorecards: { wikiLinkTemplate: 'https://mocked-wiki-url/{id}/{title}' },
31+
});
32+
const sharedErrorApi = { post: () => {} };
2733

2834
describe('ScoreCard-EmptyData', () => {
2935
class MockClient implements ScoringDataApi {
@@ -40,7 +46,6 @@ describe('ScoreCard-EmptyData', () => {
4046
throw new Error('Method not implemented.');
4147
}
4248
}
43-
4449
const mockClient = new MockClient();
4550

4651
const entity: Entity = {
@@ -54,13 +59,13 @@ describe('ScoreCard-EmptyData', () => {
5459
it('should render a progress bar', async () => {
5560
jest.useFakeTimers();
5661

57-
const errorApi = { post: () => {} };
5862
const { getByTestId, findByTestId } = render(
5963
<ThemeProvider theme={lightTheme}>
6064
<TestApiProvider
6165
apis={[
62-
[errorApiRef, errorApi],
66+
[errorApiRef, sharedErrorApi],
6367
[scoringDataApiRef, mockClient],
68+
[configApiRef, sharedConfigApiMock],
6469
]}
6570
>
6671
<EntityProvider entity={entity}>
@@ -110,13 +115,13 @@ describe('ScoreCard-TestWithData', () => {
110115
it('should render a progress bar', async () => {
111116
jest.useFakeTimers();
112117

113-
const errorApi = { post: () => {} };
114118
const { getByTestId, findByTestId } = render(
115119
<ThemeProvider theme={lightTheme}>
116120
<TestApiProvider
117121
apis={[
118-
[errorApiRef, errorApi],
122+
[errorApiRef, sharedErrorApi],
119123
[scoringDataApiRef, mockClient],
124+
[configApiRef, sharedConfigApiMock],
120125
]}
121126
>
122127
<EntityProvider entity={entity}>

plugins/score-card/src/components/ScoreCard/ScoreCard.tsx

+15-6
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
Table,
3030
TableColumn,
3131
} from '@backstage/core-components';
32-
import { errorApiRef, useApi } from '@backstage/core-plugin-api';
32+
import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api';
3333
import { scoreToColorConverter } from '../../helpers/scoreToColorConverter';
3434
import { getWarningPanel } from '../../helpers/getWarningPanel';
3535
import {
@@ -63,6 +63,7 @@ const useStyles = makeStyles(theme => ({
6363
const useScoringDataLoader = () => {
6464
const errorApi = useApi(errorApiRef);
6565
const scorigDataApi = useApi(scoringDataApiRef);
66+
const config = useApi(configApiRef);
6667
const { entity } = useEntity();
6768

6869
const { error, value, loading } = useAsync(
@@ -76,7 +77,11 @@ const useScoringDataLoader = () => {
7677
}
7778
}, [error, errorApi]);
7879

79-
return { loading, value, error };
80+
const wikiLinkTemplate =
81+
config.getOptionalString('scorecards.wikiLinkTemplate') ??
82+
'https://TBD/XXX/_wiki/wikis/XXX.wiki/{id}';
83+
84+
return { loading, value, wikiLinkTemplate, error };
8085
};
8186

8287
export const ScoreCard = ({
@@ -85,9 +90,13 @@ export const ScoreCard = ({
8590
entity?: Entity;
8691
variant?: InfoCardVariants;
8792
}) => {
88-
const { loading, error, value: data } = useScoringDataLoader();
89-
90-
// let's load the entity data from url defined in config
93+
// let's load the entity data from url defined in config etc
94+
const {
95+
loading,
96+
error,
97+
value: data,
98+
wikiLinkTemplate,
99+
} = useScoringDataLoader();
91100

92101
const classes = useStyles();
93102

@@ -105,7 +114,7 @@ export const ScoreCard = ({
105114
// let's define the main table columns
106115
const columns: TableColumn<SystemScoreTableEntry>[] = [
107116
areaColumn(data),
108-
titleColumn,
117+
titleColumn(wikiLinkTemplate),
109118
detailsColumn,
110119
scorePercentColumn,
111120
];

plugins/score-card/src/components/ScoreCard/columns/titleColumn.tsx

+23-18
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,27 @@ import { TableColumn } from '@backstage/core-components';
1818
import { Link } from '@material-ui/core';
1919
import React from 'react';
2020
import { SystemScoreTableEntry } from '../helpers/getScoreTableEntries';
21+
import { getWikiUrl } from '../helpers/getWikiUrl';
2122

22-
export const titleColumn: TableColumn<SystemScoreTableEntry> = {
23-
title: <div style={{ minWidth: '7rem' }}>Requirement</div>,
24-
field: 'title',
25-
grouping: false,
26-
width: '1%',
27-
render: systemScoreEntry => (
28-
<span>
29-
<Link
30-
href={`https://TBD/XXX/_wiki/wikis/XXX.wiki/${systemScoreEntry.id}`}
31-
target="_blank"
32-
data-id={systemScoreEntry.id}
33-
>
34-
{systemScoreEntry.title}
35-
</Link>
36-
{systemScoreEntry.isOptional ? ' (Optional)' : null}
37-
</span>
38-
),
39-
};
23+
export function titleColumn(
24+
wikiLinkTemplate: string,
25+
): TableColumn<SystemScoreTableEntry> {
26+
return {
27+
title: <div style={{ minWidth: '7rem' }}>Requirement</div>,
28+
field: 'title',
29+
grouping: false,
30+
width: '1%',
31+
render: systemScoreEntry => (
32+
<span>
33+
<Link
34+
href={getWikiUrl(wikiLinkTemplate, systemScoreEntry)}
35+
target="_blank"
36+
data-id={systemScoreEntry.id}
37+
>
38+
{systemScoreEntry.title}
39+
</Link>
40+
{systemScoreEntry.isOptional ? ' (Optional)' : null}
41+
</span>
42+
),
43+
};
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2022 Oriflame
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+
import { getWikiUrl } from './getWikiUrl';
17+
import { ScoreSuccessEnum, SystemScoreEntry } from '../../../api/types';
18+
19+
describe('helper-getWikiUrl', () => {
20+
const entry: SystemScoreEntry = {
21+
id: 123,
22+
title: 'some-score-title',
23+
isOptional: false,
24+
scorePercent: 50,
25+
scoreSuccess: ScoreSuccessEnum.Partial,
26+
scoreHints: 'score hints',
27+
details: 'lorem ipsum details',
28+
};
29+
30+
it('should replace known params', () => {
31+
const url: string = getWikiUrl('http://someurl/{id}/{title}', entry);
32+
expect(url).toEqual('http://someurl/123/some-score-title');
33+
});
34+
35+
it('should handle null', () => {
36+
const url: string = getWikiUrl('http://someurl/{id}/{title}', null);
37+
expect(url).toEqual('http://someurl//');
38+
});
39+
40+
it('should handle undefined', () => {
41+
const url: string = getWikiUrl('http://someurl/{id}/{title}', undefined);
42+
expect(url).toEqual('http://someurl//');
43+
});
44+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2022 Oriflame
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 { SystemScoreEntry } from '../../../api/types';
18+
19+
export function getWikiUrl(
20+
wikiLinkTemplate: string,
21+
entry: SystemScoreEntry | null | undefined,
22+
): string {
23+
if (!entry) return wikiLinkTemplate.replace(/\{[^\}]+\}/g, '');
24+
return wikiLinkTemplate.replace(/\{[^\}]+\}/g, matched => {
25+
const keyName = matched.substring(1, matched.length - 1);
26+
const value = entry[keyName as keyof SystemScoreEntry];
27+
return !value ? '' : value.toString();
28+
});
29+
}

0 commit comments

Comments
 (0)