diff --git a/.changeset/fifty-tigers-learn.md b/.changeset/fifty-tigers-learn.md new file mode 100644 index 0000000000..465c8c54a5 --- /dev/null +++ b/.changeset/fifty-tigers-learn.md @@ -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. diff --git a/.changeset/wild-ghosts-deny.md b/.changeset/wild-ghosts-deny.md new file mode 100644 index 0000000000..2ad99bfdd2 --- /dev/null +++ b/.changeset/wild-ghosts-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Export `EntityRow` type and `CreateComponentButton` component diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index fd3bc353e9..bc937e2aff 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -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", diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index 223734504b..c52c5b88fc 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -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({ diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index 1ed47811ef..36cd988c8c 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -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[]; +}; + +export const ApiExplorerPage = ({ + initiallySelectedFilter = 'owned', + columns, +}: ApiExplorerPageProps) => { + const styles = useStyles(); return ( - {createComponentLink && ( - - )} + All your APIs - +
+ +
+
+ +
+
); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx deleted file mode 100644 index bcb310be46..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.test.tsx +++ /dev/null @@ -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( - - - , - ), - ); - 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( - - - , - ), - ); - expect(rendered.getByText(/api1/)).toBeInTheDocument(); - expect(rendered.getByText(/api2/)).toBeInTheDocument(); - expect(rendered.getByText(/api3/)).toBeInTheDocument(); - }); -}); diff --git a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx b/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx deleted file mode 100644 index 78b5f1d240..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/ApiExplorerTable.tsx +++ /dev/null @@ -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[] = [ - { - title: 'Name', - field: 'resolved.name', - highlight: true, - render: ({ entity }) => ( - - ), - }, - { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }, - { - title: 'Type', - field: 'entity.spec.type', - render: ({ entity }) => , - }, - { - title: 'Description', - field: 'entity.metadata.description', - render: ({ entity }) => ( - - ), - width: 'auto', - }, - { - title: 'Tags', - field: 'entity.metadata.tags', - cellStyle: { - padding: '0px 16px 0px 20px', - }, - render: ({ entity }) => ( - <> - {entity.metadata.tags && - entity.metadata.tags.map(t => ( - - ))} - - ), - }, -]; - -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( - 'apiTable', - ); - - if (error) { - return ( - - - - ); - } - - 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 ( - - 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} - /> - ); -}; diff --git a/plugins/api-docs/src/components/ApiExplorerTable/index.ts b/plugins/api-docs/src/components/ApiExplorerTable/index.ts deleted file mode 100644 index a9c79861e8..0000000000 --- a/plugins/api-docs/src/components/ApiExplorerTable/index.ts +++ /dev/null @@ -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'; diff --git a/plugins/catalog/src/components/CatalogTable/index.ts b/plugins/catalog/src/components/CatalogTable/index.ts index 280d5b4bcb..460720245e 100644 --- a/plugins/catalog/src/components/CatalogTable/index.ts +++ b/plugins/catalog/src/components/CatalogTable/index.ts @@ -15,3 +15,4 @@ */ export { CatalogTable } from './CatalogTable'; +export type { EntityRow } from './types'; diff --git a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx index 896e9bada3..8c30c9f9be 100644 --- a/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx +++ b/plugins/catalog/src/components/CreateComponentButton/CreateComponentButton.tsx @@ -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'} ); }; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 1596de0e8d..aca1db8940 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -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';