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:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Make themes configurable through extensions, and switched default themes to use extensions instead.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
---
|
||||
|
||||
Added `createThemeExtension` and `coreExtensionData.theme`.
|
||||
@@ -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'),
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user