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';