diff --git a/.changeset/new-beers-drive.md b/.changeset/new-beers-drive.md new file mode 100644 index 0000000000..370734b7c5 --- /dev/null +++ b/.changeset/new-beers-drive.md @@ -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. diff --git a/.changeset/sharp-falcons-clean.md b/.changeset/sharp-falcons-clean.md new file mode 100644 index 0000000000..bc247be451 --- /dev/null +++ b/.changeset/sharp-falcons-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Register default implementation for the `Translation API` on the new `createApp`. diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index eaaa2b0560..6f6081fece 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -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 diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 8b411ac38b..e770a6b4c1 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -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: {}, diff --git a/plugins/catalog/alpha-api-report.md b/plugins/catalog/alpha-api-report.md index db76a2e0f3..aba33fc092 100644 --- a/plugins/catalog/alpha-api-report.md +++ b/plugins/catalog/alpha-api-report.md @@ -3,8 +3,14 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +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; + loader: (options: { config: TConfig }) => Promise; +}): Extension; + +// @alpha (undocumented) +const _default: BackstagePlugin< + { + catalogIndex: RouteRef; + catalogEntity: RouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + }, + { + viewTechDoc: ExternalRouteRef< + { + name: string; + kind: string; + namespace: string; + }, + true + >; + createComponent: ExternalRouteRef; + createFromTemplate: ExternalRouteRef< + { + namespace: string; + templateName: string; + }, + true + >; + } +>; export default _default; // @alpha (undocumented) diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx index 5e78a52f9b..6eda8c3d26 100644 --- a/plugins/catalog/src/alpha.tsx +++ b/plugins/catalog/src/alpha.tsx @@ -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; + loader: (options: { config: TConfig }) => Promise; +}) { + 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: ( + + }> + + + + ), + }); + }, + }); +} + +const CatalogEntityTagFilter = createCatalogFilterExtension({ + id: 'entity.tag', + loader: async () => { + const { EntityTagPicker } = await import('@backstage/plugin-catalog-react'); + return ; + }, +}); + +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 ; + }, +}); + +const CatalogEntityTypeFilter = createCatalogFilterExtension({ + id: 'entity.type', + loader: async () => { + const { EntityTypePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +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 ; + }, +}); + +const CatalogEntityNamespaceFilter = createCatalogFilterExtension({ + id: 'entity.namespace', + loader: async () => { + const { EntityNamespacePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityLifecycleFilter = createCatalogFilterExtension({ + id: 'entity.lifecycle', + loader: async () => { + const { EntityLifecyclePicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({ + id: 'entity.processing.status', + loader: async () => { + const { EntityProcessingStatusPicker } = await import( + '@backstage/plugin-catalog-react' + ); + return ; + }, +}); + +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 ; + }, +}); + +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 {filters}} />; + }, +}); + +const CatalogEntityPage = createPageExtension({ + id: 'plugin.catalog.page.entity', + defaultPath: '/catalog/:namespace/:kind/:name', + routeRef: convertLegacyRouteRef(entityRouteRef), + loader: async () => { + const Component = () => { + return ( + +
🚧 Work In Progress
+
+ ); + }; + return ; + }, +}); + +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, ], }); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index fb9b282d57..e50c69b8ec 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -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 = } = props; + const orgName = + useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage'; + const createComponentLink = useRouteRef(createComponentRouteRef); + const { t } = useTranslationRef(catalogTranslationRef); + + return ( + + + + + All your software catalog entities + + + + {filters} + {content} + + + + + ); +} + /** * 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 ( - - - - - All your software catalog entities - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + } + content={ + + } + /> ); } diff --git a/plugins/catalog/src/components/CatalogPage/index.ts b/plugins/catalog/src/components/CatalogPage/index.ts index 9a3d083f41..258fedf15c 100644 --- a/plugins/catalog/src/components/CatalogPage/index.ts +++ b/plugins/catalog/src/components/CatalogPage/index.ts @@ -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';