Merge pull request #20355 from backstage/camilaibs/migrate-catalog-plugin-to-declarative-integration

[DI] Start `Catalog` frontend packages migration
This commit is contained in:
Camila Belo
2023-10-18 14:28:15 +02:00
committed by GitHub
8 changed files with 346 additions and 40 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': patch
---
Create declarative extensions for the `Catalog` plugin; this initial plugin preset contains sidebar item, index page and filter extensions, all distributed via `/alpha` subpath.
The `EntityPage` will be migrated in a follow-up patch.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Register default implementation for the `Translation API` on the new `createApp`.
+2
View File
@@ -4,6 +4,8 @@ app:
routes:
bindings:
plugin.pages.externalRoutes.pageX: plugin.pages.routes.pageX
# waiting for https://github.com/backstage/backstage/pull/20605
# catalog.externalRoutes.viewTechDoc: techdocs.routes.docRoot
extensions:
- apis.plugin.graphiql.browse.gitlab: true
@@ -492,6 +492,21 @@ function createApiHolder(
factory: () => AppThemeSelector.createWithStorage(themeExtensions),
});
factoryRegistry.register('static', {
api: appLanguageApiRef,
deps: {},
factory: () => AppLanguageSelector.createWithStorage(),
});
factoryRegistry.register('default', {
api: translationApiRef,
deps: { languageApi: appLanguageApiRef },
factory: ({ languageApi }) =>
I18nextTranslationApi.create({
languageApi,
}),
});
factoryRegistry.register('static', {
api: configApiRef,
deps: {},
+45 -1
View File
@@ -3,8 +3,14 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
import { Extension } from '@backstage/frontend-plugin-api';
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { PortableSchema } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
// @alpha (undocumented)
export const CatalogApi: Extension<{}>;
@@ -15,7 +21,45 @@ export const CatalogSearchResultListItemExtension: Extension<{
}>;
// @alpha (undocumented)
const _default: BackstagePlugin<{}, {}>;
export function createCatalogFilterExtension<
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(options: {
id: string;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
}): Extension<TConfig>;
// @alpha (undocumented)
const _default: BackstagePlugin<
{
catalogIndex: RouteRef<undefined>;
catalogEntity: RouteRef<{
name: string;
kind: string;
namespace: string;
}>;
},
{
viewTechDoc: ExternalRouteRef<
{
name: string;
kind: string;
namespace: string;
},
true
>;
createComponent: ExternalRouteRef<undefined, true>;
createFromTemplate: ExternalRouteRef<
{
namespace: string;
templateName: string;
},
true
>;
}
>;
export default _default;
// @alpha (undocumented)
+210
View File
@@ -14,23 +14,45 @@
* limitations under the License.
*/
import React from 'react';
import HomeIcon from '@material-ui/icons/Home';
import {
createApiFactory,
discoveryApiRef,
fetchApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
import { CatalogClient } from '@backstage/catalog-client';
import {
createSchemaFromZod,
createApiExtension,
createPageExtension,
createPlugin,
createNavItemExtension,
createExtension,
coreExtensionData,
AnyExtensionInputMap,
PortableSchema,
ExtensionBoundary,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import {
AsyncEntityProvider,
catalogApiRef,
entityRouteRef,
starredEntitiesApiRef,
} from '@backstage/plugin-catalog-react';
import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
import { DefaultStarredEntitiesApi } from './apis';
import {
createComponentRouteRef,
createFromTemplateRouteRef,
rootRouteRef,
viewTechDocRouteRef,
} from './routes';
import { Progress } from '@backstage/core-components';
import { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl';
/** @alpha */
export const CatalogApi = createApiExtension({
@@ -65,12 +87,200 @@ export const CatalogSearchResultListItemExtension =
),
});
/** @alpha */
export function createCatalogFilterExtension<
TInputs extends AnyExtensionInputMap,
TConfig = never,
>(options: {
id: string;
inputs?: TInputs;
configSchema?: PortableSchema<TConfig>;
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
}) {
return createExtension({
id: `catalog.filter.${options.id}`,
attachTo: { id: 'plugin.catalog.page.index', input: 'filters' },
inputs: options.inputs ?? {},
configSchema: options.configSchema,
output: {
element: coreExtensionData.reactElement,
},
factory({ bind, config, source }) {
const LazyComponent = React.lazy(() =>
options
.loader({ config })
.then(element => ({ default: () => element })),
);
bind({
element: (
<ExtensionBoundary source={source}>
<React.Suspense fallback={<Progress />}>
<LazyComponent />
</React.Suspense>
</ExtensionBoundary>
),
});
},
});
}
const CatalogEntityTagFilter = createCatalogFilterExtension({
id: 'entity.tag',
loader: async () => {
const { EntityTagPicker } = await import('@backstage/plugin-catalog-react');
return <EntityTagPicker />;
},
});
const CatalogEntityKindFilter = createCatalogFilterExtension({
id: 'entity.kind',
configSchema: createSchemaFromZod(z =>
z.object({
initialFilter: z.string().default('component'),
}),
),
loader: async ({ config }) => {
const { EntityKindPicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityKindPicker initialFilter={config.initialFilter} />;
},
});
const CatalogEntityTypeFilter = createCatalogFilterExtension({
id: 'entity.type',
loader: async () => {
const { EntityTypePicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityTypePicker />;
},
});
const CatalogEntityOwnerFilter = createCatalogFilterExtension({
id: 'entity.mode',
configSchema: createSchemaFromZod(z =>
z.object({
mode: z.enum(['owners-only', 'all']).optional(),
}),
),
loader: async ({ config }) => {
const { EntityOwnerPicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityOwnerPicker mode={config.mode} />;
},
});
const CatalogEntityNamespaceFilter = createCatalogFilterExtension({
id: 'entity.namespace',
loader: async () => {
const { EntityNamespacePicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityNamespacePicker />;
},
});
const CatalogEntityLifecycleFilter = createCatalogFilterExtension({
id: 'entity.lifecycle',
loader: async () => {
const { EntityLifecyclePicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityLifecyclePicker />;
},
});
const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({
id: 'entity.processing.status',
loader: async () => {
const { EntityProcessingStatusPicker } = await import(
'@backstage/plugin-catalog-react'
);
return <EntityProcessingStatusPicker />;
},
});
const CatalogUserListFilter = createCatalogFilterExtension({
id: 'user.list',
configSchema: createSchemaFromZod(z =>
z.object({
initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'),
}),
),
loader: async ({ config }) => {
const { UserListPicker } = await import('@backstage/plugin-catalog-react');
return <UserListPicker initialFilter={config.initialFilter} />;
},
});
const CatalogIndexPage = createPageExtension({
id: 'plugin.catalog.page.index',
defaultPath: '/catalog',
routeRef: convertLegacyRouteRef(rootRouteRef),
inputs: {
filters: createExtensionInput({
element: coreExtensionData.reactElement,
}),
},
loader: async ({ inputs }) => {
const { BaseCatalogPage } = await import('./components/CatalogPage');
const filters = inputs.filters.map(filter => filter.element);
return <BaseCatalogPage filters={<>{filters}</>} />;
},
});
const CatalogEntityPage = createPageExtension({
id: 'plugin.catalog.page.entity',
defaultPath: '/catalog/:namespace/:kind/:name',
routeRef: convertLegacyRouteRef(entityRouteRef),
loader: async () => {
const Component = () => {
return (
<AsyncEntityProvider {...useEntityFromUrl()}>
<div>🚧 Work In Progress</div>
</AsyncEntityProvider>
);
};
return <Component />;
},
});
const CatalogNavItem = createNavItemExtension({
id: 'catalog.nav.index',
routeRef: convertLegacyRouteRef(rootRouteRef),
title: 'Catalog',
icon: HomeIcon,
});
/** @alpha */
export default createPlugin({
id: 'catalog',
routes: {
catalogIndex: convertLegacyRouteRef(rootRouteRef),
catalogEntity: convertLegacyRouteRef(entityRouteRef),
},
externalRoutes: {
viewTechDoc: convertLegacyRouteRef(viewTechDocRouteRef),
createComponent: convertLegacyRouteRef(createComponentRouteRef),
createFromTemplate: convertLegacyRouteRef(createFromTemplateRouteRef),
},
extensions: [
CatalogApi,
StarredEntitiesApi,
CatalogSearchResultListItemExtension,
CatalogEntityKindFilter,
CatalogEntityTypeFilter,
CatalogUserListFilter,
CatalogEntityOwnerFilter,
CatalogEntityLifecycleFilter,
CatalogEntityTagFilter,
CatalogEntityProcessingStatusFilter,
CatalogEntityNamespaceFilter,
CatalogIndexPage,
CatalogEntityPage,
CatalogNavItem,
],
});
@@ -44,6 +44,41 @@ import { CatalogTable, CatalogTableRow } from '../CatalogTable';
import { catalogTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @internal */
export interface BaseCatalogPageProps {
filters: ReactNode;
content?: ReactNode;
}
/** @internal */
export function BaseCatalogPage(props: BaseCatalogPageProps) {
const { filters, content = <CatalogTable /> } = props;
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
const { t } = useTranslationRef(catalogTranslationRef);
return (
<PageWithHeader title={t('catalog_page_title', { orgName })} themeId="home">
<Content>
<ContentHeader title="">
<CreateButton
title={t('catalog_page_create_button_title')}
to={createComponentLink && createComponentLink()}
/>
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>{filters}</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>{content}</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
);
}
/**
* Props for root catalog pages.
*
@@ -69,44 +104,29 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
emptyContent,
ownerPickerMode,
} = props;
const orgName =
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
const createComponentLink = useRouteRef(createComponentRouteRef);
const { t } = useTranslationRef(catalogTranslationRef);
return (
<PageWithHeader title={t('catalog_page_title', { orgName })} themeId="home">
<Content>
<ContentHeader title="">
<CreateButton
title={t('catalog_page_create_button_title')}
to={createComponentLink && createComponentLink()}
/>
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider>
<CatalogFilterLayout>
<CatalogFilterLayout.Filters>
<EntityKindPicker initialFilter={initialKind} />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker mode={ownerPickerMode} />
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</CatalogFilterLayout.Filters>
<CatalogFilterLayout.Content>
<CatalogTable
columns={columns}
actions={actions}
tableOptions={tableOptions}
emptyContent={emptyContent}
/>
</CatalogFilterLayout.Content>
</CatalogFilterLayout>
</EntityListProvider>
</Content>
</PageWithHeader>
<BaseCatalogPage
filters={
<>
<EntityKindPicker initialFilter={initialKind} />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityOwnerPicker mode={ownerPickerMode} />
<EntityLifecyclePicker />
<EntityTagPicker />
<EntityProcessingStatusPicker />
<EntityNamespacePicker />
</>
}
content={
<CatalogTable
columns={columns}
actions={actions}
tableOptions={tableOptions}
emptyContent={emptyContent}
/>
}
/>
);
}
@@ -15,5 +15,8 @@
*/
export { CatalogPage } from './CatalogPage';
export { DefaultCatalogPage } from './DefaultCatalogPage';
export type { DefaultCatalogPageProps } from './DefaultCatalogPage';
export { BaseCatalogPage, DefaultCatalogPage } from './DefaultCatalogPage';
export type {
BaseCatalogPageProps,
DefaultCatalogPageProps,
} from './DefaultCatalogPage';