Display owner and system as entity page links in the tables of the api-docs plugin

This commit is contained in:
Oliver Sand
2021-01-29 16:14:24 +01:00
parent 7ee15d2c5b
commit 7fc89bae29
22 changed files with 339 additions and 234 deletions
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-catalog': patch
'@backstage/plugin-catalog-react': patch
'@backstage/plugin-org': patch
---
Display owner and system as entity page links in the tables of the `api-docs`
plugin.
Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to
`@backstage/plugin-catalog-react` and export it from there to use it by other
plugins.
@@ -14,7 +14,13 @@
* limitations under the License.
*/
import { ApiEntityV1alpha1, Entity } from '@backstage/catalog-model';
import {
ApiEntityV1alpha1,
Entity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import {
Table,
TableColumn,
@@ -22,24 +28,54 @@ import {
TableState,
useQueryParamState,
} from '@backstage/core';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { Chip } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { EntityLink } from '../EntityLink';
const columns: TableColumn<Entity>[] = [
type EntityRow = ApiEntityV1alpha1 & {
row: {
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'metadata.name',
highlight: true,
render: (entity: any) => (
<EntityLink entity={entity}>{entity.metadata.name}</EntityLink>
render: entity => (
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
),
},
{
title: 'System',
field: 'row.partOfSystemRelationTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'spec.owner',
field: 'row.ownedByRelationsTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Lifecycle',
@@ -48,9 +84,7 @@ const columns: TableColumn<Entity>[] = [
{
title: 'Type',
field: 'spec.type',
render: (entity: Entity) => (
<ApiTypeTitle apiEntity={entity as ApiEntityV1alpha1} />
),
render: entity => <ApiTypeTitle apiEntity={entity} />,
},
{
title: 'Description',
@@ -62,7 +96,7 @@ const columns: TableColumn<Entity>[] = [
cellStyle: {
padding: '0px 16px 0px 20px',
},
render: (entity: Entity) => (
render: entity => (
<>
{entity.metadata.tags &&
entity.metadata.tags.map(t => (
@@ -123,8 +157,32 @@ export const ApiExplorerTable = ({
);
}
const rows = entities.map(e => {
const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY);
return {
...(e as ApiEntityV1alpha1),
row: {
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle:
partOfSystemRelations.length > 0
? formatEntityRefTitle(partOfSystemRelations[0], {
defaultKind: 'system',
})
: undefined,
partOfSystemRelations,
},
};
});
return (
<Table
<Table<EntityRow>
isLoading={loading}
columns={columns}
options={{
@@ -134,7 +192,7 @@ export const ApiExplorerTable = ({
padding: 'dense',
showEmptyDataSourceMessage: !loading,
}}
data={entities}
data={rows}
filters={filters}
initialState={queryParamState}
onStateChange={setQueryParamState}
@@ -14,24 +14,59 @@
* limitations under the License.
*/
import { ApiEntity } from '@backstage/catalog-model';
import {
ApiEntity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { EntityLink } from '../EntityLink';
const columns: TableColumn<ApiEntity>[] = [
type EntityRow = ApiEntity & {
row: {
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'metadata.name',
highlight: true,
render: (entity: any) => (
<EntityLink entity={entity}>{entity.metadata.name}</EntityLink>
render: entity => (
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
),
},
{
title: 'System',
field: 'row.partOfSystemRelationTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'spec.owner',
field: 'row.ownedByRelationsTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Lifecycle',
@@ -40,7 +75,7 @@ const columns: TableColumn<ApiEntity>[] = [
{
title: 'Type',
field: 'spec.type',
render: (entity: ApiEntity) => <ApiTypeTitle apiEntity={entity} />,
render: entity => <ApiTypeTitle apiEntity={entity} />,
},
{
title: 'Description',
@@ -65,8 +100,35 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities
// TODO: For now we skip all APIs that we can't find without a warning!
.filter(e => e !== undefined)
.map(e => {
const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY);
return {
...(e as ApiEntity),
row: {
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle:
partOfSystemRelations.length > 0
? formatEntityRefTitle(partOfSystemRelations[0], {
defaultKind: 'system',
})
: undefined,
partOfSystemRelations,
},
};
});
return (
<Table<ApiEntity>
<Table<EntityRow>
columns={columns}
title={title}
style={tableStyle}
@@ -77,8 +139,7 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
actionsColumnIndex: -1,
padding: 'dense',
}}
// TODO: For now we skip all APIs that we can't find without a warning!
data={entities.filter(e => e !== undefined) as ApiEntity[]}
data={rows}
/>
);
};
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model';
import {
Entity,
RELATION_CONSUMES_API,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
@@ -99,10 +104,27 @@ describe('<ConsumedApisCard />', () => {
},
spec: {
type: 'openapi',
owner: 'Test',
lifecycle: 'production',
definition: '...',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
@@ -121,6 +143,7 @@ describe('<ConsumedApisCard />', () => {
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
import {
Entity,
RELATION_OWNED_BY,
RELATION_PART_OF,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
@@ -99,10 +104,27 @@ describe('<ProvidedApisCard />', () => {
},
spec: {
type: 'openapi',
owner: 'Test',
lifecycle: 'production',
definition: '...',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
type: 'openapi',
@@ -120,6 +142,7 @@ describe('<ProvidedApisCard />', () => {
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/OpenAPI/)).toBeInTheDocument();
expect(getByText(/MySystem/)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
@@ -14,23 +14,58 @@
* limitations under the License.
*/
import { ComponentEntity } from '@backstage/catalog-model';
import {
ComponentEntity,
EntityName,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import React from 'react';
import { EntityLink } from '../EntityLink';
const columns: TableColumn<ComponentEntity>[] = [
type EntityRow = ComponentEntity & {
row: {
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
ownedByRelations: EntityName[];
};
};
const columns: TableColumn<EntityRow>[] = [
{
title: 'Name',
field: 'metadata.name',
highlight: true,
render: (entity: any) => (
<EntityLink entity={entity}>{entity.metadata.name}</EntityLink>
render: entity => (
<EntityRefLink entityRef={entity}>{entity.metadata.name}</EntityRefLink>
),
},
{
title: 'System',
field: 'row.partOfSystemRelationTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.partOfSystemRelations}
defaultKind="system"
/>
),
},
{
title: 'Owner',
field: 'spec.owner',
field: 'row.ownedByRelationsTitle',
render: entity => (
<EntityRefLinks
entityRefs={entity.row.ownedByRelations}
defaultKind="group"
/>
),
},
{
title: 'Lifecycle',
@@ -68,8 +103,35 @@ export const ComponentsTable = ({
tableStyle.height = 'calc(100% - 10px)';
}
const rows = entities
// TODO: For now we skip all Components that we can't find without a warning!
.filter(e => e !== undefined)
.map(e => {
const partOfSystemRelations = getEntityRelations(e, RELATION_PART_OF, {
kind: 'system',
});
const ownedByRelations = getEntityRelations(e, RELATION_OWNED_BY);
return {
...(e as ComponentEntity),
row: {
ownedByRelationsTitle: ownedByRelations
.map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle:
partOfSystemRelations.length > 0
? formatEntityRefTitle(partOfSystemRelations[0], {
defaultKind: 'system',
})
: undefined,
partOfSystemRelations,
},
};
});
return (
<Table<ComponentEntity>
<Table<EntityRow>
columns={columns}
title={title}
style={tableStyle}
@@ -80,8 +142,7 @@ export const ComponentsTable = ({
actionsColumnIndex: -1,
padding: 'dense',
}}
// TODO: For now we skip all APIs that we can't find without a warning!
data={entities.filter(e => e !== undefined) as ComponentEntity[]}
data={rows}
/>
);
};
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model';
import {
Entity,
RELATION_API_CONSUMED_BY,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
@@ -104,9 +109,26 @@ describe('<ConsumingComponentsCard />', () => {
},
spec: {
type: 'service',
owner: 'Test',
lifecycle: 'production',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
const { getByText } = await renderInTestApp(
@@ -119,6 +141,7 @@ describe('<ConsumingComponentsCard />', () => {
expect(getByText(/Consumers/i)).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
@@ -14,7 +14,12 @@
* limitations under the License.
*/
import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model';
import {
Entity,
RELATION_API_PROVIDED_BY,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
@@ -104,9 +109,26 @@ describe('<ProvidingComponentsCard />', () => {
},
spec: {
type: 'service',
owner: 'Test',
lifecycle: 'production',
},
relations: [
{
type: RELATION_PART_OF,
target: {
kind: 'System',
name: 'MySystem',
namespace: 'default',
},
},
{
type: RELATION_OWNED_BY,
target: {
kind: 'Group',
name: 'Test',
namespace: 'default',
},
},
],
});
const { getByText } = await renderInTestApp(
@@ -119,6 +141,7 @@ describe('<ProvidingComponentsCard />', () => {
expect(getByText(/Providers/i)).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
expect(getByText(/Test/i)).toBeInTheDocument();
expect(getByText(/MySystem/i)).toBeInTheDocument();
expect(getByText(/production/i)).toBeInTheDocument();
});
});
@@ -1,41 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityLink } from './EntityLink';
describe('<EntityLink />', () => {
it('provides link to entity page', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'my-name',
namespace: 'my-namespace',
},
};
const { getByText } = await renderInTestApp(
<EntityLink entity={entity}>inner</EntityLink>,
);
expect(getByText(/inner/i)).toBeInTheDocument();
expect(getByText(/inner/i)).toHaveAttribute(
'href',
'/catalog/my-namespace/component/my-name',
);
});
});
@@ -1,43 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 {
entityRoute,
entityRouteParams,
} from '@backstage/plugin-catalog-react';
import { Link } from '@material-ui/core';
import React, { PropsWithChildren } from 'react';
import { generatePath, Link as RouterLink } from 'react-router-dom';
type Props = {
entity: Entity;
};
// TODO: Could be useful for others too, as part of the catalog plugin
export const EntityLink = ({ entity, children }: PropsWithChildren<Props>) => {
return (
<Link
component={RouterLink}
to={generatePath(
`/catalog/${entityRoute.path}`,
entityRouteParams(entity),
)}
>
{children}
</Link>
);
};
+1
View File
@@ -24,3 +24,4 @@ export {
entityRouteRef,
rootRoute,
} from './routes';
export * from './utils';
@@ -13,5 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { EntityLink } from './EntityLink';
export { getEntityRelations } from './getEntityRelations';
export { isOwnerOf } from './isOwnerOf';
@@ -19,10 +19,12 @@ import {
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { EntityRefLinks } from '@backstage/plugin-catalog-react';
import {
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { Chip, Grid, makeStyles, Typography } from '@material-ui/core';
import React from 'react';
import { getEntityRelations } from '../getEntityRelations';
import { AboutField } from './AboutField';
const useStyles = makeStyles({
@@ -22,7 +22,7 @@ import {
SupportButton,
useApi,
} from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react';
import { rootRoute as scaffolderRootRoute } from '@backstage/plugin-scaffolder';
import { Button, makeStyles } from '@material-ui/core';
import SettingsIcon from '@material-ui/icons/Settings';
@@ -33,7 +33,6 @@ import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import { ButtonGroup, CatalogFilter } from '../CatalogFilter/CatalogFilter';
import { CatalogTable } from '../CatalogTable/CatalogTable';
import { isOwnerOf } from '../isOwnerOf';
import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import { useOwnUser } from '../useOwnUser';
import CatalogLayout from './CatalogLayout';
@@ -24,6 +24,7 @@ import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { Chip } from '@material-ui/core';
import Edit from '@material-ui/icons/Edit';
@@ -37,7 +38,6 @@ import {
favouriteEntityIcon,
favouriteEntityTooltip,
} from '../FavouriteEntity/FavouriteEntity';
import { getEntityRelations } from '../getEntityRelations';
type EntityRow = Entity & {
row: {
@@ -14,12 +14,10 @@
* limitations under the License.
*/
import React from 'react';
import { InfoCard, useApi, Progress } from '@backstage/core';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { useAsync } from 'react-use';
import Alert from '@material-ui/lab/Alert';
import { InfoCard, Progress, useApi } from '@backstage/core';
import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react';
import { pageTheme } from '@backstage/theme';
import {
Box,
createStyles,
@@ -28,8 +26,9 @@ import {
Theme,
Typography,
} from '@material-ui/core';
import { pageTheme } from '@backstage/theme';
import { isOwnerOf } from '../../isOwnerOf';
import Alert from '@material-ui/lab/Alert';
import React from 'react';
import { useAsync } from 'react-use';
type EntitiesKinds = 'Component' | 'API';
type EntitiesTypes =
@@ -1,42 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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, EntityName } from '@backstage/catalog-model';
// TODO: this file is copied from /packages/app/catalog/src/components/getEntityRelations.ts and
// should be replaced once common relation-functions are introduced.
/**
* Get the related entity references.
*/
export function getEntityRelations(
entity: Entity | undefined,
relationType: string,
filter?: { kind: string },
): EntityName[] {
let entityNames =
entity?.relations
?.filter(r => r.type === relationType)
?.map(r => r.target) || [];
if (filter?.kind) {
entityNames = entityNames?.filter(
e => e.kind.toLowerCase() === filter.kind.toLowerCase(),
);
}
return entityNames;
}
-55
View File
@@ -1,55 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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,
EntityName,
getEntityName,
RELATION_MEMBER_OF,
RELATION_OWNED_BY,
} from '@backstage/catalog-model';
import { getEntityRelations } from './getEntityRelations';
// TODO: this file is copied from /packages/app/catalog/src/components/isOwnerOf.ts and
// should be replaced once common relation-functions are introduced.
/**
* Check if one entity is owned by another. Returns true, if the entity is owned by the
* owner directly, or if the entity is owned by a group that the owner is a member of.
*/
export function isOwnerOf(owner: Entity, owned: Entity) {
const possibleOwners: EntityName[] = [
...getEntityRelations(owner, RELATION_MEMBER_OF, { kind: 'group' }),
...(owner ? [getEntityName(owner)] : []),
];
const owners = getEntityRelations(owned, RELATION_OWNED_BY);
for (const owner of owners) {
if (
possibleOwners.find(
o =>
owner.kind.toLowerCase() === o.kind.toLowerCase() &&
owner.namespace.toLowerCase() === o.namespace.toLowerCase() &&
owner.name.toLowerCase() === o.name.toLowerCase(),
) !== undefined
) {
return true;
}
}
return false;
}