diff --git a/.changeset/pink-coins-sniff.md b/.changeset/pink-coins-sniff.md
new file mode 100644
index 0000000000..dd2bc86b2a
--- /dev/null
+++ b/.changeset/pink-coins-sniff.md
@@ -0,0 +1,16 @@
+---
+'@backstage/plugin-api-docs': patch
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-catalog-react': patch
+---
+
+Introduce new cards to `@backstage/plugin-catalog` that can be added to entity pages:
+
+- `EntityHasSystemsCard` to display systems of a domain.
+- `EntityHasComponentsCard` to display components of a system.
+- `EntityHasSubcomponentsCard` to display subcomponents of a subcomponent.
+- In addition, `EntityHasApisCard` to display APIs of a system is added to `@backstage/plugin-api-docs`.
+
+`@backstage/plugin-catalog-react` now provides `and` to build own cards for components and systems.
+The styling of the tables and new cards was also applied to the existing `EntityConsumedApisCard`,
+`EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`.
diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx
index e849667c19..7fb624b9e9 100644
--- a/packages/app/src/components/catalog/EntityPage.tsx
+++ b/packages/app/src/components/catalog/EntityPage.tsx
@@ -16,8 +16,10 @@
import {
ApiEntity,
+ DomainEntity,
Entity,
GroupEntity,
+ SystemEntity,
UserEntity,
} from '@backstage/catalog-model';
import { EmptyState } from '@backstage/core';
@@ -25,11 +27,15 @@ import {
ApiDefinitionCard,
ConsumedApisCard,
ConsumingComponentsCard,
+ EntityHasApisCard,
ProvidedApisCard,
ProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
AboutCard,
+ EntityHasComponentsCard,
+ EntityHasSubcomponentsCard,
+ EntityHasSystemsCard,
EntityLinksCard,
EntityPageLayout,
} from '@backstage/plugin-catalog';
@@ -206,6 +212,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
)}
+
+
+
);
@@ -425,6 +434,51 @@ const GroupEntityPage = ({ entity }: { entity: Entity }) => (
);
+const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => (
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const SystemEntityPage = ({ entity }: { entity: Entity }) => (
+
+ }
+ />
+
+);
+
+const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => (
+
+
+
+
+
+
+
+
+);
+
+const DomainEntityPage = ({ entity }: { entity: Entity }) => (
+
+ }
+ />
+
+);
+
export const EntityPage = () => {
const { entity } = useEntity();
@@ -437,6 +491,10 @@ export const EntityPage = () => {
return ;
case 'user':
return ;
+ case 'system':
+ return ;
+ case 'domain':
+ return ;
default:
return ;
}
diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx
index 30716a61fb..76daf15945 100644
--- a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx
@@ -27,12 +27,14 @@ import {
formatEntityRefTitle,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
+import { makeStyles } from '@material-ui/core';
import React from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
type EntityRow = {
entity: ApiEntity;
resolved: {
+ name: string;
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
@@ -43,10 +45,10 @@ type EntityRow = {
const columns: TableColumn[] = [
{
title: 'Name',
- field: 'entity.metadata.name',
+ field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
- {entity.metadata.name}
+
),
},
{
@@ -88,10 +90,25 @@ const columns: TableColumn[] = [
type Props = {
title: string;
variant?: string;
- entities: (ApiEntity | undefined)[];
+ entities: ApiEntity[];
+ emptyComponent?: JSX.Element;
};
-export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
+const useStyles = makeStyles(theme => ({
+ empty: {
+ padding: theme.spacing(2),
+ display: 'flex',
+ justifyContent: 'center',
+ },
+}));
+
+export const ApisTable = ({
+ entities,
+ title,
+ emptyComponent,
+ variant = 'gridItem',
+}: Props) => {
+ const classes = useStyles();
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
@@ -101,43 +118,42 @@ export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => {
tableStyle.height = 'calc(100% - 10px)';
}
- const rows = entities
- // TODO: For now we skip all APIs that we can't find without a warning!
- .filter(e => e !== undefined)
- .map(entity => {
- const partOfSystemRelations = getEntityRelations(
- entity,
- RELATION_PART_OF,
- {
- kind: 'system',
- },
- );
- const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
-
- return {
- entity: entity as ApiEntity,
- resolved: {
- ownedByRelationsTitle: ownedByRelations
- .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
- .join(', '),
- ownedByRelations,
- partOfSystemRelationTitle: partOfSystemRelations
- .map(r =>
- formatEntityRefTitle(r, {
- defaultKind: 'system',
- }),
- )
- .join(', '),
- partOfSystemRelations,
- },
- };
+ const rows = entities.map(entity => {
+ const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
+ kind: 'system',
});
+ const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
+
+ return {
+ entity,
+ resolved: {
+ name: formatEntityRefTitle(entity, {
+ defaultKind: 'API',
+ }),
+ ownedByRelationsTitle: ownedByRelations
+ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
+ .join(', '),
+ ownedByRelations,
+ partOfSystemRelationTitle: partOfSystemRelations
+ .map(r =>
+ formatEntityRefTitle(r, {
+ defaultKind: 'system',
+ }),
+ )
+ .join(', '),
+ partOfSystemRelations,
+ },
+ };
+ });
return (
columns={columns}
title={title}
style={tableStyle}
+ emptyComponent={
+ emptyComponent && {emptyComponent}
+ }
options={{
// TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
search: false,
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
index 051b9588a0..5d2978a198 100644
--- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-import {
- Entity,
- RELATION_CONSUMES_API,
- RELATION_OWNED_BY,
- RELATION_PART_OF,
-} from '@backstage/catalog-model';
+import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -79,7 +74,7 @@ describe('', () => {
);
expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
- expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
+ expect(getByText(/No Component consumes this API/i)).toBeInTheDocument();
});
it('shows consumed APIs', async () => {
@@ -108,34 +103,7 @@ describe('', () => {
name: 'target-name',
namespace: 'my-namespace',
},
- spec: {
- type: 'openapi',
- lifecycle: 'production',
- definition: '...',
- },
- relations: [
- {
- type: RELATION_PART_OF,
- target: {
- kind: 'System',
- name: 'MySystem',
- namespace: 'default',
- },
- },
- {
- type: RELATION_OWNED_BY,
- target: {
- kind: 'Group',
- name: 'Test',
- namespace: 'default',
- },
- },
- ],
- });
- apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
- type: 'openapi',
- title: 'OpenAPI',
- component: () => ,
+ spec: {},
});
const { getByText } = await renderInTestApp(
@@ -147,12 +115,8 @@ describe('', () => {
);
await waitFor(() => {
- expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
+ expect(getByText('Consumed APIs')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
- expect(getByText(/OpenAPI/)).toBeInTheDocument();
- expect(getByText(/Test/i)).toBeInTheDocument();
- expect(getByText(/MySystem/i)).toBeInTheDocument();
- expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx
index 6124ea979a..7e57a23b06 100644
--- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx
@@ -19,10 +19,15 @@ import {
Entity,
RELATION_CONSUMES_API,
} from '@backstage/catalog-model';
-import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
-import { MissingConsumesApisEmptyState } from '../EmptyState';
import { ApisTable } from './ApisTable';
const ApisCard = ({
@@ -56,31 +61,31 @@ export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => {
);
}
- if (error) {
+ if (error || !entities) {
return (
- }
/>
);
}
- if (!entities || entities.length === 0) {
- return (
-
-
-
- );
- }
-
return (
+ No Component consumes this API.{' '}
+
+ Learn how to consume APIs.
+
+
+ }
+ entities={entities as ApiEntity[]}
/>
);
};
diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
new file mode 100644
index 0000000000..7b97f0867e
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import {
+ CatalogApi,
+ catalogApiRef,
+ EntityProvider,
+} from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ApiDocsConfig, apiDocsConfigRef } from '../../config';
+import { HasApisCard } from './HasApisCard';
+
+describe('', () => {
+ const apiDocsConfig: jest.Mocked = {
+ getApiDefinitionWidget: jest.fn(),
+ } as any;
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
+ apiDocsConfigRef,
+ apiDocsConfig,
+ );
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('APIs')).toBeInTheDocument();
+ expect(getByText(/No API is part of this system/i)).toBeInTheDocument();
+ });
+
+ it('shows related APIs', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'API',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_HAS_PART,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'API',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText('APIs')).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx
new file mode 100644
index 0000000000..5ed38e91c7
--- /dev/null
+++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ApiEntity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
+import React, { PropsWithChildren } from 'react';
+import { ApisTable } from './ApisTable';
+
+const ApisCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ variant?: string;
+};
+
+export const HasApisCard = ({ variant = 'gridItem' }: Props) => {
+ const { entity } = useEntity();
+ const { entities, loading, error } = useRelatedEntities(entity, {
+ type: RELATION_HAS_PART,
+ kind: 'API',
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !entities) {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+ return (
+
+ No API is part of this system.{' '}
+
+ Learn how to add APIs.
+
+
+ }
+ entities={entities as ApiEntity[]}
+ />
+ );
+};
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
index 482534f58a..bb4d0ac82f 100644
--- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-import {
- Entity,
- RELATION_OWNED_BY,
- RELATION_PART_OF,
- RELATION_PROVIDES_API,
-} from '@backstage/catalog-model';
+import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -79,7 +74,7 @@ describe('', () => {
);
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
- expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
+ expect(getByText(/No Component provides this API/i)).toBeInTheDocument();
});
it('shows consumed APIs', async () => {
@@ -108,34 +103,7 @@ describe('', () => {
name: 'target-name',
namespace: 'my-namespace',
},
- spec: {
- type: 'openapi',
- lifecycle: 'production',
- definition: '...',
- },
- relations: [
- {
- type: RELATION_PART_OF,
- target: {
- kind: 'System',
- name: 'MySystem',
- namespace: 'default',
- },
- },
- {
- type: RELATION_OWNED_BY,
- target: {
- kind: 'Group',
- name: 'Test',
- namespace: 'default',
- },
- },
- ],
- });
- apiDocsConfig.getApiDefinitionWidget.mockReturnValue({
- type: 'openapi',
- title: 'OpenAPI',
- component: () => ,
+ spec: {},
});
const { getByText } = await renderInTestApp(
@@ -149,10 +117,6 @@ describe('', () => {
await waitFor(() => {
expect(getByText(/Provided APIs/i)).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
- expect(getByText(/OpenAPI/)).toBeInTheDocument();
- expect(getByText(/MySystem/)).toBeInTheDocument();
- expect(getByText(/Test/i)).toBeInTheDocument();
- expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx
index 92155e65f5..e760d7a8f0 100644
--- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx
+++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx
@@ -19,10 +19,15 @@ import {
Entity,
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
-import { EmptyState, InfoCard, Progress } from '@backstage/core';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
-import { MissingProvidesApisEmptyState } from '../EmptyState';
import { ApisTable } from './ApisTable';
const ApisCard = ({
@@ -56,31 +61,31 @@ export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => {
);
}
- if (error) {
+ if (error || !entities) {
return (
- }
/>
);
}
- if (!entities || entities.length === 0) {
- return (
-
-
-
- );
- }
-
return (
+ No Component provides this API.{' '}
+
+ Learn how to provide APIs.
+
+
+ }
+ entities={entities as ApiEntity[]}
/>
);
};
diff --git a/plugins/api-docs/src/components/ApisCards/index.ts b/plugins/api-docs/src/components/ApisCards/index.ts
index 2a01a1dc6e..9243bdedd2 100644
--- a/plugins/api-docs/src/components/ApisCards/index.ts
+++ b/plugins/api-docs/src/components/ApisCards/index.ts
@@ -15,4 +15,5 @@
*/
export { ConsumedApisCard } from './ConsumedApisCard';
+export { HasApisCard } from './HasApisCard';
export { ProvidedApisCard } from './ProvidedApisCard';
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
index f45a48842d..7944b3f90a 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-import {
- Entity,
- RELATION_API_CONSUMED_BY,
- RELATION_OWNED_BY,
- RELATION_PART_OF,
-} from '@backstage/catalog-model';
+import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -77,8 +72,8 @@ describe('', () => {
,
);
- expect(getByText(/Consumers/i)).toBeInTheDocument();
- expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument();
+ expect(getByText('Consumers')).toBeInTheDocument();
+ expect(getByText(/No component consumes this API/i)).toBeInTheDocument();
});
it('shows consuming components', async () => {
@@ -113,28 +108,7 @@ describe('', () => {
name: 'target-name',
namespace: 'my-namespace',
},
- spec: {
- type: 'service',
- lifecycle: 'production',
- },
- relations: [
- {
- type: RELATION_PART_OF,
- target: {
- kind: 'System',
- name: 'MySystem',
- namespace: 'default',
- },
- },
- {
- type: RELATION_OWNED_BY,
- target: {
- kind: 'Group',
- name: 'Test',
- namespace: 'default',
- },
- },
- ],
+ spec: {},
});
const { getByText } = await renderInTestApp(
@@ -146,11 +120,8 @@ describe('', () => {
);
await waitFor(() => {
- expect(getByText(/Consumers/i)).toBeInTheDocument();
+ expect(getByText('Consumers')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
- expect(getByText(/Test/i)).toBeInTheDocument();
- expect(getByText(/MySystem/i)).toBeInTheDocument();
- expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx
index 9ed5bf6d2a..ed987b33f5 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx
@@ -19,11 +19,20 @@ import {
Entity,
RELATION_API_CONSUMED_BY,
} from '@backstage/catalog-model';
-import { EmptyState, InfoCard, Progress } from '@backstage/core';
-import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import {
+ ComponentsTable,
+ useEntity,
+ useRelatedEntities,
+} from '@backstage/plugin-catalog-react';
+
import React, { PropsWithChildren } from 'react';
-import { MissingConsumesApisEmptyState } from '../EmptyState';
-import { ComponentsTable } from './ComponentsTable';
const ComponentsCard = ({
children,
@@ -56,31 +65,31 @@ export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => {
);
}
- if (error) {
+ if (error || !entities) {
return (
- }
/>
);
}
- if (!entities || entities.length === 0) {
- return (
-
-
-
- );
- }
-
return (
+ No component consumes this API.{' '}
+
+ Learn how to consume APIs.
+
+
+ }
+ entities={entities as ComponentEntity[]}
/>
);
};
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
index 4c55393d13..ab1751b9ba 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx
@@ -14,12 +14,7 @@
* limitations under the License.
*/
-import {
- Entity,
- RELATION_API_PROVIDED_BY,
- RELATION_OWNED_BY,
- RELATION_PART_OF,
-} from '@backstage/catalog-model';
+import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model';
import { ApiProvider, ApiRegistry } from '@backstage/core';
import {
CatalogApi,
@@ -77,8 +72,8 @@ describe('', () => {
,
);
- expect(getByText(/Providers/i)).toBeInTheDocument();
- expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument();
+ expect(getByText('Providers')).toBeInTheDocument();
+ expect(getByText(/No component provides this API/i)).toBeInTheDocument();
});
it('shows providing components', async () => {
@@ -113,28 +108,7 @@ describe('', () => {
name: 'target-name',
namespace: 'my-namespace',
},
- spec: {
- type: 'service',
- lifecycle: 'production',
- },
- relations: [
- {
- type: RELATION_PART_OF,
- target: {
- kind: 'System',
- name: 'MySystem',
- namespace: 'default',
- },
- },
- {
- type: RELATION_OWNED_BY,
- target: {
- kind: 'Group',
- name: 'Test',
- namespace: 'default',
- },
- },
- ],
+ spec: {},
});
const { getByText } = await renderInTestApp(
@@ -146,11 +120,8 @@ describe('', () => {
);
await waitFor(() => {
- expect(getByText(/Providers/i)).toBeInTheDocument();
+ expect(getByText('Providers')).toBeInTheDocument();
expect(getByText(/target-name/i)).toBeInTheDocument();
- expect(getByText(/Test/i)).toBeInTheDocument();
- expect(getByText(/MySystem/i)).toBeInTheDocument();
- expect(getByText(/production/i)).toBeInTheDocument();
});
});
});
diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx
index 06377ca0d9..39e179dd10 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx
+++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx
@@ -19,11 +19,19 @@ import {
Entity,
RELATION_API_PROVIDED_BY,
} from '@backstage/catalog-model';
-import { EmptyState, InfoCard, Progress } from '@backstage/core';
-import { useEntity, useRelatedEntities } from '@backstage/plugin-catalog-react';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import {
+ ComponentsTable,
+ useEntity,
+ useRelatedEntities,
+} from '@backstage/plugin-catalog-react';
import React, { PropsWithChildren } from 'react';
-import { MissingProvidesApisEmptyState } from '../EmptyState';
-import { ComponentsTable } from './ComponentsTable';
const ComponentsCard = ({
children,
@@ -56,31 +64,31 @@ export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => {
);
}
- if (error) {
+ if (error || !entities) {
return (
- }
/>
);
}
- if (!entities || entities.length === 0) {
- return (
-
-
-
- );
- }
-
return (
+ No component provides this API.{' '}
+
+ Learn how to provide APIs.
+
+
+ }
+ entities={entities as ComponentEntity[]}
/>
);
};
diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts
index e7484f77c3..8bab7f28f4 100644
--- a/plugins/api-docs/src/index.ts
+++ b/plugins/api-docs/src/index.ts
@@ -24,4 +24,5 @@ export {
EntityConsumingComponentsCard,
EntityProvidedApisCard,
EntityProvidingComponentsCard,
+ EntityHasApisCard,
} from './plugin';
diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts
index 429fd60095..1d57cc6cc2 100644
--- a/plugins/api-docs/src/plugin.ts
+++ b/plugins/api-docs/src/plugin.ts
@@ -106,3 +106,11 @@ export const EntityProvidingComponentsCard = apiDocsPlugin.provide(
},
}),
);
+
+export const EntityHasApisCard = apiDocsPlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () => import('./components/ApisCards').then(m => m.HasApisCard),
+ },
+ }),
+);
diff --git a/plugins/catalog-react/src/components/EntityTables/ComponentsTable.test.tsx b/plugins/catalog-react/src/components/EntityTables/ComponentsTable.test.tsx
new file mode 100644
index 0000000000..f29d1cfe60
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityTables/ComponentsTable.test.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ComponentEntity,
+ RELATION_OWNED_BY,
+ RELATION_PART_OF,
+} from '@backstage/catalog-model';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { ComponentsTable } from './ComponentsTable';
+
+describe('', () => {
+ it('shows empty table', async () => {
+ const { getByText } = await renderInTestApp(
+ EMPTY}
+ />,
+ );
+
+ expect(getByText('My Components')).toBeInTheDocument();
+ expect(getByText('EMPTY')).toBeInTheDocument();
+ });
+
+ it('shows components', async () => {
+ const entities: ComponentEntity[] = [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-component',
+ namespace: 'my-namespace',
+ description: 'Some description',
+ },
+ spec: {
+ type: 'service',
+ lifecycle: 'production',
+ owner: 'owner-data',
+ },
+ relations: [
+ {
+ type: RELATION_PART_OF,
+ target: {
+ kind: 'System',
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ },
+ {
+ type: RELATION_OWNED_BY,
+ target: {
+ kind: 'Group',
+ name: 'Test',
+ namespace: 'default',
+ },
+ },
+ ],
+ },
+ ];
+
+ const { getByText } = await renderInTestApp(
+ EMPTY}
+ />,
+ );
+
+ await waitFor(() => {
+ expect(getByText('My Components')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-component')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-system')).toBeInTheDocument();
+ expect(getByText('Test')).toBeInTheDocument();
+ expect(getByText('production')).toBeInTheDocument();
+ expect(getByText('service')).toBeInTheDocument();
+ expect(getByText('Some description')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/catalog-react/src/components/EntityTables/ComponentsTable.tsx
similarity index 64%
rename from plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx
rename to plugins/catalog-react/src/components/EntityTables/ComponentsTable.tsx
index bd9b0c765f..b79c7e705a 100644
--- a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx
+++ b/plugins/catalog-react/src/components/EntityTables/ComponentsTable.tsx
@@ -21,17 +21,19 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Table, TableColumn } from '@backstage/core';
+import { makeStyles } from '@material-ui/core';
+import React from 'react';
+import { getEntityRelations } from '../../utils';
import {
EntityRefLink,
EntityRefLinks,
formatEntityRefTitle,
- getEntityRelations,
-} from '@backstage/plugin-catalog-react';
-import React from 'react';
+} from '../EntityRefLink';
type EntityRow = {
entity: ComponentEntity;
resolved: {
+ name: string;
partOfSystemRelationTitle?: string;
partOfSystemRelations: EntityName[];
ownedByRelationsTitle?: string;
@@ -42,10 +44,10 @@ type EntityRow = {
const columns: TableColumn[] = [
{
title: 'Name',
- field: 'entity.metadata.name',
+ field: 'resolved.name',
highlight: true,
render: ({ entity }) => (
- {entity.metadata.name}
+
),
},
{
@@ -86,15 +88,25 @@ const columns: TableColumn[] = [
type Props = {
title: string;
variant?: string;
- entities: (ComponentEntity | undefined)[];
+ entities: ComponentEntity[];
+ emptyComponent?: JSX.Element;
};
-// TODO: In theory this could also be systems!
+const useStyles = makeStyles(theme => ({
+ empty: {
+ padding: theme.spacing(2),
+ display: 'flex',
+ justifyContent: 'center',
+ },
+}));
+
export const ComponentsTable = ({
entities,
title,
+ emptyComponent,
variant = 'gridItem',
}: Props) => {
+ const classes = useStyles();
const tableStyle: React.CSSProperties = {
minWidth: '0',
width: '100%',
@@ -104,43 +116,42 @@ export const ComponentsTable = ({
tableStyle.height = 'calc(100% - 10px)';
}
- const rows = entities
- // TODO: For now we skip all Components that we can't find without a warning!
- .filter(e => e !== undefined)
- .map(entity => {
- const partOfSystemRelations = getEntityRelations(
- entity,
- RELATION_PART_OF,
- {
- kind: 'system',
- },
- );
- const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
-
- return {
- entity: entity as ComponentEntity,
- resolved: {
- ownedByRelationsTitle: ownedByRelations
- .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
- .join(', '),
- ownedByRelations,
- partOfSystemRelationTitle: partOfSystemRelations
- .map(r =>
- formatEntityRefTitle(r, {
- defaultKind: 'system',
- }),
- )
- .join(', '),
- partOfSystemRelations,
- },
- };
+ const rows = entities.map(entity => {
+ const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
+ kind: 'system',
});
+ const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
+
+ return {
+ entity: entity,
+ resolved: {
+ name: formatEntityRefTitle(entity, {
+ defaultKind: 'Component',
+ }),
+ ownedByRelationsTitle: ownedByRelations
+ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
+ .join(', '),
+ ownedByRelations,
+ partOfSystemRelationTitle: partOfSystemRelations
+ .map(r =>
+ formatEntityRefTitle(r, {
+ defaultKind: 'system',
+ }),
+ )
+ .join(', '),
+ partOfSystemRelations,
+ },
+ };
+ });
return (
columns={columns}
title={title}
style={tableStyle}
+ emptyComponent={
+ emptyComponent && {emptyComponent}
+ }
options={{
// TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
search: false,
diff --git a/plugins/catalog-react/src/components/EntityTables/SystemsTable.test.tsx b/plugins/catalog-react/src/components/EntityTables/SystemsTable.test.tsx
new file mode 100644
index 0000000000..9f89d7592e
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityTables/SystemsTable.test.tsx
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ RELATION_OWNED_BY,
+ RELATION_PART_OF,
+ SystemEntity,
+} from '@backstage/catalog-model';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { SystemsTable } from './SystemsTable';
+
+describe('', () => {
+ it('shows empty table', async () => {
+ const { getByText } = await renderInTestApp(
+ EMPTY}
+ />,
+ );
+
+ expect(getByText('My Systems')).toBeInTheDocument();
+ expect(getByText('EMPTY')).toBeInTheDocument();
+ });
+
+ it('shows systems', async () => {
+ const entities: SystemEntity[] = [
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ description: 'Some description',
+ },
+ spec: {
+ owner: 'owner-data',
+ },
+ relations: [
+ {
+ type: RELATION_PART_OF,
+ target: {
+ kind: 'Domain',
+ name: 'my-domain',
+ namespace: 'my-namespace',
+ },
+ },
+ {
+ type: RELATION_OWNED_BY,
+ target: {
+ kind: 'Group',
+ name: 'Test',
+ namespace: 'default',
+ },
+ },
+ ],
+ },
+ ];
+
+ const { getByText } = await renderInTestApp(
+ EMPTY}
+ />,
+ );
+
+ await waitFor(() => {
+ expect(getByText('My Systems')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-system')).toBeInTheDocument();
+ expect(getByText('my-namespace/my-domain')).toBeInTheDocument();
+ expect(getByText('Test')).toBeInTheDocument();
+ expect(getByText('Some description')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/catalog-react/src/components/EntityTables/SystemsTable.tsx b/plugins/catalog-react/src/components/EntityTables/SystemsTable.tsx
new file mode 100644
index 0000000000..8f30ea0543
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityTables/SystemsTable.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ EntityName,
+ RELATION_OWNED_BY,
+ RELATION_PART_OF,
+ SystemEntity,
+} from '@backstage/catalog-model';
+import { Table, TableColumn } from '@backstage/core';
+import { makeStyles } from '@material-ui/core';
+import React from 'react';
+import { getEntityRelations } from '../../utils';
+import {
+ EntityRefLink,
+ EntityRefLinks,
+ formatEntityRefTitle,
+} from '../EntityRefLink';
+
+type EntityRow = {
+ entity: SystemEntity;
+ resolved: {
+ name: string;
+ partOfDomainRelationTitle?: string;
+ partOfDomainRelations: EntityName[];
+ ownedByRelationsTitle?: string;
+ ownedByRelations: EntityName[];
+ };
+};
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ field: 'resolved.name',
+ highlight: true,
+ render: ({ entity }) => (
+
+ ),
+ },
+ {
+ title: 'Domain',
+ field: 'resolved.partOfDomainRelationTitle',
+ render: ({ resolved }) => (
+
+ ),
+ },
+ {
+ title: 'Owner',
+ field: 'resolved.ownedByRelationsTitle',
+ render: ({ resolved }) => (
+
+ ),
+ },
+ {
+ title: 'Description',
+ field: 'entity.metadata.description',
+ width: 'auto',
+ },
+];
+
+type Props = {
+ title: string;
+ variant?: string;
+ entities: SystemEntity[];
+ emptyComponent?: JSX.Element;
+};
+
+const useStyles = makeStyles(theme => ({
+ empty: {
+ padding: theme.spacing(2),
+ display: 'flex',
+ justifyContent: 'center',
+ },
+}));
+
+export const SystemsTable = ({
+ entities,
+ title,
+ emptyComponent,
+ variant = 'gridItem',
+}: Props) => {
+ const classes = useStyles();
+ const tableStyle: React.CSSProperties = {
+ minWidth: '0',
+ width: '100%',
+ };
+
+ if (variant === 'gridItem') {
+ tableStyle.height = 'calc(100% - 10px)';
+ }
+
+ const rows = entities.map(entity => {
+ const partOfDomainRelations = getEntityRelations(entity, RELATION_PART_OF, {
+ kind: 'domain',
+ });
+ const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
+
+ return {
+ entity,
+ resolved: {
+ name: formatEntityRefTitle(entity, {
+ defaultKind: 'System',
+ }),
+ ownedByRelationsTitle: ownedByRelations
+ .map(r => formatEntityRefTitle(r, { defaultKind: 'group' }))
+ .join(', '),
+ ownedByRelations,
+ partOfDomainRelationTitle: partOfDomainRelations
+ .map(r =>
+ formatEntityRefTitle(r, {
+ defaultKind: 'domain',
+ }),
+ )
+ .join(', '),
+ partOfDomainRelations,
+ },
+ };
+ });
+
+ return (
+
+ columns={columns}
+ title={title}
+ style={tableStyle}
+ emptyComponent={
+ emptyComponent && {emptyComponent}
+ }
+ options={{
+ // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px;
+ search: false,
+ paging: false,
+ actionsColumnIndex: -1,
+ padding: 'dense',
+ }}
+ data={rows}
+ />
+ );
+};
diff --git a/plugins/catalog-react/src/components/EntityTables/index.ts b/plugins/catalog-react/src/components/EntityTables/index.ts
new file mode 100644
index 0000000000..f3e4f416ac
--- /dev/null
+++ b/plugins/catalog-react/src/components/EntityTables/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { ComponentsTable } from './ComponentsTable';
+export { SystemsTable } from './SystemsTable';
diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts
index fcfa8207b3..463aeb6250 100644
--- a/plugins/catalog-react/src/components/index.ts
+++ b/plugins/catalog-react/src/components/index.ts
@@ -13,5 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export * from './EntityRefLink';
export * from './EntityProvider';
+export * from './EntityRefLink';
+export * from './EntityTables';
diff --git a/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.test.tsx
new file mode 100644
index 0000000000..58df0a53f8
--- /dev/null
+++ b/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.test.tsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import {
+ CatalogApi,
+ catalogApiRef,
+ EntityProvider,
+} from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { HasComponentsCard } from './HasComponentsCard';
+
+describe('', () => {
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi);
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('Components')).toBeInTheDocument();
+ expect(
+ getByText(/No component is part of this system/i),
+ ).toBeInTheDocument();
+ });
+
+ it('shows related components', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'my-system',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'Component',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_HAS_PART,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText('Components')).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.tsx
new file mode 100644
index 0000000000..c9b5879a05
--- /dev/null
+++ b/plugins/catalog/src/components/ComponentsCard/HasComponentsCard.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import {
+ ComponentsTable,
+ useEntity,
+ useRelatedEntities,
+} from '@backstage/plugin-catalog-react';
+import React, { PropsWithChildren } from 'react';
+
+const ComponentsCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ variant?: string;
+};
+
+export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => {
+ const { entity } = useEntity();
+ const { entities, loading, error } = useRelatedEntities(entity, {
+ type: RELATION_HAS_PART,
+ kind: 'Component',
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !entities) {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+ return (
+
+ No component is part of this system.{' '}
+
+ Learn how to add components.
+
+
+ }
+ entities={entities as ComponentEntity[]}
+ />
+ );
+};
diff --git a/plugins/catalog/src/components/ComponentsCard/index.ts b/plugins/catalog/src/components/ComponentsCard/index.ts
new file mode 100644
index 0000000000..059392c3e8
--- /dev/null
+++ b/plugins/catalog/src/components/ComponentsCard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { HasComponentsCard } from './HasComponentsCard';
diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
new file mode 100644
index 0000000000..79b69d35ff
--- /dev/null
+++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import {
+ CatalogApi,
+ catalogApiRef,
+ EntityProvider,
+} from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { HasSubcomponentsCard } from './HasSubcomponentsCard';
+
+describe('', () => {
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi);
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-components',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('Subcomponents')).toBeInTheDocument();
+ expect(
+ getByText(/No subcomponent is part of this component/i),
+ ).toBeInTheDocument();
+ });
+
+ it('shows related subcomponents', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'my-component',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'Component',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_HAS_PART,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText('Subcomponents')).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx
new file mode 100644
index 0000000000..541ccf795d
--- /dev/null
+++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import {
+ ComponentsTable,
+ useEntity,
+ useRelatedEntities,
+} from '@backstage/plugin-catalog-react';
+import React, { PropsWithChildren } from 'react';
+
+const SubcomponentsCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ variant?: string;
+};
+
+export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => {
+ const { entity } = useEntity();
+ const { entities, loading, error } = useRelatedEntities(entity, {
+ type: RELATION_HAS_PART,
+ kind: 'Component',
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !entities) {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+ return (
+
+ No subcomponent is part of this component.{' '}
+
+ Learn how to add subcomponents.
+
+
+ }
+ entities={entities as ComponentEntity[]}
+ />
+ );
+};
diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts
new file mode 100644
index 0000000000..cef0221d3b
--- /dev/null
+++ b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { HasSubcomponentsCard } from './HasSubcomponentsCard';
diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
new file mode 100644
index 0000000000..08923a59c8
--- /dev/null
+++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity, RELATION_HAS_PART } from '@backstage/catalog-model';
+import { ApiProvider, ApiRegistry } from '@backstage/core';
+import {
+ CatalogApi,
+ catalogApiRef,
+ EntityProvider,
+} from '@backstage/plugin-catalog-react';
+import { renderInTestApp } from '@backstage/test-utils';
+import { waitFor } from '@testing-library/react';
+import React from 'react';
+import { HasSystemsCard } from './HasSystemsCard';
+
+describe('', () => {
+ const catalogApi: jest.Mocked = {
+ getLocationById: jest.fn(),
+ getEntityByName: jest.fn(),
+ getEntities: jest.fn(),
+ addLocation: jest.fn(),
+ getLocationByEntity: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ } as any;
+ let Wrapper: React.ComponentType;
+
+ beforeEach(() => {
+ const apis = ApiRegistry.with(catalogApiRef, catalogApi);
+
+ Wrapper = ({ children }: { children?: React.ReactNode }) => (
+ {children}
+ );
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('shows empty list if no relations', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Domain',
+ metadata: {
+ name: 'my-domain',
+ namespace: 'my-namespace',
+ },
+ relations: [],
+ };
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ expect(getByText('Systems')).toBeInTheDocument();
+ expect(getByText(/No system is part of this domain/i)).toBeInTheDocument();
+ });
+
+ it('shows related systems', async () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Domain',
+ metadata: {
+ name: 'my-domain',
+ namespace: 'my-namespace',
+ },
+ relations: [
+ {
+ target: {
+ kind: 'System',
+ namespace: 'my-namespace',
+ name: 'target-name',
+ },
+ type: RELATION_HAS_PART,
+ },
+ ],
+ };
+ catalogApi.getEntityByName.mockResolvedValue({
+ apiVersion: 'v1',
+ kind: 'System',
+ metadata: {
+ name: 'target-name',
+ namespace: 'my-namespace',
+ },
+ spec: {},
+ });
+
+ const { getByText } = await renderInTestApp(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() => {
+ expect(getByText('Systems')).toBeInTheDocument();
+ expect(getByText(/target-name/i)).toBeInTheDocument();
+ });
+ });
+});
diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx
new file mode 100644
index 0000000000..0af5f1747d
--- /dev/null
+++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { RELATION_HAS_PART, SystemEntity } from '@backstage/catalog-model';
+import {
+ CodeSnippet,
+ InfoCard,
+ Link,
+ Progress,
+ WarningPanel,
+} from '@backstage/core';
+import {
+ SystemsTable,
+ useEntity,
+ useRelatedEntities,
+} from '@backstage/plugin-catalog-react';
+import React, { PropsWithChildren } from 'react';
+
+const SystemsCard = ({
+ children,
+ variant = 'gridItem',
+}: PropsWithChildren<{ variant?: string }>) => {
+ return (
+
+ {children}
+
+ );
+};
+
+type Props = {
+ variant?: string;
+};
+
+export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => {
+ const { entity } = useEntity();
+ const { entities, loading, error } = useRelatedEntities(entity, {
+ type: RELATION_HAS_PART,
+ });
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (error || !entities) {
+ return (
+
+ }
+ />
+
+ );
+ }
+
+ return (
+
+ No system is part of this domain.{' '}
+
+ Learn how to add systems.
+
+
+ }
+ entities={entities as SystemEntity[]}
+ />
+ );
+};
diff --git a/plugins/catalog/src/components/HasSystemsCard/index.ts b/plugins/catalog/src/components/HasSystemsCard/index.ts
new file mode 100644
index 0000000000..a29d257e7e
--- /dev/null
+++ b/plugins/catalog/src/components/HasSystemsCard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2020 Spotify AB
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { HasSystemsCard } from './HasSystemsCard';
diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts
index ce8518eb55..6d1704503e 100644
--- a/plugins/catalog/src/index.ts
+++ b/plugins/catalog/src/index.ts
@@ -20,10 +20,14 @@ export { EntityPageLayout } from './components/EntityPageLayout';
export * from './components/EntitySwitch';
export { Router } from './components/Router';
export {
+ CatalogEntityPage,
+ CatalogIndexPage,
catalogPlugin,
catalogPlugin as plugin,
- CatalogIndexPage,
- CatalogEntityPage,
EntityAboutCard,
- EntityLinksCard,
+ EntityHasComponentsCard,
+ EntityHasSubcomponentsCard,
+ EntityHasSystemsCard,
+ EntityLinksCard
} from './plugin';
+
diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts
index c2b910ce68..3b2a6f58f4 100644
--- a/plugins/catalog/src/plugin.ts
+++ b/plugins/catalog/src/plugin.ts
@@ -17,10 +17,10 @@
import { CatalogClient } from '@backstage/catalog-client';
import {
createApiFactory,
- createPlugin,
- discoveryApiRef,
createComponentExtension,
+ createPlugin,
createRoutableExtension,
+ discoveryApiRef,
identityApiRef,
} from '@backstage/core';
import {
@@ -60,9 +60,7 @@ export const CatalogIndexPage = catalogPlugin.provide(
export const CatalogEntityPage = catalogPlugin.provide(
createRoutableExtension({
component: () =>
- import('./components/CatalogEntityPage/CatalogEntityPage').then(
- m => m.CatalogEntityPage,
- ),
+ import('./components/CatalogEntityPage').then(m => m.CatalogEntityPage),
mountPoint: entityRouteRef,
}),
);
@@ -83,3 +81,32 @@ export const EntityLinksCard = catalogPlugin.provide(
},
}),
);
+
+export const EntityHasSystemsCard = catalogPlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/HasSystemsCard').then(m => m.HasSystemsCard),
+ },
+ }),
+);
+
+export const EntityHasComponentsCard = catalogPlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/ComponentsCard').then(m => m.HasComponentsCard),
+ },
+ }),
+);
+
+export const EntityHasSubcomponentsCard = catalogPlugin.provide(
+ createComponentExtension({
+ component: {
+ lazy: () =>
+ import('./components/HasSubcomponentsCard').then(
+ m => m.HasSubcomponentsCard,
+ ),
+ },
+ }),
+);