diff --git a/plugins/org/src/components/Cards/OwnershipCard/AggregatedRelationsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx similarity index 93% rename from plugins/org/src/components/Cards/OwnershipCard/AggregatedRelationsGrid.tsx rename to plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx index 4839e4db56..e62013d688 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/AggregatedRelationsGrid.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/ComponentsGrid.tsx @@ -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, ); diff --git a/plugins/org/src/components/Cards/OwnershipCard/DirectRelationsGrid.tsx b/plugins/org/src/components/Cards/OwnershipCard/DirectRelationsGrid.tsx deleted file mode 100644 index a7cd521ecc..0000000000 --- a/plugins/org/src/components/Cards/OwnershipCard/DirectRelationsGrid.tsx +++ /dev/null @@ -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 ( - - - - {counter} - - - {pluralize(name, counter)} - - - - ); -}; - -export const DirectRelationsGrid = ({ - entity, - entityFilterKind, -}: { - entity: Entity; - entityFilterKind?: string[]; -}) => { - const catalogLink = useRouteRef(catalogIndexRouteRef); - - const { componentsWithCounters, loading, error } = useDirectEntities( - entity, - entityFilterKind, - ); - - if (loading) { - return ; - } else if (error) { - return ; - } - - return ( - - {componentsWithCounters?.map(c => ( - - - - ))} - - ); -}; diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 37541a786a..f188e6de01 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -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 ( - - ); -}; - -const aggregatedRelationsGrid = ( - entity: Entity, - entityFilterKind?: string[], -) => { - return ( - - ); -}; - 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 ( @@ -118,7 +94,11 @@ export const OwnershipCard = ({ - {renderedGrid} + ); }; diff --git a/plugins/org/src/components/Cards/OwnershipCard/useAggregatedEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useAggregatedEntities.ts deleted file mode 100644 index 52390cccef..0000000000 --- a/plugins/org/src/components/Cards/OwnershipCard/useAggregatedEntities.ts +++ /dev/null @@ -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>(); - const processedEntities = new Set(); - 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, - }; -} diff --git a/plugins/org/src/components/Cards/OwnershipCard/useDirectEntities.ts b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts similarity index 62% rename from plugins/org/src/components/Cards/OwnershipCard/useDirectEntities.ts rename to plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts index 6b8886a5e6..d8a269c868 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/useDirectEntities.ts +++ b/plugins/org/src/components/Cards/OwnershipCard/useGetEntities.ts @@ -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 => { + const requestedEntities: Entity[] = []; + const outstandingEntities = new Map>(); + const processedEntities = new Set(); + 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,