feat: Allow logical conditions on scorecards (#514)
Signed-off-by: Gustaf Räntilä <g.rantila@gmail.com>
This commit is contained in:
parent
066b3bdb93
commit
331daba36b
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue