feat: Allow logical conditions on scorecards (#514)

Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
Gustaf Räntilä 2024-11-13 21:53:38 +01:00 committed by GitHub
parent 066b3bdb93
commit 331daba36b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 84 additions and 24 deletions

View File

@ -0,0 +1,5 @@
---
'@backstage-community/plugin-tech-insights-common': patch
---
Cache identical API calls for a few seconds. This prevents fetching the same checks multiple times when having several Scorecards with the same (or all) checks, although with different filters.

View File

@ -0,0 +1,5 @@
---
'@backstage-community/plugin-tech-insights': patch
---
Added optional `filter` prop to `EntityTechInsightsScorecardContent` and `EntityTechInsightsScorecardCard` for easier and more flexible filtering of what checks to display.

View File

@ -66,6 +66,7 @@
"@backstage-community/plugin-tech-insights": "workspace:^",
"@backstage-community/plugin-tech-insights-backend": "workspace:^",
"@backstage-community/plugin-tech-insights-backend-module-jsonfc": "workspace:^",
"fast-json-stable-stringify": "^2.1.0",
"knip": "^5.27.4"
}
}

View File

@ -62,6 +62,7 @@
"@backstage/errors": "^1.2.4",
"@backstage/types": "^1.1.1",
"@types/luxon": "^3.0.0",
"fast-json-stable-stringify": "^2.1.0",
"luxon": "^3.0.0",
"qs": "^6.12.3"
},

View File

