stitch targetRef and backwards compat inject it on read
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -231,6 +231,7 @@ export type EntityRefContext = {
|
||||
export type EntityRelation = {
|
||||
type: string;
|
||||
target: EntityName;
|
||||
targetRef: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
|
||||
@@ -66,7 +66,8 @@ describe('DefaultApiExplorerPage', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: { namespace: 'default', kind: 'Group', name: 'tools' },
|
||||
targetRef: 'group:default/tools',
|
||||
target: { namespace: 'default', kind: 'group', name: 'tools' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -95,10 +95,11 @@ describe('<ConsumedApisCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'API',
|
||||
kind: 'api',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'api:my-namespace/target-name',
|
||||
type: RELATION_CONSUMES_API,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -95,10 +95,11 @@ describe('<HasApisCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'API',
|
||||
kind: 'api',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'api:my-namespace/target-name',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -95,10 +95,11 @@ describe('<ProvidedApisCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'API',
|
||||
kind: 'api',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'api:my-namespace/target-name',
|
||||
type: RELATION_PROVIDES_API,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -98,10 +98,11 @@ describe('<ConsumingComponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_API_CONSUMED_BY,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -98,10 +98,11 @@ describe('<ProvidingComponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_API_PROVIDED_BY,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -79,8 +79,9 @@ describe('CatalogIdentityClient', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
targetRef: 'group:default/team-a',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
namespace: 'default',
|
||||
name: 'team-a',
|
||||
},
|
||||
@@ -100,8 +101,9 @@ describe('CatalogIdentityClient', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
targetRef: 'group:reality/screen-actors-guild',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
namespace: 'reality',
|
||||
name: 'screen-actors-guild',
|
||||
},
|
||||
|
||||
@@ -120,11 +120,11 @@ export class CatalogIdentityClient {
|
||||
e =>
|
||||
e!.relations
|
||||
?.filter(r => r.type === RELATION_MEMBER_OF)
|
||||
.map(r => r.target) ?? [],
|
||||
.map(r => r.targetRef) ?? [],
|
||||
);
|
||||
|
||||
const newEntityRefs = [
|
||||
...new Set(resolvedEntityRefs.concat(memberOf).map(stringifyEntityRef)),
|
||||
...new Set(resolvedEntityRefs.map(stringifyEntityRef).concat(memberOf)),
|
||||
];
|
||||
|
||||
logger?.debug(`Found catalog membership: ${newEntityRefs.join()}`);
|
||||
|
||||
@@ -27,11 +27,9 @@ export function getEntityClaims(entity: UserEntity): TokenParams['claims'] {
|
||||
const membershipRefs =
|
||||
entity.relations
|
||||
?.filter(
|
||||
r =>
|
||||
r.type === RELATION_MEMBER_OF &&
|
||||
r.target.kind.toLocaleLowerCase('en-US') === 'group',
|
||||
r => r.type === RELATION_MEMBER_OF && r.targetRef.startsWith('group:'),
|
||||
)
|
||||
.map(r => stringifyEntityRef(r.target)) ?? [];
|
||||
.map(r => r.targetRef) ?? [];
|
||||
|
||||
return {
|
||||
sub: userRef,
|
||||
|
||||
@@ -40,7 +40,7 @@ export type EntityPagination = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Matches rows in the entities_search table.
|
||||
* Matches rows in the search table.
|
||||
* @public
|
||||
*/
|
||||
export type EntitiesSearchFilter = {
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('isEntityOwner', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'user:default/spiderman',
|
||||
target: {
|
||||
kind: 'user',
|
||||
namespace: 'default',
|
||||
@@ -52,6 +53,7 @@ describe('isEntityOwner', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'user:default/green-goblin',
|
||||
target: {
|
||||
kind: 'user',
|
||||
namespace: 'default',
|
||||
|
||||
@@ -14,16 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
RELATION_OWNED_BY,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
|
||||
import { createCatalogPermissionRule } from './util';
|
||||
|
||||
/**
|
||||
* A catalog {@link @backstage/plugin-permission-node#PermissionRule} which
|
||||
* filters for entities with a specified owner.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const isEntityOwner = createCatalogPermissionRule({
|
||||
@@ -36,7 +33,7 @@ export const isEntityOwner = createCatalogPermissionRule({
|
||||
|
||||
return resource.relations
|
||||
.filter(relation => relation.type === RELATION_OWNED_BY)
|
||||
.some(relation => claims.includes(stringifyEntityRef(relation.target)));
|
||||
.some(relation => claims.includes(relation.targetRef));
|
||||
},
|
||||
toQuery: (claims: string[]) => ({
|
||||
key: 'relations.ownedBy',
|
||||
|
||||
@@ -469,6 +469,62 @@ describe('DefaultEntitiesCatalog', () => {
|
||||
expect(entities.length).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(databases.eachSupportedId())(
|
||||
'should return both target and targetRef for entities',
|
||||
async databaseId => {
|
||||
const { knex } = await createDatabase(databaseId);
|
||||
await addEntity(
|
||||
knex,
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'one' },
|
||||
spec: {},
|
||||
relations: [{ type: 'r', targetRef: 'x:y/z' } as any],
|
||||
},
|
||||
[],
|
||||
);
|
||||
await addEntity(
|
||||
knex,
|
||||
{
|
||||
apiVersion: 'a',
|
||||
kind: 'k',
|
||||
metadata: { name: 'two' },
|
||||
spec: {},
|
||||
relations: [
|
||||
{
|
||||
type: 'r',
|
||||
target: { kind: 'x', namespace: 'y', name: 'z' },
|
||||
} as any,
|
||||
],
|
||||
},
|
||||
[],
|
||||
);
|
||||
const catalog = new DefaultEntitiesCatalog(knex);
|
||||
|
||||
const { entities } = await catalog.entities();
|
||||
|
||||
expect(
|
||||
entities.find(e => e.metadata.name === 'one')!.relations,
|
||||
).toEqual([
|
||||
{
|
||||
type: 'r',
|
||||
targetRef: 'x:y/z',
|
||||
target: { kind: 'x', namespace: 'y', name: 'z' },
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
entities.find(e => e.metadata.name === 'two')!.relations,
|
||||
).toEqual([
|
||||
{
|
||||
type: 'r',
|
||||
targetRef: 'x:y/z',
|
||||
target: { kind: 'x', namespace: 'y', name: 'z' },
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('removeEntityByUid', () => {
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import {
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { InputError, NotFoundError } from '@backstage/errors';
|
||||
import { Knex } from 'knex';
|
||||
import lodash from 'lodash';
|
||||
@@ -194,11 +198,32 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
|
||||
};
|
||||
}
|
||||
|
||||
const dbResponse = rows.map(e => JSON.parse(e.final_entity!));
|
||||
let entities: Entity[] = rows.map(e => JSON.parse(e.final_entity!));
|
||||
|
||||
const entities = dbResponse.map(e =>
|
||||
request?.fields ? request.fields(e) : e,
|
||||
);
|
||||
if (request?.fields) {
|
||||
entities = entities.map(e => request.fields!(e));
|
||||
}
|
||||
|
||||
// TODO(freben): This is added as a compatibility guarantee, until we can be
|
||||
// sure that all adopters have re-stitched their entities so that the new
|
||||
// targetRef field is present on them, and that they have stopped consuming
|
||||
// the now-removed old field
|
||||
for (const entity of entities) {
|
||||
if (entity.relations) {
|
||||
for (const relation of entity.relations) {
|
||||
if (!relation.targetRef) {
|
||||
// This is the case where an old-form entity, not yet stitched with
|
||||
// the updated code, was in the database
|
||||
relation.targetRef = stringifyEntityRef(relation.target);
|
||||
} else if (!relation.target) {
|
||||
// This is the case where a new-form entity, stitched with the
|
||||
// updated code, was in the database but we still want to produce
|
||||
// the old data shape as well for compatibility reasons
|
||||
relation.target = parseEntityRef(relation.targetRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
entities,
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('Stitcher', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'looksAt',
|
||||
targetRef: 'k:ns/other',
|
||||
target: {
|
||||
kind: 'k',
|
||||
namespace: 'ns',
|
||||
@@ -159,6 +160,7 @@ describe('Stitcher', () => {
|
||||
relations: expect.arrayContaining([
|
||||
{
|
||||
type: 'looksAt',
|
||||
targetRef: 'k:ns/other',
|
||||
target: {
|
||||
kind: 'k',
|
||||
namespace: 'ns',
|
||||
@@ -167,6 +169,7 @@ describe('Stitcher', () => {
|
||||
},
|
||||
{
|
||||
type: 'looksAt',
|
||||
targetRef: 'k:ns/third',
|
||||
target: {
|
||||
kind: 'k',
|
||||
namespace: 'ns',
|
||||
|
||||
@@ -186,6 +186,7 @@ export class Stitcher {
|
||||
.map(row => ({
|
||||
type: row.relationType!,
|
||||
target: parseEntityRef(row.relationTarget!),
|
||||
targetRef: row.relationTarget!,
|
||||
}));
|
||||
if (statusItems.length) {
|
||||
entity.status = {
|
||||
@@ -210,7 +211,7 @@ export class Stitcher {
|
||||
}
|
||||
|
||||
// This may throw if the entity is invalid, so we call it before
|
||||
// the final_entites write, even though we may end up not needing
|
||||
// the final_entities write, even though we may end up not needing
|
||||
// to write the search index.
|
||||
const searchEntries = buildEntitySearch(entityId, entity);
|
||||
|
||||
|
||||
@@ -141,8 +141,16 @@ describe('buildEntitySearch', () => {
|
||||
it('adds relations', () => {
|
||||
const input: Entity = {
|
||||
relations: [
|
||||
{ type: 't1', target: { kind: 'k', namespace: 'ns', name: 'a' } },
|
||||
{ type: 't2', target: { kind: 'k', namespace: 'ns', name: 'b' } },
|
||||
{
|
||||
type: 't1',
|
||||
targetRef: 'k:ns/a',
|
||||
target: { kind: 'k', namespace: 'ns', name: 'a' },
|
||||
},
|
||||
{
|
||||
type: 't2',
|
||||
targetRef: 'k:ns/b',
|
||||
target: { kind: 'k', namespace: 'ns', name: 'b' },
|
||||
},
|
||||
],
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
@@ -182,8 +190,16 @@ describe('buildEntitySearch', () => {
|
||||
expect(() =>
|
||||
buildEntitySearch('eid', {
|
||||
relations: [
|
||||
{ type: 'dup', target: { kind: 'k', namespace: 'ns', name: 'a' } },
|
||||
{ type: 'DUP', target: { kind: 'k', namespace: 'ns', name: 'b' } },
|
||||
{
|
||||
type: 'dup',
|
||||
targetRef: 'k:ns/a',
|
||||
target: { kind: 'k', namespace: 'ns', name: 'a' },
|
||||
},
|
||||
{
|
||||
type: 'DUP',
|
||||
targetRef: 'k:ns/b',
|
||||
target: { kind: 'k', namespace: 'ns', name: 'b' },
|
||||
},
|
||||
],
|
||||
apiVersion: 'a',
|
||||
kind: 'b',
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
DEFAULT_NAMESPACE,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { DbSearchRow } from '../database/tables';
|
||||
|
||||
@@ -181,7 +177,7 @@ export function buildEntitySearch(
|
||||
for (const relation of entity.relations ?? []) {
|
||||
raw.push({
|
||||
key: `relations.${relation.type}`,
|
||||
value: stringifyEntityRef(relation.target),
|
||||
value: relation.targetRef,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +118,11 @@ const entities = (
|
||||
},
|
||||
relations: relations.map(([type, k, n]) => ({
|
||||
target: { kind: k, name: n, namespace: DEFAULT_NAMESPACE },
|
||||
targetRef: stringifyEntityRef({
|
||||
kind: k,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
name: n,
|
||||
}),
|
||||
type,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ describe('<CatalogGraphPage/>', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'b:d/e',
|
||||
target: {
|
||||
kind: 'b',
|
||||
namespace: 'd',
|
||||
@@ -76,6 +77,7 @@ describe('<CatalogGraphPage/>', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_HAS_PART,
|
||||
targetRef: 'b:d/c',
|
||||
target: {
|
||||
kind: 'b',
|
||||
namespace: 'd',
|
||||
|
||||
+9
@@ -54,6 +54,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -62,6 +63,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -80,6 +82,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
{
|
||||
@@ -88,6 +91,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
],
|
||||
@@ -106,6 +110,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
{
|
||||
@@ -114,6 +119,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -122,6 +128,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c2',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c2',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -140,6 +147,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
],
|
||||
@@ -225,6 +233,7 @@ describe('<EntityRelationsGraph/>', () => {
|
||||
name: 'some-component',
|
||||
namespace: 'default',
|
||||
},
|
||||
targetRef: 'component:default/some-component',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
],
|
||||
|
||||
+8
@@ -49,6 +49,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -57,6 +58,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -75,6 +77,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
{
|
||||
@@ -83,6 +86,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
],
|
||||
@@ -101,6 +105,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
{
|
||||
@@ -109,6 +114,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -117,6 +123,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c2',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c2',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -135,6 +142,7 @@ describe('useEntityRelationGraph', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { useEffect } from 'react';
|
||||
import { useEntityStore } from './useEntityStore';
|
||||
|
||||
@@ -64,13 +64,15 @@ export function useEntityRelationGraph({
|
||||
if (
|
||||
(!relations || relations.includes(rel.type)) &&
|
||||
(!kinds ||
|
||||
kinds.includes(rel.target.kind.toLocaleLowerCase('en-US')))
|
||||
kinds.some(kind =>
|
||||
rel.targetRef.startsWith(
|
||||
`${kind.toLocaleLowerCase('en-US')}:`,
|
||||
),
|
||||
))
|
||||
) {
|
||||
const relationEntityRef = stringifyEntityRef(rel.target);
|
||||
|
||||
if (!processedEntityRefs.has(relationEntityRef)) {
|
||||
nextDepthRefQueue.push(relationEntityRef);
|
||||
expectedEntities.add(relationEntityRef);
|
||||
if (!processedEntityRefs.has(rel.targetRef)) {
|
||||
nextDepthRefQueue.push(rel.targetRef);
|
||||
expectedEntities.add(rel.targetRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -49,6 +49,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -57,6 +58,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -75,6 +77,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
{
|
||||
@@ -83,6 +86,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_OWNED_BY,
|
||||
},
|
||||
],
|
||||
@@ -101,6 +105,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
{
|
||||
@@ -109,6 +114,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'a1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'k:d/a1',
|
||||
type: RELATION_OWNER_OF,
|
||||
},
|
||||
{
|
||||
@@ -117,6 +123,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c2',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c2',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
@@ -135,6 +142,7 @@ describe('useEntityRelationNodesAndEdges', () => {
|
||||
name: 'c1',
|
||||
namespace: 'd',
|
||||
},
|
||||
targetRef: 'b:d/c1',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
],
|
||||
|
||||
+12
-15
@@ -13,10 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { DEFAULT_NAMESPACE } from '@backstage/catalog-model';
|
||||
import { MouseEvent, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import { RelationPairs, ALL_RELATION_PAIRS } from './relations';
|
||||
@@ -100,11 +97,9 @@ export function useEntityRelationNodesAndEdges({
|
||||
|
||||
if (entity) {
|
||||
entity?.relations?.forEach(rel => {
|
||||
const targetRef = stringifyEntityRef(rel.target);
|
||||
|
||||
// Check if the related entity should be displayed, if not, ignore
|
||||
// the relation too
|
||||
if (!entities[targetRef]) {
|
||||
if (!entities[rel.targetRef]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,12 +109,14 @@ export function useEntityRelationNodesAndEdges({
|
||||
|
||||
if (
|
||||
kinds &&
|
||||
!kinds.includes(rel.target.kind.toLocaleLowerCase('en-US'))
|
||||
!kinds.some(kind =>
|
||||
rel.targetRef.startsWith(`${kind.toLocaleLowerCase('en-US')}:`),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!unidirectional || !visitedNodes.has(targetRef)) {
|
||||
if (!unidirectional || !visitedNodes.has(rel.targetRef)) {
|
||||
if (mergeRelations) {
|
||||
const pair = relationPairs.find(
|
||||
([l, r]) => l === rel.type || r === rel.type,
|
||||
@@ -127,24 +124,24 @@ export function useEntityRelationNodesAndEdges({
|
||||
const [left] = pair;
|
||||
|
||||
edges.push({
|
||||
from: left === rel.type ? entityRef : targetRef,
|
||||
to: left === rel.type ? targetRef : entityRef,
|
||||
from: left === rel.type ? entityRef : rel.targetRef,
|
||||
to: left === rel.type ? rel.targetRef : entityRef,
|
||||
relations: pair,
|
||||
label: 'visible',
|
||||
});
|
||||
} else {
|
||||
edges.push({
|
||||
from: entityRef,
|
||||
to: targetRef,
|
||||
to: rel.targetRef,
|
||||
relations: [rel.type],
|
||||
label: 'visible',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!visitedNodes.has(targetRef)) {
|
||||
nodeQueue.push(targetRef);
|
||||
visitedNodes.add(targetRef);
|
||||
if (!visitedNodes.has(rel.targetRef)) {
|
||||
nodeQueue.push(rel.targetRef);
|
||||
visitedNodes.add(rel.targetRef);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ export const EntityRefLink: (props: EntityRefLinkProps) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityRefLinkProps = {
|
||||
entityRef: Entity | EntityName;
|
||||
entityRef: Entity | EntityName | string;
|
||||
defaultKind?: string;
|
||||
title?: string;
|
||||
children?: React_2.ReactNode;
|
||||
@@ -592,10 +592,7 @@ export function useOwnUser(): AsyncState<UserEntity | undefined>;
|
||||
// @public (undocumented)
|
||||
export function useRelatedEntities(
|
||||
entity: Entity,
|
||||
{
|
||||
type,
|
||||
kind,
|
||||
}: {
|
||||
relationFilter: {
|
||||
type?: string;
|
||||
kind?: string;
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockEntityListContextProvider } from '../../testUtils/providers';
|
||||
@@ -31,6 +31,7 @@ const sampleEntities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/some-owner',
|
||||
target: {
|
||||
name: 'some-owner',
|
||||
namespace: 'default',
|
||||
@@ -39,6 +40,7 @@ const sampleEntities: Entity[] = [
|
||||
},
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/some-owner-2',
|
||||
target: {
|
||||
name: 'some-owner-2',
|
||||
namespace: 'default',
|
||||
@@ -56,6 +58,7 @@ const sampleEntities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/another-owner',
|
||||
target: {
|
||||
name: 'another-owner',
|
||||
namespace: 'default',
|
||||
@@ -73,6 +76,7 @@ const sampleEntities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/some-owner',
|
||||
target: {
|
||||
name: 'some-owner',
|
||||
namespace: 'default',
|
||||
@@ -96,7 +100,7 @@ describe('<EntityOwnerPicker/>', () => {
|
||||
|
||||
fireEvent.click(rendered.getByTestId('owner-picker-expand'));
|
||||
sampleEntities
|
||||
.flatMap(e => e.relations?.map(r => r.target.name))
|
||||
.flatMap(e => e.relations?.map(r => parseEntityRef(r.targetRef).name))
|
||||
.forEach(owner => {
|
||||
expect(rendered.getByText(owner as string)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
Entity,
|
||||
EntityName,
|
||||
DEFAULT_NAMESPACE,
|
||||
parseEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import React, { forwardRef } from 'react';
|
||||
import { entityRouteRef } from '../../routes';
|
||||
@@ -32,7 +33,7 @@ import { Tooltip } from '@material-ui/core';
|
||||
* @public
|
||||
*/
|
||||
export type EntityRefLinkProps = {
|
||||
entityRef: Entity | EntityName;
|
||||
entityRef: Entity | EntityName | string;
|
||||
defaultKind?: string;
|
||||
title?: string;
|
||||
children?: React.ReactNode;
|
||||
@@ -52,7 +53,12 @@ export const EntityRefLink = forwardRef<any, EntityRefLinkProps>(
|
||||
let namespace;
|
||||
let name;
|
||||
|
||||
if ('metadata' in entityRef) {
|
||||
if (typeof entityRef === 'string') {
|
||||
const parsed = parseEntityRef(entityRef);
|
||||
kind = parsed.kind;
|
||||
namespace = parsed.namespace;
|
||||
name = parsed.name;
|
||||
} else if ('metadata' in entityRef) {
|
||||
kind = entityRef.kind;
|
||||
namespace = entityRef.metadata.namespace;
|
||||
name = entityRef.metadata.name;
|
||||
@@ -63,15 +69,13 @@ export const EntityRefLink = forwardRef<any, EntityRefLinkProps>(
|
||||
}
|
||||
|
||||
kind = kind.toLocaleLowerCase('en-US');
|
||||
namespace = namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE;
|
||||
|
||||
const routeParams = {
|
||||
kind,
|
||||
namespace: namespace?.toLocaleLowerCase('en-US') ?? DEFAULT_NAMESPACE,
|
||||
name,
|
||||
};
|
||||
const formattedEntityRefTitle = formatEntityRefTitle(entityRef, {
|
||||
defaultKind,
|
||||
});
|
||||
const routeParams = { kind, namespace, name };
|
||||
const formattedEntityRefTitle = formatEntityRefTitle(
|
||||
{ kind, namespace, name },
|
||||
{ defaultKind },
|
||||
);
|
||||
|
||||
const link = (
|
||||
<Link {...linkProps} ref={ref} to={entityRoute(routeParams)}>
|
||||
|
||||
@@ -44,17 +44,19 @@ describe('systemEntityColumns', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'domain:my-namespace/my-domain',
|
||||
target: {
|
||||
kind: 'Domain',
|
||||
kind: 'domain',
|
||||
name: 'my-domain',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/test',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
name: 'Test',
|
||||
kind: 'group',
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
@@ -79,7 +81,7 @@ describe('systemEntityColumns', () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
|
||||
expect(getByText('my-namespace/my-domain')).toBeInTheDocument();
|
||||
expect(getByText('Test')).toBeInTheDocument();
|
||||
expect(getByText('test')).toBeInTheDocument();
|
||||
expect(getByText(/Some/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -104,17 +106,19 @@ describe('componentEntityColumns', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'system:my-namespace/my-system',
|
||||
target: {
|
||||
kind: 'System',
|
||||
kind: 'system',
|
||||
name: 'my-system',
|
||||
namespace: 'my-namespace',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/test',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
name: 'Test',
|
||||
kind: 'group',
|
||||
name: 'test',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
@@ -139,7 +143,7 @@ describe('componentEntityColumns', () => {
|
||||
await waitFor(() => {
|
||||
expect(getByText('my-namespace/my-component')).toBeInTheDocument();
|
||||
expect(getByText('my-namespace/my-system')).toBeInTheDocument();
|
||||
expect(getByText('Test')).toBeInTheDocument();
|
||||
expect(getByText('test')).toBeInTheDocument();
|
||||
expect(getByText('production')).toBeInTheDocument();
|
||||
expect(getByText('service')).toBeInTheDocument();
|
||||
expect(getByText(/Some/)).toBeInTheDocument();
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import { ComponentEntity, SystemEntity } from '@backstage/catalog-model';
|
||||
import { columnFactories } from './columns';
|
||||
|
||||
import { TableColumn } from '@backstage/core-components';
|
||||
import { columnFactories } from './columns';
|
||||
|
||||
export const systemEntityColumns: TableColumn<SystemEntity>[] = [
|
||||
columnFactories.createEntityRefColumn({ defaultKind: 'system' }),
|
||||
|
||||
+38
-7
@@ -14,23 +14,54 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
import WorkIcon from '@material-ui/icons/Work';
|
||||
import React from 'react';
|
||||
|
||||
const DEFAULT_ICON = WorkIcon;
|
||||
|
||||
function getKind(
|
||||
kind: string | undefined,
|
||||
entityRef: string | undefined,
|
||||
): string | undefined {
|
||||
if (kind) {
|
||||
return kind.toLocaleLowerCase('en-US');
|
||||
}
|
||||
|
||||
if (entityRef) {
|
||||
try {
|
||||
return parseEntityRef(entityRef).kind.toLocaleLowerCase('en-US');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function useIcon(kind: string | undefined, entityRef: string | undefined) {
|
||||
const app = useApp();
|
||||
|
||||
const actualKind = getKind(kind, entityRef);
|
||||
if (!actualKind) {
|
||||
return DEFAULT_ICON;
|
||||
}
|
||||
|
||||
const icon = app.getSystemIcon(`kind:${actualKind}`);
|
||||
return icon || DEFAULT_ICON;
|
||||
}
|
||||
|
||||
export function EntityKindIcon(props: {
|
||||
kind: string;
|
||||
kind?: string;
|
||||
entityRef?: string;
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
const app = useApp();
|
||||
|
||||
const { kind, ...otherProps } = props;
|
||||
const Icon =
|
||||
app.getSystemIcon(`kind:${kind.toLocaleLowerCase('en-US')}`) ?? WorkIcon;
|
||||
|
||||
const { kind, entityRef, ...otherProps } = props;
|
||||
const Icon = useIcon(kind, entityRef);
|
||||
return <Icon {...otherProps} />;
|
||||
}
|
||||
|
||||
+7
-5
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AlphaEntity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { AlphaEntity } from '@backstage/catalog-model';
|
||||
import {
|
||||
Box,
|
||||
DialogContentText,
|
||||
@@ -56,7 +56,7 @@ export function OverviewPage(props: { entity: AlphaEntity }) {
|
||||
} = props.entity;
|
||||
|
||||
const groupedRelations = groupBy(
|
||||
sortBy(relations, r => stringifyEntityRef(r.target)),
|
||||
sortBy(relations, r => r.targetRef),
|
||||
'type',
|
||||
);
|
||||
|
||||
@@ -143,12 +143,14 @@ export function OverviewPage(props: { entity: AlphaEntity }) {
|
||||
<div key={index}>
|
||||
<List dense subheader={<ListSubheader>{type}</ListSubheader>}>
|
||||
{groupRelations.map(group => (
|
||||
<ListItem key={stringifyEntityRef(group.target)}>
|
||||
<ListItem key={group.targetRef}>
|
||||
<ListItemIcon>
|
||||
<EntityKindIcon kind={group.target.kind} />
|
||||
<EntityKindIcon entityRef={group.targetRef} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={<EntityRefLink entityRef={group.target} />}
|
||||
primary={
|
||||
<EntityRefLink entityRef={group.targetRef} />
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
@@ -103,6 +103,7 @@ const backendEntities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/testuser',
|
||||
target: { kind: 'User', namespace: 'default', name: 'testUser' },
|
||||
},
|
||||
],
|
||||
@@ -136,6 +137,7 @@ const backendEntities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/testuser',
|
||||
target: { kind: 'User', namespace: 'default', name: 'testUser' },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -45,6 +45,7 @@ const entities: Entity[] = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
|
||||
@@ -70,10 +70,12 @@ describe('useEntityOwnership', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/user1',
|
||||
target: { kind: 'User', namespace: 'default', name: 'user1' },
|
||||
},
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/group1',
|
||||
target: { kind: 'Group', namespace: 'default', name: 'group1' },
|
||||
},
|
||||
],
|
||||
@@ -92,6 +94,7 @@ describe('useEntityOwnership', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
targetRef: 'group:default/group1',
|
||||
target: { kind: 'Group', namespace: 'default', name: 'group1' },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Entity, EntityRelation } from '@backstage/catalog-model';
|
||||
import { Entity, parseEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { chunk, groupBy } from 'lodash';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
@@ -24,28 +24,28 @@ const BATCH_SIZE = 20;
|
||||
/** @public */
|
||||
export function useRelatedEntities(
|
||||
entity: Entity,
|
||||
{ type, kind }: { type?: string; kind?: string },
|
||||
relationFilter: { type?: string; kind?: string },
|
||||
): {
|
||||
entities: Entity[] | undefined;
|
||||
loading: boolean;
|
||||
error: Error | undefined;
|
||||
} {
|
||||
const filterByTypeLower = relationFilter?.type?.toLocaleLowerCase('en-US');
|
||||
const filterByKindLower = relationFilter?.kind?.toLocaleLowerCase('en-US');
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const {
|
||||
loading,
|
||||
value: entities,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
const relations =
|
||||
entity.relations &&
|
||||
entity.relations.filter(
|
||||
const relations = entity.relations
|
||||
?.map(r => ({ type: r.type, target: parseEntityRef(r.targetRef) }))
|
||||
.filter(
|
||||
r =>
|
||||
(!type ||
|
||||
r.type.toLocaleLowerCase('en-US') ===
|
||||
type.toLocaleLowerCase('en-US')) &&
|
||||
(!kind ||
|
||||
r.target.kind.toLocaleLowerCase('en-US') ===
|
||||
kind.toLocaleLowerCase('en-US')),
|
||||
(!filterByTypeLower ||
|
||||
r.type.toLocaleLowerCase('en-US') === filterByTypeLower) &&
|
||||
(!filterByKindLower || r.target.kind === filterByKindLower),
|
||||
);
|
||||
|
||||
if (!relations) {
|
||||
@@ -57,7 +57,7 @@ export function useRelatedEntities(
|
||||
// `filter=kind=component,namespace=default,name=example1&filter=kind=component,namespace=default,name=example2`
|
||||
// with grouping, we can generate a query a string like
|
||||
// `filter=kind=component,namespace=default,name=example1,example2`
|
||||
const relationsByKindAndNamespace: EntityRelation[][] = Object.values(
|
||||
const relationsByKindAndNamespace = Object.values(
|
||||
groupBy(relations, ({ target }) => {
|
||||
return `${target.kind}:${target.namespace}`.toLocaleLowerCase('en-US');
|
||||
}),
|
||||
@@ -96,7 +96,7 @@ export function useRelatedEntities(
|
||||
);
|
||||
|
||||
return results.flatMap(r => r.items);
|
||||
}, [entity, type]);
|
||||
}, [entity, filterByTypeLower, filterByKindLower]);
|
||||
|
||||
return {
|
||||
entities,
|
||||
|
||||
@@ -31,17 +31,19 @@ describe('getEntityRelations', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: { kind: 'group', name: 'member' },
|
||||
targetRef: 'group:default/member',
|
||||
target: { kind: 'group', namespace: 'default', name: 'member' },
|
||||
},
|
||||
{
|
||||
type: RELATION_CHILD_OF,
|
||||
target: { kind: 'group', name: 'child' },
|
||||
targetRef: 'group:default/child',
|
||||
target: { kind: 'group', namespace: 'default', name: 'child' },
|
||||
},
|
||||
],
|
||||
} as Entity;
|
||||
|
||||
expect(getEntityRelations(entity, RELATION_MEMBER_OF)).toEqual([
|
||||
{ kind: 'group', name: 'member' },
|
||||
{ kind: 'group', namespace: 'default', name: 'member' },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -50,17 +52,19 @@ describe('getEntityRelations', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: { kind: 'group', name: 'member' },
|
||||
targetRef: 'group:default/member',
|
||||
target: { kind: 'group', namespace: 'default', name: 'member' },
|
||||
},
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: { kind: 'user', name: 'child' },
|
||||
targetRef: 'user:default/child',
|
||||
target: { kind: 'user', namespace: 'default', name: 'child' },
|
||||
},
|
||||
],
|
||||
} as Entity;
|
||||
|
||||
expect(
|
||||
getEntityRelations(entity, RELATION_MEMBER_OF, { kind: 'Group' }),
|
||||
).toEqual([{ kind: 'group', name: 'member' }]);
|
||||
).toEqual([{ kind: 'group', namespace: 'default', name: 'member' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, EntityName } from '@backstage/catalog-model';
|
||||
import { Entity, EntityName, parseEntityRef } from '@backstage/catalog-model';
|
||||
|
||||
// TODO(freben): This should be returning entity refs instead
|
||||
/**
|
||||
* Get the related entity references.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getEntityRelations(
|
||||
@@ -28,10 +30,10 @@ export function getEntityRelations(
|
||||
let entityNames =
|
||||
entity?.relations
|
||||
?.filter(r => r.type === relationType)
|
||||
?.map(r => r.target) || [];
|
||||
.map(r => parseEntityRef(r.targetRef)) || [];
|
||||
|
||||
if (filter?.kind) {
|
||||
entityNames = entityNames?.filter(
|
||||
entityNames = entityNames.filter(
|
||||
e =>
|
||||
e.kind.toLocaleLowerCase('en-US') ===
|
||||
filter.kind.toLocaleLowerCase('en-US'),
|
||||
|
||||
@@ -31,6 +31,7 @@ describe('isOwnerOf', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/user',
|
||||
target: { kind: 'user', namespace: 'default', name: 'user' },
|
||||
},
|
||||
],
|
||||
@@ -46,6 +47,7 @@ describe('isOwnerOf', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
targetRef: 'group:default/group',
|
||||
target: { kind: 'group', namespace: 'default', name: 'group' },
|
||||
},
|
||||
],
|
||||
@@ -54,6 +56,7 @@ describe('isOwnerOf', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/group',
|
||||
target: { kind: 'group', namespace: 'default', name: 'group' },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('<AboutCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
|
||||
@@ -47,6 +47,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/o',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'o',
|
||||
@@ -55,6 +56,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'system:default/s',
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 's',
|
||||
@@ -63,6 +65,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'domain:default/d',
|
||||
target: {
|
||||
kind: 'domain',
|
||||
name: 'd',
|
||||
@@ -152,6 +155,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
@@ -160,6 +164,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'system:default/system',
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
@@ -257,6 +262,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
@@ -265,6 +271,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'system:default/system',
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
@@ -273,6 +280,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'component:default/parent-software',
|
||||
target: {
|
||||
kind: 'component',
|
||||
name: 'parent-software',
|
||||
@@ -369,6 +377,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
@@ -528,6 +537,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
@@ -536,6 +546,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'system:default/system',
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
@@ -625,6 +636,7 @@ describe('<AboutContent />', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'user:default/guest',
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
@@ -633,6 +645,7 @@ describe('<AboutContent />', () => {
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
targetRef: 'domain:default/domain',
|
||||
target: {
|
||||
kind: 'domain',
|
||||
name: 'domain',
|
||||
|
||||
@@ -73,7 +73,8 @@ describe('DefaultCatalogPage', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: { kind: 'Group', name: 'tools', namespace: 'default' },
|
||||
targetRef: 'group:default/tools',
|
||||
target: { kind: 'group', name: 'tools', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -90,8 +91,9 @@ describe('DefaultCatalogPage', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/not-tools',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
name: 'not-tools',
|
||||
namespace: 'default',
|
||||
},
|
||||
@@ -110,6 +112,7 @@ describe('DefaultCatalogPage', () => {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
targetRef: 'group:default/tools',
|
||||
target: { namespace: 'default', kind: 'Group', name: 'tools' },
|
||||
},
|
||||
],
|
||||
|
||||
+2
-1
@@ -81,10 +81,11 @@ describe('<DependencyOfComponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_DEPENDENCY_OF,
|
||||
},
|
||||
],
|
||||
|
||||
+2
-1
@@ -81,10 +81,11 @@ describe('<DependsOnComponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_DEPENDS_ON,
|
||||
},
|
||||
],
|
||||
|
||||
+2
-1
@@ -81,10 +81,11 @@ describe('<DependsOnResourcesCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Resource',
|
||||
kind: 'resource',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'resource:my-namespace/target-name',
|
||||
type: RELATION_DEPENDS_ON,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -81,10 +81,11 @@ describe('<HasComponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -76,10 +76,11 @@ describe('<HasResourcesCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Resource',
|
||||
kind: 'resource',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'resource:my-namespace/target-name',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -81,10 +81,11 @@ describe('<HasSubcomponentsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Component',
|
||||
kind: 'component',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'component:my-namespace/target-name',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -79,10 +79,11 @@ describe('<HasSystemsCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'System',
|
||||
kind: 'system',
|
||||
namespace: 'my-namespace',
|
||||
name: 'target-name',
|
||||
},
|
||||
targetRef: 'system:my-namespace/target-name',
|
||||
type: RELATION_HAS_PART,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -103,10 +103,11 @@ describe('<SystemDiagramCard />', () => {
|
||||
relations: [
|
||||
{
|
||||
target: {
|
||||
kind: 'Domain',
|
||||
kind: 'domain',
|
||||
namespace: 'namespace',
|
||||
name: 'domain',
|
||||
},
|
||||
targetRef: 'domain:namespace/domain',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
],
|
||||
@@ -166,6 +167,7 @@ describe('<SystemDiagramCard />', () => {
|
||||
namespace: 'namespace',
|
||||
name: 'alongdomainthatshouldgettruncated',
|
||||
},
|
||||
targetRef: 'domain:namespace/alongdomainthatshouldgettruncated',
|
||||
type: RELATION_PART_OF,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -42,7 +42,8 @@ const entity = (name?: string) =>
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: { kind: 'Group', namespace: 'default', name },
|
||||
targetRef: `group:default/${name}`,
|
||||
target: { kind: 'group', namespace: 'default', name },
|
||||
},
|
||||
],
|
||||
} as Entity);
|
||||
|
||||
@@ -28,6 +28,7 @@ export default {
|
||||
|
||||
const dummyDepartment = {
|
||||
type: 'childOf',
|
||||
targetRef: 'group:default/department-a',
|
||||
target: {
|
||||
namespace: 'default',
|
||||
kind: 'group',
|
||||
|
||||
@@ -54,6 +54,7 @@ const makeUser = ({
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/team-a',
|
||||
target: {
|
||||
namespace: 'default',
|
||||
kind: 'group',
|
||||
|
||||
@@ -59,6 +59,7 @@ describe('MemberTab Test', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/team-d',
|
||||
target: {
|
||||
kind: 'group',
|
||||
name: 'team-d',
|
||||
|
||||
@@ -69,9 +69,10 @@ const makeComponent = ({ type, name }: { type: string; name: string }) => ({
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/team-a',
|
||||
target: {
|
||||
namespace: 'default',
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
name: 'team-a',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -43,10 +43,11 @@ const items = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/my-team',
|
||||
target: {
|
||||
name: 'my-team',
|
||||
namespace: 'default',
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -62,10 +63,11 @@ const items = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/my-team',
|
||||
target: {
|
||||
name: 'my-team',
|
||||
namespace: 'default',
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -82,10 +84,11 @@ const items = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/my-team',
|
||||
target: {
|
||||
name: 'my-team',
|
||||
namespace: 'default',
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -99,10 +102,11 @@ const items = [
|
||||
relations: [
|
||||
{
|
||||
type: 'ownedBy',
|
||||
targetRef: 'group:default/my-team',
|
||||
target: {
|
||||
name: 'my-team',
|
||||
namespace: 'default',
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -135,9 +139,10 @@ describe('OwnershipCard', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/examplegroup',
|
||||
target: {
|
||||
kind: 'group',
|
||||
name: 'ExampleGroup',
|
||||
name: 'examplegroup',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
@@ -260,8 +265,9 @@ describe('OwnershipCard', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/my-team',
|
||||
target: {
|
||||
kind: 'Group',
|
||||
kind: 'group',
|
||||
name: 'my-team',
|
||||
namespace: 'default',
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export default {
|
||||
|
||||
const dummyGroup = {
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/team-a',
|
||||
target: {
|
||||
namespace: 'default',
|
||||
kind: 'group',
|
||||
|
||||
@@ -42,6 +42,7 @@ describe('UserSummary Test', () => {
|
||||
relations: [
|
||||
{
|
||||
type: 'memberOf',
|
||||
targetRef: 'group:default/examplegroup',
|
||||
target: {
|
||||
kind: 'group',
|
||||
name: 'ExampleGroup',
|
||||
@@ -70,9 +71,9 @@ describe('UserSummary Test', () => {
|
||||
'src',
|
||||
'https://example.com/staff/calum.jpeg',
|
||||
);
|
||||
expect(rendered.getByText('ExampleGroup')).toHaveAttribute(
|
||||
expect(rendered.getByText('examplegroup')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/group/ExampleGroup',
|
||||
'/catalog/default/group/examplegroup',
|
||||
);
|
||||
expect(rendered.getByText('Super awesome human')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ const defaultEntityListResponse: GetEntitiesResponse = {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/my-team',
|
||||
target: { name: 'team-a', kind: 'group', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ const defaultEntityListResponse: GetEntitiesResponse = {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/team-a',
|
||||
target: { name: 'team-a', kind: 'group', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
|
||||
+1
@@ -57,6 +57,7 @@ const defaultEntityListResponse: GetEntitiesResponse = {
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
targetRef: 'group:default/team-a',
|
||||
target: { name: 'team-a', kind: 'group', namespace: 'default' },
|
||||
},
|
||||
],
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from '@backstage/backend-common';
|
||||
import {
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
RELATION_OWNED_BY,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
@@ -154,9 +155,7 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
entityTitle: entity.metadata.title,
|
||||
componentType: entity.spec?.type?.toString() || 'other',
|
||||
lifecycle: (entity.spec?.lifecycle as string) || '',
|
||||
owner:
|
||||
entity.relations?.find(r => r.type === RELATION_OWNED_BY)
|
||||
?.target?.name || '',
|
||||
owner: getSimpleEntityOwnerString(entity),
|
||||
authorization: {
|
||||
resourceRef: stringifyEntityRef(entity),
|
||||
},
|
||||
@@ -202,3 +201,14 @@ export class DefaultTechDocsCollator implements DocumentCollator {
|
||||
}, {} as EntityInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function getSimpleEntityOwnerString(entity: Entity): string {
|
||||
if (entity.relations) {
|
||||
const owner = entity.relations.find(r => r.type === RELATION_OWNED_BY);
|
||||
if (owner) {
|
||||
const { name } = parseEntityRef(owner.targetRef);
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('DocsTable test', () => {
|
||||
namespace: 'default',
|
||||
name: 'owned',
|
||||
},
|
||||
targetRef: 'user:default/owned',
|
||||
type: 'ownedBy',
|
||||
},
|
||||
],
|
||||
@@ -86,6 +87,7 @@ describe('DocsTable test', () => {
|
||||
namespace: 'default',
|
||||
name: 'not-owned',
|
||||
},
|
||||
targetRef: 'user:default/not-owned',
|
||||
type: 'ownedBy',
|
||||
},
|
||||
],
|
||||
@@ -137,6 +139,7 @@ describe('DocsTable test', () => {
|
||||
namespace: 'default',
|
||||
name: 'owned',
|
||||
},
|
||||
targetRef: 'user:default/owned',
|
||||
type: 'ownedBy',
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user