Merge pull request #13732 from acierto/entity-ref-link
Provide custom displayed text in EntityRefLinks
This commit is contained in:
@@ -279,13 +279,28 @@ export type EntityRefLinkProps = {
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
// @public
|
||||
export function EntityRefLinks(props: EntityRefLinksProps): JSX.Element;
|
||||
export function EntityRefLinks<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
>(props: EntityRefLinksProps<TRef>): JSX.Element;
|
||||
|
||||
// @public
|
||||
export type EntityRefLinksProps = {
|
||||
entityRefs: (string | Entity | CompoundEntityRef)[];
|
||||
defaultKind?: string;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
export type EntityRefLinksProps<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
> = (
|
||||
| {
|
||||
defaultKind?: string;
|
||||
entityRefs: TRef[];
|
||||
fetchEntities?: false;
|
||||
getTitle?(entity: TRef): string | undefined;
|
||||
}
|
||||
| {
|
||||
defaultKind?: string;
|
||||
entityRefs: TRef[];
|
||||
fetchEntities: true;
|
||||
getTitle(entity: Entity): string | undefined;
|
||||
}
|
||||
) &
|
||||
Omit<LinkProps, 'to'>;
|
||||
|
||||
// @public
|
||||
export function entityRouteParams(entity: Entity): {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
* Copyright 2022 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.
|
||||
@@ -13,41 +13,72 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { EntityRefLink } from './EntityRefLink';
|
||||
import { LinkProps } from '@backstage/core-components';
|
||||
import { FetchedEntityRefLinks } from './FetchedEntityRefLinks';
|
||||
|
||||
/**
|
||||
* Props for {@link EntityRefLink}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type EntityRefLinksProps = {
|
||||
entityRefs: (string | Entity | CompoundEntityRef)[];
|
||||
defaultKind?: string;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
export type EntityRefLinksProps<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
> = (
|
||||
| {
|
||||
defaultKind?: string;
|
||||
entityRefs: TRef[];
|
||||
fetchEntities?: false;
|
||||
getTitle?(entity: TRef): string | undefined;
|
||||
}
|
||||
| {
|
||||
defaultKind?: string;
|
||||
entityRefs: TRef[];
|
||||
fetchEntities: true;
|
||||
getTitle(entity: Entity): string | undefined;
|
||||
}
|
||||
) &
|
||||
Omit<LinkProps, 'to'>;
|
||||
|
||||
/**
|
||||
* Shows a list of clickable links to entities.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function EntityRefLinks(props: EntityRefLinksProps) {
|
||||
const { entityRefs, defaultKind, ...linkProps } = props;
|
||||
export function EntityRefLinks<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
>(props: EntityRefLinksProps<TRef>) {
|
||||
const { entityRefs, defaultKind, fetchEntities, getTitle, ...linkProps } =
|
||||
props;
|
||||
|
||||
if (fetchEntities) {
|
||||
return (
|
||||
<FetchedEntityRefLinks
|
||||
{...linkProps}
|
||||
defaultKind={defaultKind}
|
||||
entityRefs={entityRefs}
|
||||
getTitle={getTitle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{entityRefs.map((r, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && ', '}
|
||||
<EntityRefLink
|
||||
{...linkProps}
|
||||
entityRef={r}
|
||||
defaultKind={defaultKind}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
{entityRefs.map((r: TRef, i: number) => {
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && ', '}
|
||||
<EntityRefLink
|
||||
{...linkProps}
|
||||
defaultKind={defaultKind}
|
||||
entityRef={r}
|
||||
title={getTitle ? getTitle(r) : undefined}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright 2022 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 { FetchedEntityRefLinks } from './FetchedEntityRefLinks';
|
||||
import { entityRouteRef } from '../../routes';
|
||||
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
|
||||
describe('<FetchedEntityRefLinks />', () => {
|
||||
const getTitle = (e: Entity): string =>
|
||||
(e.spec?.profile!! as JsonObject).displayName!!.toString()!!;
|
||||
|
||||
it('should fetch entities and render the custom display text', async () => {
|
||||
const entityRefs = [
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'software',
|
||||
},
|
||||
{
|
||||
kind: 'API',
|
||||
namespace: 'default',
|
||||
name: 'interface',
|
||||
},
|
||||
];
|
||||
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: () =>
|
||||
Promise.resolve({
|
||||
items: entityRefs.map(ref => ({
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: ref.kind,
|
||||
metadata: {
|
||||
name: ref.name,
|
||||
namespace: ref.namespace,
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: ref.name.toLocaleUpperCase('en-US'),
|
||||
},
|
||||
type: 'organization',
|
||||
},
|
||||
})),
|
||||
}),
|
||||
};
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.getByText('SOFTWARE')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/software',
|
||||
);
|
||||
|
||||
expect(rendered.getByText('INTERFACE')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/api/interface',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use entities as they are provided and render the custom display text', async () => {
|
||||
const entityRefs = [
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'tool',
|
||||
},
|
||||
{
|
||||
kind: 'API',
|
||||
namespace: 'default',
|
||||
name: 'implementation',
|
||||
},
|
||||
].map(ref => ({
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: ref.kind,
|
||||
metadata: {
|
||||
name: ref.name,
|
||||
namespace: ref.namespace,
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: ref.name.toLocaleUpperCase('en-US'),
|
||||
},
|
||||
type: 'organization',
|
||||
},
|
||||
}));
|
||||
|
||||
const catalogApi: Partial<CatalogApi> = {};
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.getByText('TOOL')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/tool',
|
||||
);
|
||||
|
||||
expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/api/implementation',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle heterogeneous array of values to render the custom display text', async () => {
|
||||
const entityRefs = [
|
||||
...[
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'tool',
|
||||
},
|
||||
{
|
||||
kind: 'API',
|
||||
namespace: 'default',
|
||||
name: 'implementation',
|
||||
},
|
||||
].map(ref => ({
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: ref.kind,
|
||||
metadata: {
|
||||
name: ref.name,
|
||||
namespace: ref.namespace,
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: ref.name.toLocaleUpperCase('en-US'),
|
||||
},
|
||||
type: 'organization',
|
||||
},
|
||||
})),
|
||||
{
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'interface',
|
||||
},
|
||||
];
|
||||
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: () =>
|
||||
Promise.resolve({
|
||||
items: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'interface',
|
||||
namespace: 'default',
|
||||
},
|
||||
spec: {
|
||||
profile: {
|
||||
displayName: 'INTERFACE',
|
||||
},
|
||||
type: 'organization',
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
const rendered = await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
|
||||
<FetchedEntityRefLinks entityRefs={entityRefs} getTitle={getTitle} />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name/*': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(rendered.getByText('TOOL')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/tool',
|
||||
);
|
||||
|
||||
expect(rendered.getByText('IMPLEMENTATION')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/api/implementation',
|
||||
);
|
||||
|
||||
expect(rendered.getByText('INTERFACE')).toHaveAttribute(
|
||||
'href',
|
||||
'/catalog/default/component/interface',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright 2022 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,
|
||||
CompoundEntityRef,
|
||||
parseEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { EntityRefLink } from './EntityRefLink';
|
||||
import { ErrorPanel, LinkProps, Progress } from '@backstage/core-components';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import { catalogApiRef } from '../../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* Props for {@link FetchedEntityRefLinks}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type FetchedEntityRefLinksProps<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
> = {
|
||||
defaultKind?: string;
|
||||
entityRefs: TRef[];
|
||||
getTitle(entity: Entity): string | undefined;
|
||||
} & Omit<LinkProps, 'to'>;
|
||||
|
||||
/**
|
||||
* Shows a list of clickable links to entities with auto-fetching of entities
|
||||
* for customising a displayed text via title attribute.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function FetchedEntityRefLinks<
|
||||
TRef extends string | CompoundEntityRef | Entity,
|
||||
>(props: FetchedEntityRefLinksProps<TRef>) {
|
||||
const { entityRefs, defaultKind, getTitle, ...linkProps } = props;
|
||||
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
|
||||
const {
|
||||
value: entities = new Array<Entity>(),
|
||||
loading,
|
||||
error,
|
||||
} = useAsync(async () => {
|
||||
const refs = entityRefs.reduce((acc, current) => {
|
||||
return 'metadata' in current ? acc : [...acc, parseEntityRef(current)];
|
||||
}, new Array<CompoundEntityRef>());
|
||||
|
||||
const pureEntities = entityRefs.filter(
|
||||
ref => 'metadata' in ref,
|
||||
) as Array<Entity>;
|
||||
|
||||
return refs.length > 0
|
||||
? [
|
||||
...(
|
||||
await catalogApi.getEntities({
|
||||
filter: refs.map(ref => ({
|
||||
kind: ref.kind,
|
||||
'metadata.namespace': ref.namespace,
|
||||
'metadata.name': ref.name,
|
||||
})),
|
||||
})
|
||||
).items,
|
||||
...pureEntities,
|
||||
]
|
||||
: pureEntities;
|
||||
}, [entityRefs]);
|
||||
|
||||
if (loading) {
|
||||
return <Progress />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ErrorPanel error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{entities.map((r: Entity, i) => {
|
||||
return (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && ', '}
|
||||
<EntityRefLink
|
||||
{...linkProps}
|
||||
defaultKind={defaultKind}
|
||||
entityRef={r}
|
||||
title={getTitle(r as Entity)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user