diff --git a/workspaces/app-defaults/packages/app/package.json b/workspaces/app-defaults/packages/app/package.json index b8071fbabc..e7f995d22b 100644 --- a/workspaces/app-defaults/packages/app/package.json +++ b/workspaces/app-defaults/packages/app/package.json @@ -44,6 +44,7 @@ "@mui/icons-material": "^5.18.0", "@mui/material": "^5.18.0", "@red-hat-developer-hub/backstage-plugin-app-auth": "workspace:^", + "@red-hat-developer-hub/backstage-plugin-app-defaults": "workspace:^", "@red-hat-developer-hub/backstage-plugin-app-integrations": "workspace:^", "@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^", "@red-hat-developer-hub/backstage-plugin-global-header": "^1.21.0", diff --git a/workspaces/app-defaults/packages/app/src/App.tsx b/workspaces/app-defaults/packages/app/src/App.tsx index bcc187c200..3963c0fbae 100644 --- a/workspaces/app-defaults/packages/app/src/App.tsx +++ b/workspaces/app-defaults/packages/app/src/App.tsx @@ -15,14 +15,8 @@ */ import { createApp } from '@backstage/frontend-defaults'; -import catalogPlugin from '@backstage/plugin-catalog/alpha'; -import scaffolderPlugin from '@backstage/plugin-scaffolder/alpha'; import { appAuthModule } from '@red-hat-developer-hub/backstage-plugin-app-auth'; import { appIntegrationsModule } from '@red-hat-developer-hub/backstage-plugin-app-integrations'; -import { - appDrawerModule, - templateCardModule, -} from '@red-hat-developer-hub/backstage-plugin-app-react'; import { globalHeaderModule, globalHeaderTranslationsModule, @@ -33,14 +27,10 @@ import { templateCardDemoModule } from './modules/template-card-demo'; export default createApp({ features: [ - catalogPlugin, - scaffolderPlugin, navModule, appAuthModule, appIntegrationsModule, - appDrawerModule, drawerDemoModule, - templateCardModule, templateCardDemoModule, globalHeaderModule, globalHeaderTranslationsModule, diff --git a/workspaces/app-defaults/plugins/app-auth/report-alpha.api.md b/workspaces/app-defaults/plugins/app-auth/report-alpha.api.md index ee2ecd14e6..b7d276545f 100644 --- a/workspaces/app-defaults/plugins/app-auth/report-alpha.api.md +++ b/workspaces/app-defaults/plugins/app-auth/report-alpha.api.md @@ -9,38 +9,38 @@ import { TranslationRef } from '@backstage/frontend-plugin-api'; export const signInTranslationRef: TranslationRef< 'plugin.app-auth.sign-in', { - readonly 'signIn.page.title': 'Select a sign-in method'; + readonly 'signIn.config.missingAuthEnvironment.description': 'Set {{ authEnvKey }} in your app configuration (for example {{ devEnv }} for local use or {{ prodEnv }} in deployed environments). Without it, the app cannot safely determine which sign-in options to offer.'; readonly 'signIn.config.missingAuthEnvironment.error': 'Missing required app configuration: auth.environment must be set'; readonly 'signIn.config.missingAuthEnvironment.panelTitle': 'Sign-in is not available'; - readonly 'signIn.config.missingAuthEnvironment.description': 'Set {{ authEnvKey }} in your app configuration (for example {{ devEnv }} for local use or {{ prodEnv }} in deployed environments). Without it, the app cannot safely determine which sign-in options to offer.'; + readonly 'signIn.page.title': 'Select a sign-in method'; + readonly 'signIn.providers.oidc.title': 'OIDC'; + readonly 'signIn.providers.oidc.message': 'Sign in using OIDC'; + readonly 'signIn.providers.keycloak.title': 'Keycloak'; + readonly 'signIn.providers.keycloak.message': 'Sign in using Keycloak'; + readonly 'signIn.providers.pingfederate.title': 'PingFederate'; + readonly 'signIn.providers.pingfederate.message': 'Sign in using PingFederate'; readonly 'signIn.providers.auth0.title': 'Auth0'; readonly 'signIn.providers.auth0.message': 'Sign in using Auth0'; + readonly 'signIn.providers.saml.title': 'SAML'; + readonly 'signIn.providers.saml.message': 'Sign in using SAML'; + readonly 'signIn.providers.github.title': 'GitHub'; + readonly 'signIn.providers.github.message': 'Sign in using GitHub'; + readonly 'signIn.providers.gitlab.title': 'GitLab'; + readonly 'signIn.providers.gitlab.message': 'Sign in using GitLab'; + readonly 'signIn.providers.bitbucket.title': 'Bitbucket'; + readonly 'signIn.providers.bitbucket.message': 'Sign in using Bitbucket'; readonly 'signIn.providers.atlassian.title': 'Atlassian'; readonly 'signIn.providers.atlassian.message': 'Sign in using Atlassian'; readonly 'signIn.providers.microsoft.title': 'Microsoft'; readonly 'signIn.providers.microsoft.message': 'Sign in using Microsoft'; - readonly 'signIn.providers.bitbucket.title': 'Bitbucket'; - readonly 'signIn.providers.bitbucket.message': 'Sign in using Bitbucket'; readonly 'signIn.providers.bitbucketServer.title': 'Bitbucket Server'; readonly 'signIn.providers.bitbucketServer.message': 'Sign in using Bitbucket Server'; - readonly 'signIn.providers.github.title': 'GitHub'; - readonly 'signIn.providers.github.message': 'Sign in using GitHub'; - readonly 'signIn.providers.gitlab.title': 'GitLab'; - readonly 'signIn.providers.gitlab.message': 'Sign in using GitLab'; readonly 'signIn.providers.google.title': 'Google'; readonly 'signIn.providers.google.message': 'Sign in using Google'; - readonly 'signIn.providers.oidc.title': 'OIDC'; - readonly 'signIn.providers.oidc.message': 'Sign in using OIDC'; - readonly 'signIn.providers.keycloak.title': 'Keycloak'; - readonly 'signIn.providers.keycloak.message': 'Sign in using Keycloak'; - readonly 'signIn.providers.pingfederate.title': 'PingFederate'; - readonly 'signIn.providers.pingfederate.message': 'Sign in using PingFederate'; readonly 'signIn.providers.okta.title': 'Okta'; readonly 'signIn.providers.okta.message': 'Sign in using Okta'; readonly 'signIn.providers.onelogin.title': 'OneLogin'; readonly 'signIn.providers.onelogin.message': 'Sign in using OneLogin'; - readonly 'signIn.providers.saml.title': 'SAML'; - readonly 'signIn.providers.saml.message': 'Sign in using SAML'; } >; ``` diff --git a/workspaces/app-defaults/plugins/app-defaults/package.json b/workspaces/app-defaults/plugins/app-defaults/package.json index 8c4eb0f123..f7631075c2 100644 --- a/workspaces/app-defaults/plugins/app-defaults/package.json +++ b/workspaces/app-defaults/plugins/app-defaults/package.json @@ -43,8 +43,21 @@ "postpack": "backstage-cli package postpack" }, "dependencies": { + "@backstage/catalog-client": "^1.16.1", + "@backstage/core-components": "^0.18.11", + "@backstage/core-plugin-api": "^1.12.7", "@backstage/frontend-plugin-api": "^0.17.2", - "@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^" + "@backstage/plugin-api-docs": "^0.14.2", + "@backstage/plugin-catalog": "^2.0.6", + "@backstage/plugin-catalog-graph": "^0.6.5", + "@backstage/plugin-catalog-react": "^3.1.0", + "@backstage/plugin-scaffolder": "^1.38.0", + "@backstage/plugin-techdocs": "^1.17.7", + "@backstage/plugin-techdocs-common": "^0.1.1", + "@backstage/ui": "^0.16.0", + "@mui/material": "5.18.0", + "@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^", + "@remixicon/react": ">=4.6.0 <4.9.0" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0 || ^18.0.0" diff --git a/workspaces/app-defaults/plugins/app-defaults/report.api.md b/workspaces/app-defaults/plugins/app-defaults/report.api.md index 5463d2b244..4143fb82d1 100644 --- a/workspaces/app-defaults/plugins/app-defaults/report.api.md +++ b/workspaces/app-defaults/plugins/app-defaults/report.api.md @@ -3,10 +3,2979 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnyApiFactory } from '@backstage/frontend-plugin-api'; +import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { CatalogExportSettings } from '@backstage/plugin-catalog/alpha'; +import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { EntityCardType } from '@backstage/plugin-catalog-react/alpha'; +import { EntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha'; +import { EntityContextMenuItemParams } from '@backstage/plugin-catalog-react/alpha'; +import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api'; +import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; +import { ExtensionInput } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FilterPredicate } from '@backstage/filter-predicates'; +import { FormField } from '@backstage/plugin-scaffolder-react/alpha'; +import { FrontendFeatureLoader } from '@backstage/frontend-plugin-api'; import { FrontendModule } from '@backstage/frontend-plugin-api'; +import { IconElement } from '@backstage/frontend-plugin-api'; +import { IconLinkVerticalProps } from '@backstage/core-components'; +import { JSX as JSX_2 } from 'react'; +import { JSXElementConstructor } from 'react'; +import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api'; +import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api'; +import { PathParams } from '@backstage/core-plugin-api'; +import { ReactElement } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api'; +import { ScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; +import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; +import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; +import { SearchResultListItemBlueprintParams } from '@backstage/plugin-search-react/alpha'; +import { SubRouteRef } from '@backstage/core-plugin-api'; +import { TechDocsAddonOptions } from '@backstage/plugin-techdocs-react'; // @public -const appDefaultsModule: FrontendModule; -export { appDefaultsModule }; -export default appDefaultsModule; +export const apiDocsPluginOverride: OverridableFrontendPlugin< + { + root: RouteRef; + }, + { + registerApi: ExternalRouteRef; + }, + { + 'api:api-docs/config': OverridableExtensionDefinition<{ + kind: 'api'; + name: 'config'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'entity-card:api-docs/consumed-apis': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'consumed-apis'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:api-docs/consuming-components': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'consuming-components'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:api-docs/definition': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'definition'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:api-docs/has-apis': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-apis'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:api-docs/provided-apis': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'provided-apis'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:api-docs/providing-components': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'providing-components'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-content:api-docs/apis': OverridableExtensionDefinition<{ + kind: 'entity-content'; + name: 'apis'; + config: { + path: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + group: string | false | undefined; + icon: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + group?: string | false | undefined; + icon?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'catalog.entity-content-group', + { + optional: true; + } + > + | ExtensionDataRef< + string | ReactElement>, + 'catalog.entity-content-icon', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + defaultTitle?: [Error: `Use the 'title' param instead`]; + title: string; + defaultGroup?: [Error: `Use the 'group' param instead`]; + group?: + | ( + | 'overview' + | 'documentation' + | 'development' + | 'deployment' + | 'operation' + | 'observability' + ) + | (string & {}); + icon?: string | ReactElement; + loader: () => Promise; + routeRef?: RouteRef_2; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'entity-content:api-docs/definition': OverridableExtensionDefinition<{ + kind: 'entity-content'; + name: 'definition'; + config: { + path: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + group: string | false | undefined; + icon: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + group?: string | false | undefined; + icon?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'catalog.entity-content-group', + { + optional: true; + } + > + | ExtensionDataRef< + string | ReactElement>, + 'catalog.entity-content-icon', + { + optional: true; + } + >; + inputs: {}; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + defaultTitle?: [Error: `Use the 'title' param instead`]; + title: string; + defaultGroup?: [Error: `Use the 'group' param instead`]; + group?: + | ( + | 'overview' + | 'documentation' + | 'development' + | 'deployment' + | 'operation' + | 'observability' + ) + | (string & {}); + icon?: string | ReactElement; + loader: () => Promise; + routeRef?: RouteRef_2; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'page:api-docs': OverridableExtensionDefinition<{ + config: { + initiallySelectedFilter: 'all' | 'owned' | 'starred' | undefined; + path: string | undefined; + title: string | undefined; + }; + configInput: { + initiallySelectedFilter?: 'all' | 'owned' | 'starred' | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: undefined; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + } +>; + +// @public +export const appDefaultsModule: FrontendModule; + +// @public +export const catalogGraphPluginOverride: OverridableFrontendPlugin< + { + catalogGraph: RouteRef; + }, + { + catalogEntity: ExternalRouteRef< + { + name: string; + kind: string; + namespace: string; + }, + true + >; + }, + { + 'api:catalog-graph': OverridableExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'entity-card:catalog-graph/relations': OverridableExtensionDefinition<{ + config: { + kinds: string[] | undefined; + relations: string[] | undefined; + maxDepth: number | undefined; + unidirectional: boolean | undefined; + mergeRelations: boolean | undefined; + showArrowHeads: boolean | undefined; + direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined; + relationPairs: [string, string][] | undefined; + zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; + title: string | undefined; + height: number | undefined; + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + kinds?: string[] | undefined; + relations?: string[] | undefined; + maxDepth?: number | undefined; + unidirectional?: boolean | undefined; + mergeRelations?: boolean | undefined; + showArrowHeads?: boolean | undefined; + direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined; + relationPairs?: [string, string][] | undefined; + zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; + title?: string | undefined; + height?: number | undefined; + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + kind: 'entity-card'; + name: 'relations'; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'page:catalog-graph': OverridableExtensionDefinition<{ + config: { + selectedKinds: string[] | undefined; + selectedRelations: string[] | undefined; + rootEntityRefs: string[] | undefined; + maxDepth: number | undefined; + unidirectional: boolean | undefined; + mergeRelations: boolean | undefined; + showArrowHeads: boolean | undefined; + direction: 'TB' | 'BT' | 'LR' | 'RL' | undefined; + showFilters: boolean | undefined; + curve: 'curveStepBefore' | 'curveMonotoneX' | undefined; + kinds: string[] | undefined; + relations: string[] | undefined; + relationPairs: [string, string][] | undefined; + zoom: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + path: string | undefined; + title: string | undefined; + }; + configInput: { + selectedKinds?: string[] | undefined; + selectedRelations?: string[] | undefined; + rootEntityRefs?: string[] | undefined; + maxDepth?: number | undefined; + unidirectional?: boolean | undefined; + mergeRelations?: boolean | undefined; + showArrowHeads?: boolean | undefined; + direction?: 'TB' | 'BT' | 'LR' | 'RL' | undefined; + showFilters?: boolean | undefined; + curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; + kinds?: string[] | undefined; + relations?: string[] | undefined; + relationPairs?: [string, string][] | undefined; + zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: undefined; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + } +>; + +// @public +export const catalogPluginOverride: OverridableFrontendPlugin< + { + 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 + >; + unregisterRedirect: ExternalRouteRef; + }, + { + 'api:catalog': OverridableExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:catalog/entity-presentation': OverridableExtensionDefinition<{ + kind: 'api'; + name: 'entity-presentation'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:catalog/starred-entities': OverridableExtensionDefinition<{ + kind: 'api'; + name: 'starred-entities'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'catalog-filter:catalog/kind': OverridableExtensionDefinition<{ + config: { + initialFilter: string; + }; + configInput: { + initialFilter?: string | undefined; + }; + output: ExtensionDataRef; + inputs: {}; + kind: 'catalog-filter'; + name: 'kind'; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/lifecycle': OverridableExtensionDefinition<{ + kind: 'catalog-filter'; + name: 'lifecycle'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/list': OverridableExtensionDefinition<{ + config: { + initialFilter: 'all' | 'owned' | 'starred'; + }; + configInput: { + initialFilter?: 'all' | 'owned' | 'starred' | undefined; + }; + output: ExtensionDataRef; + inputs: {}; + kind: 'catalog-filter'; + name: 'list'; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/mode': OverridableExtensionDefinition<{ + config: { + mode: 'all' | 'owners-only' | undefined; + }; + configInput: { + mode?: 'all' | 'owners-only' | undefined; + }; + output: ExtensionDataRef; + inputs: {}; + kind: 'catalog-filter'; + name: 'mode'; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/namespace': OverridableExtensionDefinition<{ + kind: 'catalog-filter'; + name: 'namespace'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/processing-status': OverridableExtensionDefinition<{ + kind: 'catalog-filter'; + name: 'processing-status'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/tag': OverridableExtensionDefinition<{ + kind: 'catalog-filter'; + name: 'tag'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'catalog-filter:catalog/type': OverridableExtensionDefinition<{ + kind: 'catalog-filter'; + name: 'type'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'entity-card:catalog/about': OverridableExtensionDefinition<{ + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: { + iconLinks: ExtensionInput< + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'entity-card'; + name: 'about'; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/depends-on-components': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'depends-on-components'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/depends-on-resources': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'depends-on-resources'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/has-components': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-components'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/has-resources': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-resources'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/has-subcomponents': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-subcomponents'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/has-subdomains': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-subdomains'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/has-systems': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'has-systems'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/labels': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'labels'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-card:catalog/links': OverridableExtensionDefinition<{ + kind: 'entity-card'; + name: 'links'; + config: { + filter: FilterPredicate | undefined; + type: 'content' | 'info' | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + type?: 'content' | 'info' | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >; + inputs: {}; + params: { + loader: () => Promise; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + type?: EntityCardType; + }; + }>; + 'entity-content:catalog/overview': OverridableExtensionDefinition<{ + config: { + path: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + group: string | false | undefined; + icon: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + group?: string | false | undefined; + icon?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'catalog.entity-content-group', + { + optional: true; + } + > + | ExtensionDataRef< + string | ReactElement>, + 'catalog.entity-content-icon', + { + optional: true; + } + >; + inputs: { + layouts: ExtensionInput< + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + (props: EntityContentLayoutProps) => React.JSX.Element, + 'catalog.entity-content-layout.component', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + cards: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + EntityCardType, + 'catalog.entity-card-type', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'entity-content'; + name: 'overview'; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + defaultTitle?: [Error: `Use the 'title' param instead`]; + title: string; + defaultGroup?: [Error: `Use the 'group' param instead`]; + group?: + | ( + | 'overview' + | 'documentation' + | 'development' + | 'deployment' + | 'operation' + | 'observability' + ) + | (string & {}); + icon?: string | ReactElement; + loader: () => Promise; + routeRef?: RouteRef_2; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'entity-context-menu-item:catalog/copy-entity-url': OverridableExtensionDefinition<{ + kind: 'entity-context-menu-item'; + name: 'copy-entity-url'; + config: { + filter: FilterPredicate | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + >; + inputs: {}; + params: EntityContextMenuItemParams; + }>; + 'entity-context-menu-item:catalog/inspect-entity': OverridableExtensionDefinition<{ + kind: 'entity-context-menu-item'; + name: 'inspect-entity'; + config: { + filter: FilterPredicate | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + >; + inputs: {}; + params: EntityContextMenuItemParams; + }>; + 'entity-context-menu-item:catalog/unregister-entity': OverridableExtensionDefinition<{ + kind: 'entity-context-menu-item'; + name: 'unregister-entity'; + config: { + filter: FilterPredicate | undefined; + }; + configInput: { + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + >; + inputs: {}; + params: EntityContextMenuItemParams; + }>; + 'entity-icon-link:catalog/view-source': OverridableExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'view-source'; + config: { + label: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + }; + configInput: { + label?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + inputs: {}; + params: { + useProps: () => Omit; + filter?: FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'page:catalog': OverridableExtensionDefinition<{ + config: { + pagination: + | boolean + | { + mode: 'offset' | 'cursor'; + limit?: number | undefined; + offset?: number | undefined; + }; + exportSettings: + | { + enabled?: boolean | undefined; + disableBuiltinExporters?: boolean | undefined; + } + | undefined; + path: string | undefined; + title: string | undefined; + }; + configInput: { + pagination?: + | boolean + | { + mode: 'offset' | 'cursor'; + limit?: number | undefined; + offset?: number | undefined; + } + | undefined; + exportSettings?: + | { + enabled?: boolean | undefined; + disableBuiltinExporters?: boolean | undefined; + } + | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + filters: ExtensionInput< + ConfigurableExtensionDataRef, + { + singleton: false; + optional: false; + internal: false; + } + >; + exportConfig: ExtensionInput< + ConfigurableExtensionDataRef< + { + exporters?: CatalogExportSettings['exporters']; + columns?: CatalogExportSettings['columns']; + onSuccess?: CatalogExportSettings['onSuccess']; + onError?: CatalogExportSettings['onError']; + }, + 'catalog.export-customization', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: undefined; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + 'page:catalog/entity': OverridableExtensionDefinition<{ + config: { + groups: + | Record< + string, + { + title: string; + icon?: string | undefined; + aliases?: string[] | undefined; + contentOrder?: 'title' | 'natural' | undefined; + } + >[] + | undefined; + defaultContentOrder: 'title' | 'natural'; + showNavItemIcons: boolean; + path: string | undefined; + title: string | undefined; + }; + configInput: { + groups?: + | Record< + string, + { + title: string; + icon?: string | undefined; + aliases?: string[] | undefined; + contentOrder?: 'title' | 'natural' | undefined; + } + >[] + | undefined; + defaultContentOrder?: 'title' | 'natural' | undefined; + showNavItemIcons?: boolean | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + headers: ExtensionInput< + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + contents: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-title', + {} + > + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-content-group', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string | ReactElement>, + 'catalog.entity-content-icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + contextMenuItems: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: 'entity'; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + 'search-result-list-item:catalog': OverridableExtensionDefinition<{ + kind: 'search-result-list-item'; + name: undefined; + config: { + noTrack: boolean; + }; + configInput: { + noTrack?: boolean | undefined; + }; + output: ExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate; + component: SearchResultItemExtensionComponent; + icon?: JSX_2.Element; + }, + 'search.search-result-list-item.item', + {} + >; + inputs: {}; + params: SearchResultListItemBlueprintParams; + }>; + } +>; + +// @public +const _default: FrontendFeatureLoader; +export default _default; + +// @public +export const docsPluginOverride: OverridableFrontendPlugin< + { + root: RouteRef; + docRoot: RouteRef<{ + name: string; + kind: string; + namespace: string; + }>; + entityContent: RouteRef; + }, + {}, + { + 'api:techdocs': OverridableExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:techdocs/addons': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: { + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'api'; + name: 'addons'; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:techdocs/storage': OverridableExtensionDefinition<{ + kind: 'api'; + name: 'storage'; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'empty-state:techdocs/entity-content': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >; + inputs: { + [x: string]: ExtensionInput< + ExtensionDataRef, + { + singleton: boolean; + optional: boolean; + internal?: boolean; + } + >; + }; + params: never; + kind: 'empty-state'; + name: 'entity-content'; + }>; + 'entity-content:techdocs': OverridableExtensionDefinition<{ + config: { + path: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + group: string | false | undefined; + icon: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + group?: string | false | undefined; + icon?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'catalog.entity-content-group', + { + optional: true; + } + > + | ExtensionDataRef< + string | ReactElement>, + 'catalog.entity-content-icon', + { + optional: true; + } + >; + inputs: { + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + emptyState: ExtensionInput< + ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >, + { + singleton: true; + optional: true; + internal: false; + } + >; + }; + kind: 'entity-content'; + name: undefined; + params: { + defaultPath?: [Error: `Use the 'path' param instead`]; + path: string; + defaultTitle?: [Error: `Use the 'title' param instead`]; + title: string; + defaultGroup?: [Error: `Use the 'group' param instead`]; + group?: + | ( + | 'overview' + | 'documentation' + | 'development' + | 'deployment' + | 'operation' + | 'observability' + ) + | (string & {}); + icon?: string | ReactElement; + loader: () => Promise; + routeRef?: RouteRef_2; + filter?: string | FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'entity-icon-link:techdocs/read-docs': OverridableExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'read-docs'; + config: { + label: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + }; + configInput: { + label?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + inputs: {}; + params: { + useProps: () => Omit; + filter?: FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'page:techdocs': OverridableExtensionDefinition<{ + kind: 'page'; + name: undefined; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + 'page:techdocs/reader': OverridableExtensionDefinition<{ + config: { + withoutSearch: boolean; + withoutHeader: boolean; + path: string | undefined; + title: string | undefined; + }; + configInput: { + withoutSearch?: boolean | undefined; + withoutHeader?: boolean | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + addons: ExtensionInput< + ConfigurableExtensionDataRef< + TechDocsAddonOptions, + 'techdocs.addon', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: 'reader'; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + 'plugin-header-action:techdocs': OverridableExtensionDefinition<{ + kind: 'plugin-header-action'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: { + loader: () => Promise; + }; + }>; + 'search-result-list-item:techdocs': OverridableExtensionDefinition<{ + config: { + title: string | undefined; + lineClamp: number; + asLink: boolean; + asListItem: boolean; + noTrack: boolean; + }; + configInput: { + title?: string | undefined; + lineClamp?: number | undefined; + asLink?: boolean | undefined; + asListItem?: boolean | undefined; + noTrack?: boolean | undefined; + }; + output: ExtensionDataRef< + { + predicate?: SearchResultItemExtensionPredicate; + component: SearchResultItemExtensionComponent; + icon?: JSX_2.Element; + }, + 'search.search-result-list-item.item', + {} + >; + inputs: {}; + kind: 'search-result-list-item'; + name: undefined; + params: SearchResultListItemBlueprintParams; + }>; + } +>; + +// @public +export const scaffolderPluginOverride: OverridableFrontendPlugin< + { + root: RouteRef; + selectedTemplate: SubRouteRef< + PathParams<'/templates/:namespace/:templateName'> + >; + ongoingTask: SubRouteRef>; + actions: SubRouteRef; + listTasks: SubRouteRef; + edit: SubRouteRef; + templatingExtensions: SubRouteRef; + }, + { + registerComponent: ExternalRouteRef; + viewTechDoc: ExternalRouteRef< + { + name: string; + kind: string; + namespace: string; + }, + true + >; + }, + { + 'api:scaffolder': OverridableExtensionDefinition<{ + kind: 'api'; + name: undefined; + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: {}; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:scaffolder/form-decorators': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: { + formDecorators: ExtensionInput< + ConfigurableExtensionDataRef< + ScaffolderFormDecorator, + 'scaffolder.form-decorator-loader', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'api'; + name: 'form-decorators'; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'api:scaffolder/form-fields': OverridableExtensionDefinition<{ + config: {}; + configInput: {}; + output: ExtensionDataRef; + inputs: { + formFields: ExtensionInput< + ConfigurableExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'api'; + name: 'form-fields'; + params: < + TApi, + TImpl extends TApi, + TDeps extends { [name in string]: unknown }, + >( + params: ApiFactory, + ) => ExtensionBlueprintParams; + }>; + 'entity-icon-link:scaffolder/launch-template': OverridableExtensionDefinition<{ + kind: 'entity-icon-link'; + name: 'launch-template'; + config: { + label: string | undefined; + title: string | undefined; + filter: FilterPredicate | undefined; + }; + configInput: { + label?: string | undefined; + title?: string | undefined; + filter?: FilterPredicate | undefined; + }; + output: + | ExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ExtensionDataRef< + () => IconLinkVerticalProps, + 'entity-icon-link-props', + {} + >; + inputs: {}; + params: { + useProps: () => Omit; + filter?: FilterPredicate | ((entity: Entity) => boolean); + }; + }>; + 'page:scaffolder': OverridableExtensionDefinition<{ + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: { + pages: ExtensionInput< + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'core.title', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + formFields: ExtensionInput< + ConfigurableExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >, + { + singleton: false; + optional: false; + internal: false; + } + >; + }; + kind: 'page'; + name: undefined; + params: { + path: string; + title?: string; + icon?: IconElement; + loader?: () => Promise; + routeRef?: RouteRef_2; + noHeader?: boolean; + }; + }>; + 'scaffolder-form-field:scaffolder/entity-name-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-name-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/entity-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/entity-tags-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'entity-tags-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/multi-entity-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'multi-entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/my-groups-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'my-groups-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/owned-entity-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'owned-entity-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/owner-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'owner-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/repo-branch-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'repo-branch-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/repo-owner-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'repo-owner-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'scaffolder-form-field:scaffolder/repo-url-picker': OverridableExtensionDefinition<{ + kind: 'scaffolder-form-field'; + name: 'repo-url-picker'; + config: {}; + configInput: {}; + output: ExtensionDataRef< + () => Promise, + 'scaffolder.form-field-loader', + {} + >; + inputs: {}; + params: { + field: () => Promise; + }; + }>; + 'sub-page:scaffolder/actions': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'actions'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef_2; + }; + }>; + 'sub-page:scaffolder/editor': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'editor'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef_2; + }; + }>; + 'sub-page:scaffolder/tasks': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'tasks'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef_2; + }; + }>; + 'sub-page:scaffolder/templates': OverridableExtensionDefinition<{ + config: { + enableBackstageUi: boolean; + groups: + | { + title: string; + filter: FilterPredicate; + }[] + | undefined; + path: string | undefined; + title: string | undefined; + }; + configInput: { + enableBackstageUi?: boolean | undefined; + groups?: + | { + title: string; + filter: FilterPredicate; + }[] + | undefined; + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + kind: 'sub-page'; + name: 'templates'; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef_2; + }; + }>; + 'sub-page:scaffolder/templating-extensions': OverridableExtensionDefinition<{ + kind: 'sub-page'; + name: 'templating-extensions'; + config: { + path: string | undefined; + title: string | undefined; + }; + configInput: { + path?: string | undefined; + title?: string | undefined; + }; + output: + | ExtensionDataRef + | ExtensionDataRef< + RouteRef_2, + 'core.routing.ref', + { + optional: true; + } + > + | ExtensionDataRef + | ExtensionDataRef + | ExtensionDataRef< + IconElement, + 'core.icon', + { + optional: true; + } + >; + inputs: {}; + params: { + path: string; + title: string; + icon?: IconElement; + loader: () => Promise; + routeRef?: RouteRef_2; + }; + }>; + } +>; + +// (No @packageDocumentation comment for this package) ``` diff --git a/workspaces/app-defaults/plugins/app-defaults/src/api-docs/CustomApiDocsPage.tsx b/workspaces/app-defaults/plugins/app-defaults/src/api-docs/CustomApiDocsPage.tsx new file mode 100644 index 0000000000..3459cfeb40 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/api-docs/CustomApiDocsPage.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { ButtonLink } from '@backstage/ui'; +import { translationRef } from '../translations/ref'; +import { EmptyCatalogGate } from '../catalog/EmptyCatalogGate'; + +/** + * @internal + */ +export function CustomApiDocsPage(props: { children: React.ReactNode }) { + const { t } = useTranslationRef(translationRef); + return ( + + {t('apiDocs.emptyState.action')} + + ), + }} + > + {props.children} + + ); +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/api-docs/apiDocsPluginOverride.tsx b/workspaces/app-defaults/plugins/app-defaults/src/api-docs/apiDocsPluginOverride.tsx new file mode 100644 index 0000000000..123de9e072 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/api-docs/apiDocsPluginOverride.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { coreExtensionData } from '@backstage/frontend-plugin-api'; +import apiDocsPlugin from '@backstage/plugin-api-docs/alpha'; +import { CustomApiDocsPage } from './CustomApiDocsPage'; + +/** + * Override of the Backstage API docs plugin that adds an empty state + * when no API entities are available. + * + * @public + */ +export const apiDocsPluginOverride = apiDocsPlugin.withOverrides({ + extensions: [ + apiDocsPlugin.getExtension('page:api-docs').override({ + factory(originalFactory) { + const original = originalFactory(); + const originalElement = original.get(coreExtensionData.reactElement); + return [ + ...original, + coreExtensionData.reactElement( + {originalElement}, + ), + ]; + }, + }), + ], +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/CustomCatalogGraphPage.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/CustomCatalogGraphPage.tsx new file mode 100644 index 0000000000..a3efc5f24c --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/CustomCatalogGraphPage.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { ButtonLink } from '@backstage/ui'; +import { translationRef } from '../translations/ref'; +import { EmptyCatalogGate } from '../catalog/EmptyCatalogGate'; + +/** + * @internal + */ +export function CustomCatalogGraphPage(props: { children: React.ReactNode }) { + const { t } = useTranslationRef(translationRef); + return ( + + {t('catalogGraph.emptyState.action')} + + ), + }} + > + {props.children} + + ); +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/catalogGraphPluginOverride.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/catalogGraphPluginOverride.tsx new file mode 100644 index 0000000000..080617d76e --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog-graph/catalogGraphPluginOverride.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { coreExtensionData } from '@backstage/frontend-plugin-api'; +import catalogGraphPlugin from '@backstage/plugin-catalog-graph/alpha'; +import { CustomCatalogGraphPage } from './CustomCatalogGraphPage'; + +/** + * Override of the Backstage catalog graph plugin that adds an empty state + * when no catalog entities are available. + * + * @public + */ +export const catalogGraphPluginOverride = catalogGraphPlugin.withOverrides({ + extensions: [ + catalogGraphPlugin.getExtension('page:catalog-graph').override({ + factory(originalFactory) { + const original = originalFactory(); + const originalElement = original.get(coreExtensionData.reactElement); + return [ + ...original, + coreExtensionData.reactElement( + {originalElement}, + ), + ]; + }, + }), + ], +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/CustomCatalogPage.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog/CustomCatalogPage.tsx new file mode 100644 index 0000000000..accd271bf2 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/CustomCatalogPage.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { ButtonLink } from '@backstage/ui'; +import { translationRef } from '../translations/ref'; +import { EmptyCatalogGate } from './EmptyCatalogGate'; + +/** + * @internal + */ +export function CustomCatalogPage(props: { children: React.ReactNode }) { + const { t } = useTranslationRef(translationRef); + return ( + + {t('catalog.emptyState.action')} + + ), + }} + > + {props.children} + + ); +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.test.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.test.tsx new file mode 100644 index 0000000000..0a3f8cf87a --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.test.tsx @@ -0,0 +1,165 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { render, screen, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import { errorApiRef } from '@backstage/core-plugin-api'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { EmptyCatalogGate } from './EmptyCatalogGate'; + +jest.mock('../components/empty-state/EmptyState', () => ({ + EmptyState: (props: any) => ( +
+ {props.title} + {props.description} + {props.action} +
+ ), +})); + +const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; + +function Wrapper({ + mockCatalogApi, + children, +}: { + mockCatalogApi: any; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + +const testEmptyState = { + title: 'Empty title', + description: 'Empty description', + action: , +}; + +describe('EmptyCatalogGate', () => { + it('shows loading spinner initially', () => { + const mockApi = { + getEntityFacets: jest.fn(() => new Promise(() => {})), + }; + + render( + + + Original page content + + , + ); + + expect(screen.getByRole('progressbar')).toBeInTheDocument(); + }); + + it('shows empty state with custom props when no entities exist', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 0 }] }, + }), + }; + + render( + + + Original page content + + , + ); + + await waitFor(() => { + expect(screen.getByTestId('empty-state')).toBeInTheDocument(); + expect(screen.getByText('Empty title')).toBeInTheDocument(); + expect(screen.getByText('Empty description')).toBeInTheDocument(); + expect(screen.getByText('Go somewhere')).toBeInTheDocument(); + }); + expect(screen.queryByText('Original page content')).not.toBeInTheDocument(); + }); + + it('renders children when entities exist', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }), + }; + + render( + + + Original page content + + , + ); + + await waitFor(() => { + expect(screen.getByText('Original page content')).toBeInTheDocument(); + }); + }); + + it('passes filter to the catalog API', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'API', count: 2 }] }, + }), + }; + + render( + + + API docs content + + , + ); + + await waitFor(() => { + expect(screen.getByText('API docs content')).toBeInTheDocument(); + }); + + expect(mockApi.getEntityFacets).toHaveBeenCalledWith({ + facets: ['kind'], + filter: { kind: 'API' }, + }); + }); + + it('shows error panel on API failure', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockRejectedValue(new Error('Network error')), + }; + + render( + + + Original page content + + , + ); + + await waitFor(() => { + expect( + screen.queryByText('Original page content'), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.tsx new file mode 100644 index 0000000000..bfece34815 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/EmptyCatalogGate.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 type { EntityFilterQuery } from '@backstage/catalog-client'; +import { ErrorPanel } from '@backstage/core-components'; +import { Container, Flex } from '@backstage/ui'; +import CircularProgress from '@mui/material/CircularProgress'; +import { useCatalogEntities } from './useCatalogEntities'; +import type { EmptyStateProps } from '../components/empty-state/EmptyState'; +import { EmptyState } from '../components/empty-state/EmptyState'; + +/** + * Renders children when catalog entities matching the filter exist, + * or an empty state when none are found. + * + * @internal + */ +export function EmptyCatalogGate(props: { + filter?: EntityFilterQuery; + emptyState: EmptyStateProps; + children: React.ReactNode; +}) { + const state = useCatalogEntities(props.filter); + + if (state.status === 'loading') { + return ( + + + + ); + } + + if (state.status === 'error') { + return ( + + + + ); + } + + if (!state.hasEntities) { + return ( + + + + ); + } + + return <>{props.children}; +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/catalogPluginOverride.tsx b/workspaces/app-defaults/plugins/app-defaults/src/catalog/catalogPluginOverride.tsx new file mode 100644 index 0000000000..3654026407 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/catalogPluginOverride.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { coreExtensionData } from '@backstage/frontend-plugin-api'; +import catalogPlugin from '@backstage/plugin-catalog/alpha'; +import { CustomCatalogPage } from './CustomCatalogPage'; + +/** + * Override of the Backstage catalog plugin that adds an empty state + * when no catalog entities are available. + * + * @public + */ +export const catalogPluginOverride = catalogPlugin.withOverrides({ + extensions: [ + catalogPlugin.getExtension('page:catalog').override({ + factory(originalFactory) { + const original = originalFactory(); + const originalElement = original.get(coreExtensionData.reactElement); + return [ + ...original, + coreExtensionData.reactElement( + {originalElement}, + ), + ]; + }, + }), + ], +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.test.ts b/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.test.ts new file mode 100644 index 0000000000..d8a56c4d42 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { renderHook, waitFor } from '@testing-library/react'; +import { TestApiProvider } from '@backstage/test-utils'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { useCatalogEntities } from './useCatalogEntities'; +import { createElement } from 'react'; + +function createWrapper(mockCatalogApi: any) { + return ({ children }: { children: React.ReactNode }) => + createElement(TestApiProvider, { + apis: [[catalogApiRef, mockCatalogApi]], + children, + }); +} + +describe('useCatalogEntities', () => { + it('returns loading initially', () => { + const mockApi = { + getEntityFacets: jest.fn(() => new Promise(() => {})), + }; + + const { result } = renderHook(() => useCatalogEntities(), { + wrapper: createWrapper(mockApi), + }); + + expect(result.current).toEqual({ status: 'loading' }); + }); + + it('returns hasEntities true when entities exist', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }), + }; + + const { result } = renderHook(() => useCatalogEntities(), { + wrapper: createWrapper(mockApi), + }); + + await waitFor(() => { + expect(result.current).toEqual({ status: 'success', hasEntities: true }); + }); + }); + + it('returns hasEntities false when catalog is empty', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 0 }] }, + }), + }; + + const { result } = renderHook(() => useCatalogEntities(), { + wrapper: createWrapper(mockApi), + }); + + await waitFor(() => { + expect(result.current).toEqual({ + status: 'success', + hasEntities: false, + }); + }); + }); + + it('passes filter to getEntityFacets', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Template', count: 2 }] }, + }), + }; + + renderHook(() => useCatalogEntities({ kind: 'Template' }), { + wrapper: createWrapper(mockApi), + }); + + await waitFor(() => { + expect(mockApi.getEntityFacets).toHaveBeenCalledWith({ + facets: ['kind'], + filter: { kind: 'Template' }, + }); + }); + }); + + it('passes no filter when filter is undefined', async () => { + const mockApi = { + getEntityFacets: jest.fn().mockResolvedValue({ + facets: { kind: [{ value: 'Component', count: 5 }] }, + }), + }; + + renderHook(() => useCatalogEntities(), { + wrapper: createWrapper(mockApi), + }); + + await waitFor(() => { + expect(mockApi.getEntityFacets).toHaveBeenCalledWith({ + facets: ['kind'], + filter: undefined, + }); + }); + }); + + it('returns error state on API failure', async () => { + const error = new Error('Network error'); + const mockApi = { + getEntityFacets: jest.fn().mockRejectedValue(error), + }; + + const { result } = renderHook(() => useCatalogEntities(), { + wrapper: createWrapper(mockApi), + }); + + await waitFor(() => { + expect(result.current).toEqual({ status: 'error', error }); + }); + }); +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.ts b/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.ts new file mode 100644 index 0000000000..d1d2d4e8d5 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/catalog/useCatalogEntities.ts @@ -0,0 +1,48 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useEffect, useState } from 'react'; +import { useApi } from '@backstage/frontend-plugin-api'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import type { EntityFilterQuery } from '@backstage/catalog-client'; + +type State = + | { status: 'loading' } + | { status: 'success'; hasEntities: boolean } + | { status: 'error'; error: Error }; + +export function useCatalogEntities(filter?: EntityFilterQuery): State { + const catalogApi = useApi(catalogApiRef); + const [state, setState] = useState({ status: 'loading' }); + + useEffect(() => { + catalogApi + .getEntityFacets({ + facets: ['kind'], + filter, + }) + .then(response => { + setState({ + status: 'success', + hasEntities: response.facets.kind.some(f => f.count > 0), + }); + }) + .catch(err => setState({ status: 'error', error: err })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [catalogApi, JSON.stringify(filter)]); + + return state; +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/EmptyState.tsx b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/EmptyState.tsx new file mode 100644 index 0000000000..9885f92966 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/EmptyState.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { Flex, Text } from '@backstage/ui'; +import { SupportButton } from './SupportButton'; + +import notFoundImage from './notfound.png'; + +/** + * @internal + */ +export interface EmptyStateProps { + title: string; + description: string; + action?: React.ReactNode; +} + +/** + * @internal + */ +export const EmptyState = (props: EmptyStateProps) => { + return ( + + + {props.title} + + {props.description} + + + {props.action} + + + + + {null} + + + ); +}; diff --git a/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/SupportButton.tsx b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/SupportButton.tsx new file mode 100644 index 0000000000..e09688ab6b --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/SupportButton.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { configApiRef, useApi } from '@backstage/core-plugin-api'; +import { ButtonLink } from '@backstage/ui'; +import { RiExternalLinkLine } from '@remixicon/react'; + +export const SupportButton = ({ supportUrl }: { supportUrl?: string }) => { + const configApi = useApi(configApiRef); + const finalSupportUrl = + supportUrl ?? + configApi.getOptionalString('app.support.url') ?? + 'https://access.redhat.com/documentation/red_hat_developer_hub'; + + return ( + } + > + Support + + ); +}; diff --git a/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/notfound.png b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/notfound.png new file mode 100644 index 0000000000..c80bd40a47 Binary files /dev/null and b/workspaces/app-defaults/plugins/app-defaults/src/components/empty-state/notfound.png differ diff --git a/workspaces/app-defaults/plugins/app-defaults/src/docs/CustomDocsPage.tsx b/workspaces/app-defaults/plugins/app-defaults/src/docs/CustomDocsPage.tsx new file mode 100644 index 0000000000..55930c3e34 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/docs/CustomDocsPage.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { TECHDOCS_ANNOTATION } from '@backstage/plugin-techdocs-common'; +import { ButtonLink } from '@backstage/ui'; +import { translationRef } from '../translations/ref'; +import { EmptyCatalogGate } from '../catalog/EmptyCatalogGate'; + +/** + * @internal + */ +export function CustomDocsPage(props: { children: React.ReactNode }) { + const { t } = useTranslationRef(translationRef); + return ( + + {t('docs.emptyState.action')} + + ), + }} + > + {props.children} + + ); +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/docs/docsPluginOverride.tsx b/workspaces/app-defaults/plugins/app-defaults/src/docs/docsPluginOverride.tsx new file mode 100644 index 0000000000..7c1bfd9325 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/docs/docsPluginOverride.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { coreExtensionData } from '@backstage/frontend-plugin-api'; +import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; +import { CustomDocsPage } from './CustomDocsPage'; + +/** + * Override of the Backstage TechDocs plugin that adds an empty state + * when no documented entities are available. + * + * @public + */ +export const docsPluginOverride = techdocsPlugin.withOverrides({ + extensions: [ + techdocsPlugin.getExtension('page:techdocs').override({ + factory(originalFactory) { + const original = originalFactory(); + const originalElement = original.get(coreExtensionData.reactElement); + return [ + ...original, + coreExtensionData.reactElement( + {originalElement}, + ), + ]; + }, + }), + ], +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/index.ts b/workspaces/app-defaults/plugins/app-defaults/src/index.ts index 1ec5454f7e..3aaecc4299 100644 --- a/workspaces/app-defaults/plugins/app-defaults/src/index.ts +++ b/workspaces/app-defaults/plugins/app-defaults/src/index.ts @@ -20,6 +20,43 @@ * @packageDocumentation */ -export { appDefaultsModule } from './appDefaultsModule'; +import { createFrontendFeatureLoader } from '@backstage/frontend-plugin-api'; -export { appDefaultsModule as default } from './appDefaultsModule'; +import { appDefaultsModule } from './appDefaultsModule'; +import { apiDocsPluginOverride } from './api-docs/apiDocsPluginOverride'; +import { catalogPluginOverride } from './catalog/catalogPluginOverride'; +import { catalogGraphPluginOverride } from './catalog-graph/catalogGraphPluginOverride'; +import { docsPluginOverride } from './docs/docsPluginOverride'; +import { scaffolderPluginOverride } from './scaffolder/scaffolderPluginOverride'; + +export { appDefaultsModule }; + +export { apiDocsPluginOverride }; + +export { catalogGraphPluginOverride }; + +export { catalogPluginOverride }; + +export { docsPluginOverride }; + +export { scaffolderPluginOverride }; + +/** + * Feature loader that registers the RHDH app defaults module and all + * plugin overrides (catalog, catalog graph, scaffolder, API docs, TechDocs) + * that add empty-state pages when no entities are available. + * + * @public + */ +export default createFrontendFeatureLoader({ + loader() { + return [ + appDefaultsModule, + catalogPluginOverride, + catalogGraphPluginOverride, + scaffolderPluginOverride, + apiDocsPluginOverride, + docsPluginOverride, + ]; + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/CustomScaffolderPage.tsx b/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/CustomScaffolderPage.tsx new file mode 100644 index 0000000000..18d5793547 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/CustomScaffolderPage.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { ButtonLink } from '@backstage/ui'; +import { translationRef } from '../translations/ref'; +import { EmptyCatalogGate } from '../catalog/EmptyCatalogGate'; + +/** + * @internal + */ +export function CustomScaffolderPage(props: { children: React.ReactNode }) { + const { t } = useTranslationRef(translationRef); + return ( + + {t('scaffolder.emptyState.action')} + + ), + }} + > + {props.children} + + ); +} diff --git a/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/scaffolderPluginOverride.tsx b/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/scaffolderPluginOverride.tsx new file mode 100644 index 0000000000..a007136505 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/scaffolder/scaffolderPluginOverride.tsx @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { coreExtensionData } from '@backstage/frontend-plugin-api'; +import scaffolderPlugin from '@backstage/plugin-scaffolder/alpha'; +import { CustomScaffolderPage } from './CustomScaffolderPage'; + +/** + * Override of the Backstage scaffolder plugin that adds an empty state + * when no templates are available. + * + * @public + */ +export const scaffolderPluginOverride = scaffolderPlugin.withOverrides({ + extensions: [ + scaffolderPlugin.getExtension('sub-page:scaffolder/templates').override({ + factory(originalFactory) { + const original = originalFactory(); + const originalElement = original.get(coreExtensionData.reactElement); + return [ + ...original, + coreExtensionData.reactElement( + {originalElement}, + ), + ]; + }, + }), + // add the other extensions to keep the origin order (/templates sub-page first) + scaffolderPlugin.getExtension('sub-page:scaffolder/actions'), + scaffolderPlugin.getExtension('sub-page:scaffolder/editor'), + scaffolderPlugin.getExtension('sub-page:scaffolder/tasks'), + scaffolderPlugin.getExtension('sub-page:scaffolder/templating-extensions'), + ], +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/de.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/de.ts new file mode 100644 index 0000000000..f3c4d91bdf --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/de.ts @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { translationRef } from './ref'; + +/** + * @internal + */ +export const de = createTranslationMessages({ + ref: translationRef, + messages: { + 'catalog.emptyState.title': 'Keine Katalogeinträge gefunden', + 'catalog.emptyState.description': + 'Einträge werden hier angezeigt, sobald sie im Katalog registriert sind.', + 'catalog.emptyState.action': 'Komponente registrieren', + 'catalogGraph.emptyState.title': 'Keine Katalogeinträge gefunden', + 'catalogGraph.emptyState.description': + 'Der Kataloggraph wird hier angezeigt, sobald Entitäten im Katalog registriert sind.', + 'catalogGraph.emptyState.action': 'Zum Katalog', + 'scaffolder.emptyState.title': 'Keine Vorlagen verfügbar', + 'scaffolder.emptyState.description': + 'Software-Vorlagen werden hier angezeigt, sobald sie im Katalog registriert sind.', + 'scaffolder.emptyState.action': 'Vorlage registrieren', + 'apiDocs.emptyState.title': 'Keine APIs verfügbar', + 'apiDocs.emptyState.description': + 'API-Definitionen werden hier angezeigt, sobald sie im Katalog registriert sind.', + 'apiDocs.emptyState.action': 'API registrieren', + 'docs.emptyState.title': 'Keine Dokumentation verfügbar', + 'docs.emptyState.description': + 'Dokumentation wird hier angezeigt, sobald Entitäten mit TechDocs-Annotationen registriert sind.', + 'docs.emptyState.action': 'Mehr erfahren', + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/es.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/es.ts new file mode 100644 index 0000000000..cadae744a4 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/es.ts @@ -0,0 +1,48 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { translationRef } from './ref'; + +/** + * @internal + */ +export const es = createTranslationMessages({ + ref: translationRef, + messages: { + 'catalog.emptyState.title': 'No se encontraron elementos en el catálogo', + 'catalog.emptyState.description': + 'Los elementos aparecerán aquí una vez que se registren en el catálogo.', + 'catalog.emptyState.action': 'Registrar un componente', + 'catalogGraph.emptyState.title': + 'No se encontraron elementos en el catálogo', + 'catalogGraph.emptyState.description': + 'El grafo del catálogo aparecerá aquí una vez que se registren entidades en el catálogo.', + 'catalogGraph.emptyState.action': 'Ir al catálogo', + 'scaffolder.emptyState.title': 'No hay plantillas disponibles', + 'scaffolder.emptyState.description': + 'Las plantillas de software aparecerán aquí una vez que se registren en el catálogo.', + 'scaffolder.emptyState.action': 'Registrar una plantilla', + 'apiDocs.emptyState.title': 'No hay APIs disponibles', + 'apiDocs.emptyState.description': + 'Las definiciones de API aparecerán aquí una vez que se registren en el catálogo.', + 'apiDocs.emptyState.action': 'Registrar una API', + 'docs.emptyState.title': 'No hay documentación disponible', + 'docs.emptyState.description': + 'La documentación aparecerá aquí una vez que se registren entidades con anotaciones de TechDocs.', + 'docs.emptyState.action': 'Más información', + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/fr.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/fr.ts new file mode 100644 index 0000000000..751cdfef33 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/fr.ts @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { translationRef } from './ref'; + +/** + * @internal + */ +export const fr = createTranslationMessages({ + ref: translationRef, + messages: { + 'catalog.emptyState.title': 'Aucun élément trouvé dans le catalogue', + 'catalog.emptyState.description': + 'Les éléments apparaîtront ici une fois enregistrés dans le catalogue.', + 'catalog.emptyState.action': 'Enregistrer un composant', + 'catalogGraph.emptyState.title': 'Aucun élément trouvé dans le catalogue', + 'catalogGraph.emptyState.description': + 'Le graphe du catalogue apparaîtra ici une fois que des entités seront enregistrées dans le catalogue.', + 'catalogGraph.emptyState.action': 'Aller au catalogue', + 'scaffolder.emptyState.title': 'Aucun modèle disponible', + 'scaffolder.emptyState.description': + 'Les modèles de logiciels apparaîtront ici une fois enregistrés dans le catalogue.', + 'scaffolder.emptyState.action': 'Enregistrer un modèle', + 'apiDocs.emptyState.title': 'Aucune API disponible', + 'apiDocs.emptyState.description': + "Les définitions d'API apparaîtront ici une fois enregistrées dans le catalogue.", + 'apiDocs.emptyState.action': 'Enregistrer une API', + 'docs.emptyState.title': 'Aucune documentation disponible', + 'docs.emptyState.description': + 'La documentation apparaîtra ici une fois que des entités avec des annotations TechDocs seront enregistrées.', + 'docs.emptyState.action': 'En savoir plus', + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/it.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/it.ts new file mode 100644 index 0000000000..371955fc52 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/it.ts @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { translationRef } from './ref'; + +/** + * @internal + */ +export const it = createTranslationMessages({ + ref: translationRef, + messages: { + 'catalog.emptyState.title': 'Nessun elemento trovato nel catalogo', + 'catalog.emptyState.description': + 'Gli elementi appariranno qui una volta registrati nel catalogo.', + 'catalog.emptyState.action': 'Registra un componente', + 'catalogGraph.emptyState.title': 'Nessun elemento trovato nel catalogo', + 'catalogGraph.emptyState.description': + 'Il grafo del catalogo apparirà qui una volta che le entità saranno registrate nel catalogo.', + 'catalogGraph.emptyState.action': 'Vai al catalogo', + 'scaffolder.emptyState.title': 'Nessun modello disponibile', + 'scaffolder.emptyState.description': + 'I modelli software appariranno qui una volta registrati nel catalogo.', + 'scaffolder.emptyState.action': 'Registra un modello', + 'apiDocs.emptyState.title': 'Nessuna API disponibile', + 'apiDocs.emptyState.description': + 'Le definizioni API appariranno qui una volta registrate nel catalogo.', + 'apiDocs.emptyState.action': "Registra un'API", + 'docs.emptyState.title': 'Nessuna documentazione disponibile', + 'docs.emptyState.description': + 'La documentazione apparirà qui una volta che le entità con annotazioni TechDocs saranno registrate.', + 'docs.emptyState.action': 'Scopri di più', + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/ja.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/ja.ts new file mode 100644 index 0000000000..d916c629f5 --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/ja.ts @@ -0,0 +1,47 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationMessages } from '@backstage/frontend-plugin-api'; +import { translationRef } from './ref'; + +/** + * @internal + */ +export const ja = createTranslationMessages({ + ref: translationRef, + messages: { + 'catalog.emptyState.title': 'カタログアイテムが見つかりません', + 'catalog.emptyState.description': + 'カタログに登録されると、ここにアイテムが表示されます。', + 'catalog.emptyState.action': 'コンポーネントを登録', + 'catalogGraph.emptyState.title': 'カタログアイテムが見つかりません', + 'catalogGraph.emptyState.description': + 'カタログにエンティティが登録されると、ここにカタロググラフが表示されます。', + 'catalogGraph.emptyState.action': 'カタログへ移動', + 'scaffolder.emptyState.title': 'テンプレートがありません', + 'scaffolder.emptyState.description': + 'カタログに登録されると、ここにソフトウェアテンプレートが表示されます。', + 'scaffolder.emptyState.action': 'テンプレートを登録', + 'apiDocs.emptyState.title': 'API がありません', + 'apiDocs.emptyState.description': + 'カタログに登録されると、ここに API 定義が表示されます。', + 'apiDocs.emptyState.action': 'API を登録', + 'docs.emptyState.title': 'ドキュメントがありません', + 'docs.emptyState.description': + 'TechDocs アノテーションを持つエンティティが登録されると、ここにドキュメントが表示されます。', + 'docs.emptyState.action': '詳細を見る', + }, +}); diff --git a/workspaces/app-defaults/plugins/app-defaults/src/translations/ref.ts b/workspaces/app-defaults/plugins/app-defaults/src/translations/ref.ts new file mode 100644 index 0000000000..7aaab4b4ec --- /dev/null +++ b/workspaces/app-defaults/plugins/app-defaults/src/translations/ref.ts @@ -0,0 +1,66 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 { createTranslationRef } from '@backstage/frontend-plugin-api'; + +/** + * @internal + */ +export const translationRef = createTranslationRef({ + id: 'plugin.app-defaults', + messages: { + catalog: { + emptyState: { + title: 'No catalog items found', + description: + 'Items will appear here once they are registered in the catalog.', + action: 'Register a component', + }, + }, + catalogGraph: { + emptyState: { + title: 'No catalog items found', + description: + 'The catalog graph will appear here once entities are registered in the catalog.', + action: 'Go to catalog', + }, + }, + scaffolder: { + emptyState: { + title: 'No templates available', + description: + 'Software templates will appear here once they are registered in the catalog.', + action: 'Register a template', + }, + }, + apiDocs: { + emptyState: { + title: 'No APIs available', + description: + 'API definitions will appear here once they are registered in the catalog.', + action: 'Register an API', + }, + }, + docs: { + emptyState: { + title: 'No documentation available', + description: + 'Documentation will appear here once entities with TechDocs annotations are registered.', + action: 'Learn more', + }, + }, + }, +}); diff --git a/workspaces/app-defaults/yarn.lock b/workspaces/app-defaults/yarn.lock index 75cee91ba6..0168da2578 100644 --- a/workspaces/app-defaults/yarn.lock +++ b/workspaces/app-defaults/yarn.lock @@ -9481,14 +9481,27 @@ __metadata: languageName: unknown linkType: soft -"@red-hat-developer-hub/backstage-plugin-app-defaults@workspace:plugins/app-defaults": +"@red-hat-developer-hub/backstage-plugin-app-defaults@workspace:^, @red-hat-developer-hub/backstage-plugin-app-defaults@workspace:plugins/app-defaults": version: 0.0.0-use.local resolution: "@red-hat-developer-hub/backstage-plugin-app-defaults@workspace:plugins/app-defaults" dependencies: + "@backstage/catalog-client": "npm:^1.16.1" "@backstage/cli": "npm:^0.36.3" + "@backstage/core-components": "npm:^0.18.11" + "@backstage/core-plugin-api": "npm:^1.12.7" "@backstage/frontend-plugin-api": "npm:^0.17.2" + "@backstage/plugin-api-docs": "npm:^0.14.2" + "@backstage/plugin-catalog": "npm:^2.0.6" + "@backstage/plugin-catalog-graph": "npm:^0.6.5" + "@backstage/plugin-catalog-react": "npm:^3.1.0" + "@backstage/plugin-scaffolder": "npm:^1.38.0" + "@backstage/plugin-techdocs": "npm:^1.17.7" + "@backstage/plugin-techdocs-common": "npm:^0.1.1" "@backstage/test-utils": "npm:^1.7.19" + "@backstage/ui": "npm:^0.16.0" + "@mui/material": "npm:5.18.0" "@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^" + "@remixicon/react": "npm:>=4.6.0 <4.9.0" "@testing-library/jest-dom": "npm:^6.0.0" "@testing-library/react": "npm:^16.0.0" "@types/react": "npm:^18" @@ -14283,6 +14296,7 @@ __metadata: "@mui/material": "npm:^5.18.0" "@playwright/test": "npm:1.62.1" "@red-hat-developer-hub/backstage-plugin-app-auth": "workspace:^" + "@red-hat-developer-hub/backstage-plugin-app-defaults": "workspace:^" "@red-hat-developer-hub/backstage-plugin-app-integrations": "workspace:^" "@red-hat-developer-hub/backstage-plugin-app-react": "workspace:^" "@red-hat-developer-hub/backstage-plugin-global-header": "npm:^1.21.0"