update to just use one hooks and one component grid in ownership card

Signed-off-by: Prasetya Aria Wibawa <prasetya.wibawa@grabtaxi.com>
This commit is contained in:
Prasetya Aria Wibawa
2022-03-24 14:20:20 +07:00
parent 69d652f3cc
commit 103b72b704
5 changed files with 82 additions and 352 deletions
@@ -28,7 +28,7 @@ import {
import React from 'react';
import pluralize from 'pluralize';
import { catalogIndexRouteRef } from '../../../routes';
import { useAggregatedEntities } from './useAggregatedEntities';
import { useGetEntities } from './useGetEntities';
const useStyles = makeStyles((theme: BackstageTheme) =>
createStyles({
@@ -86,16 +86,19 @@ const EntityCountTile = ({
};
// can only be used for group entity
export const AggregatedRelationsGrid = ({
export const ComponentsGrid = ({
entity,
relationsType,
entityFilterKind,
}: {
entity: Entity;
relationsType: string;
entityFilterKind?: string[];
}) => {
const catalogLink = useRouteRef(catalogIndexRouteRef);
const { componentsWithCounters, loading, error } = useAggregatedEntities(
const { componentsWithCounters, loading, error } = useGetEntities(
entity,
relationsType,
entityFilterKind,
);
@@ -1,122 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 { Entity } from '@backstage/catalog-model';
import { Link, Progress, ResponseErrorPanel } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import {
Box,
createStyles,
Grid,
makeStyles,
Typography,
} from '@material-ui/core';
import React from 'react';
import pluralize from 'pluralize';
import { catalogIndexRouteRef } from '../../../routes';
import { useDirectEntities } from './useDirectEntities';
const useStyles = makeStyles((theme: BackstageTheme) =>
createStyles({
card: {
border: `1px solid ${theme.palette.divider}`,
boxShadow: theme.shadows[2],
borderRadius: '4px',
padding: theme.spacing(2),
color: '#fff',
transition: `${theme.transitions.duration.standard}ms`,
'&:hover': {
boxShadow: theme.shadows[4],
},
},
bold: {
fontWeight: theme.typography.fontWeightBold,
},
entityTypeBox: {
background: (props: { type: string }) =>
theme.getPageTheme({ themeId: props.type }).backgroundImage,
},
}),
);
const EntityCountTile = ({
counter,
type,
name,
url,
}: {
counter: number;
type: string;
name: string;
url: string;
}) => {
const classes = useStyles({ type });
return (
<Link to={url} variant="body2">
<Box
className={`${classes.card} ${classes.entityTypeBox}`}
display="flex"
flexDirection="column"
alignItems="center"
>
<Typography className={classes.bold} variant="h6">
{counter}
</Typography>
<Typography className={classes.bold} variant="h6">
{pluralize(name, counter)}
</Typography>
</Box>
</Link>
);
};
export const DirectRelationsGrid = ({
entity,
entityFilterKind,
}: {
entity: Entity;
entityFilterKind?: string[];
}) => {
const catalogLink = useRouteRef(catalogIndexRouteRef);
const { componentsWithCounters, loading, error } = useDirectEntities(
entity,
entityFilterKind,
);
if (loading) {
return <Progress />;
} else if (error) {
return <ResponseErrorPanel error={error} />;
}
return (
<Grid container>
{componentsWithCounters?.map(c => (
<Grid item xs={6} md={6} lg={4} key={c.name}>
<EntityCountTile
counter={c.counter}
type={c.type}
name={c.name}
url={`${catalogLink()}/?${c.queryParams}`}
/>
</Grid>
))}
</Grid>
);
};
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { InfoCard, InfoCardVariants } from '@backstage/core-components';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
@@ -27,8 +26,7 @@ import {
Tooltip,
} from '@material-ui/core';
import React, { useState } from 'react';
import { DirectRelationsGrid } from './DirectRelationsGrid';
import { AggregatedRelationsGrid } from './AggregatedRelationsGrid';
import { ComponentsGrid } from './ComponentsGrid';
const useStyles = makeStyles(theme => ({
list: {
@@ -53,24 +51,6 @@ const useStyles = makeStyles(theme => ({
},
}));
const directRelationsGrid = (entity: Entity, entityFilterKind?: string[]) => {
return (
<DirectRelationsGrid entity={entity} entityFilterKind={entityFilterKind} />
);
};
const aggregatedRelationsGrid = (
entity: Entity,
entityFilterKind?: string[],
) => {
return (
<AggregatedRelationsGrid
entity={entity}
entityFilterKind={entityFilterKind}
/>
);
};
export const OwnershipCard = ({
variant,
entityFilterKind,
@@ -82,10 +62,6 @@ export const OwnershipCard = ({
const { entity } = useEntity();
const isGroup = entity.kind === 'Group';
const [relationsType, setRelationsType] = useState('direct');
const renderedGrid =
relationsType !== 'direct' && isGroup
? aggregatedRelationsGrid(entity, entityFilterKind)
: directRelationsGrid(entity, entityFilterKind);
return (
<InfoCard title="Ownership" variant={variant}>
@@ -118,7 +94,11 @@ export const OwnershipCard = ({
</ListItemSecondaryAction>
</ListItem>
</List>
{renderedGrid}
<ComponentsGrid
entity={entity}
relationsType={relationsType}
entityFilterKind={entityFilterKind}
/>
</InfoCard>
);
};
@@ -1,183 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* 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 {
Entity,
RELATION_PARENT_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
catalogApiRef,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import limiterFactory from 'p-limit';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import qs from 'qs';
const limiter = limiterFactory(10);
type EntityTypeProps = {
kind: string;
type: string;
count: number;
};
const getQueryParams = (
ownerEntitiesRef: string[],
selectedEntity: EntityTypeProps,
): string => {
const { kind, type } = selectedEntity;
// removing 'group:default/' from the string entity ref 'group:default/team-a'
const owners = ownerEntitiesRef.map(owner => owner.split('/')[1]);
const filters = {
kind,
type,
owners,
user: 'all',
};
const queryParams = qs.stringify({
filters,
});
return queryParams;
};
export function useAggregatedEntities(
entity: Entity,
entityFilterKind?: string[],
): {
componentsWithCounters:
| {
counter: number;
type: string;
name: string;
queryParams: string;
}[]
| undefined;
loading: boolean;
error?: Error;
} {
const catalogApi = useApi(catalogApiRef);
const requestedEntities: Entity[] = [];
const outstandingEntities = new Map<string, Promise<Entity | undefined>>();
const processedEntities = new Set<string>();
requestedEntities.push(entity);
let currentEntity = entity;
const kinds = entityFilterKind ?? ['Component', 'API', 'System'];
const {
loading,
error,
value: componentsWithCounters,
} = useAsync(async () => {
while (requestedEntities.length > 0) {
const childRelations = getEntityRelations(
currentEntity,
RELATION_PARENT_OF,
{
kind: 'Group',
},
);
await Promise.all(
childRelations.map(childGroup =>
limiter(async () => {
const promise = catalogApi.getEntityByRef(childGroup);
outstandingEntities.set(childGroup.name, promise);
try {
const processedEntity = await promise;
if (processedEntity) {
requestedEntities.push(processedEntity);
}
} finally {
outstandingEntities.delete(childGroup.name);
}
}),
),
);
requestedEntities.shift();
processedEntities.add(
stringifyEntityRef({
kind: currentEntity.kind,
namespace: currentEntity.metadata.namespace,
name: currentEntity.metadata.name,
}),
);
// always set currentEntity to the first element of array requestedEntities
currentEntity = requestedEntities[0];
}
const owners = Array.from(processedEntities);
const ownedAggregationEntitiesList = await catalogApi.getEntities({
filter: [
{
kind: kinds,
'relations.ownedBy': owners,
},
],
fields: [
'kind',
'metadata.name',
'metadata.namespace',
'spec.type',
'relations',
],
});
const counts = ownedAggregationEntitiesList.items.reduce(
(acc: EntityTypeProps[], ownedEntity) => {
const match = acc.find(
x =>
x.kind === ownedEntity.kind &&
x.type === (ownedEntity.spec?.type ?? ownedEntity.kind),
);
if (match) {
match.count += 1;
} else {
acc.push({
kind: ownedEntity.kind,
type: ownedEntity.spec?.type?.toString() ?? ownedEntity.kind,
count: 1,
});
}
return acc;
},
[],
);
// 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(owners, topOwnedEntity),
})) as Array<{
counter: number;
type: string;
name: string;
queryParams: string;
}>;
}, [catalogApi, entity]);
return {
componentsWithCounters,
loading,
error,
};
}
@@ -17,16 +17,20 @@
import {
Entity,
RELATION_MEMBER_OF,
RELATION_PARENT_OF,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import {
CatalogApi,
catalogApiRef,
getEntityRelations,
humanizeEntityRef,
} from '@backstage/plugin-catalog-react';
import qs from 'qs';
import limiterFactory from 'p-limit';
import { useApi } from '@backstage/core-plugin-api';
import useAsync from 'react-use/lib/useAsync';
import qs from 'qs';
const limiter = limiterFactory(10);
type EntityTypeProps = {
kind: string;
@@ -35,24 +39,17 @@ type EntityTypeProps = {
};
const getQueryParams = (
ownerEntity: Entity,
ownersEntityRef: string[],
selectedEntity: EntityTypeProps,
): string => {
const ownerName = humanizeEntityRef(ownerEntity, { defaultKind: 'group' });
const { kind, type } = selectedEntity;
const owners = ownersEntityRef.map(owner => owner.split('/')[1]);
const filters = {
kind,
type,
owners: [ownerName],
owners,
user: 'all',
};
if (ownerEntity.kind === 'User') {
const ownerGroups = getEntityRelations(ownerEntity, RELATION_MEMBER_OF, {
kind: 'Group',
});
const ownerGroupsName = ownerGroups.map(ownerGroup => ownerGroup.name);
filters.owners = [...filters.owners, ...ownerGroupsName];
}
const queryParams = qs.stringify({
filters,
});
@@ -78,8 +75,59 @@ const getOwnersEntityRef = (owner: Entity): string[] => {
return owners;
};
export function useDirectEntities(
const getAggregatedOwnersEntityRef = async (
parentGroup: Entity,
catalogApi: CatalogApi,
): Promise<string[]> => {
const requestedEntities: Entity[] = [];
const outstandingEntities = new Map<string, Promise<Entity | undefined>>();
const processedEntities = new Set<string>();
requestedEntities.push(parentGroup);
let currentEntity = parentGroup;
while (requestedEntities.length > 0) {
const childRelations = getEntityRelations(
currentEntity,
RELATION_PARENT_OF,
{
kind: 'Group',
},
);
await Promise.all(
childRelations.map(childGroup =>
limiter(async () => {
const promise = catalogApi.getEntityByRef(childGroup);
outstandingEntities.set(childGroup.name, promise);
try {
const processedEntity = await promise;
if (processedEntity) {
requestedEntities.push(processedEntity);
}
} finally {
outstandingEntities.delete(childGroup.name);
}
}),
),
);
requestedEntities.shift();
processedEntities.add(
stringifyEntityRef({
kind: currentEntity.kind,
namespace: currentEntity.metadata.namespace,
name: currentEntity.metadata.name,
}),
);
// always set currentEntity to the first element of array requestedEntities
currentEntity = requestedEntities[0];
}
return Array.from(processedEntities);
};
export function useGetEntities(
entity: Entity,
relationsType: string,
entityFilterKind?: string[],
): {
componentsWithCounters:
@@ -94,14 +142,18 @@ export function useDirectEntities(
error?: Error;
} {
const catalogApi = useApi(catalogApiRef);
const kinds = entityFilterKind ?? ['Component', 'API', 'System'];
const isGroup = entity.kind === 'Group';
const {
loading,
error,
value: componentsWithCounters,
} = useAsync(async () => {
const kinds = entityFilterKind ?? ['Component', 'API', 'System'];
const owners = getOwnersEntityRef(entity);
const owners =
relationsType === 'aggregated' && isGroup
? await getAggregatedOwnersEntityRef(entity, catalogApi)
: getOwnersEntityRef(entity);
const ownedEntitiesList = await catalogApi.getEntities({
filter: [
{
@@ -146,14 +198,14 @@ export function useDirectEntities(
counter: topOwnedEntity.count,
type: topOwnedEntity.type,
name: topOwnedEntity.type.toLocaleUpperCase('en-US'),
queryParams: getQueryParams(entity, topOwnedEntity),
queryParams: getQueryParams(owners, topOwnedEntity),
})) as Array<{
counter: number;
type: string;
name: string;
queryParams: string;
}>;
}, [catalogApi, entity]);
}, [catalogApi, entity, relationsType]);
return {
componentsWithCounters,