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:
@@ -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],
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
export { createPlugin } from './Plugin';
|
||||
export type {
|
||||
AnyExternalRoutes,
|
||||
AnyMetadata,
|
||||
AnyPluginOptions,
|
||||
AnyRoutes,
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user