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:
+6
-3
@@ -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,
|
||||
};
|
||||
}
|
||||
+70
-18
@@ -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,
|
||||
Reference in New Issue
Block a user