Make use of proposed metadata
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Added a "create something similar" button to the `<AboutCard>` that is visible and links to the scaffolder template corresponding to the entity's `backstage.io/source-template` annotation, if present.
|
||||
@@ -31,7 +31,7 @@ import { AboutCard } from './AboutCard';
|
||||
import { ConfigReader } from '@backstage/core-app-api';
|
||||
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { permissionApiRef } from '@backstage/plugin-permission-react';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
@@ -44,6 +44,7 @@ describe('<AboutCard />', () => {
|
||||
const catalogApi: jest.Mocked<CatalogApi> = {
|
||||
getLocationById: jest.fn(),
|
||||
getEntityByName: jest.fn(),
|
||||
getEntityByRef: jest.fn(),
|
||||
getEntities: jest.fn(),
|
||||
addLocation: jest.fn(),
|
||||
getLocationByRef: jest.fn(),
|
||||
@@ -266,6 +267,138 @@ describe('<AboutCard />', () => {
|
||||
expect(screen.getByText('View Source').closest('a')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders "create something similar" button', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/source-template': 'template:default/foo-template',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
|
||||
// Return any valid value to indicate access to the template is okay.
|
||||
catalogApi.getEntityByRef.mockImplementation(async ref => {
|
||||
expect(ref).toBe('template:default/foo-template');
|
||||
return entity;
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: '...',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
'/create/templates/:namespace/:templateName':
|
||||
createFromTemplateRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const editLink = screen
|
||||
.getByTitle('Create something similar')
|
||||
.closest('a');
|
||||
expect(editLink).toHaveAttribute(
|
||||
'href',
|
||||
'/create/templates/default/foo-template',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render "create something similar" button if template does not exist', async () => {
|
||||
const entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
annotations: {
|
||||
'backstage.io/source-template': 'template:default/gone-template',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
},
|
||||
};
|
||||
|
||||
// Return any valid value to indicate access to the template is okay.
|
||||
catalogApi.getEntityByRef.mockImplementation(async ref => {
|
||||
expect(ref).toBe('template:default/gone-template');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
scmIntegrationsApiRef,
|
||||
ScmIntegrationsApi.fromConfig(
|
||||
new ConfigReader({
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token: '...',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
],
|
||||
[catalogApiRef, catalogApi],
|
||||
[permissionApiRef, {}],
|
||||
]}
|
||||
>
|
||||
<EntityProvider entity={entity}>
|
||||
<AboutCard />
|
||||
</EntityProvider>
|
||||
</TestApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
'/create/templates/:namespace/:templateName':
|
||||
createFromTemplateRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTitle('Create something similar'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'url:https://backstage.io/catalog-info.yaml',
|
||||
'file:../../catalog-info.yaml',
|
||||
|
||||
@@ -54,12 +54,14 @@ import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutContent } from './AboutContent';
|
||||
import CachedIcon from '@material-ui/icons/Cached';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import CopyIcon from '@material-ui/icons/FileCopy';
|
||||
import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import { isTemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { parseEntityRef } from '@backstage/catalog-model';
|
||||
import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha';
|
||||
import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { useSourceTemplateCompoundEntityRef } from './hooks';
|
||||
|
||||
const TECHDOCS_ANNOTATION = 'backstage.io/techdocs-ref';
|
||||
|
||||
@@ -108,6 +110,7 @@ export function AboutCard(props: AboutCardProps) {
|
||||
const errorApi = useApi(errorApiRef);
|
||||
const viewTechdocLink = useRouteRef(viewTechDocRouteRef);
|
||||
const templateRoute = useRouteRef(createFromTemplateRouteRef);
|
||||
const sourceTemplateRef = useSourceTemplateCompoundEntityRef(entity);
|
||||
const { allowed: canRefresh } = useEntityPermission(
|
||||
catalogEntityRefreshPermission,
|
||||
);
|
||||
@@ -236,6 +239,18 @@ export function AboutCard(props: AboutCardProps) {
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
{sourceTemplateRef && templateRoute && (
|
||||
<IconButton
|
||||
component={Link}
|
||||
title="Create something similar"
|
||||
to={templateRoute({
|
||||
namespace: sourceTemplateRef.namespace,
|
||||
templateName: sourceTemplateRef.name,
|
||||
})}
|
||||
>
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
subheader={<HeaderIconLinkRow links={subHeaderLinks} />}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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 {
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
parseEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
|
||||
// todo: should this be a constant in a scaffolder package?
|
||||
const SOURCE_TEMPLATE_ANNOTATION = 'backstage.io/source-template';
|
||||
|
||||
/**
|
||||
* Returns the compound entity ref of the source template that was used to
|
||||
* create this entity (assuming that it still exists and the user has access
|
||||
* to it).
|
||||
*/
|
||||
export const useSourceTemplateCompoundEntityRef = (entity: Entity) => {
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { value: sourceTemplateRef } = useAsync(async () => {
|
||||
const refCandidate =
|
||||
entity.metadata.annotations?.[SOURCE_TEMPLATE_ANNOTATION];
|
||||
let compoundRefCandidate: CompoundEntityRef | undefined;
|
||||
|
||||
if (!refCandidate) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for access and that this template still exists.
|
||||
const template = await catalogApi.getEntityByRef(refCandidate);
|
||||
compoundRefCandidate = parseEntityRef(refCandidate);
|
||||
|
||||
return template !== undefined ? compoundRefCandidate : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}, [catalogApi, entity]);
|
||||
|
||||
return sourceTemplateRef;
|
||||
};
|
||||
Reference in New Issue
Block a user