frontend-*-api: add support for theme extensions

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Vincenzo Scamporlino <vincenzos@spotify.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-29 15:31:52 +02:00
parent a1ac0ed0fa
commit 52366db5b3
11 changed files with 152 additions and 7 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Make themes configurable through extensions, and switched default themes to use extensions instead.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
---
Added `createThemeExtension` and `coreExtensionData.theme`.
+4 -1
View File
@@ -25,7 +25,8 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1"
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3"
},
"configSchema": "config.d.ts",
"files": [
@@ -39,8 +40,10 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-graphiql": "workspace:^",
"@backstage/theme": "workspace:^",
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21"
},
@@ -0,0 +1,44 @@
/*
* Copyright 2023 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 {
UnifiedThemeProvider,
themes as builtinThemes,
} from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import { createThemeExtension } from '@backstage/frontend-plugin-api';
export const LightTheme = createThemeExtension({
id: 'light',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.light} children={children} />
),
});
export const DarkTheme = createThemeExtension({
id: 'dark',
title: 'Dark Theme',
variant: 'dark',
icon: <DarkIcon />,
Provider: ({ children }) => (
<UnifiedThemeProvider theme={builtinThemes.dark} children={children} />
),
});
@@ -18,10 +18,11 @@ import {
createExtension,
createPageExtension,
createPlugin,
createThemeExtension,
} from '@backstage/frontend-plugin-api';
import { createInstances } from './createApp';
import { MockConfigApi } from '@backstage/test-utils';
import { createApp, createInstances } from './createApp';
import { screen } from '@testing-library/react';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import React from 'react';
import { createRouteRef } from '@backstage/core-plugin-api';
@@ -104,3 +105,33 @@ describe('createInstances', () => {
);
});
});
describe('createApp', () => {
it('should allow themes to be installed', async () => {
const app = createApp({
configLoader: async () =>
new MockConfigApi({
app: {
extensions: [{ 'themes.light': false }, { 'themes.dark': false }],
},
}),
plugins: [
createPlugin({
id: 'test',
extensions: [
createThemeExtension({
id: 'derp',
title: 'Derp',
variant: 'dark',
Provider: () => <div>Derp</div>,
}),
],
}),
],
});
await renderWithEffects(app.createRoot());
await expect(screen.findByText('Derp')).resolves.toBeInTheDocument();
});
});
@@ -50,6 +50,7 @@ import {
attachComponentData,
useRouteRef,
identityApiRef,
AppTheme,
} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './discovery';
import {
@@ -77,10 +78,10 @@ import {
apis as defaultApis,
components as defaultComponents,
icons as defaultIcons,
themes as defaultThemes,
} from '../../../app-defaults/src/defaults';
import { BrowserRouter, Route } from 'react-router-dom';
import { SidebarItem } from '@backstage/core-components';
import { DarkTheme, LightTheme } from '../extensions/themes';
/** @public */
export interface ExtensionTreeNode {
@@ -171,7 +172,14 @@ export function createInstances(options: {
plugins: BackstagePlugin[];
config: Config;
}) {
const builtinExtensions = [Core, CoreRoutes, CoreNav, CoreLayout];
const builtinExtensions = [
Core,
CoreRoutes,
CoreNav,
CoreLayout,
LightTheme,
DarkTheme,
];
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
@@ -375,6 +383,12 @@ function createApiHolder(
?.map(e => e.getData(coreExtensionData.apiFactory))
.filter((x): x is AnyApiFactory => !!x) ?? [];
const themeExtensions =
coreExtension.attachments
.get('themes')
?.map(e => e.getData(coreExtensionData.theme))
.filter((x): x is AppTheme => !!x) ?? [];
for (const factory of [...defaultApis, ...pluginApis]) {
factoryRegistry.register('default', factory);
}
@@ -422,7 +436,7 @@ function createApiHolder(
api: appThemeApiRef,
deps: {},
// TODO: add extension for registering themes
factory: () => AppThemeSelector.createWithStorage(defaultThemes),
factory: () => AppThemeSelector.createWithStorage(themeExtensions),
});
factoryRegistry.register('static', {
@@ -7,6 +7,7 @@
import { AnyApiFactory } from '@backstage/core-plugin-api';
import { AnyApiRef } from '@backstage/core-plugin-api';
import { AppTheme } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { JSX as JSX_2 } from 'react';
@@ -71,6 +72,7 @@ export const coreExtensionData: {
apiFactory: ConfigurableExtensionDataRef<AnyApiFactory, {}>;
routeRef: ConfigurableExtensionDataRef<RouteRef, {}>;
navTarget: ConfigurableExtensionDataRef<NavTarget, {}>;
theme: ConfigurableExtensionDataRef<AppTheme, {}>;
};
// @public (undocumented)
@@ -199,6 +201,9 @@ export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
): PortableSchema<TOutput>;
// @public (undocumented)
export function createThemeExtension(theme: AppTheme): Extension<never>;
// @public (undocumented)
export interface Extension<TConfig> {
// (undocumented)
@@ -0,0 +1,32 @@
/*
* Copyright 2023 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 { createExtension, coreExtensionData } from '../wiring';
import { AppTheme } from '@backstage/core-plugin-api';
/** @public */
export function createThemeExtension(theme: AppTheme) {
return createExtension({
id: `themes.${theme.id}`,
at: 'core/themes',
output: {
theme: coreExtensionData.theme,
},
factory({ bind }) {
bind({ theme });
},
});
}
@@ -17,3 +17,4 @@
export { createApiExtension } from './createApiExtension';
export { createPageExtension } from './createPageExtension';
export { createNavItemExtension } from './createNavItemExtension';
export { createThemeExtension } from './createThemeExtension';
@@ -17,6 +17,7 @@
import { JSX } from 'react';
import {
AnyApiFactory,
AppTheme,
IconComponent,
RouteRef,
} from '@backstage/core-plugin-api';
@@ -36,4 +37,5 @@ export const coreExtensionData = {
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
navTarget: createExtensionDataRef<NavTarget>('core.nav.target'),
theme: createExtensionDataRef<AppTheme>('core.theme'),
};
+3
View File
@@ -4307,9 +4307,12 @@ __metadata:
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-graphiql": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/theme": "workspace:^"
"@backstage/types": "workspace:^"
"@material-ui/core": ^4.12.4
"@material-ui/icons": ^4.11.3
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3
"@types/react": ^16.13.1 || ^17.0.0
lodash: ^4.17.21
peerDependencies: