Merge pull request #15381 from sblausten/systems-tab

Allow Systems or other component types to be shown on the Explore Page
This commit is contained in:
Patrik Oldsberg
2023-04-04 20:27:00 +02:00
committed by GitHub
14 changed files with 431 additions and 7 deletions
+5 -2
View File
@@ -2,7 +2,7 @@
Welcome to the explore plugin!
This plugin helps to visualize the domains and tools in your ecosystem.
This plugin helps to visualize the top level entities like domains, groups and tools in your ecosystem.
## Setup
@@ -117,7 +117,10 @@ export const ExplorePage = () => {
subtitle="Browse our ecosystem"
>
<ExploreLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
<CatalogKindExploreContent kind="domain" />
</ExploreLayout.Route>
<ExploreLayout.Route path="systems" title="Systems">
<CatalogKindExploreContent kind="system" />
</ExploreLayout.Route>
<ExploreLayout.Route path="inner-source" title="InnerSource">
<AcmeInnserSourceExplorerContent />
+6
View File
@@ -32,6 +32,12 @@ export const catalogEntityRouteRef: ExternalRouteRef<
true
>;
// @public (undocumented)
export const CatalogKindExploreContent: (props: {
title?: string;
kind: string;
}) => JSX.Element;
// @public (undocumented)
export const DomainCard: (props: { entity: DomainEntity }) => JSX.Element;
+1
View File
@@ -47,6 +47,7 @@
"@material-ui/lab": "4.0.0-alpha.61",
"@types/react": "^16.13.1 || ^17.0.0",
"classnames": "^2.2.6",
"pluralize": "^8.0.0",
"react-use": "^17.2.4"
},
"peerDependencies": {
@@ -0,0 +1,136 @@
/*
* 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 { DomainEntity } from '@backstage/catalog-model';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { waitFor } from '@testing-library/react';
import React from 'react';
import { CatalogKindExploreContent } from './CatalogKindExploreContent';
describe('<CatalogKindExploreContent />', () => {
const catalogApi = {
addLocation: jest.fn(),
getEntities: jest.fn(),
getLocationByRef: jest.fn(),
getLocationById: jest.fn(),
removeLocationById: jest.fn(),
removeEntityByUid: jest.fn(),
getEntityByRef: jest.fn(),
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</TestApiProvider>
);
const mountedRoutes = {
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
};
beforeEach(() => {
jest.resetAllMocks();
});
it('renders a grid of domains', async () => {
const entities: DomainEntity[] = [
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
metadata: {
name: 'playback',
},
spec: {
owner: 'guest',
},
},
{
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
metadata: {
name: 'artists',
},
spec: {
owner: 'guest',
},
},
];
catalogApi.getEntities.mockResolvedValue({ items: entities });
const { getByText } = await renderInTestApp(
<Wrapper>
<CatalogKindExploreContent kind="domain" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() => {
expect(getByText('artists')).toBeInTheDocument();
expect(getByText('playback')).toBeInTheDocument();
});
});
it('renders a custom title', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
const { getByText } = await renderInTestApp(
<Wrapper>
<CatalogKindExploreContent kind="domain" title="Our Areas" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() => expect(getByText('Our Areas')).toBeInTheDocument());
});
it('renders empty state', async () => {
catalogApi.getEntities.mockResolvedValue({ items: [] });
const { getByText } = await renderInTestApp(
<Wrapper>
<CatalogKindExploreContent kind="domain" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() =>
expect(getByText('No domains to display')).toBeInTheDocument(),
);
});
it('renders a friendly error if it cannot collect domains', async () => {
const catalogError = new Error('Network timeout');
catalogApi.getEntities.mockRejectedValueOnce(catalogError);
const { getByText } = await renderInTestApp(
<Wrapper>
<CatalogKindExploreContent kind="domain" />
</Wrapper>,
mountedRoutes,
);
await waitFor(() =>
expect(getByText(/Could not load domains/)).toBeInTheDocument(),
);
});
});
@@ -0,0 +1,108 @@
/*
* 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 } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Button } from '@material-ui/core';
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { EntityCard } from '../EntityCard';
import {
Content,
ContentHeader,
EmptyState,
ItemCardGrid,
Progress,
SupportButton,
WarningPanel,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import pluralize from 'pluralize';
const Body = (props: { kind: string }) => {
const { kind } = props;
const kindPlural = pluralize(kind);
const catalogApi = useApi(catalogApiRef);
const {
value: entities,
loading,
error,
} = useAsync(async () => {
const response = await catalogApi.getEntities({
filter: { kind },
});
return response.items as Entity[];
}, [kind]);
if (loading) {
return <Progress />;
}
if (error) {
return (
<WarningPanel severity="error" title={`Could not load ${kindPlural}.`}>
{error.message}
</WarningPanel>
);
}
if (!entities?.length) {
return (
<EmptyState
missing="info"
title={`No ${kindPlural} to display`}
description={`You haven't added any ${kindPlural} yet.`}
action={
<Button
variant="contained"
color="primary"
href={`https://backstage.io/docs/features/software-catalog/descriptor-format#kind-${kind}`}
>
Read more
</Button>
}
/>
);
}
return (
<ItemCardGrid>
{entities.map((entity, index) => (
<EntityCard key={index} entity={entity} />
))}
</ItemCardGrid>
);
};
/** @public */
export const CatalogKindExploreContent = (props: {
title?: string;
kind: string;
}) => {
const { kind, title } = props;
const kindLowercase = kind.toLocaleLowerCase();
const kindCapitalized = `${kind[0].toLocaleUpperCase()}${kind.substring(1)}`;
return (
<Content noPadding>
<ContentHeader title={title || `${pluralize(kindCapitalized)}`}>
<SupportButton>
Discover the {pluralize(kindLowercase)} in your ecosystem.
</SupportButton>
</ContentHeader>
<Body kind={kindLowercase} />
</Content>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { CatalogKindExploreContent } from './CatalogKindExploreContent';
@@ -15,11 +15,11 @@
*/
import React from 'react';
import { DomainExplorerContent } from '../DomainExplorerContent';
import { ExploreLayout } from '../ExploreLayout';
import { GroupsExplorerContent } from '../GroupsExplorerContent';
import { ToolExplorerContent } from '../ToolExplorerContent';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { CatalogKindExploreContent } from '../CatalogKindExploreContent';
export const DefaultExplorePage = () => {
const configApi = useApi(configApiRef);
@@ -32,7 +32,7 @@ export const DefaultExplorePage = () => {
subtitle="Discover solutions available in your ecosystem"
>
<ExploreLayout.Route path="domains" title="Domains">
<DomainExplorerContent />
<CatalogKindExploreContent kind="domain" />
</ExploreLayout.Route>
<ExploreLayout.Route path="groups" title="Groups">
<GroupsExplorerContent />
@@ -19,7 +19,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { Button } from '@material-ui/core';
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { DomainCard } from '../DomainCard';
import {
Content,
ContentHeader,
@@ -30,6 +29,7 @@ import {
WarningPanel,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { EntityCard } from '../EntityCard';
const Body = () => {
const catalogApi = useApi(catalogApiRef);
@@ -78,7 +78,7 @@ const Body = () => {
return (
<ItemCardGrid>
{entities.map((entity, index) => (
<DomainCard key={index} entity={entity} />
<EntityCard key={index} entity={entity} />
))}
</ItemCardGrid>
);
@@ -0,0 +1,54 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { entityRouteRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp } from '@backstage/test-utils';
import React from 'react';
import { EntityCard } from './EntityCard';
describe('<EntityCard />', () => {
it('renders an entity card', async () => {
const entity: Entity = {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Domain',
metadata: {
name: 'artists',
description: 'Everything about artists',
tags: ['a-tag'],
},
spec: {
owner: 'guest',
},
};
const { getByText } = await renderInTestApp(
<EntityCard entity={entity} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(getByText('artists')).toBeInTheDocument();
expect(getByText('Everything about artists')).toBeInTheDocument();
expect(getByText('a-tag')).toBeInTheDocument();
expect(getByText('Explore').parentElement).toHaveAttribute(
'href',
'/catalog/default/domain/artists',
);
});
});
@@ -0,0 +1,75 @@
/*
* 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 } from '@backstage/catalog-model';
import {
EntityRefLinks,
entityRouteParams,
getEntityRelations,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import {
Box,
Card,
CardActions,
CardContent,
CardMedia,
Chip,
} from '@material-ui/core';
import React from 'react';
import { LinkButton, ItemCardHeader } from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
/** @public */
export const EntityCard = (props: { entity: Entity }) => {
const { entity } = props;
const catalogEntityRoute = useRouteRef(entityRouteRef);
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
const url = catalogEntityRoute(entityRouteParams(entity));
const owner = (
<EntityRefLinks
entityRefs={ownedByRelations}
defaultKind="group"
color="inherit"
/>
);
return (
<Card>
<CardMedia>
<ItemCardHeader title={entity.metadata.name} subtitle={owner} />
</CardMedia>
<CardContent>
{entity.metadata.tags?.length ? (
<Box>
{entity.metadata.tags.map(tag => (
<Chip size="small" label={tag} key={tag} />
))}
</Box>
) : null}
{entity.metadata.description}
</CardContent>
<CardActions>
<LinkButton to={url} color="primary">
Explore
</LinkButton>
</CardActions>
</Card>
);
};
@@ -0,0 +1,17 @@
/*
* 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.
*/
export { EntityCard } from './EntityCard';
+2 -1
View File
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export * from './DomainCard';
export * from './CatalogKindExploreContent';
export * from './ExploreLayout';
export * from './DomainCard';