diff --git a/.changeset/nervous-hounds-sleep.md b/.changeset/nervous-hounds-sleep.md new file mode 100644 index 0000000000..530ff348f7 --- /dev/null +++ b/.changeset/nervous-hounds-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Make themes configurable through extensions, and switched default themes to use extensions instead. diff --git a/.changeset/silly-swans-invent.md b/.changeset/silly-swans-invent.md new file mode 100644 index 0000000000..68b094aba7 --- /dev/null +++ b/.changeset/silly-swans-invent.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Added `createThemeExtension` and `coreExtensionData.theme`. diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json index 0982cb0157..6c45e9239a 100644 --- a/packages/frontend-app-api/package.json +++ b/packages/frontend-app-api/package.json @@ -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" }, diff --git a/packages/frontend-app-api/src/extensions/themes.tsx b/packages/frontend-app-api/src/extensions/themes.tsx new file mode 100644 index 0000000000..ea11b6a638 --- /dev/null +++ b/packages/frontend-app-api/src/extensions/themes.tsx @@ -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: , + Provider: ({ children }) => ( + + ), +}); + +export const DarkTheme = createThemeExtension({ + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + icon: , + Provider: ({ children }) => ( + + ), +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 903b044ed3..f6172f5a4b 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -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: () =>
Derp
, + }), + ], + }), + ], + }); + + await renderWithEffects(app.createRoot()); + + await expect(screen.findByText('Derp')).resolves.toBeInTheDocument(); + }); +}); diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index dc7db79022..deb19d919d 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -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', { diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 5855976bc4..e28d7d5993 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -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; routeRef: ConfigurableExtensionDataRef; navTarget: ConfigurableExtensionDataRef; + theme: ConfigurableExtensionDataRef; }; // @public (undocumented) @@ -199,6 +201,9 @@ export function createSchemaFromZod( schemaCreator: (zImpl: typeof z) => ZodSchema, ): PortableSchema; +// @public (undocumented) +export function createThemeExtension(theme: AppTheme): Extension; + // @public (undocumented) export interface Extension { // (undocumented) diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts new file mode 100644 index 0000000000..5c13dd199e --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -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 }); + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index 0ec0cdf652..00cf4f919c 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -17,3 +17,4 @@ export { createApiExtension } from './createApiExtension'; export { createPageExtension } from './createPageExtension'; export { createNavItemExtension } from './createNavItemExtension'; +export { createThemeExtension } from './createThemeExtension'; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 079d5bcb18..6913501907 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -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('core.api.factory'), routeRef: createExtensionDataRef('core.routing.ref'), navTarget: createExtensionDataRef('core.nav.target'), + theme: createExtensionDataRef('core.theme'), }; diff --git a/yarn.lock b/yarn.lock index f7e19c3179..3dd3f6315b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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: