diff --git a/.changeset/silent-jars-complain.md b/.changeset/silent-jars-complain.md new file mode 100644 index 0000000000..b5027f1815 --- /dev/null +++ b/.changeset/silent-jars-complain.md @@ -0,0 +1,5 @@ +--- +'@oriflame/backstage-plugin-score-card': minor +--- + +Added config options to customize reviewer and review date display behavior diff --git a/plugins/score-card/config.d.ts b/plugins/score-card/config.d.ts index 8704db2e17..a47cb2d63d 100644 --- a/plugins/score-card/config.d.ts +++ b/plugins/score-card/config.d.ts @@ -24,6 +24,18 @@ export interface Config { * @visibility frontend */ jsonDataUrl?: string; + display?: { + /** + * Whether to display the reviewer column in the score card table. + * @visibility frontend + */ + reviewer?: string; + /** + * Whether to display the review date column in the score card table. + * @visibility frontend + */ + reviewDate?: string; + }; /** * The template for the link to the wiki, e.g. "https://TBD/XXX/_wiki/wikis/XXX.wiki/{id}" * @visibility frontend diff --git a/plugins/score-card/dev/app-config.yaml b/plugins/score-card/dev/app-config.yaml index dd03505bf2..5707982605 100644 --- a/plugins/score-card/dev/app-config.yaml +++ b/plugins/score-card/dev/app-config.yaml @@ -12,3 +12,6 @@ backend: scorecards: jsonDataUrl: http://127.0.0.1:8090/sample-data/ #this needs to be served via http-server as we get http 200 with WDS server instead of http 404 for nonExistent entity + display: + reviewer: always + reviewDate: always diff --git a/plugins/score-card/src/components/ScoreCard/ScoreCard.tsx b/plugins/score-card/src/components/ScoreCard/ScoreCard.tsx index dc144d18ef..9d67932b11 100644 --- a/plugins/score-card/src/components/ScoreCard/ScoreCard.tsx +++ b/plugins/score-card/src/components/ScoreCard/ScoreCard.tsx @@ -42,6 +42,7 @@ import { scorePercentColumn } from './columns/scorePercentColumn'; import { titleColumn } from './columns/titleColumn'; import { getReviewerLink } from './sub-components/getReviewerLink'; import { scoringDataApiRef } from '../../api'; +import { useDisplayConfig } from '../../config/DisplayConfig'; // lets prepare some styles const useStyles = makeStyles(theme => ({ @@ -122,6 +123,8 @@ export const ScoreCard = ({ const allEntries = getScoreTableEntries(data); + const displayPolicies = useDisplayConfig().getDisplayPolicies(); + return ( - {getReviewerLink(data)} + {getReviewerLink(data, displayPolicies)} )} diff --git a/plugins/score-card/src/components/ScoreCard/sub-components/getReviewerLink.tsx b/plugins/score-card/src/components/ScoreCard/sub-components/getReviewerLink.tsx index 58ed0b579f..cdcef0f32b 100644 --- a/plugins/score-card/src/components/ScoreCard/sub-components/getReviewerLink.tsx +++ b/plugins/score-card/src/components/ScoreCard/sub-components/getReviewerLink.tsx @@ -16,18 +16,35 @@ import { EntityRefLink } from '@backstage/plugin-catalog-react'; import React from 'react'; import { EntityScoreExtended } from '../../../api/types'; +import { DisplayPolicies, DisplayPolicy } from '../../../config/types'; + +export function getReviewerLink( + value: EntityScoreExtended, + displayPolicies: DisplayPolicies, +) { + const displayReviewer = displayPolicies.reviewer !== DisplayPolicy.Never; + const displayReviewDate = displayPolicies.reviewDate !== DisplayPolicy.Never; + + if (!displayReviewer && !displayReviewDate) { + return null; + } -export function getReviewerLink(value: EntityScoreExtended) { return (
{value.reviewer ? ( <> - Review done by  - - {value.reviewer?.name} - -  at  - {value.reviewDate ? value.reviewDate.toLocaleDateString() : 'unknown'} + Review done + {displayReviewer && ' by '} + {displayReviewer && ( + + {value.reviewer?.name} + + )} + {displayReviewDate && + ` at + ${(value.reviewDate + ? value.reviewDate.toLocaleDateString() + : 'unknown')}`} ) : ( <>Not yet reviewed. diff --git a/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.test.tsx b/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.test.tsx index 0207537e4f..0f12c64983 100644 --- a/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.test.tsx +++ b/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.test.tsx @@ -16,11 +16,11 @@ import React from 'react'; import { act, render } from '@testing-library/react'; import { ScoreCardTable } from './ScoreCardTable'; -import { TestApiProvider } from '@backstage/test-utils'; +import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; import { ScoringDataApi, scoringDataApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; import { EntityScoreExtended } from '../../api/types'; -import { errorApiRef } from '@backstage/core-plugin-api'; +import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { MemoryRouter as Router } from 'react-router-dom'; @@ -53,6 +53,7 @@ describe('ScoreBoardPage-EmptyData', () => { apis={[ [errorApiRef, errorApi], [scoringDataApiRef, mockClient], + [configApiRef, new MockConfigApi({})], ]} > @@ -68,6 +69,68 @@ describe('ScoreBoardPage-EmptyData', () => { await findByTestId('score-board-table'); jest.useRealTimers(); }); + + it.each([ + ['always', 1], + ['if-data-present', 0], + ['never', 0], + ])('should apply reviewer column display policy', async (displayPolicy, isPresent) => { + const errorApi = { post: () => {} }; + const displayPolicies = { reviewer: displayPolicy }; + const configApi = new MockConfigApi({ scorecards: {display: displayPolicies} }); + const { findByTestId, queryAllByText } = render( + + + + + + + , + ); + + await findByTestId('score-board-table'); + + const reviewerColumn = await queryAllByText('Reviewer') + + expect(reviewerColumn).toHaveLength(isPresent) + }); + + it.each([ + ['always', 1], + ['if-data-present', 0], + ['never', 0], + ])('should apply review date column display policy', async (displayPolicy, isPresent) => { + const errorApi = { post: () => {} }; + const displayPolicies = { reviewDate: displayPolicy }; + const configApi = new MockConfigApi({ scorecards: {display: displayPolicies} }); + const { findByTestId, queryAllByText } = render( + + + + + + + , + ); + + await findByTestId('score-board-table'); + + const reviewerColumn = await queryAllByText('Date') + + expect(reviewerColumn).toHaveLength(isPresent) + }); }); describe('ScoreCard-TestWithData', () => { @@ -119,6 +182,7 @@ describe('ScoreCard-TestWithData', () => { apis={[ [errorApiRef, errorApi], [scoringDataApiRef, mockClient], + [configApiRef, new MockConfigApi({})], ]} > @@ -135,4 +199,66 @@ describe('ScoreCard-TestWithData', () => { expect(podcastRow).toHaveTextContent('podcastsystemAB+DFFC'); }); + + it.each([ + ['always', 1], + ['if-data-present', 1], + ['never', 0], + ])('should apply reviewer column display policy', async (displayPolicy, isPresent) => { + const errorApi = { post: () => {} }; + const displayPolicies = { reviewer: displayPolicy }; + const configApi = new MockConfigApi({ scorecards: {display: displayPolicies} }); + const { findByTestId, queryAllByText } = render( + + + + + + + , + ); + + await findByTestId('score-board-table'); + + const reviewerColumn = await queryAllByText('Reviewer') + + expect(reviewerColumn).toHaveLength(isPresent) + }); + + it.each([ + ['always', 1], + ['if-data-present', 1], + ['never', 0], + ])('should apply review date column display policy', async (displayPolicy, isPresent) => { + const errorApi = { post: () => {} }; + const displayPolicies = { reviewDate: displayPolicy }; + const configApi = new MockConfigApi({ scorecards: {display: displayPolicies} }); + const { findByTestId, queryAllByText } = render( + + + + + + + , + ); + + await findByTestId('score-board-table'); + + const reviewerColumn = await queryAllByText('Date') + + expect(reviewerColumn).toHaveLength(isPresent) + }); }); diff --git a/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.tsx b/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.tsx index 1c61419528..98d96e7a73 100644 --- a/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.tsx +++ b/plugins/score-card/src/components/ScoreCardTable/ScoreCardTable.tsx @@ -24,6 +24,8 @@ import { scoringDataApiRef } from '../../api'; import { EntityScoreExtended } from '../../api/types'; import { EntityRefLink } from '@backstage/plugin-catalog-react'; import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { useDisplayConfig } from '../../config/DisplayConfig'; +import { DisplayPolicy } from '../../config/types'; const useScoringAllDataLoader = (entityKindFilter?: string[]) => { const errorApi = useApi(errorApiRef); @@ -49,6 +51,8 @@ type ScoreTableProps = { }; export const ScoreTable = ({ title, scores }: ScoreTableProps) => { + const displayPolicies = useDisplayConfig().getDisplayPolicies(); + const columns: TableColumn[] = [ { title: 'Name', @@ -85,7 +89,14 @@ export const ScoreTable = ({ title, scores }: ScoreTableProps) => { ) : null, }, - { + ]; + + if ( + displayPolicies.reviewer === DisplayPolicy.Always || + (displayPolicies.reviewer === DisplayPolicy.IfDataPresent && + scores.some(s => !!s.scoringReviewer)) + ) { + columns.push({ title: 'Reviewer', field: 'scoringReviewer', render: entityScore => @@ -96,16 +107,24 @@ export const ScoreTable = ({ title, scores }: ScoreTableProps) => { ) : null, - }, - { + }); + } + + if ( + displayPolicies.reviewDate === DisplayPolicy.Always || + (displayPolicies.reviewDate === DisplayPolicy.IfDataPresent && + scores.some(s => !!s.scoringReviewDate)) + ) { + columns.push({ title: 'Date', field: 'scoringReviewDate', render: entityScore => entityScore.reviewDate ? ( <>{entityScore.reviewDate.toLocaleDateString()} ) : null, - }, - ]; + }); + } + scores .flatMap(s => { return s.areaScores ?? []; diff --git a/plugins/score-card/src/config/DisplayConfig.test.ts b/plugins/score-card/src/config/DisplayConfig.test.ts new file mode 100644 index 0000000000..2d143a4328 --- /dev/null +++ b/plugins/score-card/src/config/DisplayConfig.test.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Oriflame + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockConfigApi } from '@backstage/test-utils'; +import { DisplayConfig } from './DisplayConfig'; +import { DisplayPolicy } from './types'; + +describe('display config', () => { + it.each([ + [ + { reviewer: 'always', reviewDate: 'always' }, + { reviewer: DisplayPolicy.Always, reviewDate: DisplayPolicy.Always }, + ], + [ + { reviewer: 'never', reviewDate: 'never' }, + { reviewer: DisplayPolicy.Never, reviewDate: DisplayPolicy.Never }, + ], + [ + { reviewer: 'if-data-present', reviewDate: 'if-data-present' }, + { + reviewer: DisplayPolicy.IfDataPresent, + reviewDate: DisplayPolicy.IfDataPresent, + }, + ], + [ + { reviewDate: 'if-data-present' }, + { + reviewer: DisplayPolicy.Always, + reviewDate: DisplayPolicy.IfDataPresent, + }, + ], + [ + { reviewer: 'never' }, + { reviewer: DisplayPolicy.Never, reviewDate: DisplayPolicy.Always }, + ], + [{}, { reviewer: DisplayPolicy.Always, reviewDate: DisplayPolicy.Always }], + ])('gets expected display policies from config', (policies, expected) => { + const mockConfig = new MockConfigApi({ scorecards: { display: policies } }); + + const config = new DisplayConfig({ configApi: mockConfig }); + + const displayPolicies = config.getDisplayPolicies(); + expect(displayPolicies).toEqual(expected); + }); +}); diff --git a/plugins/score-card/src/config/DisplayConfig.ts b/plugins/score-card/src/config/DisplayConfig.ts new file mode 100644 index 0000000000..608e561316 --- /dev/null +++ b/plugins/score-card/src/config/DisplayConfig.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2022 Oriflame + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigApi, configApiRef, useApi } from '@backstage/core-plugin-api'; +import { DisplayPolicies, DisplayPolicy } from './types'; + +export class DisplayConfig { + configApi: ConfigApi; + + constructor({ configApi }: { configApi: ConfigApi }) { + this.configApi = configApi; + } + + public getDisplayPolicies(): DisplayPolicies { + const displayConfig = + this.configApi.getOptionalConfig('scorecards.display'); + return { + reviewer: + (displayConfig?.getOptionalString('reviewer') as DisplayPolicy) ?? + DisplayPolicy.Always, + reviewDate: + (displayConfig?.getOptionalString('reviewDate') as DisplayPolicy) ?? + DisplayPolicy.Always, + }; + } +} + +export const useDisplayConfig = () => { + const configApi = useApi(configApiRef); + return new DisplayConfig({ configApi }); +}; diff --git a/plugins/score-card/src/config/types.ts b/plugins/score-card/src/config/types.ts new file mode 100644 index 0000000000..a20b47f26c --- /dev/null +++ b/plugins/score-card/src/config/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 Oriflame + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum DisplayPolicy { + IfDataPresent = 'if-data-present', + Never = 'never', + Always = 'always', +} + +export interface DisplayPolicies { + reviewer: DisplayPolicy; + reviewDate: DisplayPolicy; +}