transitive ownership of entities for group kind
Signed-off-by: Prasetya Aria Wibawa <prasetya.wibawa@grabtaxi.com>
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"pluralize": "^8.0.0",
|
||||
"qs": "^6.10.1",
|
||||
"p-limit": "^3.1.0",
|
||||
"react-router": "6.0.0-beta.0",
|
||||
"react-router-dom": "6.0.0-beta.0",
|
||||
"react-use": "^17.2.4"
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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,122 +14,61 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, UserEntity } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { InfoCard, InfoCardVariants } from '@backstage/core-components';
|
||||
import { useEntity } from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
InfoCard,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
Progress,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
humanizeEntityRef,
|
||||
isOwnerOf,
|
||||
useEntity,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
Box,
|
||||
createStyles,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
makeStyles,
|
||||
Typography,
|
||||
Switch,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import qs from 'qs';
|
||||
import React from 'react';
|
||||
import pluralize from 'pluralize';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogIndexRouteRef } from '../../../routes';
|
||||
import React, { useState } from 'react';
|
||||
import { DirectRelationsGrid } from './DirectRelationsGrid';
|
||||
import { TransitiveRelationsGrid } from './TransitiveRelationsGrid';
|
||||
|
||||
type EntityTypeProps = {
|
||||
kind: string;
|
||||
type: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
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],
|
||||
},
|
||||
const useStyles = makeStyles(theme => ({
|
||||
list: {
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
padding: `0 0 12px`,
|
||||
},
|
||||
bold: {
|
||||
fontWeight: theme.typography.fontWeightBold,
|
||||
},
|
||||
listItemText: {
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
paddingRight: 0,
|
||||
paddingLeft: 0,
|
||||
},
|
||||
entityTypeBox: {
|
||||
background: (props: { type: string }) =>
|
||||
theme.getPageTheme({ themeId: props.type }).backgroundImage,
|
||||
},
|
||||
listItemSecondaryAction: {
|
||||
[theme.breakpoints.down('xs')]: {
|
||||
width: '100%',
|
||||
top: 'auto',
|
||||
right: 'auto',
|
||||
position: 'relative',
|
||||
transform: 'unset',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const EntityCountTile = ({
|
||||
counter,
|
||||
type,
|
||||
name,
|
||||
url,
|
||||
}: {
|
||||
counter: number;
|
||||
type: string;
|
||||
name: string;
|
||||
url: string;
|
||||
}) => {
|
||||
const classes = useStyles({ type });
|
||||
},
|
||||
}));
|
||||
|
||||
const directRelationsGrid = (entity: Entity, entityFilterKind?: string[]) => {
|
||||
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>
|
||||
<DirectRelationsGrid entity={entity} entityFilterKind={entityFilterKind} />
|
||||
);
|
||||
};
|
||||
|
||||
const getQueryParams = (
|
||||
owner: Entity,
|
||||
selectedEntity: EntityTypeProps,
|
||||
): string => {
|
||||
const ownerName = humanizeEntityRef(owner, { defaultKind: 'group' });
|
||||
const { kind, type } = selectedEntity;
|
||||
const filters = {
|
||||
kind,
|
||||
type,
|
||||
owners: [ownerName],
|
||||
user: 'all',
|
||||
};
|
||||
if (owner.kind === 'User') {
|
||||
const user = owner as UserEntity;
|
||||
filters.owners = [...filters.owners, ...(user.spec.memberOf ?? [])];
|
||||
}
|
||||
const queryParams = qs.stringify(
|
||||
{
|
||||
filters,
|
||||
},
|
||||
{
|
||||
arrayFormat: 'repeat',
|
||||
},
|
||||
const transitiveRelationsGrid = (
|
||||
entity: Entity,
|
||||
entityFilterKind?: string[],
|
||||
) => {
|
||||
return (
|
||||
<TransitiveRelationsGrid
|
||||
entity={entity}
|
||||
entityFilterKind={entityFilterKind}
|
||||
/>
|
||||
);
|
||||
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
export const OwnershipCard = ({
|
||||
@@ -139,90 +78,46 @@ export const OwnershipCard = ({
|
||||
variant?: InfoCardVariants;
|
||||
entityFilterKind?: string[];
|
||||
}) => {
|
||||
const [relationsType, setRelationsType] = useState('direct');
|
||||
const classes = useStyles();
|
||||
const { entity } = useEntity();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const catalogLink = useRouteRef(catalogIndexRouteRef);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
value: componentsWithCounters,
|
||||
} = useAsync(async () => {
|
||||
const kinds = entityFilterKind ?? ['Component', 'API'];
|
||||
const entitiesList = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: kinds,
|
||||
},
|
||||
fields: [
|
||||
'kind',
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'spec.type',
|
||||
'relations',
|
||||
],
|
||||
});
|
||||
|
||||
const ownedEntitiesList = entitiesList.items.filter(component =>
|
||||
isOwnerOf(entity, component),
|
||||
);
|
||||
|
||||
const counts = ownedEntitiesList.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(entity, topOwnedEntity),
|
||||
})) as Array<{
|
||||
counter: number;
|
||||
type: string;
|
||||
name: string;
|
||||
queryParams: string;
|
||||
}>;
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
} else if (error) {
|
||||
return <ResponseErrorPanel error={error} />;
|
||||
}
|
||||
const isGroup = entity.kind === 'Group';
|
||||
const renderedGrid =
|
||||
relationsType !== 'direct' && isGroup
|
||||
? transitiveRelationsGrid(entity, entityFilterKind)
|
||||
: directRelationsGrid(entity, entityFilterKind);
|
||||
|
||||
return (
|
||||
<InfoCard title="Ownership" variant={variant}>
|
||||
<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>
|
||||
<List dense>
|
||||
<ListItem className={classes.list}>
|
||||
<ListItemText className={classes.listItemText} />
|
||||
<ListItemSecondaryAction className={classes.listItemSecondaryAction}>
|
||||
Direct Relations
|
||||
<Tooltip
|
||||
placement="top"
|
||||
arrow
|
||||
title={`${relationsType === 'direct' ? 'Direct' : 'Transitive'
|
||||
} Relations`}
|
||||
>
|
||||
<Switch
|
||||
color="primary"
|
||||
// checked={isPinned}
|
||||
onChange={() =>
|
||||
relationsType === 'direct'
|
||||
? setRelationsType('transitive')
|
||||
: setRelationsType('direct')
|
||||
}
|
||||
name="pin"
|
||||
inputProps={{ 'aria-label': 'Pin Sidebar Switch' }}
|
||||
disabled={!isGroup}
|
||||
/>
|
||||
</Tooltip>
|
||||
Transitive Relations
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
{renderedGrid}
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 { useTransitiveEntities } from './useTransitiveEntities';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
// can only be used for group entity
|
||||
export const TransitiveRelationsGrid = ({
|
||||
entity,
|
||||
entityFilterKind,
|
||||
}: {
|
||||
entity: Entity;
|
||||
entityFilterKind?: string[];
|
||||
}) => {
|
||||
const catalogLink = useRouteRef(catalogIndexRouteRef);
|
||||
const { componentsWithCounters, loading, error } = useTransitiveEntities(
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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, UserEntity } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
catalogApiRef,
|
||||
formatEntityRefTitle,
|
||||
isOwnerOf,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import qs from 'qs';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
type EntityTypeProps = {
|
||||
kind: string;
|
||||
type: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const getQueryParams = (
|
||||
owner: Entity,
|
||||
selectedEntity: EntityTypeProps,
|
||||
): string => {
|
||||
const ownerName = formatEntityRefTitle(owner, { defaultKind: 'group' });
|
||||
const { kind, type } = selectedEntity;
|
||||
const filters = {
|
||||
kind,
|
||||
type,
|
||||
owners: [ownerName],
|
||||
user: 'all',
|
||||
};
|
||||
if (owner.kind === 'User') {
|
||||
const user = owner as UserEntity;
|
||||
filters.owners = [...filters.owners, ...user.spec.memberOf];
|
||||
}
|
||||
const queryParams = qs.stringify({
|
||||
filters,
|
||||
});
|
||||
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
export function useDirectEntities(
|
||||
entity: Entity,
|
||||
entityFilterKind?: string[],
|
||||
): {
|
||||
componentsWithCounters:
|
||||
| {
|
||||
counter: number;
|
||||
type: string;
|
||||
name: string;
|
||||
queryParams: string;
|
||||
}[]
|
||||
| undefined;
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
} {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
value: componentsWithCounters,
|
||||
} = useAsync(async () => {
|
||||
const kinds = entityFilterKind ?? ['Component', 'API', 'System'];
|
||||
const entitiesList = await catalogApi.getEntities({
|
||||
filter: {
|
||||
kind: kinds,
|
||||
},
|
||||
fields: [
|
||||
'kind',
|
||||
'metadata.name',
|
||||
'metadata.namespace',
|
||||
'spec.type',
|
||||
'relations',
|
||||
],
|
||||
});
|
||||
|
||||
const ownedEntitiesList = entitiesList.items.filter(component =>
|
||||
isOwnerOf(entity, component),
|
||||
);
|
||||
|
||||
const counts = ownedEntitiesList.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(entity, topOwnedEntity),
|
||||
})) as Array<{
|
||||
counter: number;
|
||||
type: string;
|
||||
name: string;
|
||||
queryParams: string;
|
||||
}>;
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
return {
|
||||
componentsWithCounters,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* 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 } 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';
|
||||
import { CatalogListResponse } from '@backstage/catalog-client';
|
||||
import qs from 'qs';
|
||||
|
||||
const limiter = limiterFactory(10);
|
||||
|
||||
type EntityTypeProps = {
|
||||
kind: string;
|
||||
type: string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const getQueryParams = (
|
||||
owners: string[],
|
||||
selectedEntity: EntityTypeProps,
|
||||
): string => {
|
||||
const { kind, type } = selectedEntity;
|
||||
const filters = {
|
||||
kind,
|
||||
type,
|
||||
owners,
|
||||
user: 'all',
|
||||
};
|
||||
const queryParams = qs.stringify({
|
||||
filters,
|
||||
});
|
||||
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
export function useTransitiveEntities(
|
||||
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 isLoop = true;
|
||||
let currentEntity = entity;
|
||||
const kinds = entityFilterKind ?? ['Component', 'API', 'System'];
|
||||
|
||||
const {
|
||||
loading,
|
||||
error,
|
||||
value: componentsWithCounters,
|
||||
} = useAsync(async () => {
|
||||
while (isLoop) {
|
||||
const childRelations = getEntityRelations(
|
||||
currentEntity,
|
||||
RELATION_PARENT_OF,
|
||||
{
|
||||
kind: 'Group',
|
||||
},
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
childRelations.map(childGroup =>
|
||||
limiter(async () => {
|
||||
const promise = catalogApi.getEntityByName({
|
||||
kind: 'Group',
|
||||
namespace: 'default',
|
||||
name: childGroup.name,
|
||||
});
|
||||
|
||||
outstandingEntities.set(childGroup.name, promise);
|
||||
try {
|
||||
const processedEntity = await promise;
|
||||
if (processedEntity) {
|
||||
requestedEntities.push(processedEntity);
|
||||
}
|
||||
} finally {
|
||||
outstandingEntities.delete(childGroup.name);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
requestedEntities.shift();
|
||||
processedEntities.add(currentEntity.metadata.name);
|
||||
currentEntity = requestedEntities[0];
|
||||
if (requestedEntities.length === 0) isLoop = false;
|
||||
}
|
||||
|
||||
const owners = Array.from(processedEntities);
|
||||
|
||||
const ownedAggregationEntitiesList: CatalogListResponse<Entity> =
|
||||
await catalogApi.getEntities({
|
||||
filter: [
|
||||
{
|
||||
kind: kinds,
|
||||
'spec.owner': 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user