diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 35c61f2f16..aa3cafc2c9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -138,6 +138,11 @@ const app = createApp({ const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); +catalogPlugin.reconfigure({ + // TODO: remove it, only for testing here + createButtonTitle: 'Maybe Create Component', +}); + const routes = ( diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 1b518ad0a4..467145acfc 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -34,8 +34,6 @@ import { createSubRouteRef, createRoutableExtension, analyticsApiRef, - createMetadataRef, - useMetadata, } from '@backstage/core-plugin-api'; import { AppManager } from './AppManager'; import { AppComponents, AppIcons } from './types'; @@ -167,179 +165,6 @@ describe('Integration Test', () => { const icons = {} as AppIcons; - it('should be possible to define customizable values via metadata', async () => { - type CustomPluginMetadataProps = { - pluginLabel: string; - }; - - const customPluginMetadataRef = - createMetadataRef({ - id: 'custom.plugin.provider', - }); - - const customPluginMetadata = { - [customPluginMetadataRef.id]: { - pluginLabel: 'Default Label', - }, - }; - - const extRouteCustomRef = createExternalRouteRef({ - id: 'extRouteCustomRef', - params: ['x'], - }); - - const customPlugin = createPlugin({ - id: 'custom-plugin', - externalRoutes: { - extRouteCustomRef, - }, - metadata: [customPluginMetadata], - }); - - const customPluginRef = createRouteRef({ id: 'custom-ref', params: ['x'] }); - - const CustomPlugin = () => { - const { pluginLabel } = useMetadata( - customPluginMetadataRef, - ); - - return ( - <> -
{pluginLabel}
- - ); - }; - - const CustomComponent = customPlugin.provide( - createRoutableExtension({ - name: 'CustomComponent', - component: () => Promise.resolve(() => ), - mountPoint: customPluginRef, - }), - ); - - const app = new AppManager({ - apis: [], - defaultApis: [], - themes: [], - icons, - plugins: [customPlugin], - components, - configLoader: async () => [], - bindRoutes: ({ bind }) => { - bind(customPlugin.externalRoutes, { - extRouteCustomRef: customPluginRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - - await renderWithEffects( - - - - } /> - - - , - ); - - expect(await screen.getByText('Default Label')).toBeInTheDocument(); - }); - - it('should be possible to reconfigure customizable values via metadata', async () => { - type CustomPluginMetadataProps = { - pluginLabel: string; - }; - - const customPluginMetadataRef = - createMetadataRef({ - id: 'custom.plugin.provider', - }); - - const customPluginMetadata = { - [customPluginMetadataRef.id]: { - pluginLabel: 'Default Label', - }, - }; - - const extRouteCustomRef = createExternalRouteRef({ - id: 'extRouteCustomRef', - params: ['x'], - }); - - const customPlugin = createPlugin({ - id: 'custom-plugin', - externalRoutes: { - extRouteCustomRef, - }, - metadata: [customPluginMetadata], - }); - - customPlugin.reconfigure({ - [customPluginMetadataRef.id]: { - pluginLabel: 'Custom Label', - }, - }); - - const customPluginRef = createRouteRef({ - id: 'custom-ref-2', - params: ['x'], - }); - - const RedefinedCustom = () => { - const { pluginLabel } = useMetadata( - customPluginMetadataRef, - ); - - return ( - <> -
{pluginLabel}
- - ); - }; - - const RedefinedCustomPlugin = customPlugin.provide( - createRoutableExtension({ - name: 'RedefinedCustomPlugin', - component: () => Promise.resolve(() => ), - mountPoint: customPluginRef, - }), - ); - - const app = new AppManager({ - apis: [], - defaultApis: [], - themes: [], - icons, - plugins: [customPlugin], - components, - configLoader: async () => [], - bindRoutes: ({ bind }) => { - bind(customPlugin.externalRoutes, { - extRouteCustomRef: customPluginRef, - }); - }, - }); - - const Provider = app.getProvider(); - const Router = app.getRouter(); - - await renderWithEffects( - - - - } /> - - - , - ); - - expect(screen.getByText('Custom Label')).toBeInTheDocument(); - }); - it('runs happy paths', async () => { const app = new AppManager({ apis: [noOpAnalyticsApi], diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 20b8161a8b..75bab5c2df 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -47,7 +47,6 @@ import { IdentityApi, identityApiRef, BackstagePlugin, - MetadataHolder, } from '@backstage/core-plugin-api'; import { ApiFactoryRegistry, ApiResolver } from '../apis/system'; import { @@ -81,7 +80,6 @@ import { defaultConfigLoader } from './defaultConfigLoader'; import { ApiRegistry } from '../apis/system/ApiRegistry'; import { resolveRouteBindings } from './resolveRouteBindings'; import { BackstageRouteObject } from '../routing/types'; -import { MetadataProvider, MetadataRegistry } from '../metadata'; type CompatiblePlugin = | BackstagePlugin @@ -175,7 +173,6 @@ export class AppManager implements BackstageApp { private readonly appIdentityProxy = new AppIdentityProxy(); private readonly apiFactoryRegistry: ApiFactoryRegistry; - private readonly metadataRegistry: MetadataRegistry; constructor(options: AppOptions) { this.apis = options.apis ?? []; @@ -187,7 +184,6 @@ export class AppManager implements BackstageApp { this.defaultApis = options.defaultApis ?? []; this.bindRoutes = options.bindRoutes; this.apiFactoryRegistry = new ApiFactoryRegistry(); - this.metadataRegistry = new MetadataRegistry(); } getPlugins(): BackstagePlugin[] { @@ -302,25 +298,23 @@ export class AppManager implements BackstageApp { return ( - - - - + + + - - {children} - - - - - + {children} + + + + ); }; @@ -401,22 +395,6 @@ export class AppManager implements BackstageApp { return AppRouter; } - private getMetadataHolder(): MetadataHolder { - for (const plugin of this.plugins) { - if (plugin.getMetadata) { - for (const entry of plugin.getMetadata()) { - for (const entryKey in entry) { - if (entry.hasOwnProperty(entryKey)) { - this.metadataRegistry.register(entryKey, entry[entryKey]); - } - } - } - } - } - - return this.metadataRegistry; - } - private getApiHolder(): ApiHolder { if (this.apiHolder) { // Register additional plugins if they have been added. diff --git a/packages/core-app-api/src/index.ts b/packages/core-app-api/src/index.ts index d7b24dbf28..a5e6b649e9 100644 --- a/packages/core-app-api/src/index.ts +++ b/packages/core-app-api/src/index.ts @@ -21,6 +21,5 @@ */ export * from './apis'; -export * from './metadata'; export * from './app'; export * from './routing'; diff --git a/packages/core-app-api/src/metadata/system/MetadataAggregator.test.ts b/packages/core-app-api/src/metadata/system/MetadataAggregator.test.ts deleted file mode 100644 index ba6aa05c6f..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataAggregator.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createMetadataRef } from '@backstage/core-plugin-api'; -import { MetadataAggregator } from './MetadataAggregator'; -import { MetadataRegistry } from './MetadataRegistry'; - -describe('MetadataAggregator', () => { - const apiARef = createMetadataRef({ id: '1' }); - const apiBRef = createMetadataRef({ id: '2' }); - - it('should forward implementations', () => { - const holder1 = new MetadataRegistry(); - holder1.register(apiARef.id, { label: 'label 1' }); - const holder2 = new MetadataRegistry(); - holder2.register(apiBRef.id, { label: 'label 2' }); - - const agg = new MetadataAggregator(holder1, holder2); - expect(agg.get(apiARef)).toEqual({ label: 'label 1' }); - expect(agg.get(apiBRef)).toEqual({ label: 'label 2' }); - }); - - it('should return the first implementation', () => { - const holder1 = new MetadataRegistry(); - holder1.register(apiARef.id, { label: 'label 1' }); - const holder2 = new MetadataRegistry(); - holder2.register(apiARef.id, { label: 'label 2' }); - - const agg = new MetadataAggregator(holder1, holder2); - expect(agg.get(apiARef)).toEqual({ label: 'label 1' }); - }); -}); diff --git a/packages/core-app-api/src/metadata/system/MetadataAggregator.ts b/packages/core-app-api/src/metadata/system/MetadataAggregator.ts deleted file mode 100644 index ebc26961c1..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataAggregator.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MetadataRef, MetadataHolder } from '@backstage/core-plugin-api'; - -/** - * An MetadataHolder that queries multiple other holders from for - * an metadata implementation, returning the first one encountered.. - */ -export class MetadataAggregator implements MetadataHolder { - private readonly holders: MetadataHolder[]; - - constructor(...holders: MetadataHolder[]) { - this.holders = holders; - } - - get(ref: MetadataRef): T | undefined { - for (const holder of this.holders) { - const metadata = holder.get(ref); - if (metadata) { - return metadata; - } - } - return undefined; - } -} diff --git a/packages/core-app-api/src/metadata/system/MetadataProvider.test.tsx b/packages/core-app-api/src/metadata/system/MetadataProvider.test.tsx deleted file mode 100644 index dfbd96eb31..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataProvider.test.tsx +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { - useMetadata, - createMetadataRef, - MetadataRef, - MetadataHolder, -} from '@backstage/core-plugin-api'; -import { MetadataProvider } from './MetadataProvider'; -import { MetadataRegistry } from './MetadataRegistry'; -import { render } from '@testing-library/react'; -import { withLogCollector } from '@backstage/test-utils'; -import { useVersionedContext } from '@backstage/version-bridge'; - -describe('MetadataProvider', () => { - type Metadata = () => string; - const metadataRef = createMetadataRef({ id: 'x' }); - - const MyHookConsumer = () => { - const payload = useMetadata(metadataRef); - return

hook message: {payload}

; - }; - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - }); - - it('should provide nested access to apis', () => { - const aRef = createMetadataRef({ id: 'a' }); - const bRef = createMetadataRef({ id: 'b' }); - - const MyComponent = () => { - const a = useMetadata(aRef); - const b = useMetadata(bRef); - return ( -
- a={a} b={b} -
- ); - }; - - const renderedHook = render( - - - - - , - ); - renderedHook.getByText('a=z b=y'); - }); - - it('should error if metadata is not available', () => { - expect( - withLogCollector(['error'], () => { - expect(() => { - render( - - - , - ); - }).toThrow('No implementation available for metadataRef{x}'); - }).error, - ).toEqual([ - expect.stringMatching( - /^Error: Uncaught \[Error: No implementation available for metadataRef{x}\]/, - ), - expect.stringMatching( - /^The above error occurred in the component/, - ), - ]); - }); -}); - -describe('v1 consumer', () => { - function useMockApiV1(apiRef: MetadataRef): T { - const impl = useVersionedContext<{ 1: MetadataHolder }>('metadata-context') - ?.atVersion(1) - ?.get(apiRef); - if (!impl) { - throw new Error('no impl'); - } - return impl; - } - - type Api = () => string; - const apiRef = createMetadataRef({ id: 'x' }); - const registry = MetadataRegistry.from([[apiRef, () => 'hello']]); - - const MyHookConsumerV1 = () => { - const api = useMockApiV1(apiRef); - return

hook message: {api()}

; - }; - - it('should provide apis', () => { - const renderedHook = render( - - - , - ); - renderedHook.getByText('hook message: hello'); - }); -}); diff --git a/packages/core-app-api/src/metadata/system/MetadataProvider.tsx b/packages/core-app-api/src/metadata/system/MetadataProvider.tsx deleted file mode 100644 index a072f0a84a..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataProvider.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useContext, ReactNode, PropsWithChildren } from 'react'; -import PropTypes from 'prop-types'; -import { MetadataHolder } from '@backstage/core-plugin-api'; -import { - createVersionedValueMap, - createVersionedContext, -} from '@backstage/version-bridge'; -import { MetadataAggregator } from './MetadataAggregator'; - -/** - * Prop types for the MetadataProvider component. - * - * @public - */ -export type MetadataProviderProps = { - metadata: MetadataHolder; - children: ReactNode; -}; - -const MetadataContext = createVersionedContext<{ 1: MetadataHolder }>( - 'metadata-context', -); - -/** - * Provides an {@link @backstage/core-plugin-api#MetadataHolder} for consumption in - * the React tree. - * - * @public - */ -export const MetadataProvider = ( - props: PropsWithChildren, -) => { - const { children, metadata } = props; - const parentHolder = useContext(MetadataContext)?.atVersion(1); - const holder = parentHolder - ? new MetadataAggregator(metadata, parentHolder) - : metadata; - - return ( - - ); -}; - -MetadataProvider.propTypes = { - metadata: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired, - children: PropTypes.node, -}; diff --git a/packages/core-app-api/src/metadata/system/MetadataRegistry.test.ts b/packages/core-app-api/src/metadata/system/MetadataRegistry.test.ts deleted file mode 100644 index 4acac8565a..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataRegistry.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { createMetadataRef } from '@backstage/core-plugin-api'; -import { MetadataRegistry } from './MetadataRegistry'; - -describe('MetadataRegistry', () => { - const x1Ref = createMetadataRef({ id: 'x1' }); - const x1DuplicateRef = createMetadataRef({ id: 'x1' }); - const x2Ref = createMetadataRef({ id: 'x2' }); - - it('should be created', () => { - const registry = MetadataRegistry.from([]); - expect(registry.get(x1Ref)).toBe(undefined); - }); - - it('should be created with metadata', () => { - const registry = MetadataRegistry.from([ - [x1Ref, 3], - [x2Ref, 'y'], - ]); - expect(registry.get(x1Ref)).toBe(3); - expect(registry.get(x1DuplicateRef)).toBe(3); - expect(registry.get(x2Ref)).toBe('y'); - }); -}); diff --git a/packages/core-app-api/src/metadata/system/MetadataRegistry.ts b/packages/core-app-api/src/metadata/system/MetadataRegistry.ts deleted file mode 100644 index 053a52b5f6..0000000000 --- a/packages/core-app-api/src/metadata/system/MetadataRegistry.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MetadataRef, MetadataHolder } from '@backstage/core-plugin-api'; - -type MetadataImpl = readonly [MetadataRef, T]; - -export class MetadataRegistry implements MetadataHolder { - private readonly registry = new Map(); - - register(id: string, payload: any): boolean { - this.registry.set(id, payload); - return true; - } - - get(ref: MetadataRef): T | undefined { - return this.registry.get(ref.id); - } - - static from(entries: MetadataImpl[]) { - const registry = new MetadataRegistry(); - for (const entry of entries) { - registry.register(entry[0].id, entry[1]); - } - return registry; - } -} diff --git a/packages/core-app-api/src/metadata/system/index.ts b/packages/core-app-api/src/metadata/system/index.ts deleted file mode 100644 index faa7c814c8..0000000000 --- a/packages/core-app-api/src/metadata/system/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { MetadataRegistry } from './MetadataRegistry'; -export { MetadataProvider } from './MetadataProvider'; -export { MetadataAggregator } from './MetadataAggregator'; diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 4377761d89..124dbc8966 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -37,6 +37,7 @@ "@backstage/types": "^1.0.0", "@backstage/version-bridge": "^1.0.1", "history": "^5.0.0", + "lodash": "^4.17.21", "prop-types": "^15.7.2", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" diff --git a/packages/core-plugin-api/src/extensions/extensions.tsx b/packages/core-plugin-api/src/extensions/extensions.tsx index 19da082ee1..c648214c22 100644 --- a/packages/core-plugin-api/src/extensions/extensions.tsx +++ b/packages/core-plugin-api/src/extensions/extensions.tsx @@ -21,6 +21,7 @@ import { RouteRef, useRouteRef } from '../routing'; import { attachComponentData } from './componentData'; import { Extension, BackstagePlugin } from '../plugin/types'; import { PluginErrorBoundary } from './PluginErrorBoundary'; +import { PluginOptionsProvider } from '../plugin-options'; /** * Lazy or synchronous retrieving of extension components. @@ -235,6 +236,15 @@ export function createReactExtension< | { id?: string } | undefined; + const renderComponent = () => + plugin.getPluginOptions() ? ( + + + + ) : ( + + ); + return ( }> @@ -245,7 +255,7 @@ export function createReactExtension< ...(mountPoint && { routeRef: mountPoint.id }), }} > - + {renderComponent()} diff --git a/packages/core-plugin-api/src/index.ts b/packages/core-plugin-api/src/index.ts index 172f00295b..e8edd38ff3 100644 --- a/packages/core-plugin-api/src/index.ts +++ b/packages/core-plugin-api/src/index.ts @@ -22,7 +22,7 @@ export * from './analytics'; export * from './apis'; -export * from './metadata'; +export * from './plugin-options'; export * from './app'; export * from './extensions'; export * from './icons'; diff --git a/packages/core-plugin-api/src/metadata/MetadataRef.ts b/packages/core-plugin-api/src/metadata/MetadataRef.ts deleted file mode 100644 index f261fb5352..0000000000 --- a/packages/core-plugin-api/src/metadata/MetadataRef.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { MetadataRef } from './types'; - -export type MetadataRefConfig = { - id: string; -}; - -class MetadataRefImpl implements MetadataRef { - constructor(private readonly config: MetadataRefConfig) {} - - get id(): string { - return this.config.id; - } - - get T(): T { - throw new Error(`tried to read MetadataRef.T of ${this}`); - } - - toString() { - return `metadataRef{${this.config.id}}`; - } -} - -/** - * Creates a reference to a metadata. - * - * @param config - The descriptor of the metadata to reference. - * @returns A metadata reference. - * @public - */ -export function createMetadataRef( - config: MetadataRefConfig, -): MetadataRef { - return new MetadataRefImpl(config); -} diff --git a/packages/core-plugin-api/src/metadata/index.ts b/packages/core-plugin-api/src/metadata/index.ts deleted file mode 100644 index 29d5532edd..0000000000 --- a/packages/core-plugin-api/src/metadata/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export { useMetadata, useMetadataHolder } from './useMetadata'; -export { createMetadataRef } from './MetadataRef'; -export * from './types'; diff --git a/packages/core-plugin-api/src/metadata/types.ts b/packages/core-plugin-api/src/metadata/types.ts deleted file mode 100644 index 5b721b5b55..0000000000 --- a/packages/core-plugin-api/src/metadata/types.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * Metadata reference. - * - * @public - */ -export type MetadataRef = { - id: string; - T: T; -}; - -/** - * Provides lookup of metadata through their {@link MetadataRef}s. - * - * @public - */ -export type MetadataHolder = { - get(key: MetadataRef): T | undefined; -}; diff --git a/packages/core-plugin-api/src/metadata/useMetadata.tsx b/packages/core-plugin-api/src/metadata/useMetadata.tsx deleted file mode 100644 index 99ce68f1e4..0000000000 --- a/packages/core-plugin-api/src/metadata/useMetadata.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { MetadataHolder, MetadataRef } from './types'; -import { useVersionedContext } from '@backstage/version-bridge'; - -/** - * React hook for retrieving {@link MetadataHolder} - * - * @public - */ -export function useMetadataHolder(): MetadataHolder { - const versionedHolder = useVersionedContext<{ 1: MetadataHolder }>( - 'metadata-context', - ); - if (!versionedHolder) { - throw new Error('Metadata context is not available'); - } - - const metadataHolder = versionedHolder.atVersion(1); - if (!metadataHolder) { - throw new Error('Metadata context v1 not available'); - } - return metadataHolder; -} - -/** - * React hook for retrieving metadata. - * - * @param metadataRef - Reference of the metadata to use. - * @public - */ -export function useMetadata(metadataRef: MetadataRef): T { - const metadataHolder = useMetadataHolder(); - - const metadata = metadataHolder.get(metadataRef); - if (!metadata) { - throw new Error(`No implementation available for ${metadataRef}`); - } - return metadata; -} diff --git a/packages/core-app-api/src/metadata/index.ts b/packages/core-plugin-api/src/plugin-options/index.ts similarity index 88% rename from packages/core-app-api/src/metadata/index.ts rename to packages/core-plugin-api/src/plugin-options/index.ts index 59aa474fee..83901f526a 100644 --- a/packages/core-app-api/src/metadata/index.ts +++ b/packages/core-plugin-api/src/plugin-options/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './system'; +export { usePluginOptions, PluginOptionsProvider } from './usePluginOptions'; diff --git a/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx b/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx new file mode 100644 index 0000000000..5e8952ee76 --- /dev/null +++ b/packages/core-plugin-api/src/plugin-options/usePluginOptions.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createVersionedContext, + createVersionedValueMap, + useVersionedContext, +} from '@backstage/version-bridge'; +import { AnyPluginOptions } from '../plugin'; +import React, { ReactNode } from 'react'; + +const contextKey: string = 'pluginOptions-context'; + +/** + * Properties for the AsyncEntityProvider component. + * + * @public + */ +export interface PluginOptionsProviderProps { + children: ReactNode; + pluginOptions?: AnyPluginOptions; +} + +export const PluginOptionsProvider = ({ + children, + pluginOptions, +}: PluginOptionsProviderProps) => { + const value = { pluginOptions }; + const { Provider } = createVersionedContext<{ 1: AnyPluginOptions }>( + contextKey, + ); + return ( + + {children} + + ); +}; + +/** + * Grab the current entity from the context, throws if the entity has not yet been loaded + * or is not available. + * + * @public + */ +export function usePluginOptions< + TPluginOptions extends AnyPluginOptions = AnyPluginOptions, +>(): TPluginOptions { + const versionedHolder = useVersionedContext<{ 1: TPluginOptions }>( + contextKey, + ); + + if (!versionedHolder) { + throw new Error('Plugin Options context is not available'); + } + + const value = versionedHolder.atVersion(1); + if (!value) { + throw new Error('Plugin Options v1 is not available'); + } + + return value.pluginOptions; +} diff --git a/packages/core-plugin-api/src/plugin/Plugin.tsx b/packages/core-plugin-api/src/plugin/Plugin.tsx index 999d154166..6565289542 100644 --- a/packages/core-plugin-api/src/plugin/Plugin.tsx +++ b/packages/core-plugin-api/src/plugin/Plugin.tsx @@ -20,7 +20,7 @@ import { Extension, AnyRoutes, AnyExternalRoutes, - AnyMetadata, + AnyPluginOptions, PluginFeatureFlagConfig, } from './types'; import { AnyApiFactory } from '../apis'; @@ -31,19 +31,17 @@ import { AnyApiFactory } from '../apis'; export class PluginImpl< Routes extends AnyRoutes, ExternalRoutes extends AnyExternalRoutes, - PluginMetadata extends AnyMetadata, -> implements BackstagePlugin + PluginOptions extends AnyPluginOptions, +> implements BackstagePlugin { constructor( private readonly config: PluginConfig< Routes, ExternalRoutes, - PluginMetadata + PluginOptions >, ) {} - private reconfiguredMetadata: Array = []; - getId(): string { return this.config.id; } @@ -56,13 +54,6 @@ export class PluginImpl< return this.config.featureFlags?.slice() ?? []; } - getMetadata(): Iterable { - return [ - ...Array.from(this.config.metadata ?? []), - ...this.reconfiguredMetadata, - ]; - } - get routes(): Routes { return this.config.routes ?? ({} as Routes); } @@ -75,8 +66,12 @@ export class PluginImpl< return extension.expose(this); } - reconfigure(metadata: PluginMetadata): void { - this.reconfiguredMetadata.push(metadata); + reconfigure(pluginOptions: PluginOptions): void { + this.config.options = pluginOptions; + } + + getPluginOptions(): PluginOptions { + return this.config.options ?? ({} as PluginOptions); } toString() { @@ -93,9 +88,9 @@ export class PluginImpl< export function createPlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, - PluginMetadata extends AnyMetadata = {}, + PluginOptions extends AnyPluginOptions = {}, >( - config: PluginConfig, -): BackstagePlugin { + config: PluginConfig, +): BackstagePlugin { return new PluginImpl(config); } diff --git a/packages/core-plugin-api/src/plugin/index.ts b/packages/core-plugin-api/src/plugin/index.ts index 1ebe1b3446..5885fb1bc6 100644 --- a/packages/core-plugin-api/src/plugin/index.ts +++ b/packages/core-plugin-api/src/plugin/index.ts @@ -17,7 +17,7 @@ export { createPlugin } from './Plugin'; export type { AnyExternalRoutes, - AnyMetadata, + AnyPluginOptions, AnyRoutes, BackstagePlugin, Extension, diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index 8aadac36bc..953a55a9ee 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -49,7 +49,7 @@ export type AnyExternalRoutes = { [name: string]: ExternalRouteRef }; * * @public */ -export type AnyMetadata = { [name: string]: any }; +export type AnyPluginOptions = { [name: string]: any }; /** * Plugin type. @@ -59,7 +59,7 @@ export type AnyMetadata = { [name: string]: any }; export type BackstagePlugin< Routes extends AnyRoutes = {}, ExternalRoutes extends AnyExternalRoutes = {}, - PluginMetadata extends AnyMetadata = { [name: string]: any }, + PluginOptions extends AnyPluginOptions = { [name: string]: any }, > = { getId(): string; getApis(): Iterable; @@ -67,9 +67,9 @@ export type BackstagePlugin< * Returns all registered feature flags for this plugin. */ getFeatureFlags(): Iterable; - getMetadata(): Iterable; provide(extension: Extension): T; - reconfigure(metadata: PluginMetadata): void; + getPluginOptions(): PluginOptions; + reconfigure(pluginOptions: PluginOptions): void; routes: Routes; externalRoutes: ExternalRoutes; }; @@ -92,14 +92,22 @@ export type PluginFeatureFlagConfig = { export type PluginConfig< Routes extends AnyRoutes, ExternalRoutes extends AnyExternalRoutes, - PluginMetadata extends AnyMetadata, + PluginOptions extends AnyPluginOptions, > = { id: string; apis?: Iterable; routes?: Routes; externalRoutes?: ExternalRoutes; featureFlags?: PluginFeatureFlagConfig[]; - metadata?: Iterable; + options?: PluginOptions; + /** + * TODO: Not clear yet does it make sense to do it as a function. + * As for me it makes more sense to provide it as an object with default values. + * And keep only reconfigure as a function to update default values. + * Otherwise it looks like we have 2 places where it is possible to override default values. + * @param inputOptions + */ + pluginOptions(inputOptions: AnyPluginOptions): PluginOptions; }; /** diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx index 5250dcec0d..35e4aa5753 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.tsx @@ -23,7 +23,12 @@ import { TableColumn, TableProps, } from '@backstage/core-components'; -import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { + configApiRef, + useApi, + usePluginOptions, + useRouteRef, +} from '@backstage/core-plugin-api'; import { CatalogFilterLayout, EntityLifecyclePicker, @@ -52,6 +57,10 @@ export interface DefaultCatalogPageProps { tableOptions?: TableProps['options']; } +export type CatalogPageMetadataProps = { + createButtonTitle: string; +}; + export function DefaultCatalogPage(props: DefaultCatalogPageProps) { const { columns, @@ -64,6 +73,8 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage'; const createComponentLink = useRouteRef(createComponentRouteRef); + const { createButtonTitle } = usePluginOptions(); + return ( @@ -72,7 +83,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) { titleComponent={} > All your software catalog entities diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 1b9083e7ca..e1bf10e186 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -23,6 +23,7 @@ import { } from '@backstage/plugin-catalog-react'; import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; import { + AnyPluginOptions, createApiFactory, createComponentExtension, createPlugin, @@ -72,6 +73,10 @@ export const catalogPlugin = createPlugin({ createComponent: createComponentRouteRef, viewTechDoc: viewTechDocRouteRef, }, + pluginOptions: (inputOptions: AnyPluginOptions) => ({ + // TODO: remove it, only for testing here + createButtonTitle: inputOptions.createButtonTitle || 'Create', + }), }); /** @public */