@ -28,6 +28,7 @@ import {
} from '@backstage/catalog-model';
import qs from 'qs';
import { AuthService } from '@backstage/backend-plugin-api';
import stableStringify from 'fast-json-stable-stringify';
/**
* Client to fetch data from tech-insights backend
@ -36,6 +37,7 @@ import { AuthService } from '@backstage/backend-plugin-api';
export class TechInsightsClient {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi | AuthService;
private readonly apiCache = new Map<string, Promise<any>>();
constructor(options: {
discoveryApi: DiscoveryApi;
@ -124,28 +126,49 @@ export class TechInsightsClient {
});
}
private getCacheKey(path: string, init?: RequestInit): string {
return `${path} ${stableStringify(init ?? {})}`;
}
private async api<T>(path: string, init?: RequestInit): Promise<T> {
const url = await this.discoveryApi.getBaseUrl('tech-insights');
const token = await this.getToken();
const headers: HeadersInit = new Headers(init?.headers);
if (!headers.has('content-type'))
headers.set('content-type', 'application/json');
if (token && !headers.has('authorization')) {
headers.set('authorization', `Bearer ${token}`);
const cacheKey = this.getCacheKey(`${url}${path}`, init);
const cached = this.apiCache.get(cacheKey);
if (cached) {
return cached;
}
const request = new Request(`${url}${path}`, {
...init,
headers,
});
const result = (async () => {
const token = await this.getToken();
return fetch(request).then(async response => {
if (!response.ok) {
throw await ResponseError.fromResponse(response);
const headers: HeadersInit = new Headers(init?.headers);
if (!headers.has('content-type'))
headers.set('content-type', 'application/json');
if (token && !headers.has('authorization')) {
headers.set('authorization', `Bearer ${token}`);
}
return response.json() as Promise<T>;
});
const request = new Request(`${url}${path}`, {
...init,
headers,
});
return fetch(request).then(async response => {
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
});
})();
// Fill cache, and clear after 2 seconds
this.apiCache.set(cacheKey, result);
setTimeout(() => {
this.apiCache.delete(cacheKey);
}, 2000);
return result;
}
private async getToken(): Promise<string | null> {

View File

@ -57,6 +57,10 @@ You can pass an array `checksId` as a prop with the [Fact Retrievers ids](../tec
/>
```
You can also pass a `filter` function to both `EntityTechInsightsScorecardContent` and `EntityTechInsightsScorecardCard` which filters in/out check result after they have been fetched. This can be useful to filter by more logical conditions on fields like `id` or `name`, e.g. the first characters in a name.
To only show failed checks, you can pass the boolan `onlyFailed` to these components.
If you want to show checks in the overview of an entity use `EntityTechInsightsScorecardCard`.
```tsx

View File

@ -48,6 +48,7 @@ export const EntityTechInsightsScorecardCard: (props: {
title: string;
description?: string | undefined;
checksId?: string[] | undefined;
filter?: ((check: Check_2) => boolean) | undefined;
onlyFailed?: boolean | undefined;
expanded?: boolean | undefined;
}) => JSX_2.Element;
@ -57,6 +58,7 @@ export const EntityTechInsightsScorecardContent: (props: {
title: string;
description?: string | undefined;
checksId?: string[] | undefined;
filter?: ((check: Check_2) => boolean) | undefined;
}) => JSX_2.Element;
// @public @deprecated

View File

@ -22,15 +22,24 @@ import { ScorecardInfo } from '../ScorecardsInfo';
import { techInsightsApiRef } from '../../api';
import { useEntity } from '@backstage/plugin-catalog-react';
import { getCompoundEntityRef } from '@backstage/catalog-model';
import { Check } from '@backstage-community/plugin-tech-insights-common/client';
export const ScorecardsCard = (props: {
title: string;
description?: string;
checksId?: string[];
filter?: (check: Check) => boolean;
onlyFailed?: boolean;
expanded?: boolean;
}) => {
const { title, description, checksId, onlyFailed, expanded = true } = props;
const {
title,
description,
checksId,
filter,
onlyFailed,
expanded = true,
} = props;
const api = useApi(techInsightsApiRef);
const { entity } = useEntity();
const { value, loading, error } = useAsync(
@ -38,15 +47,18 @@ export const ScorecardsCard = (props: {
[api, entity, JSON.stringify(checksId)],
);
const checkResultRenderers = useMemo(() => {
if (!onlyFailed || !value) return {};
const filteredValues =
!filter || !value ? value : value.filter(val => filter(val.check));
const types = [...new Set(value.map(({ check }) => check.type))];
const checkResultRenderers = useMemo(() => {
if (!onlyFailed || !filteredValues) return {};
const types = [...new Set(filteredValues.map(({ check }) => check.type))];
const renderers = api.getCheckResultRenderers(types);
return Object.fromEntries(
renderers.map(renderer => [renderer.type, renderer]),
);
}, [api, value, onlyFailed]);
}, [api, filteredValues, onlyFailed]);
if (loading) {
return <Progress />;
@ -55,8 +67,8 @@ export const ScorecardsCard = (props: {
}
const filteredValue = !onlyFailed
? value || []
: (value || []).filter(val =>
? filteredValues || []
: (filteredValues || []).filter(val =>
checkResultRenderers[val.check.type]?.isFailed?.(val),
);

View File

@ -24,6 +24,7 @@ import { techInsightsApiRef } from '../../api/TechInsightsApi';
import { makeStyles } from '@material-ui/core/styles';
import { useEntity } from '@backstage/plugin-catalog-react';
import { getCompoundEntityRef } from '@backstage/catalog-model';
import { Check } from '@backstage-community/plugin-tech-insights-common/client';
const useStyles = makeStyles(() => ({
contentScorecards: {
@ -36,8 +37,9 @@ export const ScorecardsContent = (props: {
title: string;
description?: string;
checksId?: string[];
filter?: (check: Check) => boolean;
}) => {
const { title, description, checksId } = props;
const { title, description, checksId, filter } = props;
const classes = useStyles();
const api = useApi(techInsightsApiRef);
const { entity } = useEntity();
@ -46,6 +48,9 @@ export const ScorecardsContent = (props: {
async () => await api.runChecks({ namespace, kind, name }, checksId),
);
const filteredValues =
!filter || !value ? value : value.filter(val => filter(val.check));
if (loading) {
return <Progress />;
} else if (error) {
@ -59,7 +64,7 @@ export const ScorecardsContent = (props: {
title={title}
description={description}
entity={entity}
checkResults={value || []}
checkResults={filteredValues || []}
/>
</Content>
</Page>

View File

@ -2845,6 +2845,7 @@ __metadata:
"@backstage/errors": ^1.2.4
"@backstage/types": ^1.1.1
"@types/luxon": ^3.0.0
fast-json-stable-stringify: ^2.1.0
luxon: ^3.0.0
qs: ^6.12.3
languageName: unknown
@ -7370,6 +7371,7 @@ __metadata:
"@changesets/cli": ^2.27.1
"@spotify/prettier-config": ^15.0.0
concurrently: ^8.0.0
fast-json-stable-stringify: ^2.1.0
knip: ^5.27.4
node-gyp: ^10.0.0
prettier: ^2.3.2