From 11b81683a9985e8ce3b8b762ce0ff72db893f39d Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Wed, 29 Dec 2021 16:55:39 -0500 Subject: [PATCH] feat(customization): support customizing catalog and api index pages via outlets Signed-off-by: Phil Kuang --- .changeset/tender-ducks-tie.md | 6 + .../software-catalog/catalog-customization.md | 28 +- plugins/api-docs/api-report.md | 42 +-- .../ApiExplorerPage/ApiExplorerPage.test.tsx | 194 ++---------- .../ApiExplorerPage/ApiExplorerPage.tsx | 104 +------ .../DefaultApiExplorerPage.test.tsx | 198 ++++++++++++ .../DefaultApiExplorerPage.tsx | 118 +++++++ .../src/components/ApiExplorerPage/index.ts | 6 +- plugins/api-docs/src/components/index.ts | 1 + plugins/api-docs/src/plugin.ts | 2 +- plugins/catalog/api-report.md | 14 +- .../CatalogPage/CatalogPage.test.tsx | 283 ++--------------- .../components/CatalogPage/CatalogPage.tsx | 79 +---- .../CatalogPage/DefaultCatalogPage.test.tsx | 287 ++++++++++++++++++ .../CatalogPage/DefaultCatalogPage.tsx | 93 ++++++ .../src/components/CatalogPage/index.ts | 2 + plugins/catalog/src/index.ts | 1 + 17 files changed, 812 insertions(+), 646 deletions(-) create mode 100644 .changeset/tender-ducks-tie.md create mode 100644 plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx create mode 100644 plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx create mode 100644 plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx create mode 100644 plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx diff --git a/.changeset/tender-ducks-tie.md b/.changeset/tender-ducks-tie.md new file mode 100644 index 0000000000..ada9f2bf37 --- /dev/null +++ b/.changeset/tender-ducks-tie.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +--- + +Support customizing index page layouts via outlets diff --git a/docs/features/software-catalog/catalog-customization.md b/docs/features/software-catalog/catalog-customization.md index e80e8a1842..7bb6b79d6f 100644 --- a/docs/features/software-catalog/catalog-customization.md +++ b/docs/features/software-catalog/catalog-customization.md @@ -10,8 +10,7 @@ and find catalog entities. This is already set up by default by `@backstage/create-app`. If you want to change the default index page - such as to add a custom filter to -the catalog - you can replace the routing in `App.tsx` to point to your own -`CatalogIndexPage`. +the catalog - you can create your own `CatalogIndexPage`. > Note: The catalog index page is designed to have a minimal code footprint to > support easy customization, but creating a copy does introduce a possibility @@ -21,12 +20,11 @@ the catalog - you can replace the routing in `App.tsx` to point to your own For example, suppose that I want to allow filtering by a custom annotation added to entities, `company.com/security-tier`. To start, I'll copy the code for the -default catalog page and create a component in a -[new plugin](../../plugins/create-a-plugin.md): +default catalog page and create a component. ```tsx // imports, etc omitted for brevity. for full source see: -// https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +// https://github.com/backstage/backstage/blob/master/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx export const CustomCatalogPage = ({ columns, actions, @@ -167,21 +165,7 @@ export const CustomCatalogPage = ({ }; ``` -This page itself can be exported as a routable extension in the plugin: - -```ts -export const CustomCatalogIndexPage = myPlugin.provide( - createRoutableExtension({ - name: 'CustomCatalogIndexPage', - component: () => - import('./components/CustomCatalogPage').then(m => m.CustomCatalogPage), - mountPoint: catalogRouteRef, - }), -); -``` - -Finally, we can replace the catalog route in the Backstage application with our -new `CustomCatalogIndexPage`. +Finally, we can apply our new `CustomCatalogPage`. ```diff # packages/app/src/App.tsx @@ -189,7 +173,9 @@ const routes = ( - } /> -+ } /> ++ }> ++ ++ ``` The same method can be used to customize the _default_ filters with a different diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 111660ae07..f0e8c06aab 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -5,7 +5,6 @@ ```ts /// -import { Action } from '@material-table/core'; import { ApiEntity } from '@backstage/catalog-model'; import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; @@ -15,6 +14,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { TableColumn } from '@backstage/core-components'; +import { TableProps } from '@backstage/core-components'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts @@ -53,27 +53,17 @@ const apiDocsPlugin: BackstagePlugin< export { apiDocsPlugin }; export { apiDocsPlugin as plugin }; +// @public +export const ApiExplorerIndexPage: ( + props: DefaultApiExplorerPageProps, +) => JSX.Element; + // Warning: (ae-missing-release-tag) "ApiExplorerPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const ApiExplorerPage: ({ - initiallySelectedFilter, - columns, - actions, -}: { - initiallySelectedFilter?: UserListFilterKind | undefined; - columns?: TableColumn[] | undefined; - actions?: - | ( - | Action - | { - action: (rowData: CatalogTableRow) => Action; - position: string; - } - | ((rowData: CatalogTableRow) => Action) - )[] - | undefined; -}) => JSX.Element; +export const ApiExplorerPage: ( + props: DefaultApiExplorerPageProps, +) => JSX.Element; // Warning: (ae-missing-release-tag) "ApiTypeTitle" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -110,6 +100,20 @@ export const ConsumedApisCard: ({ variant }: Props_2) => JSX.Element; // @public (undocumented) export const ConsumingComponentsCard: ({ variant }: Props_5) => JSX.Element; +// @public +export const DefaultApiExplorerPage: ({ + initiallySelectedFilter, + columns, + actions, +}: DefaultApiExplorerPageProps) => JSX.Element; + +// @public +export type DefaultApiExplorerPageProps = { + initiallySelectedFilter?: UserListFilterKind; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; + // Warning: (ae-missing-release-tag) "defaultDefinitionWidgets" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index 3629110d47..760d1ab59d 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,185 +14,31 @@ * limitations under the License. */ -import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model'; -import { ConfigReader } from '@backstage/core-app-api'; -import { TableColumn, TableProps } from '@backstage/core-components'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; -import { CatalogTableRow } from '@backstage/plugin-catalog'; -import { - CatalogApi, - catalogApiRef, - DefaultStarredEntitiesApi, - entityRouteRef, - starredEntitiesApiRef, -} from '@backstage/plugin-catalog-react'; -import { - MockStorageApi, - TestApiProvider, - wrapInTestApp, -} from '@backstage/test-utils'; -import DashboardIcon from '@material-ui/icons/Dashboard'; -import { render } from '@testing-library/react'; import React from 'react'; -import { apiDocsConfigRef } from '../../config'; +import { renderInTestApp } from '@backstage/test-utils'; +import { useOutlet } from 'react-router'; import { ApiExplorerPage } from './ApiExplorerPage'; -describe('ApiCatalogPage', () => { - const catalogApi: Partial = { - getEntities: () => - Promise.resolve({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'API', - metadata: { - name: 'Entity1', - }, - spec: { type: 'openapi' }, - }, - ] as Entity[], - }), - 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' }, - }, - ], - }; - }, - }; +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue('Route Children'), +})); - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, +jest.mock('./DefaultApiExplorerPage', () => ({ + DefaultApiExplorerPage: jest.fn().mockReturnValue('DefaultApiExplorerPage'), +})); + +describe('ApiExplorerPage', () => { + it('renders provided router element', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('Route Children')).toBeInTheDocument(); }); - const apiDocsConfig = { - getApiDefinitionWidget: () => undefined, - }; + it('renders DefaultApiExplorerPage home when no router children are provided', async () => { + (useOutlet as jest.Mock).mockReturnValueOnce(null); + const { getByText } = await renderInTestApp(); - const storageApi = MockStorageApi.create(); - - const renderWrapped = (children: React.ReactNode) => - render( - wrapInTestApp( - - {children} - , - { - mountedRoutes: { - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, - }, - ), - ); - - // this test right now causes some red lines in the log output when running tests - // related to some theme issues in mui-table - // https://github.com/mbrn/material-table/issues/1293 - it('should render', async () => { - const { findByText } = renderWrapped(); - expect(await findByText(/My Company API Explorer/)).toBeInTheDocument(); - }); - - it('should render the default column of the grid', async () => { - const { getAllByRole } = renderWrapped(); - - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); - const columnHeaderLabels = columnHeader.map(c => c.textContent); - - expect(columnHeaderLabels).toEqual([ - 'Name', - 'System', - 'Owner', - 'Type', - 'Lifecycle', - 'Description', - 'Tags', - 'Actions', - ]); - }); - - it('should render the custom column passed as prop', async () => { - const columns: TableColumn[] = [ - { title: 'Foo', field: 'entity.foo' }, - { title: 'Bar', field: 'entity.bar' }, - { title: 'Baz', field: 'entity.spec.lifecycle' }, - ]; - const { getAllByRole } = renderWrapped( - , - ); - - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); - const columnHeaderLabels = columnHeader.map(c => c.textContent); - - expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); - }); - - it('should render the default actions of an item in the grid', async () => { - const { findByTitle, findByText } = await renderWrapped( - , - ); - expect(await findByText(/All \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/View/)).toBeInTheDocument(); - expect(await findByTitle(/View/)).toBeInTheDocument(); - expect(await findByTitle(/Edit/)).toBeInTheDocument(); - expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); - }); - - it('should render the custom actions of an item passed as prop', async () => { - const actions: TableProps['actions'] = [ - () => { - return { - icon: () => , - tooltip: 'Foo Action', - disabled: false, - onClick: () => jest.fn(), - }; - }, - () => { - return { - icon: () => , - tooltip: 'Bar Action', - disabled: true, - onClick: () => jest.fn(), - }; - }, - ]; - - const { findByTitle, findByText } = await renderWrapped( - , - ); - expect(await findByText(/All \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); - expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); - expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + expect(getByText('DefaultApiExplorerPage')).toBeInTheDocument(); }); }); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx index f28574145e..68ab0a1647 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,97 +14,19 @@ * limitations under the License. */ -import { - Content, - ContentHeader, - CreateButton, - PageWithHeader, - SupportButton, - TableColumn, - TableProps, -} from '@backstage/core-components'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { - CatalogTable, - CatalogTableRow, - FilteredEntityLayout, - EntityListContainer, - FilterContainer, -} from '@backstage/plugin-catalog'; -import { - EntityKindPicker, - EntityLifecyclePicker, - EntityListProvider, - EntityOwnerPicker, - EntityTagPicker, - EntityTypePicker, - UserListFilterKind, - UserListPicker, -} from '@backstage/plugin-catalog-react'; import React from 'react'; -import { createComponentRouteRef } from '../../routes'; +import { useOutlet } from 'react-router'; +import { + DefaultApiExplorerPage, + DefaultApiExplorerPageProps, +} from './DefaultApiExplorerPage'; -const defaultColumns: TableColumn[] = [ - CatalogTable.columns.createNameColumn({ defaultKind: 'API' }), - CatalogTable.columns.createSystemColumn(), - CatalogTable.columns.createOwnerColumn(), - CatalogTable.columns.createSpecTypeColumn(), - CatalogTable.columns.createSpecLifecycleColumn(), - CatalogTable.columns.createMetadataDescriptionColumn(), - CatalogTable.columns.createTagsColumn(), -]; +/** + * ApiExplorerPage + * @public + */ +export const ApiExplorerPage = (props: DefaultApiExplorerPageProps) => { + const outlet = useOutlet(); -type ApiExplorerPageProps = { - initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; - actions?: TableProps['actions']; -}; - -export const ApiExplorerPage = ({ - initiallySelectedFilter = 'all', - columns, - actions, -}: ApiExplorerPageProps) => { - const configApi = useApi(configApiRef); - const generatedSubtitle = `${ - configApi.getOptionalString('organization.name') ?? 'Backstage' - } API Explorer`; - const createComponentLink = useRouteRef(createComponentRouteRef); - - return ( - - - - - All your APIs - - - - - - - - - - - - - ); + return outlet || ; }; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx new file mode 100644 index 0000000000..9e4c861e24 --- /dev/null +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -0,0 +1,198 @@ +/* + * 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_MEMBER_OF } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/core-app-api'; +import { TableColumn, TableProps } from '@backstage/core-components'; +import { + ConfigApi, + configApiRef, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { CatalogTableRow } from '@backstage/plugin-catalog'; +import { + CatalogApi, + catalogApiRef, + DefaultStarredEntitiesApi, + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { + MockStorageApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { apiDocsConfigRef } from '../../config'; +import { DefaultApiExplorerPage } from './DefaultApiExplorerPage'; + +describe('DefaultApiExplorerPage', () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'Entity1', + }, + spec: { type: 'openapi' }, + }, + ] as Entity[], + }), + 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({ + organization: { + name: 'My Company', + }, + }); + + const apiDocsConfig = { + getApiDefinitionWidget: () => undefined, + }; + + const storageApi = MockStorageApi.create(); + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children} + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ); + + // this test right now causes some red lines in the log output when running tests + // related to some theme issues in mui-table + // https://github.com/mbrn/material-table/issues/1293 + it('should render', async () => { + const { findByText } = renderWrapped(); + expect(await findByText(/My Company API Explorer/)).toBeInTheDocument(); + }); + + it('should render the default column of the grid', async () => { + const { getAllByRole } = renderWrapped(); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + + expect(columnHeaderLabels).toEqual([ + 'Name', + 'System', + 'Owner', + 'Type', + 'Lifecycle', + 'Description', + 'Tags', + 'Actions', + ]); + }); + + it('should render the custom column passed as prop', async () => { + const columns: TableColumn[] = [ + { title: 'Foo', field: 'entity.foo' }, + { title: 'Bar', field: 'entity.bar' }, + { title: 'Baz', field: 'entity.spec.lifecycle' }, + ]; + const { getAllByRole } = renderWrapped( + , + ); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + + expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); + }); + + it('should render the default actions of an item in the grid', async () => { + const { findByTitle, findByText } = await renderWrapped( + , + ); + expect(await findByText(/All \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/View/)).toBeInTheDocument(); + expect(await findByTitle(/View/)).toBeInTheDocument(); + expect(await findByTitle(/Edit/)).toBeInTheDocument(); + expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); + }); + + it('should render the custom actions of an item passed as prop', async () => { + const actions: TableProps['actions'] = [ + () => { + return { + icon: () => , + tooltip: 'Foo Action', + disabled: false, + onClick: () => jest.fn(), + }; + }, + () => { + return { + icon: () => , + tooltip: 'Bar Action', + disabled: true, + onClick: () => jest.fn(), + }; + }, + ]; + + const { findByTitle, findByText } = await renderWrapped( + , + ); + expect(await findByText(/All \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); + expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); + expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + }); +}); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx new file mode 100644 index 0000000000..5ddda96daa --- /dev/null +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + Content, + ContentHeader, + CreateButton, + PageWithHeader, + SupportButton, + TableColumn, + TableProps, +} from '@backstage/core-components'; +import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + CatalogTable, + CatalogTableRow, + FilteredEntityLayout, + EntityListContainer, + FilterContainer, +} from '@backstage/plugin-catalog'; +import { + EntityKindPicker, + EntityLifecyclePicker, + EntityListProvider, + EntityOwnerPicker, + EntityTagPicker, + EntityTypePicker, + UserListFilterKind, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { createComponentRouteRef } from '../../routes'; + +const defaultColumns: TableColumn[] = [ + CatalogTable.columns.createNameColumn({ defaultKind: 'API' }), + CatalogTable.columns.createSystemColumn(), + CatalogTable.columns.createOwnerColumn(), + CatalogTable.columns.createSpecTypeColumn(), + CatalogTable.columns.createSpecLifecycleColumn(), + CatalogTable.columns.createMetadataDescriptionColumn(), + CatalogTable.columns.createTagsColumn(), +]; + +/** + * DefaultApiExplorerPageProps + * @public + */ +export type DefaultApiExplorerPageProps = { + initiallySelectedFilter?: UserListFilterKind; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; + +/** + * DefaultApiExplorerPage + * @public + */ +export const DefaultApiExplorerPage = ({ + initiallySelectedFilter = 'all', + columns, + actions, +}: DefaultApiExplorerPageProps) => { + const configApi = useApi(configApiRef); + const generatedSubtitle = `${ + configApi.getOptionalString('organization.name') ?? 'Backstage' + } API Explorer`; + const createComponentLink = useRouteRef(createComponentRouteRef); + + return ( + + + + + All your APIs + + + + + + + + + + + + + ); +}; diff --git a/plugins/api-docs/src/components/ApiExplorerPage/index.ts b/plugins/api-docs/src/components/ApiExplorerPage/index.ts index 3d170260ad..813ca90229 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/index.ts +++ b/plugins/api-docs/src/components/ApiExplorerPage/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,4 +14,6 @@ * limitations under the License. */ -export { ApiExplorerPage } from './ApiExplorerPage'; +export { ApiExplorerPage as ApiExplorerIndexPage } from './ApiExplorerPage'; +export { DefaultApiExplorerPage } from './DefaultApiExplorerPage'; +export type { DefaultApiExplorerPageProps } from './DefaultApiExplorerPage'; diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts index c0af59dc52..f4d8034b56 100644 --- a/plugins/api-docs/src/components/index.ts +++ b/plugins/api-docs/src/components/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export * from './ApiExplorerPage'; export * from './ApiDefinitionCard'; export * from './ApisCards'; export * from './AsyncApiDefinitionWidget'; diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index f400be9c7c..61695b4a7a 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -53,7 +53,7 @@ export const ApiExplorerPage = apiDocsPlugin.provide( createRoutableExtension({ name: 'ApiExplorerPage', component: () => - import('./components/ApiExplorerPage').then(m => m.ApiExplorerPage), + import('./components/ApiExplorerPage').then(m => m.ApiExplorerIndexPage), mountPoint: rootRoute, }), ); diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 80271c89af..c046e031de 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -126,15 +126,10 @@ export class CatalogClientWrapper implements CatalogApi { // @public (undocumented) export const CatalogEntityPage: () => JSX.Element; -// Warning: (ae-forgotten-export) The symbol "CatalogPageProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CatalogIndexPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const CatalogIndexPage: ({ - columns, - actions, - initiallySelectedFilter, -}: CatalogPageProps) => JSX.Element; +export const CatalogIndexPage: (props: DefaultCatalogPageProps) => JSX.Element; // Warning: (ae-forgotten-export) The symbol "CatalogKindHeaderProps" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CatalogKindHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -236,6 +231,13 @@ export function createSystemColumn(): TableColumn; // @public (undocumented) export function createTagsColumn(): TableColumn; +// @public +export type DefaultCatalogPageProps = { + initiallySelectedFilter?: UserListFilterKind; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; + // Warning: (ae-missing-release-tag) "EntityAboutCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index effd9f5bc6..6fbdcf40a2 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,272 +14,31 @@ * limitations under the License. */ -import { CatalogApi } from '@backstage/catalog-client'; -import { - Entity, - RELATION_MEMBER_OF, - RELATION_OWNED_BY, -} from '@backstage/catalog-model'; -import { TableColumn, TableProps } from '@backstage/core-components'; -import { - IdentityApi, - identityApiRef, - ProfileInfo, - storageApiRef, -} from '@backstage/core-plugin-api'; -import { - catalogApiRef, - DefaultStarredEntitiesApi, - entityRouteRef, - starredEntitiesApiRef, -} from '@backstage/plugin-catalog-react'; -import { - mockBreakpoint, - MockStorageApi, - renderWithEffects, - TestApiProvider, - wrapInTestApp, -} from '@backstage/test-utils'; -import DashboardIcon from '@material-ui/icons/Dashboard'; -import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; -import { createComponentRouteRef } from '../../routes'; -import { EntityRow } from '../CatalogTable'; +import { renderInTestApp } from '@backstage/test-utils'; +import { useOutlet } from 'react-router'; import { CatalogPage } from './CatalogPage'; +jest.mock('react-router', () => ({ + ...jest.requireActual('react-router'), + useOutlet: jest.fn().mockReturnValue('Route Children'), +})); + +jest.mock('./DefaultCatalogPage', () => ({ + DefaultCatalogPage: jest.fn().mockReturnValue('DefaultCatalogPage'), +})); + describe('CatalogPage', () => { - const origReplaceState = window.history.replaceState; - beforeEach(() => { - window.history.replaceState = jest.fn(); - }); - afterEach(() => { - window.history.replaceState = origReplaceState; + it('renders provided router element', async () => { + const { getByText } = await renderInTestApp(); + + expect(getByText('Route Children')).toBeInTheDocument(); }); - const catalogApi: Partial = { - getEntities: () => - Promise.resolve({ - items: [ - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity1', - }, - spec: { - owner: 'tools', - type: 'service', - }, - relations: [ - { - type: RELATION_OWNED_BY, - target: { kind: 'Group', name: 'tools', namespace: 'default' }, - }, - ], - }, - { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'Entity2', - }, - spec: { - owner: 'not-tools', - type: 'service', - }, - relations: [ - { - type: RELATION_OWNED_BY, - target: { - kind: 'Group', - name: 'not-tools', - namespace: 'default', - }, - }, - ], - }, - ] as Entity[], - }), - 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 testProfile: Partial = { - displayName: 'Display Name', - }; - const identityApi: Partial = { - getUserId: () => 'tools', - getIdToken: async () => undefined, - getProfile: () => testProfile, - }; - const storageApi = MockStorageApi.create(); + it('renders DefaultCatalogPage home when no router children are provided', async () => { + (useOutlet as jest.Mock).mockReturnValueOnce(null); + const { getByText } = await renderInTestApp(); - const renderWrapped = (children: React.ReactNode) => - renderWithEffects( - wrapInTestApp( - - {children} - , - { - mountedRoutes: { - '/create': createComponentRouteRef, - '/catalog/:namespace/:kind/:name': entityRouteRef, - }, - }, - ), - ); - - // TODO(freben): The test timeouts are bumped in this file, because it seems - // page and table rerenders accumulate to occasionally go over the default - // limit. We should investigate why these timeouts happen. - - it('should render the default column of the grid', async () => { - const { getAllByRole } = await renderWrapped(); - - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); - const columnHeaderLabels = columnHeader.map(c => c.textContent); - - expect(columnHeaderLabels).toEqual([ - 'Name', - 'System', - 'Owner', - 'Type', - 'Lifecycle', - 'Description', - 'Tags', - 'Actions', - ]); - }, 20_000); - - it('should render the custom column passed as prop', async () => { - const columns: TableColumn[] = [ - { title: 'Foo', field: 'entity.foo' }, - { title: 'Bar', field: 'entity.bar' }, - { title: 'Baz', field: 'entity.spec.lifecycle' }, - ]; - const { getAllByRole } = await renderWrapped( - , - ); - - const columnHeader = getAllByRole('button').filter( - c => c.tagName === 'SPAN', - ); - const columnHeaderLabels = columnHeader.map(c => c.textContent); - expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); - }, 20_000); - - it('should render the default actions of an item in the grid', async () => { - const { getByTestId, findByTitle, findByText } = await renderWrapped( - , - ); - fireEvent.click(getByTestId('user-picker-owned')); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/View/)).toBeInTheDocument(); - expect(await findByTitle(/Edit/)).toBeInTheDocument(); - expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); - }, 20_000); - - it('should render the custom actions of an item passed as prop', async () => { - const actions: TableProps['actions'] = [ - () => { - return { - icon: () => , - tooltip: 'Foo Action', - disabled: false, - onClick: () => jest.fn(), - }; - }, - () => { - return { - icon: () => , - tooltip: 'Bar Action', - disabled: true, - onClick: () => jest.fn(), - }; - }, - ]; - - const { getByTestId, findByTitle, findByText } = await renderWrapped( - , - ); - fireEvent.click(getByTestId('user-picker-owned')); - expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); - expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); - expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); - expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); - }, 20_000); - - // this test right now causes some red lines in the log output when running tests - // related to some theme issues in mui-table - // https://github.com/mbrn/material-table/issues/1293 - it('should render', async () => { - const { findByText, getByTestId } = await renderWrapped(); - fireEvent.click(getByTestId('user-picker-owned')); - await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); - fireEvent.click(getByTestId('user-picker-all')); - await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); - }, 20_000); - - it('should set initial filter correctly', async () => { - const { findByText } = await renderWrapped( - , - ); - await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); - }, 20_000); - - // this test is for fixing the bug after favoriting an entity, the matching - // entities defaulting to "owned" filter and not based on the selected filter - it('should render the correct entities filtered on the selected filter', async () => { - const { getByTestId } = await renderWrapped(); - fireEvent.click(getByTestId('user-picker-owned')); - await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); - fireEvent.click(screen.getByTestId('user-picker-starred')); - await expect( - screen.findByText(/Starred \(0\)/), - ).resolves.toBeInTheDocument(); - fireEvent.click(screen.getByTestId('user-picker-all')); - await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); - - const starredIcons = await screen.findAllByTitle('Add to favorites'); - fireEvent.click(starredIcons[0]); - await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); - - fireEvent.click(screen.getByTestId('user-picker-starred')); - await expect( - screen.findByText(/Starred \(1\)/), - ).resolves.toBeInTheDocument(); - }, 20_000); - - it('should wrap filter in drawer on smaller screens', async () => { - mockBreakpoint({ matches: true }); - const { getByRole } = await renderWrapped(); - const button = getByRole('button', { name: 'Filters' }); - expect(getByRole('presentation', { hidden: true })).toBeInTheDocument(); - fireEvent.click(button); - expect(getByRole('presentation')).toBeVisible(); - }, 20_000); + expect(getByText('DefaultCatalogPage')).toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ec29d3839b..cf49cff244 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -14,76 +14,15 @@ * limitations under the License. */ -import { - Content, - ContentHeader, - CreateButton, - PageWithHeader, - SupportButton, - TableColumn, - TableProps, -} from '@backstage/core-components'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; -import { - EntityLifecyclePicker, - EntityListProvider, - EntityOwnerPicker, - EntityTagPicker, - EntityTypePicker, - UserListFilterKind, - UserListPicker, -} from '@backstage/plugin-catalog-react'; import React from 'react'; -import { createComponentRouteRef } from '../../routes'; -import { CatalogTable } from '../CatalogTable'; -import { EntityRow } from '../CatalogTable/types'; +import { useOutlet } from 'react-router'; import { - FilteredEntityLayout, - EntityListContainer, - FilterContainer, -} from '../FilteredEntityLayout'; -import { CatalogKindHeader } from '../CatalogKindHeader'; + DefaultCatalogPage, + DefaultCatalogPageProps, +} from './DefaultCatalogPage'; -export type CatalogPageProps = { - initiallySelectedFilter?: UserListFilterKind; - columns?: TableColumn[]; - actions?: TableProps['actions']; -}; - -export const CatalogPage = ({ - columns, - actions, - initiallySelectedFilter = 'owned', -}: CatalogPageProps) => { - const orgName = - useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage'; - const createComponentLink = useRouteRef(createComponentRouteRef); - - return ( - - - - }> - - All your software catalog entities - - - - - - - - - - - - - - - - - ); +export const CatalogPage = (props: DefaultCatalogPageProps) => { + const outlet = useOutlet(); + + return outlet || ; }; diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx new file mode 100644 index 0000000000..5f21ba364b --- /dev/null +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -0,0 +1,287 @@ +/* + * 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 { CatalogApi } from '@backstage/catalog-client'; +import { + Entity, + RELATION_MEMBER_OF, + RELATION_OWNED_BY, +} from '@backstage/catalog-model'; +import { TableColumn, TableProps } from '@backstage/core-components'; +import { + IdentityApi, + identityApiRef, + ProfileInfo, + storageApiRef, +} from '@backstage/core-plugin-api'; +import { + catalogApiRef, + DefaultStarredEntitiesApi, + entityRouteRef, + starredEntitiesApiRef, +} from '@backstage/plugin-catalog-react'; +import { + mockBreakpoint, + MockStorageApi, + renderWithEffects, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import { fireEvent, screen } from '@testing-library/react'; +import React from 'react'; +import { createComponentRouteRef } from '../../routes'; +import { EntityRow } from '../CatalogTable'; +import { DefaultCatalogPage } from './DefaultCatalogPage'; + +describe('DefaultCatalogPage', () => { + const origReplaceState = window.history.replaceState; + beforeEach(() => { + window.history.replaceState = jest.fn(); + }); + afterEach(() => { + window.history.replaceState = origReplaceState; + }); + + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity1', + }, + spec: { + owner: 'tools', + type: 'service', + }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { kind: 'Group', name: 'tools', namespace: 'default' }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity2', + }, + spec: { + owner: 'not-tools', + type: 'service', + }, + relations: [ + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'not-tools', + namespace: 'default', + }, + }, + ], + }, + ] as Entity[], + }), + 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 testProfile: Partial = { + displayName: 'Display Name', + }; + const identityApi: Partial = { + getUserId: () => 'tools', + getIdToken: async () => undefined, + getProfile: () => testProfile, + }; + const storageApi = MockStorageApi.create(); + + const renderWrapped = (children: React.ReactNode) => + renderWithEffects( + wrapInTestApp( + + {children} + , + { + mountedRoutes: { + '/create': createComponentRouteRef, + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ), + ); + + // TODO(freben): The test timeouts are bumped in this file, because it seems + // page and table rerenders accumulate to occasionally go over the default + // limit. We should investigate why these timeouts happen. + + it('should render the default column of the grid', async () => { + const { getAllByRole } = await renderWrapped(); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + + expect(columnHeaderLabels).toEqual([ + 'Name', + 'System', + 'Owner', + 'Type', + 'Lifecycle', + 'Description', + 'Tags', + 'Actions', + ]); + }, 20_000); + + it('should render the custom column passed as prop', async () => { + const columns: TableColumn[] = [ + { title: 'Foo', field: 'entity.foo' }, + { title: 'Bar', field: 'entity.bar' }, + { title: 'Baz', field: 'entity.spec.lifecycle' }, + ]; + const { getAllByRole } = await renderWrapped( + , + ); + + const columnHeader = getAllByRole('button').filter( + c => c.tagName === 'SPAN', + ); + const columnHeaderLabels = columnHeader.map(c => c.textContent); + expect(columnHeaderLabels).toEqual(['Foo', 'Bar', 'Baz', 'Actions']); + }, 20_000); + + it('should render the default actions of an item in the grid', async () => { + const { getByTestId, findByTitle, findByText } = await renderWrapped( + , + ); + fireEvent.click(getByTestId('user-picker-owned')); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/View/)).toBeInTheDocument(); + expect(await findByTitle(/Edit/)).toBeInTheDocument(); + expect(await findByTitle(/Add to favorites/)).toBeInTheDocument(); + }, 20_000); + + it('should render the custom actions of an item passed as prop', async () => { + const actions: TableProps['actions'] = [ + () => { + return { + icon: () => , + tooltip: 'Foo Action', + disabled: false, + onClick: () => jest.fn(), + }; + }, + () => { + return { + icon: () => , + tooltip: 'Bar Action', + disabled: true, + onClick: () => jest.fn(), + }; + }, + ]; + + const { getByTestId, findByTitle, findByText } = await renderWrapped( + , + ); + fireEvent.click(getByTestId('user-picker-owned')); + expect(await findByText(/Owned \(1\)/)).toBeInTheDocument(); + expect(await findByTitle(/Foo Action/)).toBeInTheDocument(); + expect(await findByTitle(/Bar Action/)).toBeInTheDocument(); + expect((await findByTitle(/Bar Action/)).firstChild).toBeDisabled(); + }, 20_000); + + // this test right now causes some red lines in the log output when running tests + // related to some theme issues in mui-table + // https://github.com/mbrn/material-table/issues/1293 + it('should render', async () => { + const { findByText, getByTestId } = await renderWrapped( + , + ); + fireEvent.click(getByTestId('user-picker-owned')); + await expect(findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); + fireEvent.click(getByTestId('user-picker-all')); + await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + }, 20_000); + + it('should set initial filter correctly', async () => { + const { findByText } = await renderWrapped( + , + ); + await expect(findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + }, 20_000); + + // this test is for fixing the bug after favoriting an entity, the matching + // entities defaulting to "owned" filter and not based on the selected filter + it('should render the correct entities filtered on the selected filter', async () => { + const { getByTestId } = await renderWrapped(); + fireEvent.click(getByTestId('user-picker-owned')); + await expect(screen.findByText(/Owned \(1\)/)).resolves.toBeInTheDocument(); + fireEvent.click(screen.getByTestId('user-picker-starred')); + await expect( + screen.findByText(/Starred \(0\)/), + ).resolves.toBeInTheDocument(); + fireEvent.click(screen.getByTestId('user-picker-all')); + await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + + const starredIcons = await screen.findAllByTitle('Add to favorites'); + fireEvent.click(starredIcons[0]); + await expect(screen.findByText(/All \(2\)/)).resolves.toBeInTheDocument(); + + fireEvent.click(screen.getByTestId('user-picker-starred')); + await expect( + screen.findByText(/Starred \(1\)/), + ).resolves.toBeInTheDocument(); + }, 20_000); + + it('should wrap filter in drawer on smaller screens', async () => { + mockBreakpoint({ matches: true }); + const { getByRole } = await renderWrapped(); + const button = getByRole('button', { name: 'Filters' }); + expect(getByRole('presentation', { hidden: true })).toBeInTheDocument(); + fireEvent.click(button); + expect(getByRole('presentation')).toBeVisible(); + }, 20_000); +}); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx new file mode 100644 index 0000000000..1453a39bbc --- /dev/null +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -0,0 +1,93 @@ +/* + * 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 { + Content, + ContentHeader, + CreateButton, + PageWithHeader, + SupportButton, + TableColumn, + TableProps, +} from '@backstage/core-components'; +import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + EntityLifecyclePicker, + EntityListProvider, + EntityOwnerPicker, + EntityTagPicker, + EntityTypePicker, + UserListFilterKind, + UserListPicker, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { createComponentRouteRef } from '../../routes'; +import { CatalogTable } from '../CatalogTable'; +import { EntityRow } from '../CatalogTable/types'; +import { + FilteredEntityLayout, + EntityListContainer, + FilterContainer, +} from '../FilteredEntityLayout'; +import { CatalogKindHeader } from '../CatalogKindHeader'; + +/** + * DefaultCatalogPageProps + * @public + */ +export type DefaultCatalogPageProps = { + initiallySelectedFilter?: UserListFilterKind; + columns?: TableColumn[]; + actions?: TableProps['actions']; +}; + +export const DefaultCatalogPage = ({ + columns, + actions, + initiallySelectedFilter = 'owned', +}: DefaultCatalogPageProps) => { + const orgName = + useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage'; + const createComponentLink = useRouteRef(createComponentRouteRef); + + return ( + + + + }> + + All your software catalog entities + + + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/catalog/src/components/CatalogPage/index.ts b/plugins/catalog/src/components/CatalogPage/index.ts index dc7ed68229..ca6a13c034 100644 --- a/plugins/catalog/src/components/CatalogPage/index.ts +++ b/plugins/catalog/src/components/CatalogPage/index.ts @@ -14,3 +14,5 @@ * limitations under the License. */ export { CatalogPage } from './CatalogPage'; +export { DefaultCatalogPage } from './DefaultCatalogPage'; +export type { DefaultCatalogPageProps } from './DefaultCatalogPage'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index d804bbaf83..56cae80b6a 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -54,3 +54,4 @@ export { export type { EntityLinksEmptyStateClassKey } from './components/EntityLinksCard'; export type { SystemDiagramCardClassKey } from './components/SystemDiagramCard'; +export type { DefaultCatalogPageProps } from './components/CatalogPage';