Merge pull request #6823 from prasaria/link-group-ownership-boxes-through-filtered-catalog-page

Link group ownership boxes through filtered catalog page
This commit is contained in:
Tim Hansen
2021-08-23 07:23:33 -06:00
committed by GitHub
4 changed files with 93 additions and 129 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Link group ownership boxes through filtered catalog page
+2 -1
View File
@@ -32,7 +32,8 @@
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-router-dom": "6.0.0-beta.0",
"react-use": "^17.2.4"
"react-use": "^17.2.4",
"qs": "^6.10.1"
},
"devDependencies": {
"@backstage/cli": "^0.7.9",
@@ -19,6 +19,7 @@ import {
CatalogApi,
catalogApiRef,
EntityProvider,
catalogRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import { queryByText } from '@testing-library/react';
@@ -123,31 +124,24 @@ describe('OwnershipCard', () => {
<OwnershipCard />
</EntityProvider>
</ApiProvider>,
{
mountedRoutes: {
'/create': catalogRouteRef,
},
},
);
expect(getByText('Services')).toBeInTheDocument();
expect(getByText('OPENAPI')).toBeInTheDocument();
expect(
queryByText(getByText('Services').parentElement!, '1'),
queryByText(getByText('OPENAPI').parentElement!, '1'),
).toBeInTheDocument();
expect(getByText('Documentation')).toBeInTheDocument();
expect(getByText('SERVICE')).toBeInTheDocument();
expect(
queryByText(getByText('Documentation').parentElement!, '0'),
queryByText(getByText('SERVICE').parentElement!, '1'),
).toBeInTheDocument();
expect(getByText('APIs')).toBeInTheDocument();
expect(getByText('LIBRARY')).toBeInTheDocument();
expect(
queryByText(getByText('APIs').parentElement!, '1'),
).toBeInTheDocument();
expect(getByText('Libraries')).toBeInTheDocument();
expect(
queryByText(getByText('Libraries').parentElement!, '1'),
).toBeInTheDocument();
expect(getByText('Websites')).toBeInTheDocument();
expect(
queryByText(getByText('Websites').parentElement!, '0'),
).toBeInTheDocument();
expect(getByText('Tools')).toBeInTheDocument();
expect(
queryByText(getByText('Tools').parentElement!, '0'),
queryByText(getByText('LIBRARY').parentElement!, '1'),
).toBeInTheDocument();
});
});
@@ -19,6 +19,7 @@ import {
catalogApiRef,
isOwnerOf,
useEntity,
catalogRouteRef,
} from '@backstage/plugin-catalog-react';
import { BackstageTheme, genPageTheme } from '@backstage/theme';
import {
@@ -26,28 +27,26 @@ import {
createStyles,
Grid,
makeStyles,
Tooltip,
Typography,
Link,
} from '@material-ui/core';
import React from 'react';
import { useAsync } from 'react-use';
import { generatePath } from 'react-router';
import qs from 'qs';
import {
InfoCard,
InfoCardVariants,
Progress,
ResponseErrorPanel,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
type EntitiesKinds = 'Component' | 'API';
type EntitiesTypes =
| 'service'
| 'website'
| 'library'
| 'documentation'
| 'api'
| 'tool';
type EntityTypeProps = {
kind: string;
type: string;
count: number;
};
const createPageTheme = (
theme: BackstageTheme,
@@ -75,69 +74,30 @@ const useStyles = makeStyles((theme: BackstageTheme) =>
bold: {
fontWeight: theme.typography.fontWeightBold,
},
service: {
background: createPageTheme(theme, 'home', 'service'),
},
website: {
background: createPageTheme(theme, 'home', 'website'),
},
library: {
background: createPageTheme(theme, 'home', 'library'),
},
documentation: {
background: createPageTheme(theme, 'home', 'documentation'),
},
api: {
background: createPageTheme(theme, 'home', 'home'),
},
tool: {
background: createPageTheme(theme, 'home', 'tool'),
entityTypeBox: {
background: (props: { type: string }) =>
createPageTheme(theme, props.type, props.type),
},
}),
);
const listEntitiesBy = (
entities: Array<Entity>,
kind: EntitiesKinds,
type?: EntitiesTypes,
) =>
entities.filter(
e => e.kind === kind && (type ? e?.spec?.type === type : true),
);
const countEntitiesBy = (
entities: Array<Entity>,
kind: EntitiesKinds,
type?: EntitiesTypes,
) => listEntitiesBy(entities, kind, type).length;
const EntityCountTile = ({
counter,
className,
entities,
type,
name,
url,
}: {
counter: number;
className: EntitiesTypes;
entities: Entity[];
type: string;
name: string;
url: string;
}) => {
let entityNames;
const classes = useStyles();
if (entities.length < 20) {
entityNames = entities.map(e => e.metadata.name).join(', ');
} else {
entityNames = `${entities
.map(e => e.metadata.name)
.slice(0, 20)
.join(', ')}, ...`;
}
const classes = useStyles({ type });
return (
<Tooltip title={entityNames} arrow>
<Link href={url} target="_blank" rel="noreferrer noopenner" variant="body2">
<Box
className={`${classes.card} ${classes[className]}`}
className={`${classes.card} ${classes.entityTypeBox}`}
display="flex"
flexDirection="column"
alignItems="center"
@@ -149,10 +109,28 @@ const EntityCountTile = ({
{name}
</Typography>
</Box>
</Tooltip>
</Link>
);
};
const getQueryParams = (
owner: Entity,
selectedEntity: EntityTypeProps,
): string => {
const ownerName = owner.metadata.name;
const { kind, type } = selectedEntity;
const queryParams = qs.stringify({
filters: {
kind,
type,
owners: ownerName,
user: 'all',
},
});
return queryParams;
};
export const OwnershipCard = ({
variant,
}: {
@@ -162,6 +140,8 @@ export const OwnershipCard = ({
}) => {
const { entity } = useEntity();
const catalogApi = useApi(catalogApiRef);
const catalogLink = useRouteRef(catalogRouteRef);
const {
loading,
error,
@@ -185,56 +165,40 @@ export const OwnershipCard = ({
isOwnerOf(entity, component),
);
return [
{
counter: countEntitiesBy(ownedEntitiesList, 'Component', 'service'),
className: 'service',
entities: listEntitiesBy(ownedEntitiesList, 'Component', 'service'),
name: 'Services',
const counts = ownedEntitiesList.reduce(
(acc: EntityTypeProps[], ownedEntity) => {
if (typeof ownedEntity.spec?.type !== 'string') return acc;
const match = acc.find(
x => x.kind === ownedEntity.kind && x.type === ownedEntity.spec?.type,
);
if (match) {
match.count += 1;
} else {
acc.push({
kind: ownedEntity.kind,
type: ownedEntity.spec?.type,
count: 1,
});
}
return acc;
},
{
counter: countEntitiesBy(
ownedEntitiesList,
'Component',
'documentation',
),
className: 'documentation',
entities: listEntitiesBy(
ownedEntitiesList,
'Component',
'documentation',
),
name: 'Documentation',
},
{
counter: countEntitiesBy(ownedEntitiesList, 'API'),
className: 'api',
entities: listEntitiesBy(ownedEntitiesList, 'API'),
name: 'APIs',
},
{
counter: countEntitiesBy(ownedEntitiesList, 'Component', 'library'),
className: 'library',
entities: listEntitiesBy(ownedEntitiesList, 'Component', 'library'),
name: 'Libraries',
},
{
counter: countEntitiesBy(ownedEntitiesList, 'Component', 'website'),
className: 'website',
entities: listEntitiesBy(ownedEntitiesList, 'Component', 'website'),
name: 'Websites',
},
{
counter: countEntitiesBy(ownedEntitiesList, 'Component', 'tool'),
className: 'tool',
entities: listEntitiesBy(ownedEntitiesList, 'Component', 'tool'),
name: 'Tools',
},
] as Array<{
[],
);
// Return top N (six) entities to be displayed in ownership boxes
const topN = counts.sort((a, b) => b.count - a.count).slice(0, 6);
return topN.map(topOwnedEntity => ({
counter: topOwnedEntity.count,
type: topOwnedEntity.type,
name: topOwnedEntity.type.toLocaleUpperCase('en-US'),
queryParams: getQueryParams(entity, topOwnedEntity),
})) as Array<{
counter: number;
className: EntitiesTypes;
entities: Entity[];
type: string;
name: string;
queryParams: string;
}>;
}, [catalogApi, entity]);
@@ -251,9 +215,9 @@ export const OwnershipCard = ({
<Grid item xs={6} md={6} lg={4} key={c.name}>
<EntityCountTile
counter={c.counter}
className={c.className}
entities={c.entities}
type={c.type}
name={c.name}
url={generatePath(`${catalogLink()}/?${c.queryParams}`)}
/>
</Grid>
))}