WIP changes, still have to be implemented pluginOptions, as default values are not stored anywhere yet, only reconfigure defines it.

Docs and tests have to be updated too

Signed-off-by: bnechyporenko <bnechyporenko@bol.com>
This commit is contained in:
bnechyporenko
2022-05-20 16:05:13 +02:00
parent a9cf9211f4
commit 08d2b2a031
25 changed files with 156 additions and 778 deletions
+5
View File
@@ -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 = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
@@ -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<CustomPluginMetadataProps>({
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<CustomPluginMetadataProps>(
customPluginMetadataRef,
);
return (
<>
<div data-testid="plugin-label">{pluginLabel}</div>
</>
);
};
const CustomComponent = customPlugin.provide(
createRoutableExtension({
name: 'CustomComponent',
component: () => Promise.resolve(() => <CustomPlugin />),
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(
<Provider>
<Router>
<Routes>
<Route path="/" element={<CustomComponent />} />
</Routes>
</Router>
</Provider>,
);
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<CustomPluginMetadataProps>({
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<CustomPluginMetadataProps>(
customPluginMetadataRef,
);
return (
<>
<div data-testid="plugin-label">{pluginLabel}</div>
</>
);
};
const RedefinedCustomPlugin = customPlugin.provide(
createRoutableExtension({
name: 'RedefinedCustomPlugin',
component: () => Promise.resolve(() => <RedefinedCustom />),
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(
<Provider>
<Router>
<Routes>
<Route path="/" element={<RedefinedCustomPlugin />} />
</Routes>
</Router>
</Provider>,
);
expect(screen.getByText('Custom Label')).toBeInTheDocument();
});
it('runs happy paths', async () => {
const app = new AppManager({
apis: [noOpAnalyticsApi],
+16 -38
View File
@@ -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<any, any>
@@ -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<any, any>[] {
@@ -302,25 +298,23 @@ export class AppManager implements BackstageApp {
return (
<ApiProvider apis={this.getApiHolder()}>
<MetadataProvider metadata={this.getMetadataHolder()}>
<AppContextProvider appContext={appContext}>
<ThemeProvider>
<RoutingProvider
routePaths={routing.paths}
routeParents={routing.parents}
routeObjects={routing.objects}
routeBindings={routeBindings}
basePath={getBasePath(loadedConfig.api)}
<AppContextProvider appContext={appContext}>
<ThemeProvider>
<RoutingProvider
routePaths={routing.paths}
routeParents={routing.parents}
routeObjects={routing.objects}
routeBindings={routeBindings}
basePath={getBasePath(loadedConfig.api)}
>
<InternalAppContext.Provider
value={{ routeObjects: routing.objects }}
>
<InternalAppContext.Provider
value={{ routeObjects: routing.objects }}
>
{children}
</InternalAppContext.Provider>
</RoutingProvider>
</ThemeProvider>
</AppContextProvider>
</MetadataProvider>
{children}
</InternalAppContext.Provider>
</RoutingProvider>
</ThemeProvider>
</AppContextProvider>
</ApiProvider>
);
};
@@ -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.
-1
View File
@@ -21,6 +21,5 @@
*/
export * from './apis';
export * from './metadata';
export * from './app';
export * from './routing';
@@ -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<number>({ id: '1' });
const apiBRef = createMetadataRef<number>({ 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' });
});
});
@@ -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<T>(ref: MetadataRef<T>): T | undefined {
for (const holder of this.holders) {
const metadata = holder.get(ref);
if (metadata) {
return metadata;
}
}
return undefined;
}
}
@@ -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<Metadata>({ id: 'x' });
const MyHookConsumer = () => {
const payload = useMetadata(metadataRef);
return <p>hook message: {payload}</p>;
};
it('should provide apis', () => {
const renderedHook = render(
<MetadataProvider
metadata={MetadataRegistry.from([[metadataRef, 'hello']])}
>
<MyHookConsumer />
</MetadataProvider>,
);
renderedHook.getByText('hook message: hello');
});
it('should provide nested access to apis', () => {
const aRef = createMetadataRef<string>({ id: 'a' });
const bRef = createMetadataRef<string>({ id: 'b' });
const MyComponent = () => {
const a = useMetadata(aRef);
const b = useMetadata(bRef);
return (
<div>
a={a} b={b}
</div>
);
};
const renderedHook = render(
<MetadataProvider
metadata={MetadataRegistry.from([
[aRef, 'x'],
[bRef, 'y'],
])}
>
<MetadataProvider metadata={MetadataRegistry.from([[aRef, 'z']])}>
<MyComponent />
</MetadataProvider>
</MetadataProvider>,
);
renderedHook.getByText('a=z b=y');
});
it('should error if metadata is not available', () => {
expect(
withLogCollector(['error'], () => {
expect(() => {
render(
<MetadataProvider metadata={MetadataRegistry.from([])}>
<MyHookConsumer />
</MetadataProvider>,
);
}).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 <MyHookConsumer> component/,
),
]);
});
});
describe('v1 consumer', () => {
function useMockApiV1<T>(apiRef: MetadataRef<T>): 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<Api>({ id: 'x' });
const registry = MetadataRegistry.from([[apiRef, () => 'hello']]);
const MyHookConsumerV1 = () => {
const api = useMockApiV1(apiRef);
return <p>hook message: {api()}</p>;
};
it('should provide apis', () => {
const renderedHook = render(
<MetadataProvider metadata={registry}>
<MyHookConsumerV1 />
</MetadataProvider>,
);
renderedHook.getByText('hook message: hello');
});
});
@@ -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<MetadataProviderProps>,
) => {
const { children, metadata } = props;
const parentHolder = useContext(MetadataContext)?.atVersion(1);
const holder = parentHolder
? new MetadataAggregator(metadata, parentHolder)
: metadata;
return (
<MetadataContext.Provider
value={createVersionedValueMap({ 1: holder })}
children={children}
/>
);
};
MetadataProvider.propTypes = {
metadata: PropTypes.shape({ get: PropTypes.func.isRequired }).isRequired,
children: PropTypes.node,
};
@@ -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<number>({ id: 'x1' });
const x1DuplicateRef = createMetadataRef<number>({ id: 'x1' });
const x2Ref = createMetadataRef<string>({ 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');
});
});
@@ -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<T = unknown> = readonly [MetadataRef<T>, T];
export class MetadataRegistry implements MetadataHolder {
private readonly registry = new Map<string, any>();
register(id: string, payload: any): boolean {
this.registry.set(id, payload);
return true;
}
get<T>(ref: MetadataRef<T>): 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;
}
}
@@ -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';
+1
View File
@@ -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"
@@ -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() ? (
<PluginOptionsProvider pluginOptions={plugin.getPluginOptions()}>
<Component {...props} />
</PluginOptionsProvider>
) : (
<Component {...props} />
);
return (
<Suspense fallback={<Progress />}>
<PluginErrorBoundary app={app} plugin={plugin}>
@@ -245,7 +255,7 @@ export function createReactExtension<
...(mountPoint && { routeRef: mountPoint.id }),
}}
>
<Component {...props} />
{renderComponent()}
</AnalyticsContext>
</PluginErrorBoundary>
</Suspense>
+1 -1
View File
@@ -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';
@@ -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<T> implements MetadataRef<T> {
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<T>(
config: MetadataRefConfig,
): MetadataRef<T> {
return new MetadataRefImpl<T>(config);
}
@@ -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';
@@ -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<T> = {
id: string;
T: T;
};
/**
* Provides lookup of metadata through their {@link MetadataRef}s.
*
* @public
*/
export type MetadataHolder = {
get<T>(key: MetadataRef<T>): T | undefined;
};
@@ -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<T>(metadataRef: MetadataRef<T>): T {
const metadataHolder = useMetadataHolder();
const metadata = metadataHolder.get(metadataRef);
if (!metadata) {
throw new Error(`No implementation available for ${metadataRef}`);
}
return metadata;
}
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './system';
export { usePluginOptions, PluginOptionsProvider } from './usePluginOptions';
@@ -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 (
<Provider value={createVersionedValueMap({ 1: value })}>
{children}
</Provider>
);
};
/**
* 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;
}
+13 -18
View File
@@ -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<Routes, ExternalRoutes, PluginMetadata>
PluginOptions extends AnyPluginOptions,
> implements BackstagePlugin<Routes, ExternalRoutes, PluginOptions>
{
constructor(
private readonly config: PluginConfig<
Routes,
ExternalRoutes,
PluginMetadata
PluginOptions
>,
) {}
private reconfiguredMetadata: Array<PluginMetadata> = [];
getId(): string {
return this.config.id;
}
@@ -56,13 +54,6 @@ export class PluginImpl<
return this.config.featureFlags?.slice() ?? [];
}
getMetadata(): Iterable<PluginMetadata> {
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<Routes, ExternalRoutes, PluginMetadata>,
): BackstagePlugin<Routes, ExternalRoutes, PluginMetadata> {
config: PluginConfig<Routes, ExternalRoutes, PluginOptions>,
): BackstagePlugin<Routes, ExternalRoutes, PluginOptions> {
return new PluginImpl(config);
}
+1 -1
View File
@@ -17,7 +17,7 @@
export { createPlugin } from './Plugin';
export type {
AnyExternalRoutes,
AnyMetadata,
AnyPluginOptions,
AnyRoutes,
BackstagePlugin,
Extension,
+14 -6
View File
@@ -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<AnyApiFactory>;
@@ -67,9 +67,9 @@ export type BackstagePlugin<
* Returns all registered feature flags for this plugin.
*/
getFeatureFlags(): Iterable<PluginFeatureFlagConfig>;
getMetadata(): Iterable<PluginMetadata>;
provide<T>(extension: Extension<T>): 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<AnyApiFactory>;
routes?: Routes;
externalRoutes?: ExternalRoutes;
featureFlags?: PluginFeatureFlagConfig[];
metadata?: Iterable<PluginMetadata>;
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;
};
/**
@@ -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<CatalogTableRow>['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<CatalogPageMetadataProps>();
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<EntityListProvider>
@@ -72,7 +83,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
titleComponent={<CatalogKindHeader initialFilter={initialKind} />}
>
<CreateButton
title="Create Component"
title={createButtonTitle}
to={createComponentLink && createComponentLink()}
/>
<SupportButton>All your software catalog entities</SupportButton>
+5
View File
@@ -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 */