Merge pull request #6983 from SDA-SE/feat/better-about
Update the `AboutCard` to properly support non-standard entity types and rework the defaults for the build-in kinds
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Update the `AboutCard` to properly support non-standard entity types and rework the defaults for the build-in kinds.
|
||||
|
||||
This change also uses `useElementFilter(...)` instead of `React.children.count(...)` in `AboutField` to properly recognize whether children are available.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Use a `Link` for the edit button on the `AboutCard` instead of doing `window.open(...)`
|
||||
@@ -15,21 +15,20 @@
|
||||
*/
|
||||
|
||||
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
ConfigReader,
|
||||
} from '@backstage/core-app-api';
|
||||
import {
|
||||
ScmIntegrationsApi,
|
||||
scmIntegrationsApiRef,
|
||||
} from '@backstage/integration-react';
|
||||
import { EntityProvider } from '@backstage/plugin-catalog-react';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { AboutCard } from './AboutCard';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
ConfigReader,
|
||||
} from '@backstage/core-app-api';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutCard } from './AboutCard';
|
||||
|
||||
describe('<AboutCard />', () => {
|
||||
it('renders info', async () => {
|
||||
@@ -166,14 +165,10 @@ describe('<AboutCard />', () => {
|
||||
</ApiProvider>,
|
||||
);
|
||||
|
||||
const editButton = getByTitle('Edit Metadata');
|
||||
window.open = jest.fn();
|
||||
await act(async () => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
`https://github.com/backstage/backstage/edit/master/software.yaml`,
|
||||
'_blank',
|
||||
const editLink = getByTitle('Edit Metadata').closest('a');
|
||||
expect(editLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://github.com/backstage/backstage/edit/master/software.yaml',
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,13 @@ import {
|
||||
RELATION_CONSUMES_API,
|
||||
RELATION_PROVIDES_API,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
InfoCardVariants,
|
||||
Link,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ScmIntegrationIcon,
|
||||
scmIntegrationsApiRef,
|
||||
@@ -42,15 +49,8 @@ import DocsIcon from '@material-ui/icons/Description';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import React from 'react';
|
||||
import { AboutContent } from './AboutContent';
|
||||
|
||||
import {
|
||||
HeaderIconLinkRow,
|
||||
IconLinkVerticalProps,
|
||||
InfoCardVariants,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { viewTechDocRouteRef } from '../../routes';
|
||||
import { AboutContent } from './AboutContent';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
gridItemCard: {
|
||||
@@ -148,12 +148,11 @@ export function AboutCard({ variant }: AboutCardProps) {
|
||||
title="About"
|
||||
action={
|
||||
<IconButton
|
||||
component={Link}
|
||||
aria-label="Edit"
|
||||
disabled={!entityMetadataEditUrl}
|
||||
title="Edit Metadata"
|
||||
onClick={() => {
|
||||
window.open(entityMetadataEditUrl ?? '#', '_blank');
|
||||
}}
|
||||
to={entityMetadataEditUrl ?? '#'}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
Entity,
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_PART_OF,
|
||||
} from '@backstage/catalog-model';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { AboutContent } from './AboutContent';
|
||||
|
||||
describe('<AboutContent />', () => {
|
||||
describe('An unknown entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'custom.company/v1',
|
||||
kind: 'Unknown',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
owner: 'o',
|
||||
domain: 'd',
|
||||
system: 's',
|
||||
type: 't',
|
||||
lifecycle: 'l',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'o',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 's',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'domain',
|
||||
name: 'd',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:o');
|
||||
expect(getByText('Domain')).toBeInTheDocument();
|
||||
expect(getByText('Domain').nextSibling).toHaveTextContent('d');
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('s');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('t');
|
||||
expect(getByText('Lifecycle')).toBeInTheDocument();
|
||||
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('l');
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
entity.spec = {};
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(queryByText('Type')).not.toBeInTheDocument();
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('API entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'API',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
type: 'openapi',
|
||||
lifecycle: 'production',
|
||||
owner: 'guest',
|
||||
definition: '...',
|
||||
system: 'system',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('system');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('openapi');
|
||||
expect(getByText('Lifecycle')).toBeInTheDocument();
|
||||
expect(getByText('Lifecycle').nextSibling).toHaveTextContent(
|
||||
'production',
|
||||
);
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
delete entity.spec!.type;
|
||||
delete entity.spec!.lifecycle;
|
||||
delete entity.spec!.system;
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('No System');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
|
||||
expect(getByText('Lifecycle')).toBeInTheDocument();
|
||||
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown');
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'v1',
|
||||
kind: 'Component',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
type: 'service',
|
||||
lifecycle: 'production',
|
||||
system: 'system',
|
||||
subcomponentOf: ['parent-software'],
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'component',
|
||||
name: 'parent-software',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('system');
|
||||
expect(getByText('Parent Component')).toBeInTheDocument();
|
||||
expect(getByText('Parent Component').nextSibling).toHaveTextContent(
|
||||
'parent-software',
|
||||
);
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('service');
|
||||
expect(getByText('Lifecycle')).toBeInTheDocument();
|
||||
expect(getByText('Lifecycle').nextSibling).toHaveTextContent(
|
||||
'production',
|
||||
);
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
delete entity.spec!.type;
|
||||
delete entity.spec!.lifecycle;
|
||||
delete entity.spec!.system;
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('No System');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
|
||||
expect(getByText('Lifecycle')).toBeInTheDocument();
|
||||
expect(getByText('Lifecycle').nextSibling).toHaveTextContent('unknown');
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Domain entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Domain',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(queryByText('Type')).not.toBeInTheDocument();
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(queryByText('Type')).not.toBeInTheDocument();
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Location entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Location',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
type: 'root',
|
||||
},
|
||||
relations: [],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('root');
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
delete entity.spec!.type;
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Resource',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
type: 's3',
|
||||
owner: 'guest',
|
||||
system: 'system',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'system',
|
||||
name: 'system',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('system');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('s3');
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
delete entity.spec!.type;
|
||||
delete entity.spec!.system;
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(queryByText('Domain')).not.toBeInTheDocument();
|
||||
expect(getByText('System')).toBeInTheDocument();
|
||||
expect(getByText('System').nextSibling).toHaveTextContent('No System');
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(getByText('Type')).toBeInTheDocument();
|
||||
expect(getByText('Type').nextSibling).toHaveTextContent('unknown');
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe('System entity', () => {
|
||||
let entity: Entity;
|
||||
|
||||
beforeEach(() => {
|
||||
entity = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'System',
|
||||
metadata: {
|
||||
name: 'software',
|
||||
description: 'This is the description',
|
||||
tags: ['tag-1'],
|
||||
},
|
||||
spec: {
|
||||
owner: 'guest',
|
||||
domain: 'domain',
|
||||
},
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_OWNED_BY,
|
||||
target: {
|
||||
kind: 'user',
|
||||
name: 'guest',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: RELATION_PART_OF,
|
||||
target: {
|
||||
kind: 'domain',
|
||||
name: 'domain',
|
||||
namespace: 'default',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('renders info', async () => {
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('user:guest');
|
||||
expect(getByText('Domain')).toBeInTheDocument();
|
||||
expect(getByText('Domain').nextSibling).toHaveTextContent('domain');
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(queryByText('Type')).not.toBeInTheDocument();
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('tag-1');
|
||||
});
|
||||
|
||||
it('highlights missing required fields', async () => {
|
||||
delete entity.metadata.tags;
|
||||
delete entity.spec!.domain;
|
||||
entity.relations = [];
|
||||
|
||||
const { getByText, queryByText } = await renderInTestApp(
|
||||
<AboutContent entity={entity} />,
|
||||
);
|
||||
|
||||
expect(getByText('Description')).toBeInTheDocument();
|
||||
expect(getByText('Description').nextSibling).toHaveTextContent(
|
||||
'This is the description',
|
||||
);
|
||||
expect(getByText('Owner')).toBeInTheDocument();
|
||||
expect(getByText('Owner').nextSibling).toHaveTextContent('No Owner');
|
||||
expect(getByText('Domain')).toBeInTheDocument();
|
||||
expect(getByText('Domain').nextSibling).toHaveTextContent('No Domain');
|
||||
expect(queryByText('System')).not.toBeInTheDocument();
|
||||
expect(queryByText('Parent Component')).not.toBeInTheDocument();
|
||||
expect(queryByText('Type')).not.toBeInTheDocument();
|
||||
expect(queryByText('Lifecycle')).not.toBeInTheDocument();
|
||||
expect(getByText('Tags')).toBeInTheDocument();
|
||||
expect(getByText('Tags').nextSibling).toHaveTextContent('No Tags');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -40,9 +40,13 @@ type Props = {
|
||||
export const AboutContent = ({ entity }: Props) => {
|
||||
const classes = useStyles();
|
||||
const isSystem = entity.kind.toLocaleLowerCase('en-US') === 'system';
|
||||
const isDomain = entity.kind.toLocaleLowerCase('en-US') === 'domain';
|
||||
const isResource = entity.kind.toLocaleLowerCase('en-US') === 'resource';
|
||||
const isComponent = entity.kind.toLocaleLowerCase('en-US') === 'component';
|
||||
const isAPI = entity.kind.toLocaleLowerCase('en-US') === 'api';
|
||||
const isTemplate = entity.kind.toLocaleLowerCase('en-US') === 'template';
|
||||
const isLocation = entity.kind.toLocaleLowerCase('en-US') === 'location';
|
||||
const isGroup = entity.kind.toLocaleLowerCase('en-US') === 'group';
|
||||
|
||||
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
|
||||
kind: 'system',
|
||||
});
|
||||
@@ -65,31 +69,44 @@ export const AboutContent = ({ entity }: Props) => {
|
||||
{entity?.metadata?.description || 'No description'}
|
||||
</Typography>
|
||||
</AboutField>
|
||||
<AboutField label="Owner" gridSizes={{ xs: 12, sm: 6, lg: 4 }}>
|
||||
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="group" />
|
||||
<AboutField
|
||||
label="Owner"
|
||||
value="No Owner"
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
{ownedByRelations.length > 0 && (
|
||||
<EntityRefLinks entityRefs={ownedByRelations} defaultKind="group" />
|
||||
)}
|
||||
</AboutField>
|
||||
{isSystem && (
|
||||
{(isSystem || partOfDomainRelations.length > 0) && (
|
||||
<AboutField
|
||||
label="Domain"
|
||||
value="No Domain"
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
<EntityRefLinks
|
||||
entityRefs={partOfDomainRelations}
|
||||
defaultKind="domain"
|
||||
/>
|
||||
{partOfDomainRelations.length > 0 && (
|
||||
<EntityRefLinks
|
||||
entityRefs={partOfDomainRelations}
|
||||
defaultKind="domain"
|
||||
/>
|
||||
)}
|
||||
</AboutField>
|
||||
)}
|
||||
{!isSystem && !isDomain && (
|
||||
{(isAPI ||
|
||||
isComponent ||
|
||||
isResource ||
|
||||
partOfSystemRelations.length > 0) && (
|
||||
<AboutField
|
||||
label="System"
|
||||
value="No System"
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
>
|
||||
<EntityRefLinks
|
||||
entityRefs={partOfSystemRelations}
|
||||
defaultKind="system"
|
||||
/>
|
||||
{partOfSystemRelations.length > 0 && (
|
||||
<EntityRefLinks
|
||||
entityRefs={partOfSystemRelations}
|
||||
defaultKind="system"
|
||||
/>
|
||||
)}
|
||||
</AboutField>
|
||||
)}
|
||||
{isComponent && partOfComponentRelations.length > 0 && (
|
||||
@@ -104,14 +121,22 @@ export const AboutContent = ({ entity }: Props) => {
|
||||
/>
|
||||
</AboutField>
|
||||
)}
|
||||
{!isSystem && !isDomain && (
|
||||
{(isAPI ||
|
||||
isComponent ||
|
||||
isResource ||
|
||||
isTemplate ||
|
||||
isGroup ||
|
||||
isLocation ||
|
||||
typeof entity?.spec?.type === 'string') && (
|
||||
<AboutField
|
||||
label="Type"
|
||||
value={entity?.spec?.type as string}
|
||||
gridSizes={{ xs: 12, sm: 6, lg: 4 }}
|
||||
/>
|
||||
)}
|
||||
{!isSystem && !isDomain && !isResource && (
|
||||
{(isAPI ||
|
||||
isComponent ||
|
||||
typeof entity?.spec?.lifecycle === 'string') && (
|
||||
<AboutField
|
||||
label="Lifecycle"
|
||||
value={entity?.spec?.lifecycle as string}
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useElementFilter } from '@backstage/core-plugin-api';
|
||||
import { Grid, makeStyles, Typography } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { makeStyles, Typography, Grid } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
value: {
|
||||
@@ -45,14 +46,17 @@ type Props = {
|
||||
export const AboutField = ({ label, value, gridSizes, children }: Props) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const childElements = useElementFilter(children, c => c.getElements());
|
||||
|
||||
// Content is either children or a string prop `value`
|
||||
const content = React.Children.count(children) ? (
|
||||
children
|
||||
) : (
|
||||
<Typography variant="body2" className={classes.value}>
|
||||
{value || `unknown`}
|
||||
</Typography>
|
||||
);
|
||||
const content =
|
||||
childElements.length > 0 ? (
|
||||
childElements
|
||||
) : (
|
||||
<Typography variant="body2" className={classes.value}>
|
||||
{value || `unknown`}
|
||||
</Typography>
|
||||
);
|
||||
return (
|
||||
<Grid item {...gridSizes}>
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
|
||||
Reference in New Issue
Block a user