Merge pull request #4893 from backstage/blam/fix-api-load-order

Wait for configApi to be ready before using featureFlagsApi to register plugin output
This commit is contained in:
Fredrik Adelöw
2021-03-11 12:08:57 +01:00
committed by GitHub
3 changed files with 102 additions and 20 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-api': patch
---
Wait for `configApi` to be ready before using `featureFlagsApi`
+67
View File
@@ -14,6 +14,12 @@
* limitations under the License.
*/
import {
configApiRef,
createApiFactory,
featureFlagsApiRef,
LocalStorageFeatureFlags,
} from '../apis';
import { renderWithEffects, withLogCollector } from '@backstage/test-utils';
import { lightTheme } from '@backstage/theme';
import { render, screen } from '@testing-library/react';
@@ -245,6 +251,67 @@ describe('Integration Test', () => {
expect(screen.getByText('extLink4: <none>')).toBeInTheDocument();
});
it('should wait for the config to load before calling feature flags', async () => {
const storageFlags = new LocalStorageFeatureFlags();
jest.spyOn(storageFlags, 'registerFlag');
const apis = [
createApiFactory({
api: featureFlagsApiRef,
deps: { configApi: configApiRef },
factory() {
return storageFlags;
},
}),
];
const app = new PrivateAppImpl({
apis,
defaultApis: [],
themes: [
{
id: 'light',
title: 'Light Theme',
variant: 'light',
theme: lightTheme,
},
],
icons: defaultSystemIcons,
plugins: [
createPlugin({
id: 'test',
register: p => p.featureFlags.register('name'),
}),
],
components,
bindRoutes: ({ bind }) => {
bind(plugin1.externalRoutes, {
err: plugin1RouteRef,
errParams: plugin2RouteRef,
});
},
});
const Provider = app.getProvider();
const Router = app.getRouter();
await renderWithEffects(
<Provider>
<Router>
<Routes>
<ExposedComponent path="/" />
<HiddenComponent path="/foo" />
</Routes>
</Router>
</Provider>,
);
expect(storageFlags.registerFlag).toHaveBeenCalledWith({
name: 'name',
pluginId: 'test',
});
});
it('should throw some error when the route has duplicate params', () => {
const app = new PrivateAppImpl({
apis: [],
+30 -20
View File
@@ -14,10 +14,12 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import React, {
ComponentType,
PropsWithChildren,
ReactElement,
useEffect,
useMemo,
useState,
} from 'react';
@@ -278,24 +280,6 @@ export class PrivateAppImpl implements BackstageApp {
const appContext = new AppContextImpl(this);
const apiHolder = this.getApiHolder();
const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!;
for (const plugin of this.plugins.values()) {
for (const output of plugin.output()) {
switch (output.type) {
case 'feature-flag': {
featureFlagsApi.registerFlag({
name: output.name,
pluginId: plugin.getId(),
});
break;
}
default:
break;
}
}
}
const Provider = ({ children }: PropsWithChildren<{}>) => {
const appThemeApi = useMemo(
() => AppThemeSelector.createWithStorage(this.themes),
@@ -324,13 +308,39 @@ export class PrivateAppImpl implements BackstageApp {
appThemeApi,
);
const hasConfigApi = 'api' in loadedConfig;
if (hasConfigApi) {
const { api } = loadedConfig as { api: Config };
this.configApi = api;
}
useEffect(() => {
if (hasConfigApi) {
const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!;
for (const plugin of this.plugins.values()) {
for (const output of plugin.output()) {
switch (output.type) {
case 'feature-flag': {
featureFlagsApi.registerFlag({
name: output.name,
pluginId: plugin.getId(),
});
break;
}
default:
break;
}
}
}
}
}, [hasConfigApi, loadedConfig]);
if ('node' in loadedConfig) {
// Loading or error
return loadedConfig.node;
}
this.configApi = loadedConfig.api;
return (
<ApiProvider apis={apiHolder}>
<AppContextProvider appContext={appContext}>