diff --git a/.changeset/mighty-crews-attack.md b/.changeset/mighty-crews-attack.md
new file mode 100644
index 0000000000..c0b8587fe0
--- /dev/null
+++ b/.changeset/mighty-crews-attack.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog': patch
+---
+
+Initial entity page implementation for new frontend system at `/alpha`, with an overview page enabled by default and the about card available as an optional card.
diff --git a/.changeset/nice-apes-kneel.md b/.changeset/nice-apes-kneel.md
new file mode 100644
index 0000000000..1ecff9d223
--- /dev/null
+++ b/.changeset/nice-apes-kneel.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-techdocs': patch
+---
+
+Added entity page content for the new plugin exported via `/alpha`.
diff --git a/.changeset/old-apricots-taste.md b/.changeset/old-apricots-taste.md
new file mode 100644
index 0000000000..544ba11536
--- /dev/null
+++ b/.changeset/old-apricots-taste.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+Added new APIs at the `/alpha` subpath for creating entity page cards and content for the new frontend system.
diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index 6f6081fece..0c75e1c96c 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -4,12 +4,17 @@ 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
+ plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
extensions:
- apis.plugin.graphiql.browse.gitlab: true
+ # Entity page cards
+ - 'entity.cards.about'
+
+ # Entity page content
+ - 'entity.content.techdocs'
+
# scmAuthExtension: >-
# createScmAuthExtension({
# id: 'apis.scmAuth.addons.ghe',
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index 4e42d9b268..1275722da4 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -29,11 +29,8 @@ import {
createExtension,
createApiExtension,
createExtensionOverrides,
- createPageExtension,
} from '@backstage/frontend-plugin-api';
-import { entityRouteRef } from '@backstage/plugin-catalog-react';
import techdocsPlugin from '@backstage/plugin-techdocs/alpha';
-import { convertLegacyRouteRef } from '@backstage/core-plugin-api/alpha';
import { homePage } from './HomePage';
import { collectLegacyRoutes } from '@backstage/core-compat-api';
import { FlatRoutes } from '@backstage/core-app-api';
@@ -75,13 +72,6 @@ TODO:
/* app.tsx */
-const entityPageExtension = createPageExtension({
- id: 'catalog:entity',
- defaultPath: '/catalog/:namespace/:kind/:name',
- routeRef: convertLegacyRouteRef(entityRouteRef),
- loader: async () =>
Just a temporary mocked entity page
,
-});
-
const homePageExtension = createExtension({
id: 'myhomepage',
attachTo: { id: 'home', input: 'props' },
@@ -122,12 +112,7 @@ const app = createApp({
homePlugin,
...collectedLegacyPlugins,
createExtensionOverrides({
- extensions: [
- entityPageExtension,
- homePageExtension,
- scmAuthExtension,
- scmIntegrationApi,
- ],
+ extensions: [homePageExtension, scmAuthExtension, scmIntegrationApi],
}),
],
/* Handled through config instead */
diff --git a/plugins/catalog-react/alpha-api-report.md b/plugins/catalog-react/alpha-api-report.md
index f9b740c07c..120e162c84 100644
--- a/plugins/catalog-react/alpha-api-report.md
+++ b/plugins/catalog-react/alpha-api-report.md
@@ -3,8 +3,73 @@
> 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 { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
+import { Extension } from '@backstage/frontend-plugin-api';
+import { ExtensionInputValues } from '@backstage/frontend-plugin-api';
+import { PortableSchema } from '@backstage/frontend-plugin-api';
import { ResourcePermission } from '@backstage/plugin-permission-common';
+import { RouteRef } from '@backstage/frontend-plugin-api';
+
+// @alpha (undocumented)
+export function createEntityCardExtension<
+ TConfig,
+ TInputs extends AnyExtensionInputMap,
+>(options: {
+ id: string;
+ attachTo?: {
+ id: string;
+ input: string;
+ };
+ disabled?: boolean;
+ inputs?: TInputs;
+ configSchema?: PortableSchema;
+ loader: (options: {
+ config: TConfig;
+ inputs: Expand>;
+ }) => Promise;
+}): Extension;
+
+// @alpha (undocumented)
+export function createEntityContentExtension<
+ TConfig extends {
+ path: string;
+ title: string;
+ },
+ TInputs extends AnyExtensionInputMap,
+>(
+ options: (
+ | {
+ defaultPath: string;
+ defaultTitle: string;
+ }
+ | {
+ configSchema: PortableSchema;
+ }
+ ) & {
+ id: string;
+ attachTo?: {
+ id: string;
+ input: string;
+ };
+ disabled?: boolean;
+ inputs?: TInputs;
+ routeRef?: RouteRef;
+ loader: (options: {
+ config: TConfig;
+ inputs: Expand>;
+ }) => Promise;
+ },
+): Extension;
+
+// @alpha (undocumented)
+export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef<
+ string,
+ {}
+>;
// @alpha
export function isOwnerOf(owner: Entity, entity: Entity): boolean;
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index bb18160595..70fa5e9614 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -10,13 +10,13 @@
},
"exports": {
".": "./src/index.ts",
- "./alpha": "./src/alpha.ts",
+ "./alpha": "./src/alpha.tsx",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
- "src/alpha.ts"
+ "src/alpha.tsx"
],
"package.json": [
"package.json"
@@ -51,6 +51,7 @@
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/frontend-plugin-api": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx
new file mode 100644
index 0000000000..58762b1994
--- /dev/null
+++ b/plugins/catalog-react/src/alpha.tsx
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2023 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 React, { lazy } from 'react';
+import {
+ AnyExtensionInputMap,
+ Extension,
+ ExtensionBoundary,
+ ExtensionInputValues,
+ PortableSchema,
+ RouteRef,
+ coreExtensionData,
+ createExtension,
+ createExtensionDataRef,
+ createSchemaFromZod,
+} from '@backstage/frontend-plugin-api';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { Expand } from '../../../packages/frontend-plugin-api/src/types';
+
+export { isOwnerOf } from './utils';
+export { useEntityPermission } from './hooks/useEntityPermission';
+
+/** @alpha */
+export const entityContentTitleExtensionDataRef =
+ createExtensionDataRef('plugin.catalog.entity.content.title');
+
+/** @alpha */
+export function createEntityCardExtension<
+ TConfig,
+ TInputs extends AnyExtensionInputMap,
+>(options: {
+ id: string;
+ attachTo?: { id: string; input: string };
+ disabled?: boolean;
+ inputs?: TInputs;
+ configSchema?: PortableSchema;
+ loader: (options: {
+ config: TConfig;
+ inputs: Expand>;
+ }) => Promise;
+}): Extension {
+ const id = `entity.cards.${options.id}`;
+
+ return createExtension({
+ id,
+ attachTo: options.attachTo ?? {
+ id: 'entity.content.overview',
+ input: 'cards',
+ },
+ disabled: options.disabled ?? true,
+ output: {
+ element: coreExtensionData.reactElement,
+ },
+ inputs: options.inputs,
+ configSchema: options.configSchema,
+ factory({ bind, config, inputs, source }) {
+ const ExtensionComponent = lazy(() =>
+ options
+ .loader({ config, inputs })
+ .then(element => ({ default: () => element })),
+ );
+
+ bind({
+ element: (
+
+
+
+ ),
+ });
+ },
+ });
+}
+
+/** @alpha */
+export function createEntityContentExtension<
+ TConfig extends { path: string; title: string },
+ TInputs extends AnyExtensionInputMap,
+>(
+ options: (
+ | {
+ defaultPath: string;
+ defaultTitle: string;
+ }
+ | {
+ configSchema: PortableSchema;
+ }
+ ) & {
+ id: string;
+ attachTo?: { id: string; input: string };
+ disabled?: boolean;
+ inputs?: TInputs;
+ routeRef?: RouteRef;
+ loader: (options: {
+ config: TConfig;
+ inputs: Expand>;
+ }) => Promise;
+ },
+): Extension {
+ const id = `entity.content.${options.id}`;
+
+ const configSchema =
+ 'configSchema' in options
+ ? options.configSchema
+ : (createSchemaFromZod(z =>
+ z.object({
+ path: z.string().default(options.defaultPath),
+ title: z.string().default(options.defaultTitle),
+ }),
+ ) as PortableSchema);
+
+ return createExtension({
+ id,
+ attachTo: options.attachTo ?? {
+ id: 'plugin.catalog.page.entity',
+ input: 'contents',
+ },
+ disabled: options.disabled ?? true,
+ output: {
+ element: coreExtensionData.reactElement,
+ path: coreExtensionData.routePath,
+ routeRef: coreExtensionData.routeRef.optional(),
+ title: entityContentTitleExtensionDataRef,
+ },
+ inputs: options.inputs,
+ configSchema,
+ factory({ bind, config, inputs, source }) {
+ const ExtensionComponent = lazy(() =>
+ options
+ .loader({ config, inputs })
+ .then(element => ({ default: () => element })),
+ );
+
+ bind({
+ path: config.path,
+ title: config.title,
+ routeRef: options.routeRef,
+ element: (
+
+
+
+ ),
+ });
+ },
+ });
+}
diff --git a/plugins/catalog/alpha-api-report.md b/plugins/catalog/alpha-api-report.md
index aba33fc092..43e589bdc3 100644
--- a/plugins/catalog/alpha-api-report.md
+++ b/plugins/catalog/alpha-api-report.md
@@ -12,14 +12,6 @@ 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<{}>;
-
-// @alpha (undocumented)
-export const CatalogSearchResultListItemExtension: Extension<{
- noTrack?: boolean | undefined;
-}>;
-
// @alpha (undocumented)
export function createCatalogFilterExtension<
TInputs extends AnyExtensionInputMap,
@@ -62,8 +54,5 @@ const _default: BackstagePlugin<
>;
export default _default;
-// @alpha (undocumented)
-export const StarredEntitiesApi: Extension<{}>;
-
// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 56ae771314..9b879e3c50 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -10,13 +10,13 @@
},
"exports": {
".": "./src/index.ts",
- "./alpha": "./src/alpha.tsx",
+ "./alpha": "./src/alpha.ts",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"alpha": [
- "src/alpha.tsx"
+ "src/alpha.ts"
],
"package.json": [
"package.json"
diff --git a/plugins/catalog-react/src/alpha.ts b/plugins/catalog/src/alpha.ts
similarity index 85%
rename from plugins/catalog-react/src/alpha.ts
rename to plugins/catalog/src/alpha.ts
index e8ff21609e..e80f131817 100644
--- a/plugins/catalog-react/src/alpha.ts
+++ b/plugins/catalog/src/alpha.ts
@@ -14,5 +14,5 @@
* limitations under the License.
*/
-export { isOwnerOf } from './utils';
-export { useEntityPermission } from './hooks/useEntityPermission';
+export * from './alpha/index';
+export { default } from './alpha/index';
diff --git a/plugins/catalog/src/alpha.tsx b/plugins/catalog/src/alpha.tsx
deleted file mode 100644
index 31551c1930..0000000000
--- a/plugins/catalog/src/alpha.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-/*
- * Copyright 2023 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 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 { useEntityFromUrl } from './components/CatalogEntityPage/useEntityFromUrl';
-
-/** @alpha */
-export const CatalogApi = createApiExtension({
- factory: createApiFactory({
- api: catalogApiRef,
- deps: {
- discoveryApi: discoveryApiRef,
- fetchApi: fetchApiRef,
- },
- factory: ({ discoveryApi, fetchApi }) =>
- new CatalogClient({ discoveryApi, fetchApi }),
- }),
-});
-
-/** @alpha */
-export const StarredEntitiesApi = createApiExtension({
- factory: createApiFactory({
- api: starredEntitiesApiRef,
- deps: { storageApi: storageApiRef },
- factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }),
- }),
-});
-
-/** @alpha */
-export const CatalogSearchResultListItemExtension =
- createSearchResultListItemExtension({
- id: 'catalog',
- predicate: result => result.type === 'software-catalog',
- component: () =>
- import('./components/CatalogSearchResultListItem').then(
- m => m.CatalogSearchResultListItem,
- ),
- });
-
-/** @alpha */
-export function createCatalogFilterExtension<
- TInputs extends AnyExtensionInputMap,
- TConfig = never,
->(options: {
- id: string;
- inputs?: TInputs;
- configSchema?: PortableSchema;
- loader: (options: { config: TConfig }) => Promise;
-}) {
- const id = `catalog.filter.${options.id}`;
-
- return createExtension({
- id,
- attachTo: { id: 'plugin.catalog.page.index', input: 'filters' },
- inputs: options.inputs ?? {},
- configSchema: options.configSchema,
- output: {
- element: coreExtensionData.reactElement,
- },
- factory({ bind, config, source }) {
- const ExtensionComponent = 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/alpha/builtInFilterExtensions.tsx b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx
new file mode 100644
index 0000000000..5a7575c4f5
--- /dev/null
+++ b/plugins/catalog/src/alpha/builtInFilterExtensions.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2023 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 React from 'react';
+import { createCatalogFilterExtension } from './createCatalogFilterExtension';
+import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
+
+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 ;
+ },
+});
+
+export const builtInFilterExtensions = [
+ CatalogEntityTagFilter,
+ CatalogEntityKindFilter,
+ CatalogEntityTypeFilter,
+ CatalogEntityOwnerFilter,
+ CatalogEntityNamespaceFilter,
+ CatalogEntityLifecycleFilter,
+ CatalogEntityProcessingStatusFilter,
+ CatalogUserListFilter,
+];
diff --git a/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx
new file mode 100644
index 0000000000..5a6328078c
--- /dev/null
+++ b/plugins/catalog/src/alpha/createCatalogFilterExtension.tsx
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2023 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 React, { lazy } from 'react';
+import {
+ AnyExtensionInputMap,
+ ExtensionBoundary,
+ PortableSchema,
+ coreExtensionData,
+ createExtension,
+} from '@backstage/frontend-plugin-api';
+
+/** @alpha */
+export function createCatalogFilterExtension<
+ TInputs extends AnyExtensionInputMap,
+ TConfig = never,
+>(options: {
+ id: string;
+ inputs?: TInputs;
+ configSchema?: PortableSchema;
+ loader: (options: { config: TConfig }) => Promise;
+}) {
+ const id = `catalog.filter.${options.id}`;
+
+ return createExtension({
+ id,
+ attachTo: { id: 'plugin.catalog.page.index', input: 'filters' },
+ inputs: options.inputs ?? {},
+ configSchema: options.configSchema,
+ output: {
+ element: coreExtensionData.reactElement,
+ },
+ factory({ bind, config, source }) {
+ const ExtensionComponent = lazy(() =>
+ options
+ .loader({ config })
+ .then(element => ({ default: () => element })),
+ );
+
+ bind({
+ element: (
+
+
+
+ ),
+ });
+ },
+ });
+}
diff --git a/plugins/catalog/src/alpha/index.ts b/plugins/catalog/src/alpha/index.ts
new file mode 100644
index 0000000000..06a78ec6ea
--- /dev/null
+++ b/plugins/catalog/src/alpha/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2023 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.
+ */
+
+export { default } from './plugin';
+export { createCatalogFilterExtension } from './createCatalogFilterExtension';
diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx
new file mode 100644
index 0000000000..d9d2878213
--- /dev/null
+++ b/plugins/catalog/src/alpha/plugin.tsx
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2023 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 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 {
+ createApiExtension,
+ createPageExtension,
+ createPlugin,
+ createNavItemExtension,
+ coreExtensionData,
+ createExtensionInput,
+} from '@backstage/frontend-plugin-api';
+import {
+ AsyncEntityProvider,
+ catalogApiRef,
+ entityRouteRef,
+ starredEntitiesApiRef,
+} from '@backstage/plugin-catalog-react';
+import {
+ createEntityContentExtension,
+ createEntityCardExtension,
+ entityContentTitleExtensionDataRef,
+} from '@backstage/plugin-catalog-react/alpha';
+import { createSearchResultListItemExtension } from '@backstage/plugin-search-react/alpha';
+import { DefaultStarredEntitiesApi } from '../apis';
+import {
+ createComponentRouteRef,
+ createFromTemplateRouteRef,
+ rootRouteRef,
+ viewTechDocRouteRef,
+} from '../routes';
+import { builtInFilterExtensions } from './builtInFilterExtensions';
+import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl';
+import Grid from '@material-ui/core/Grid';
+
+/** @alpha */
+export const CatalogApi = createApiExtension({
+ factory: createApiFactory({
+ api: catalogApiRef,
+ deps: {
+ discoveryApi: discoveryApiRef,
+ fetchApi: fetchApiRef,
+ },
+ factory: ({ discoveryApi, fetchApi }) =>
+ new CatalogClient({ discoveryApi, fetchApi }),
+ }),
+});
+
+/** @alpha */
+export const StarredEntitiesApi = createApiExtension({
+ factory: createApiFactory({
+ api: starredEntitiesApiRef,
+ deps: { storageApi: storageApiRef },
+ factory: ({ storageApi }) => new DefaultStarredEntitiesApi({ storageApi }),
+ }),
+});
+
+/** @alpha */
+export const CatalogSearchResultListItemExtension =
+ createSearchResultListItemExtension({
+ id: 'catalog',
+ predicate: result => result.type === 'software-catalog',
+ component: () =>
+ import('../components/CatalogSearchResultListItem').then(
+ m => m.CatalogSearchResultListItem,
+ ),
+ });
+
+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),
+ inputs: {
+ contents: createExtensionInput({
+ element: coreExtensionData.reactElement,
+ path: coreExtensionData.routePath,
+ routeRef: coreExtensionData.routeRef.optional(),
+ title: entityContentTitleExtensionDataRef,
+ }),
+ },
+ loader: async ({ inputs }) => {
+ const { EntityLayout } = await import('../components/EntityLayout');
+ const Component = () => {
+ return (
+
+
+ {inputs.contents.map(content => (
+
+ {content.element}
+
+ ))}
+
+
+ );
+ };
+ return ;
+ },
+});
+
+const EntityAboutCard = createEntityCardExtension({
+ id: 'about',
+ loader: async () =>
+ import('../components/AboutCard').then(m => (
+
+ )),
+});
+
+const OverviewEntityContent = createEntityContentExtension({
+ id: 'overview',
+ defaultPath: '/',
+ defaultTitle: 'Overview',
+ disabled: false,
+ inputs: {
+ cards: createExtensionInput({
+ element: coreExtensionData.reactElement,
+ }),
+ },
+ loader: async ({ inputs }) => (
+
+ {inputs.cards.map(card => (
+
+ {card.element}
+
+ ))}
+
+ ),
+});
+
+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,
+ CatalogIndexPage,
+ CatalogEntityPage,
+ CatalogNavItem,
+ OverviewEntityContent,
+ EntityAboutCard,
+ ...builtInFilterExtensions,
+ ],
+});
diff --git a/plugins/techdocs/src/alpha.tsx b/plugins/techdocs/src/alpha.tsx
index b7ccb0b12c..ceb5f47364 100644
--- a/plugins/techdocs/src/alpha.tsx
+++ b/plugins/techdocs/src/alpha.tsx
@@ -42,6 +42,7 @@ import {
rootDocsRouteRef,
rootRouteRef,
} from './routes';
+import { createEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
/** @alpha */
const techDocsStorage = createApiExtension({
@@ -141,6 +142,18 @@ const TechDocsReaderPage = createPageExtension({
)),
});
+/**
+ * Component responsible for rendering techdocs on entity pages
+ *
+ * @alpha
+ */
+const TechDocsEntityContent = createEntityContentExtension({
+ id: 'techdocs',
+ defaultPath: 'docs',
+ defaultTitle: 'TechDocs',
+ loader: () => import('./Router').then(m => ),
+});
+
/** @alpha */
const TechDocsNavItem = createNavItemExtension({
id: 'plugin.techdocs.nav.index',
@@ -158,6 +171,7 @@ export default createPlugin({
TechDocsNavItem,
TechDocsIndexPage,
TechDocsReaderPage,
+ TechDocsEntityContent,
TechDocsSearchResultListItemExtension,
],
routes: {
diff --git a/yarn.lock b/yarn.lock
index 059c18d859..3075745d85 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5837,6 +5837,7 @@ __metadata:
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/errors": "workspace:^"
+ "@backstage/frontend-plugin-api": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-catalog-common": "workspace:^"
"@backstage/plugin-permission-common": "workspace:^"