feat(customization): support customizing catalog and api index pages via outlets

Signed-off-by: Phil Kuang <pkuang@factset.com>
This commit is contained in:
Phil Kuang
2021-12-29 16:55:39 -05:00
parent fbdb1ec331
commit 11b81683a9
17 changed files with 812 additions and 646 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-api-docs': patch
'@backstage/plugin-catalog': patch
---
Support customizing index page layouts via outlets
@@ -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 = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
- <Route path="/catalog" element={<CatalogIndexPage />} />
+ <Route path="/catalog" element={<CustomCatalogIndexPage />} />
+ <Route path="/catalog" element={<CatalogIndexPage />}>
+ <CustomCatalogPage />
+ </Route>
```
The same method can be used to customize the _default_ filters with a different
+23 -19
View File
@@ -5,7 +5,6 @@
```ts
/// <reference types="react" />
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<CatalogTableRow>[] | undefined;
actions?:
| (
| Action<CatalogTableRow>
| {
action: (rowData: CatalogTableRow) => Action<CatalogTableRow>;
position: string;
}
| ((rowData: CatalogTableRow) => Action<CatalogTableRow>)
)[]
| 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<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['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)
@@ -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<CatalogApi> = {
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(<ApiExplorerPage />);
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(<ApiExplorerPage />);
const storageApi = MockStorageApi.create();
const renderWrapped = (children: React.ReactNode) =>
render(
wrapInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[configApiRef, configApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>,
{
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(<ApiExplorerPage />);
expect(await findByText(/My Company API Explorer/)).toBeInTheDocument();
});
it('should render the default column of the grid', async () => {
const { getAllByRole } = renderWrapped(<ApiExplorerPage />);
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<CatalogTableRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = renderWrapped(
<ApiExplorerPage columns={columns} />,
);
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(
<ApiExplorerPage />,
);
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<CatalogTableRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { findByTitle, findByText } = await renderWrapped(
<ApiExplorerPage actions={actions} />,
);
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();
});
});
@@ -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<CatalogTableRow>[] = [
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<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['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 (
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
<CreateButton
title="Register Existing API"
to={createComponentLink?.()}
/>
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable
columns={columns || defaultColumns}
actions={actions}
/>
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
return outlet || <DefaultApiExplorerPage {...props} />;
};
@@ -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<CatalogApi> = {
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(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[configApiRef, configApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>,
{
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(<DefaultApiExplorerPage />);
expect(await findByText(/My Company API Explorer/)).toBeInTheDocument();
});
it('should render the default column of the grid', async () => {
const { getAllByRole } = renderWrapped(<DefaultApiExplorerPage />);
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<CatalogTableRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = renderWrapped(
<DefaultApiExplorerPage columns={columns} />,
);
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(
<DefaultApiExplorerPage />,
);
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<CatalogTableRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { findByTitle, findByText } = await renderWrapped(
<DefaultApiExplorerPage actions={actions} />,
);
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();
});
});
@@ -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<CatalogTableRow>[] = [
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<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['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 (
<PageWithHeader
themeId="apis"
title="APIs"
subtitle={generatedSubtitle}
pageTitleOverride="APIs"
>
<Content>
<ContentHeader title="">
<CreateButton
title="Register Existing API"
to={createComponentLink?.()}
/>
<SupportButton>All your APIs</SupportButton>
</ContentHeader>
<EntityListProvider>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="api" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable
columns={columns || defaultColumns}
actions={actions}
/>
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
};
@@ -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';
+1
View File
@@ -14,6 +14,7 @@
* limitations under the License.
*/
export * from './ApiExplorerPage';
export * from './ApiDefinitionCard';
export * from './ApisCards';
export * from './AsyncApiDefinitionWidget';
+1 -1
View File
@@ -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,
}),
);
+8 -6
View File
@@ -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<CatalogTableRow>;
// @public (undocumented)
export function createTagsColumn(): TableColumn<CatalogTableRow>;
// @public
export type DefaultCatalogPageProps = {
initiallySelectedFilter?: UserListFilterKind;
columns?: TableColumn<CatalogTableRow>[];
actions?: TableProps<CatalogTableRow>['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)
@@ -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(<CatalogPage />);
expect(getByText('Route Children')).toBeInTheDocument();
});
const catalogApi: Partial<CatalogApi> = {
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<ProfileInfo> = {
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
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(<CatalogPage />);
const renderWrapped = (children: React.ReactNode) =>
renderWithEffects(
wrapInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[identityApiRef, identityApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
]}
>
{children}
</TestApiProvider>,
{
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(<CatalogPage />);
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<EntityRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = await renderWrapped(
<CatalogPage columns={columns} />,
);
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(
<CatalogPage />,
);
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<EntityRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<CatalogPage actions={actions} />,
);
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(<CatalogPage />);
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(
<CatalogPage initiallySelectedFilter="all" />,
);
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(<CatalogPage />);
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(<CatalogPage />);
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();
});
});
@@ -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<EntityRow>[];
actions?: TableProps<EntityRow>['actions'];
};
export const CatalogPage = ({
columns,
actions,
initiallySelectedFilter = 'owned',
}: CatalogPageProps) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<EntityListProvider>
<Content>
<ContentHeader titleComponent={<CatalogKindHeader />}>
<CreateButton
title="Create Component"
to={createComponentLink && createComponentLink()}
/>
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<FilteredEntityLayout>
<FilterContainer>
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</Content>
</EntityListProvider>
</PageWithHeader>
);
export const CatalogPage = (props: DefaultCatalogPageProps) => {
const outlet = useOutlet();
return outlet || <DefaultCatalogPage {...props} />;
};
@@ -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<CatalogApi> = {
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<ProfileInfo> = {
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
getUserId: () => 'tools',
getIdToken: async () => undefined,
getProfile: () => testProfile,
};
const storageApi = MockStorageApi.create();
const renderWrapped = (children: React.ReactNode) =>
renderWithEffects(
wrapInTestApp(
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[identityApiRef, identityApi],
[storageApiRef, storageApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
]}
>
{children}
</TestApiProvider>,
{
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(<DefaultCatalogPage />);
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<EntityRow>[] = [
{ title: 'Foo', field: 'entity.foo' },
{ title: 'Bar', field: 'entity.bar' },
{ title: 'Baz', field: 'entity.spec.lifecycle' },
];
const { getAllByRole } = await renderWrapped(
<DefaultCatalogPage columns={columns} />,
);
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(
<DefaultCatalogPage />,
);
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<EntityRow>['actions'] = [
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Foo Action',
disabled: false,
onClick: () => jest.fn(),
};
},
() => {
return {
icon: () => <DashboardIcon fontSize="small" />,
tooltip: 'Bar Action',
disabled: true,
onClick: () => jest.fn(),
};
},
];
const { getByTestId, findByTitle, findByText } = await renderWrapped(
<DefaultCatalogPage actions={actions} />,
);
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(
<DefaultCatalogPage />,
);
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(
<DefaultCatalogPage initiallySelectedFilter="all" />,
);
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(<DefaultCatalogPage />);
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(<DefaultCatalogPage />);
const button = getByRole('button', { name: 'Filters' });
expect(getByRole('presentation', { hidden: true })).toBeInTheDocument();
fireEvent.click(button);
expect(getByRole('presentation')).toBeVisible();
}, 20_000);
});
@@ -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<EntityRow>[];
actions?: TableProps<EntityRow>['actions'];
};
export const DefaultCatalogPage = ({
columns,
actions,
initiallySelectedFilter = 'owned',
}: DefaultCatalogPageProps) => {
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<EntityListProvider>
<Content>
<ContentHeader titleComponent={<CatalogKindHeader />}>
<CreateButton
title="Create Component"
to={createComponentLink && createComponentLink()}
/>
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<FilteredEntityLayout>
<FilterContainer>
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker />
<EntityLifecyclePicker />
<EntityTagPicker />
</FilterContainer>
<EntityListContainer>
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</Content>
</EntityListProvider>
</PageWithHeader>
);
};
@@ -14,3 +14,5 @@
* limitations under the License.
*/
export { CatalogPage } from './CatalogPage';
export { DefaultCatalogPage } from './DefaultCatalogPage';
export type { DefaultCatalogPageProps } from './DefaultCatalogPage';
+1
View File
@@ -54,3 +54,4 @@ export {
export type { EntityLinksEmptyStateClassKey } from './components/EntityLinksCard';
export type { SystemDiagramCardClassKey } from './components/SystemDiagramCard';
export type { DefaultCatalogPageProps } from './components/CatalogPage';