Merge pull request #26135 from backstage/mob/app-plugin-pt2
NFS: Move some implementations to `ApiBlueprints` instead of directly in the app
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Moved several implementations of built-in APIs from being hardcoded in the app to instead be provided as API extensions. This moves all API-related inputs from the `app` extension to the respective API extensions. For example, extensions created with `ThemeBlueprint` are now attached to the `themes` input of `api:app-theme` rather than the `app` extension.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Removed deprecated `icons` property passing to `createApp` and `createSpecializedApp`. Use `IconBundleBlueprint.make` to create extensions instead and include them in the app.
|
||||
@@ -6,7 +6,6 @@
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -14,9 +13,6 @@ import { SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createApp(options?: {
|
||||
icons?: {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{
|
||||
config: ConfigApi;
|
||||
@@ -50,9 +46,6 @@ export type CreateAppRouteBinder = <
|
||||
|
||||
// @public
|
||||
export function createSpecializedApp(options?: {
|
||||
icons?: {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
|
||||
+13
-60
@@ -15,57 +15,22 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createComponentExtension,
|
||||
createComponentRef,
|
||||
createExtension,
|
||||
createExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppNodeSpecs } from '../../../tree/resolveAppNodeSpecs';
|
||||
import { resolveAppTree } from '../../../tree/resolveAppTree';
|
||||
import { App } from '../../../extensions/App';
|
||||
import { createComponentRef } from '@backstage/frontend-plugin-api';
|
||||
import { DefaultComponentsApi } from './DefaultComponentsApi';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { instantiateAppNodeTree } from '../../../tree/instantiateAppNodeTree';
|
||||
|
||||
const testRefA = createComponentRef({ id: 'test.a' });
|
||||
const testRefB1 = createComponentRef({ id: 'test.b' });
|
||||
const testRefB2 = createComponentRef({ id: 'test.b' });
|
||||
|
||||
const baseOverrides = createExtensionOverrides({
|
||||
extensions: [
|
||||
App,
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'root',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<div>root</div>)],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe('DefaultComponentsApi', () => {
|
||||
it('should provide components', () => {
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
baseOverrides,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createComponentExtension({
|
||||
ref: testRefA,
|
||||
loader: { sync: () => () => <div>test.a</div> },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(tree.root);
|
||||
const api = DefaultComponentsApi.fromTree(tree);
|
||||
const api = DefaultComponentsApi.fromComponents([
|
||||
{
|
||||
ref: testRefA,
|
||||
impl: () => <div>test.a</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const ComponentA = api.getComponent(testRefA);
|
||||
render(<ComponentA />);
|
||||
@@ -74,24 +39,12 @@ describe('DefaultComponentsApi', () => {
|
||||
});
|
||||
|
||||
it('should key extension refs by ID', () => {
|
||||
const tree = resolveAppTree(
|
||||
'app',
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
baseOverrides,
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createComponentExtension({
|
||||
ref: testRefB1,
|
||||
loader: { sync: () => () => <div>test.b</div> },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
instantiateAppNodeTree(tree.root);
|
||||
const api = DefaultComponentsApi.fromTree(tree);
|
||||
const api = DefaultComponentsApi.fromComponents([
|
||||
{
|
||||
ref: testRefB1,
|
||||
impl: () => <div>test.b</div>,
|
||||
},
|
||||
]);
|
||||
|
||||
const ComponentB1 = api.getComponent(testRefB1);
|
||||
const ComponentB2 = api.getComponent(testRefB2);
|
||||
|
||||
+6
-14
@@ -16,7 +16,6 @@
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import {
|
||||
AppTree,
|
||||
ComponentRef,
|
||||
ComponentsApi,
|
||||
createComponentExtension,
|
||||
@@ -30,19 +29,12 @@ import {
|
||||
export class DefaultComponentsApi implements ComponentsApi {
|
||||
#components: Map<string, ComponentType<any>>;
|
||||
|
||||
static fromTree(tree: AppTree) {
|
||||
const componentEntries = tree.root.edges.attachments
|
||||
.get('components')
|
||||
?.reduce((map, e) => {
|
||||
const data = e.instance?.getData(
|
||||
createComponentExtension.componentDataRef,
|
||||
);
|
||||
if (data) {
|
||||
map.set(data.ref.id, data.impl);
|
||||
}
|
||||
return map;
|
||||
}, new Map<string, ComponentType>());
|
||||
return new DefaultComponentsApi(componentEntries ?? new Map());
|
||||
static fromComponents(
|
||||
components: Array<typeof createComponentExtension.componentDataRef.T>,
|
||||
) {
|
||||
return new DefaultComponentsApi(
|
||||
new Map(components.map(entry => [entry.ref.id, entry.impl])),
|
||||
);
|
||||
}
|
||||
|
||||
constructor(components: Map<string, any>) {
|
||||
|
||||
@@ -16,15 +16,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
ExtensionBoundary,
|
||||
coreExtensionData,
|
||||
createComponentExtension,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
IconBundleBlueprint,
|
||||
ThemeBlueprint,
|
||||
ApiBlueprint,
|
||||
TranslationBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const App = createExtension({
|
||||
@@ -32,14 +28,6 @@ export const App = createExtension({
|
||||
attachTo: { id: 'root', input: 'default' }, // ignored
|
||||
inputs: {
|
||||
apis: createExtensionInput([ApiBlueprint.dataRefs.factory]),
|
||||
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
|
||||
components: createExtensionInput([
|
||||
createComponentExtension.componentDataRef,
|
||||
]),
|
||||
translations: createExtensionInput([
|
||||
TranslationBlueprint.dataRefs.translation,
|
||||
]),
|
||||
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
|
||||
root: createExtensionInput([coreExtensionData.reactElement], {
|
||||
singleton: true,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2024 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.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi';
|
||||
import { appLanguageApiRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { ApiBlueprint, createApiFactory } from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const AppLanguageApi = ApiBlueprint.make({
|
||||
name: 'app-language',
|
||||
params: {
|
||||
factory: createApiFactory(
|
||||
appLanguageApiRef,
|
||||
AppLanguageSelector.createWithStorage(),
|
||||
),
|
||||
},
|
||||
});
|
||||
+29
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
* Copyright 2024 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.
|
||||
@@ -21,7 +21,34 @@ import {
|
||||
} from '@backstage/theme';
|
||||
import DarkIcon from '@material-ui/icons/Brightness2';
|
||||
import LightIcon from '@material-ui/icons/WbSunny';
|
||||
import { ThemeBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
createExtensionInput,
|
||||
ThemeBlueprint,
|
||||
ApiBlueprint,
|
||||
createApiFactory,
|
||||
appThemeApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { AppThemeSelector } from '@backstage/core-app-api';
|
||||
|
||||
/**
|
||||
* Contains the themes installed into the app.
|
||||
*/
|
||||
export const AppThemeApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'app-theme',
|
||||
inputs: {
|
||||
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme]),
|
||||
},
|
||||
factory: (originalFactory, { inputs }) => {
|
||||
return originalFactory({
|
||||
factory: createApiFactory(
|
||||
appThemeApiRef,
|
||||
AppThemeSelector.createWithStorage(
|
||||
inputs.themes.map(i => i.get(ThemeBlueprint.dataRefs.theme)),
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const LightTheme = ThemeBlueprint.make({
|
||||
name: 'light',
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
createComponentExtension,
|
||||
createExtensionInput,
|
||||
ApiBlueprint,
|
||||
createApiFactory,
|
||||
componentsApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
|
||||
|
||||
/**
|
||||
* Contains the shareable components installed into the app.
|
||||
*/
|
||||
export const ComponentsApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'components',
|
||||
inputs: {
|
||||
components: createExtensionInput([
|
||||
createComponentExtension.componentDataRef,
|
||||
]),
|
||||
},
|
||||
factory: (originalFactory, { inputs }) => {
|
||||
return originalFactory({
|
||||
factory: createApiFactory(
|
||||
componentsApiRef,
|
||||
DefaultComponentsApi.fromComponents(
|
||||
inputs.components.map(i =>
|
||||
i.get(createComponentExtension.componentDataRef),
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
ApiBlueprint,
|
||||
createApiFactory,
|
||||
featureFlagsApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
|
||||
|
||||
/**
|
||||
* Contains the shareable icons installed into the app.
|
||||
*/
|
||||
export const FeatureFlagsApi = ApiBlueprint.make({
|
||||
name: 'feature-flags',
|
||||
params: {
|
||||
// TODO: properly discovery feature flags, maybe rework the whole thing
|
||||
factory: createApiFactory({
|
||||
api: featureFlagsApiRef,
|
||||
deps: {},
|
||||
factory: () => new LocalStorageFeatureFlags(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
createExtensionInput,
|
||||
IconBundleBlueprint,
|
||||
ApiBlueprint,
|
||||
createApiFactory,
|
||||
iconsApiRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { DefaultIconsApi } from '../apis/implementations/IconsApi';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
|
||||
|
||||
/**
|
||||
* Contains the shareable icons installed into the app.
|
||||
*/
|
||||
export const IconsApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'icons',
|
||||
inputs: {
|
||||
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons]),
|
||||
},
|
||||
factory: (originalFactory, { inputs }) => {
|
||||
return originalFactory({
|
||||
factory: createApiFactory(
|
||||
iconsApiRef,
|
||||
new DefaultIconsApi(
|
||||
inputs.icons
|
||||
.map(i => i.get(IconBundleBlueprint.dataRefs.icons))
|
||||
.reduce((acc, bundle) => ({ ...acc, ...bundle }), defaultIcons),
|
||||
),
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2024 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 {
|
||||
ApiBlueprint,
|
||||
TranslationBlueprint,
|
||||
createApiFactory,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
appLanguageApiRef,
|
||||
translationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
|
||||
/**
|
||||
* Contains translations that are installed in the app.
|
||||
*/
|
||||
export const TranslationsApi = ApiBlueprint.makeWithOverrides({
|
||||
name: 'translations',
|
||||
inputs: {
|
||||
translations: createExtensionInput([
|
||||
TranslationBlueprint.dataRefs.translation,
|
||||
]),
|
||||
},
|
||||
factory: (originalFactory, { inputs }) => {
|
||||
return originalFactory({
|
||||
factory: createApiFactory({
|
||||
api: translationApiRef,
|
||||
deps: { languageApi: appLanguageApiRef },
|
||||
factory: ({ languageApi }) =>
|
||||
I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: inputs.translations.map(i =>
|
||||
i.get(TranslationBlueprint.dataRefs.translation),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -278,16 +278,24 @@ describe('createApp', () => {
|
||||
]
|
||||
</app/root>
|
||||
]
|
||||
components [
|
||||
<component:core.components.progress out=[core.component.component] />
|
||||
<component:core.components.errorBoundaryFallback out=[core.component.component] />
|
||||
<component:core.components.notFoundErrorPage out=[core.component.component] />
|
||||
]
|
||||
themes [
|
||||
<theme:app/light out=[core.theme.theme] />
|
||||
<theme:app/dark out=[core.theme.theme] />
|
||||
]
|
||||
apis [
|
||||
<api:app-theme out=[core.api.factory]>
|
||||
themes [
|
||||
<theme:app/light out=[core.theme.theme] />
|
||||
<theme:app/dark out=[core.theme.theme] />
|
||||
]
|
||||
</api:app-theme>
|
||||
<api:app-language out=[core.api.factory] />
|
||||
<api:icons out=[core.api.factory] />
|
||||
<api:translations out=[core.api.factory] />
|
||||
<api:components out=[core.api.factory]>
|
||||
components [
|
||||
<component:core.components.progress out=[core.component.component] />
|
||||
<component:core.components.errorBoundaryFallback out=[core.component.component] />
|
||||
<component:core.components.notFoundErrorPage out=[core.component.component] />
|
||||
]
|
||||
</api:components>
|
||||
<api:feature-flags out=[core.api.factory] />
|
||||
<api:core.discovery out=[core.api.factory] />
|
||||
<api:core.alert out=[core.api.factory] />
|
||||
<api:core.analytics out=[core.api.factory] />
|
||||
|
||||
@@ -20,15 +20,10 @@ import {
|
||||
ApiBlueprint,
|
||||
AppTree,
|
||||
appTreeApiRef,
|
||||
componentsApiRef,
|
||||
coreExtensionData,
|
||||
FrontendFeature,
|
||||
IconBundleBlueprint,
|
||||
iconsApiRef,
|
||||
RouteResolutionApi,
|
||||
routeResolutionApiRef,
|
||||
ThemeBlueprint,
|
||||
TranslationBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { App } from '../extensions/App';
|
||||
import { AppRoutes } from '../extensions/AppRoutes';
|
||||
@@ -37,13 +32,10 @@ import { AppNav } from '../extensions/AppNav';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
appThemeApiRef,
|
||||
ConfigApi,
|
||||
configApiRef,
|
||||
IconComponent,
|
||||
featureFlagsApiRef,
|
||||
identityApiRef,
|
||||
AppTheme,
|
||||
errorApiRef,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
@@ -53,7 +45,6 @@ import {
|
||||
ApiFactoryRegistry,
|
||||
ApiProvider,
|
||||
ApiResolver,
|
||||
AppThemeSelector,
|
||||
} from '@backstage/core-app-api';
|
||||
|
||||
// TODO: Get rid of all of these
|
||||
@@ -64,29 +55,20 @@ import { AppThemeProvider } from '../../../core-app-api/src/app/AppThemeProvider
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { LocalStorageFeatureFlags } from '../../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultConfigLoader';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementations/AppLanguageApi/AppLanguageSelector';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { apis as defaultApis } from '../../../app-defaults/src/defaults';
|
||||
import { DarkTheme, LightTheme } from '../extensions/themes';
|
||||
|
||||
import {
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
} from '../extensions/elements';
|
||||
import { extractRouteInfoFromAppNode } from '../routing/extractRouteInfoFromAppNode';
|
||||
import {
|
||||
appLanguageApiRef,
|
||||
translationApiRef,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
import { CreateAppRouteBinder } from '../routing';
|
||||
import { RouteResolver } from '../routing/RouteResolver';
|
||||
import { resolveRouteBindings } from '../routing/resolveRouteBindings';
|
||||
@@ -103,12 +85,14 @@ import { AppRoot } from '../extensions/AppRoot';
|
||||
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createFrontendPlugin';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
|
||||
import { DefaultComponentsApi } from '../apis/implementations/ComponentsApi';
|
||||
import { DefaultIconsApi } from '../apis/implementations/IconsApi';
|
||||
import { stringifyError } from '@backstage/errors';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { icons as defaultIcons } from '../../../app-defaults/src/defaults';
|
||||
import { getBasePath } from '../routing/getBasePath';
|
||||
import { AppThemeApi, DarkTheme, LightTheme } from '../extensions/AppThemeApi';
|
||||
import { IconsApi } from '../extensions/IconsApi';
|
||||
import { TranslationsApi } from '../extensions/TranslationsApi';
|
||||
import { ComponentsApi } from '../extensions/ComponentsApi';
|
||||
import { AppLanguageApi } from '../extensions/AppLanguageApi';
|
||||
import { FeatureFlagsApi } from '../extensions/FeatureFlagsApi';
|
||||
|
||||
const DefaultApis = defaultApis.map(factory =>
|
||||
ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }),
|
||||
@@ -127,6 +111,12 @@ export const builtinExtensions = [
|
||||
DarkTheme,
|
||||
oauthRequestDialogAppRootElement,
|
||||
alertDisplayAppRootElement,
|
||||
AppThemeApi,
|
||||
AppLanguageApi,
|
||||
IconsApi,
|
||||
TranslationsApi,
|
||||
ComponentsApi,
|
||||
FeatureFlagsApi,
|
||||
...DefaultApis,
|
||||
].map(def => resolveExtensionDefinition(def));
|
||||
|
||||
@@ -174,8 +164,6 @@ export interface CreateAppFeatureLoader {
|
||||
|
||||
/** @public */
|
||||
export function createApp(options?: {
|
||||
/** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */
|
||||
icons?: { [key in string]: IconComponent };
|
||||
features?: (FrontendFeature | CreateAppFeatureLoader)[];
|
||||
configLoader?: () => Promise<{ config: ConfigApi }>;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
@@ -222,7 +210,6 @@ export function createApp(options?: {
|
||||
}
|
||||
|
||||
const app = createSpecializedApp({
|
||||
icons: options?.icons,
|
||||
config,
|
||||
features: [...discoveredFeatures, ...providedFeatures],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
@@ -250,8 +237,6 @@ export function createApp(options?: {
|
||||
* @public
|
||||
*/
|
||||
export function createSpecializedApp(options?: {
|
||||
/** @deprecated - Please use {@link @backstage/frontend-plugin-api#IconBundleBlueprint} to make new icon bundles which can be installed in the app seperately */
|
||||
icons?: { [key in string]: IconComponent };
|
||||
features?: FrontendFeature[];
|
||||
config?: ConfigApi;
|
||||
bindRoutes?(context: { bind: CreateAppRouteBinder }): void;
|
||||
@@ -288,7 +273,6 @@ export function createSpecializedApp(options?: {
|
||||
routeBindings,
|
||||
getBasePath(config),
|
||||
),
|
||||
options?.icons,
|
||||
);
|
||||
|
||||
if (isProtectedApp()) {
|
||||
@@ -352,7 +336,6 @@ function createApiHolder(
|
||||
configApi: ConfigApi,
|
||||
appIdentityProxy: AppIdentityProxy,
|
||||
routeResolutionApi: RouteResolutionApi,
|
||||
icons?: { [key in string]: IconComponent },
|
||||
): ApiHolder {
|
||||
const factoryRegistry = new ApiFactoryRegistry();
|
||||
|
||||
@@ -362,36 +345,10 @@ function createApiHolder(
|
||||
?.map(e => e.instance?.getData(ApiBlueprint.dataRefs.factory))
|
||||
.filter((x): x is AnyApiFactory => !!x) ?? [];
|
||||
|
||||
const themeExtensions =
|
||||
tree.root.edges.attachments
|
||||
.get('themes')
|
||||
?.map(e => e.instance?.getData(ThemeBlueprint.dataRefs.theme))
|
||||
.filter((x): x is AppTheme => !!x) ?? [];
|
||||
|
||||
const translationResources =
|
||||
tree.root.edges.attachments
|
||||
.get('translations')
|
||||
?.map(e => e.instance?.getData(TranslationBlueprint.dataRefs.translation))
|
||||
.filter(
|
||||
(x): x is typeof TranslationBlueprint.dataRefs.translation.T => !!x,
|
||||
) ?? [];
|
||||
|
||||
const extensionIcons = tree.root.edges.attachments
|
||||
.get('icons')
|
||||
?.map(e => e.instance?.getData(IconBundleBlueprint.dataRefs.icons))
|
||||
.reduce((acc, bundle) => ({ ...acc, ...bundle }), {});
|
||||
|
||||
for (const factory of pluginApis) {
|
||||
factoryRegistry.register('default', factory);
|
||||
}
|
||||
|
||||
// TODO: properly discovery feature flags, maybe rework the whole thing
|
||||
factoryRegistry.register('default', {
|
||||
api: featureFlagsApiRef,
|
||||
deps: {},
|
||||
factory: () => new LocalStorageFeatureFlags(),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: identityApiRef,
|
||||
deps: {},
|
||||
@@ -412,54 +369,12 @@ function createApiHolder(
|
||||
factory: () => routeResolutionApi,
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: componentsApiRef,
|
||||
deps: {},
|
||||
factory: () => DefaultComponentsApi.fromTree(tree),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: iconsApiRef,
|
||||
deps: {},
|
||||
factory: () =>
|
||||
new DefaultIconsApi({ ...defaultIcons, ...extensionIcons, ...icons }),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: appThemeApiRef,
|
||||
deps: {},
|
||||
// TODO: add extension for registering themes
|
||||
factory: () => AppThemeSelector.createWithStorage(themeExtensions),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: appLanguageApiRef,
|
||||
deps: {},
|
||||
factory: () => AppLanguageSelector.createWithStorage(),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: configApiRef,
|
||||
deps: {},
|
||||
factory: () => configApi,
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: appLanguageApiRef,
|
||||
deps: {},
|
||||
factory: () => AppLanguageSelector.createWithStorage(),
|
||||
});
|
||||
|
||||
factoryRegistry.register('static', {
|
||||
api: translationApiRef,
|
||||
deps: { languageApi: appLanguageApiRef },
|
||||
factory: ({ languageApi }) =>
|
||||
I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: translationResources,
|
||||
}),
|
||||
});
|
||||
|
||||
ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis());
|
||||
|
||||
return new ApiResolver(factoryRegistry);
|
||||
|
||||
@@ -25,7 +25,7 @@ const iconsDataRef = createExtensionDataRef<{
|
||||
export const IconBundleBlueprint = createExtensionBlueprint({
|
||||
kind: 'icon-bundle',
|
||||
namespace: 'app',
|
||||
attachTo: { id: 'app', input: 'icons' },
|
||||
attachTo: { id: 'api:icons', input: 'icons' },
|
||||
output: [iconsDataRef],
|
||||
config: {
|
||||
schema: {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('ThemeBlueprint', () => {
|
||||
{
|
||||
"$$type": "@backstage/ExtensionDefinition",
|
||||
"attachTo": {
|
||||
"id": "app",
|
||||
"id": "api:app-theme",
|
||||
"input": "themes",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
|
||||
@@ -29,7 +29,7 @@ const themeDataRef = createExtensionDataRef<AppTheme>().with({
|
||||
export const ThemeBlueprint = createExtensionBlueprint({
|
||||
kind: 'theme',
|
||||
namespace: 'app',
|
||||
attachTo: { id: 'app', input: 'themes' },
|
||||
attachTo: { id: 'api:app-theme', input: 'themes' },
|
||||
output: [themeDataRef],
|
||||
dataRefs: {
|
||||
theme: themeDataRef,
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('TranslationBlueprint', () => {
|
||||
{
|
||||
"$$type": "@backstage/ExtensionDefinition",
|
||||
"attachTo": {
|
||||
"id": "app",
|
||||
"id": "api:translations",
|
||||
"input": "translations",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
|
||||
@@ -28,7 +28,7 @@ const translationDataRef = createExtensionDataRef<
|
||||
*/
|
||||
export const TranslationBlueprint = createExtensionBlueprint({
|
||||
kind: 'translation',
|
||||
attachTo: { id: 'app', input: 'translations' },
|
||||
attachTo: { id: 'api:translations', input: 'translations' },
|
||||
output: [translationDataRef],
|
||||
dataRefs: {
|
||||
translation: translationDataRef,
|
||||
|
||||
@@ -35,7 +35,7 @@ export function createComponentExtension<TProps extends {}>(options: {
|
||||
kind: 'component',
|
||||
namespace: options.ref.id,
|
||||
name: options.name,
|
||||
attachTo: { id: 'app', input: 'components' },
|
||||
attachTo: { id: 'api:components', input: 'components' },
|
||||
disabled: options.disabled,
|
||||
output: [createComponentExtension.componentDataRef],
|
||||
factory() {
|
||||
|
||||
Reference in New Issue
Block a user