Skip to content

Commit

Permalink
feat(score-card): Read scoring json from annotation config (#201)
Browse files Browse the repository at this point in the history
Alternative way to dynamically set location of JSON file through the annotations field of catalog-info.yaml. To achieve this, configuration of scorecard/jsonDataUrl in app-config.yaml is optional but github.com/project-slug and scorecard/jsonDataUrl annotations are required within the catalog-info.yaml file.

---------

Co-authored-by: Scott Guymer <[email protected]>
  • Loading branch information
anand-tyagaraj and ScottGuymer authored Apr 26, 2024
1 parent 772af92 commit 35c2578
Show file tree
Hide file tree
Showing 22 changed files with 1,706 additions and 225 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-dancers-dream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@oriflame/backstage-plugin-score-card': minor
---

Alternative way to dynamically set location of JSON file through the annotations field of catalog-info.yaml. To achieve this, configuration of scorecard/jsonDataUrl alongside github.com/project-slug annotations is required within the catalog-info.yaml file.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"jest.jestCommandLine": "node_modules/.bin/jest --config node_modules/@backstage/cli/config/jest.js",

}
64 changes: 64 additions & 0 deletions packages/app/e2e-tests/entityScores.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2022 Oriflame (Based on https://github.com/RoadieHQ/roadie-backstage-plugins source copyrighted by Larder Software Limited)
*
* 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 { test, expect } from '@playwright/test';
import { failOnBrowserErrors } from '@backstage/e2e-test-utils/playwright';

failOnBrowserErrors();

test('Entity Score board: displays the score board based on sample data', async ({
page,
}) => {
page.on('load', p => {
p.evaluate(() =>
window.localStorage.setItem(
'@backstage/core:SignInPage:provider',
'guest',
),
);
});

await page.goto(
'/catalog/default/component/sample-service-entity-source/score',
);

await expect(page.getByText('Scoring')).toBeVisible();
await expect(page.getByText('Total score: Yellow')).toBeVisible();

await expect(
page.getByRole('cell', { name: 'Area: Code Green' }),
).toBeVisible();
await expect(
page.getByRole('cell', { name: 'Area: Documentation Red' }),
).toBeVisible();
await expect(
page.getByRole('cell', { name: 'Area: Operations Yellow' }),
).toBeVisible();
await expect(
page.getByRole('cell', { name: 'Area: Quality Red' }),
).toBeVisible();

await page
.getByRole('cell', { name: 'Area: Code Green' })
.getByRole('button')
.click();
await expect(page.getByText('hints: Gitflow: 100%')).toBeVisible();

const gitflowLinkHref = await page
.getByRole('link', { name: 'GitFlow' })
.getAttribute('href');

expect(gitflowLinkHref).toEqual('https://link-to-wiki/2157');
});
22 changes: 21 additions & 1 deletion packages/app/src/components/catalog/EntityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
* limitations under the License.
*/


import React from 'react';
import { Grid } from '@material-ui/core';
import {
Expand Down Expand Up @@ -54,6 +53,13 @@ const serviceEntityPage = (
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/score" title="Score">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12}>
<EntityScoreCardContent />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);

Expand All @@ -62,6 +68,13 @@ const websiteEntityPage = (
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/score" title="Score">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12}>
<EntityScoreCardContent />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);

Expand All @@ -70,6 +83,13 @@ const defaultEntityPage = (
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/score" title="Score">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12}>
<EntityScoreCardContent />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
);

Expand Down
29 changes: 29 additions & 0 deletions packages/entities/test-entity.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,32 @@ spec:
lifecycle: experimental
providesApis:
- sample-service-3
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: sample-service-entity-source
description: |
A service for testing the annotation location
annotations:
scorecard/jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/custom-annotation-location/service.json

spec:
type: service
owner: roadie
lifecycle: experimental
providesApis:
- sample-service-3
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: sample-system-entity-source
description: |
A system for testing the annotation location
annotations:
scorecard/jsonDataUrl: http://localhost:8090/plugins/score-card/sample-data/custom-annotation-location/system.json

spec:
owner: roadie
lifecycle: experimental
72 changes: 67 additions & 5 deletions plugins/score-card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Table that displays list of entities and their scores.

![Score card table](./docs/.assets/score-card-table.png)

| Props | type | Description | Default |
|--------------------|--------------------------------|-------------------------|--------------------------|
| `title` | string | Title of the table | Entities scores overview |
| `entityKindFilter` | [string[]](./src/api/types.ts) | Filter entities by kind | undefined  |
| Props | type | Description | Default |
| ------------------ | ------------------------------ | ----------------------- | ------------------------- |
| `title` | string | Title of the table | Entities scores overview  |
| `entityKindFilter` | [string[]](./src/api/types.ts) | Filter entities by kind | undefined  |

### ScoreCard

Expand All @@ -31,8 +31,33 @@ You may drill down to the details of each score together with explanation why it

![Score Card Detail](./docs/.assets/score-card-detail.png)

### EntityScoreCardTable

EntityScoreCardTable is component that allows you to see a table of results within an entity. This is useful for example at a system level to show the scores of all of the component within a system.

```tsx
<EntityLayout.Route path="/score" title="Score">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12}>
<EntityScoreCardTable />
</Grid>
</Grid>
</EntityLayout.Route>
```

![Entity Score card table](./docs/.assets/score-card-table.png)

| Props | type | Description | Default |
| ------------------ | ------------------------------ | ----------------------- | ------------------------- |
| `title` | string | Title of the table | Entities scores overview  |
| `entityKindFilter` | [string[]](./src/api/types.ts) | Filter entities by kind | undefined  |

### ScoringDataJsonClient

There are two approaches in configuring JSON file location

#### Configuring through app-config.yaml

Implementation of `ScoringDataApi` that the above components are using to load data. This client simply reads it from a JSON files located e.g. on a blob storage account.

The location of the JSON files may be configured in `app-config.yaml' like this:
Expand All @@ -49,11 +74,23 @@ In the above location it expects data in a format see [scoring data](./sample-da
```yaml
backend:
csp:
default-src: ["'self'", "raw.githubusercontent.com"]
default-src: ["'self'", 'raw.githubusercontent.com']
```

Also the server providing the data needs to have correctly configured CORS policy, i.e. return HTTP header `Access-Control-Allow-Origin` that should list domain from where you serve your backstage instance. See e.g. [how to configure CORS for Azure Blob Storage](https://learn.microsoft.com/en-us/rest/api/storageservices/cross-origin-resource-sharing--cors--support-for-the-azure-storage-services).

#### Configuring through catalog-info.yaml annotations

The JSON file can also be dynamically passed through the annotations field of `catalog-info.yaml`. To achieve this, configuration of the `scorecard/jsonDataUrl` annotations is required within the `catalog-info.yaml` file, demonstrated below:

```yaml
metadata:
annotations:
scorecard/jsonDataUrl: 'https://github.com/oriflame/backstage-plugins/blob/master/results.json'
```

**Important note**: The `results.json` file in the example above is inside a github repository. If you use private github repos you need to configure github authentication in your backstage instance. The users authentication token will then be used to retrieve the file automatically. You can use any other http location as well, but no authentication will be brokered for those.

### Configuration

All configuration options:
Expand Down Expand Up @@ -128,6 +165,31 @@ All configuration options:
);
```

5. If we want to have tabular Score board containing high level score of more than one component, we could add EntityScoreCardTable as shown below. Note that the difference between EntityScoreCardTable and ScoreCardTable is that EntityScoreCardTable works in the context of an Entity. That means that the Score JSON could also be read from the Component's catalog-info.yaml's Scorecard annotation as mentioned in [ Configuring through catalog-info.yaml annotations](#configuring-through-catalog-infoyaml-annotations)

Add EntityScoreCardTable to `packages/app/src/components/catalog/EntityPage.tsx` if you would like to view multiple component scores in tabular format:

```diff
+import { EntityScoreCardTable } from '@oriflame/backstage-plugin-score-card';
const systemPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
...
</EntityLayout.Route>
+
+ <EntityLayout.Route path="/score" title="Score">
+ <Grid container spacing={3} alignItems="stretch">
+ <Grid item xs={12}>
+ <EntityScoreCardTable />
+ </Grid>
+ </Grid>
+ </EntityLayout.Route>
+
</EntityLayoutWrapper>
);
```

## Scoring process

To find out a `score` for your service we follow this process:
Expand Down
7 changes: 5 additions & 2 deletions plugins/score-card/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"@backstage/config": "^1.1.1",
"@backstage/core-components": "^0.13.10",
"@backstage/core-plugin-api": "^1.8.2",
"@backstage/integration": "^1.9.0",
"@backstage/integration-react": "^1.1.24",
"@backstage/plugin-catalog-common": "^1.0.20",
"@backstage/plugin-catalog-react": "^1.9.3",
"@backstage/theme": "^0.5.0",
Expand All @@ -46,12 +48,13 @@
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@types/react": "^18.0.0",
"react-router-dom": "6.4.5",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-router-dom": "^6.4.3"
"react-router-dom": "^6.4.5"
},
"devDependencies": {
"@backstage/catalog-client": "^1.5.2",
Expand All @@ -69,7 +72,7 @@
"@types/react-dom": "*",
"cross-fetch": "3.1.5",
"http-server": "14.1.1",
"msw": "0.47.3"
"msw": "1.3.2"
},
"files": [
"dist",
Expand Down
Loading

0 comments on commit 35c2578

Please sign in to comment.