refactor(api-docs): rework ApiExplorerPage to use EntityListProvider
Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-api-docs': minor
|
||||
---
|
||||
|
||||
Rework `ApiExplorerPage` to utilize `EntityListProvider` to provide a consistent UI with the `CatalogIndexPage` which now exposes support for starring entities, pagination, and customizing columns.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Export `EntityRow` type and `CreateComponentButton` component
|
||||
@@ -32,6 +32,7 @@
|
||||
"@asyncapi/react-component": "^0.23.0",
|
||||
"@backstage/catalog-model": "^0.8.2",
|
||||
"@backstage/core": "^0.7.11",
|
||||
"@backstage/plugin-catalog": "^0.6.2",
|
||||
"@backstage/plugin-catalog-react": "^0.2.2",
|
||||
"@backstage/theme": "^0.2.8",
|
||||
"@material-icons/font": "^1.0.2",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model';
|
||||
import {
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
@@ -55,6 +55,19 @@ describe('ApiCatalogPage', () => {
|
||||
}),
|
||||
getLocationByEntity: () =>
|
||||
Promise.resolve({ id: 'id', type: 'github', target: 'url' }),
|
||||
getEntityByName: async entityName => {
|
||||
return {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'User',
|
||||
metadata: { name: entityName.name },
|
||||
relations: [
|
||||
{
|
||||
type: RELATION_MEMBER_OF,
|
||||
target: { namespace: 'default', kind: 'Group', name: 'tools' },
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const configApi: ConfigApi = new ConfigReader({
|
||||
|
||||
@@ -18,58 +18,64 @@ import {
|
||||
Content,
|
||||
ContentHeader,
|
||||
SupportButton,
|
||||
useApi,
|
||||
useRouteRef,
|
||||
TableColumn,
|
||||
} from '@backstage/core';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { Button } from '@material-ui/core';
|
||||
import {
|
||||
EntityKindPicker,
|
||||
EntityListProvider,
|
||||
EntityTagPicker,
|
||||
EntityTypePicker,
|
||||
UserListFilterKind,
|
||||
UserListPicker,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import {
|
||||
CatalogTable,
|
||||
CreateComponentButton,
|
||||
EntityRow,
|
||||
} from '@backstage/plugin-catalog';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
import React from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useAsync } from 'react-use';
|
||||
import { createComponentRouteRef } from '../../routes';
|
||||
import { ApiExplorerTable } from '../ApiExplorerTable';
|
||||
import { ApiExplorerLayout } from './ApiExplorerLayout';
|
||||
|
||||
export const ApiExplorerPage = () => {
|
||||
const createComponentLink = useRouteRef(createComponentRouteRef);
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const { loading, error, value: catalogResponse } = useAsync(() => {
|
||||
return catalogApi.getEntities({
|
||||
filter: { kind: 'API' },
|
||||
fields: [
|
||||
'apiVersion',
|
||||
'kind',
|
||||
'metadata',
|
||||
'relations',
|
||||
'spec.lifecycle',
|
||||
'spec.owner',
|
||||
'spec.type',
|
||||
'spec.system',
|
||||
],
|
||||
});
|
||||
}, [catalogApi]);
|
||||
const useStyles = makeStyles(theme => ({
|
||||
contentWrapper: {
|
||||
display: 'grid',
|
||||
gridTemplateAreas: "'filters' 'table'",
|
||||
gridTemplateColumns: '250px 1fr',
|
||||
gridColumnGap: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export type ApiExplorerPageProps = {
|
||||
initiallySelectedFilter?: UserListFilterKind;
|
||||
columns?: TableColumn<EntityRow>[];
|
||||
};
|
||||
|
||||
export const ApiExplorerPage = ({
|
||||
initiallySelectedFilter = 'owned',
|
||||
columns,
|
||||
}: ApiExplorerPageProps) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<ApiExplorerLayout>
|
||||
<Content>
|
||||
<ContentHeader title="">
|
||||
{createComponentLink && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
component={RouterLink}
|
||||
to={createComponentLink()}
|
||||
>
|
||||
Register Existing API
|
||||
</Button>
|
||||
)}
|
||||
<CreateComponentButton buttonLabel="Register Existing API" />
|
||||
<SupportButton>All your APIs</SupportButton>
|
||||
</ContentHeader>
|
||||
<ApiExplorerTable
|
||||
entities={catalogResponse?.items ?? []}
|
||||
loading={loading}
|
||||
error={error}
|
||||
/>
|
||||
<div className={styles.contentWrapper}>
|
||||
<EntityListProvider>
|
||||
<div>
|
||||
<EntityKindPicker initialFilter="api" hidden />
|
||||
<EntityTypePicker />
|
||||
<UserListPicker initialFilter={initiallySelectedFilter} />
|
||||
<EntityTagPicker />
|
||||
</div>
|
||||
<CatalogTable columns={columns} />
|
||||
</EntityListProvider>
|
||||
</div>
|
||||
</Content>
|
||||
</ApiExplorerLayout>
|
||||
);
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* 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 } from '@backstage/catalog-model';
|
||||
import { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
import { wrapInTestApp } from '@backstage/test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { apiDocsConfigRef } from '../../config';
|
||||
import { ApiExplorerTable } from './ApiExplorerTable';
|
||||
|
||||
const entities: Entity[] = [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'API',
|
||||
metadata: { name: 'api1' },
|
||||
spec: { type: 'openapi' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'API',
|
||||
metadata: { name: 'api2' },
|
||||
spec: { type: 'openapi' },
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'API',
|
||||
metadata: { name: 'api3' },
|
||||
spec: { type: 'grpc' },
|
||||
},
|
||||
];
|
||||
|
||||
const apiRegistry = ApiRegistry.with(apiDocsConfigRef, {
|
||||
getApiDefinitionWidget: () => undefined,
|
||||
});
|
||||
|
||||
describe('ApiCatalogTable component', () => {
|
||||
it('should render error message when error is passed in props', async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable
|
||||
entities={[]}
|
||||
loading={false}
|
||||
error={{ code: 'error' }}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
const errorMessage = await rendered.findByText(
|
||||
/Could not fetch catalog entities./,
|
||||
);
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display entity names when loading has finished and no error occurred', async () => {
|
||||
const rendered = render(
|
||||
wrapInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<ApiExplorerTable entities={entities} loading={false} />
|
||||
</ApiProvider>,
|
||||
),
|
||||
);
|
||||
expect(rendered.getByText(/api1/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/api2/)).toBeInTheDocument();
|
||||
expect(rendered.getByText(/api3/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
ApiEntityV1alpha1,
|
||||
Entity,
|
||||
EntityName,
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_PART_OF,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
CodeSnippet,
|
||||
OverflowTooltip,
|
||||
Table,
|
||||
TableColumn,
|
||||
TableFilter,
|
||||
TableState,
|
||||
useQueryParamState,
|
||||
WarningPanel,
|
||||
} from '@backstage/core';
|
||||
import {
|
||||
EntityRefLink,
|
||||
EntityRefLinks,
|
||||
formatEntityRefTitle,
|
||||
getEntityRelations,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Chip } from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { ApiTypeTitle } from '../ApiDefinitionCard';
|
||||
|
||||
type EntityRow = {
|
||||
entity: ApiEntityV1alpha1;
|
||||
resolved: {
|
||||
name: string;
|
||||
partOfSystemRelationTitle?: string;
|
||||
partOfSystemRelations: EntityName[];
|
||||
ownedByRelationsTitle?: string;
|
||||
ownedByRelations: EntityName[];
|
||||
};
|
||||
};
|
||||
|
||||
const columns: TableColumn<EntityRow>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
field: 'resolved.name',
|
||||
highlight: true,
|
||||
render: ({ entity }) => (
|
||||
<EntityRefLink entityRef={entity} defaultKind="API" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
field: 'resolved.partOfSystemRelationTitle',
|
||||
render: ({ resolved }) => (
|
||||
<EntityRefLinks
|
||||
entityRefs={resolved.partOfSystemRelations}
|
||||
defaultKind="system"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Owner',
|
||||
field: 'resolved.ownedByRelationsTitle',
|
||||
render: ({ resolved }) => (
|
||||
<EntityRefLinks
|
||||
entityRefs={resolved.ownedByRelations}
|
||||
defaultKind="group"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Lifecycle',
|
||||
field: 'entity.spec.lifecycle',
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
field: 'entity.spec.type',
|
||||
render: ({ entity }) => <ApiTypeTitle apiEntity={entity} />,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
field: 'entity.metadata.description',
|
||||
render: ({ entity }) => (
|
||||
<OverflowTooltip
|
||||
text={entity.metadata.description}
|
||||
placement="bottom-start"
|
||||
/>
|
||||
),
|
||||
width: 'auto',
|
||||
},
|
||||
{
|
||||
title: 'Tags',
|
||||
field: 'entity.metadata.tags',
|
||||
cellStyle: {
|
||||
padding: '0px 16px 0px 20px',
|
||||
},
|
||||
render: ({ entity }) => (
|
||||
<>
|
||||
{entity.metadata.tags &&
|
||||
entity.metadata.tags.map(t => (
|
||||
<Chip
|
||||
key={t}
|
||||
label={t}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
style={{ marginBottom: '0px' }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const filters: TableFilter[] = [
|
||||
{
|
||||
column: 'Owner',
|
||||
type: 'select',
|
||||
},
|
||||
{
|
||||
column: 'Type',
|
||||
type: 'multiple-select',
|
||||
},
|
||||
{
|
||||
column: 'Lifecycle',
|
||||
type: 'multiple-select',
|
||||
},
|
||||
{
|
||||
column: 'Tags',
|
||||
type: 'checkbox-tree',
|
||||
},
|
||||
];
|
||||
|
||||
type ExplorerTableProps = {
|
||||
entities: Entity[];
|
||||
loading: boolean;
|
||||
error?: any;
|
||||
};
|
||||
|
||||
export const ApiExplorerTable = ({
|
||||
entities,
|
||||
loading,
|
||||
error,
|
||||
}: ExplorerTableProps) => {
|
||||
const [queryParamState, setQueryParamState] = useQueryParamState<TableState>(
|
||||
'apiTable',
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<WarningPanel severity="error" title="Could not fetch catalog entities.">
|
||||
<CodeSnippet language="text" text={error.toString()} />
|
||||
</WarningPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const rows = entities.map(entity => {
|
||||
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
|
||||
kind: 'system',
|
||||
});
|
||||
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
|
||||
|
||||
return {
|
||||
entity: entity as ApiEntityV1alpha1,
|
||||
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 (
|
||||
<Table<EntityRow>
|
||||
isLoading={loading}
|
||||
columns={columns}
|
||||
options={{
|
||||
paging: true,
|
||||
pageSize: 20,
|
||||
pageSizeOptions: [20, 50, 100],
|
||||
actionsColumnIndex: -1,
|
||||
loadingType: 'linear',
|
||||
padding: 'dense',
|
||||
showEmptyDataSourceMessage: !loading,
|
||||
}}
|
||||
data={rows}
|
||||
filters={filters}
|
||||
initialState={queryParamState}
|
||||
onStateChange={setQueryParamState}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* 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 { ApiExplorerTable } from './ApiExplorerTable';
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export { CatalogTable } from './CatalogTable';
|
||||
export type { EntityRow } from './types';
|
||||
|
||||
@@ -20,7 +20,12 @@ import { Button } from '@material-ui/core';
|
||||
import { useRouteRef } from '@backstage/core';
|
||||
import { createComponentRouteRef } from '../../routes';
|
||||
|
||||
export const CreateComponentButton = () => {
|
||||
type CreateComponentButtonProps = {
|
||||
buttonLabel?: string;
|
||||
};
|
||||
export const CreateComponentButton = ({
|
||||
buttonLabel,
|
||||
}: CreateComponentButtonProps) => {
|
||||
const createComponentLink = useRouteRef(createComponentRouteRef);
|
||||
|
||||
if (!createComponentLink) return null;
|
||||
@@ -32,7 +37,7 @@ export const CreateComponentButton = () => {
|
||||
color="primary"
|
||||
to={createComponentLink()}
|
||||
>
|
||||
Create Component
|
||||
{buttonLabel ?? 'Create Component'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ export * from './components/AboutCard';
|
||||
export { CatalogLayout } from './components/CatalogPage';
|
||||
export { CatalogResultListItem } from './components/CatalogResultListItem';
|
||||
export { CatalogTable } from './components/CatalogTable';
|
||||
export type { EntityRow } from './components/CatalogTable';
|
||||
export { CreateComponentButton } from './components/CreateComponentButton';
|
||||
export { EntityLayout } from './components/EntityLayout';
|
||||
export * from './components/EntityOrphanWarning';
|
||||
|
||||
Reference in New Issue
Block a user