diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index dd4bf42c2c..fc2e876468 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,8 +14,6 @@ * limitations under the License. */ -import { CssBaseline, ThemeProvider } from '@material-ui/core'; -import { lightTheme, darkTheme } from '@backstage/theme'; import { createApp } from '@backstage/core'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; @@ -23,7 +21,6 @@ import Root from './components/Root'; import AlertDisplay from './components/AlertDisplay'; import * as plugins from './plugins'; import apis, { alertApiForwarder } from './apis'; -import { ThemeContextType, ThemeContext, useThemeType } from './ThemeContext'; const app = createApp({ apis, @@ -33,50 +30,15 @@ const app = createApp({ const AppProvider = app.getProvider(); const AppComponent = app.getRootComponent(); -const App: FC<{}> = () => { - const [theme, toggleTheme] = useThemeType( - localStorage.getItem('theme') || 'auto', - ); - - let backstageTheme = lightTheme; - switch (theme) { - case 'light': - backstageTheme = lightTheme; - break; - case 'dark': - backstageTheme = darkTheme; - break; - default: - if (!window.matchMedia) { - backstageTheme = lightTheme; - break; - } - backstageTheme = window.matchMedia('(prefers-color-scheme: dark)').matches - ? darkTheme - : lightTheme; - break; - } - - const themeContext: ThemeContextType = { - theme, - toggleTheme, - }; - return ( - - - - - - - - - - - - - - - ); -}; +const App: FC<{}> = () => ( + + + + + + + + +); export default App; diff --git a/packages/app/src/ThemeContext.tsx b/packages/app/src/ThemeContext.tsx deleted file mode 100644 index 3d5a04f22a..0000000000 --- a/packages/app/src/ThemeContext.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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, { useState, useEffect } from 'react'; -export type ThemeContextType = { - theme: string; - toggleTheme: () => void; -}; -export const ThemeContext = React.createContext({ - theme: 'light', - toggleTheme: () => {}, -}); - -export function useThemeType(themeId: string): [string, () => void] { - const [theme, setTheme] = useState(themeId); - useEffect(() => { - if (!window.matchMedia) { - return () => {}; - } - const mql = window.matchMedia('(prefers-color-scheme: dark)'); - const darkListener = (event: MediaQueryListEvent) => { - if (localStorage.getItem('theme') === 'auto') { - if (event.matches) { - setTheme('dark'); - } else { - setTheme('light'); - } - setTheme('auto'); - } - }; - mql.addListener(darkListener); - return () => { - mql.removeListener(darkListener); - }; - }); - function toggleTheme() { - if (theme === 'light') { - setTheme('dark'); - localStorage.setItem('theme', 'dark'); - } else if (theme === 'dark') { - if (!window.matchMedia) { - setTheme('light'); - localStorage.setItem('theme', 'light'); - setTheme('light'); - return; - } - setTheme('auto'); - localStorage.setItem('theme', 'auto'); - setTheme('auto'); - } else { - setTheme('light'); - localStorage.setItem('theme', 'light'); - } - } - return [theme, toggleTheme]; -} diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 55cba439bd..3945befb73 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -31,8 +31,8 @@ import { SidebarSpacer, SidebarDivider, SidebarSpace, + SidebarThemeToggle, } from '@backstage/core'; -import ToggleThemeSidebarItem from './ToggleThemeSidebarItem'; const useSidebarLogoStyles = makeStyles({ root: { @@ -89,7 +89,7 @@ const Root: FC<{}> = ({ children }) => ( - + {children} diff --git a/packages/app/src/components/Root/ToggleThemeSidebarItem.tsx b/packages/app/src/components/Root/ToggleThemeSidebarItem.tsx deleted file mode 100644 index 6c0cd09244..0000000000 --- a/packages/app/src/components/Root/ToggleThemeSidebarItem.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * 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, { FC } from 'react'; -import { SidebarItem } from '@backstage/core'; -import { ThemeContext } from '../../ThemeContext'; -import WbSunnyIcon from '@material-ui/icons/WbSunny'; -import Brightness2Icon from '@material-ui/icons/Brightness2'; -import ToggleOnIcon from '@material-ui/icons/ToggleOn'; -const ToggleThemeSidebarItem: FC<{}> = () => { - return ( - - {themeContext => { - let text = 'Auto'; - let icon = ToggleOnIcon; - switch (themeContext.theme) { - case 'dark': - text = 'Dark mode'; - icon = Brightness2Icon; - break; - case 'light': - text = 'Light mode'; - icon = WbSunnyIcon; - break; - default: - text = 'Auto'; - icon = ToggleOnIcon; - break; - } - - return ( - - ); - }} - - ); -}; - -export default ToggleThemeSidebarItem; diff --git a/packages/backend/package.json b/packages/backend/package.json index 3a24067da4..8643cb3c25 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,6 +19,7 @@ "@backstage/backend-common": "^0.1.1-alpha.5", "@backstage/plugin-catalog-backend": "^0.1.1-alpha.5", "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.5", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.5", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 8a7a4c9b72..ed458dedb3 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -35,6 +35,7 @@ import helmet from 'helmet'; import knex from 'knex'; import catalog from './plugins/catalog'; import scaffolder from './plugins/scaffolder'; +import auth from './plugins/auth'; import { PluginEnvironment } from './types'; const DEFAULT_PORT = 7000; @@ -63,6 +64,7 @@ async function main() { app.use(requestLoggingHandler()); app.use('/catalog', await catalog(createEnv('catalog'))); app.use('/scaffolder', await scaffolder(createEnv('scaffolder'))); + app.use('/auth', await auth(createEnv('auth'))); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts new file mode 100644 index 0000000000..c2e349f640 --- /dev/null +++ b/packages/backend/src/plugins/auth.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { createRouter } from '@backstage/plugin-auth-backend'; +import { PluginEnvironment } from '../types'; + +export default async function ({ logger }: PluginEnvironment) { + return await createRouter({ logger }); +} diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 6de2cf75d5..0c6bfbe8e7 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -57,6 +57,19 @@ module.exports = { 'warn', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }, ], + + // Importing the entire MUI icons packages kills build performance as the list of icons is huge. + 'no-restricted-imports': [ + 2, + { + paths: [ + { + name: '@material-ui/icons', + message: "Please import '@material-ui/icons/' instead.", + }, + ], + }, + ], }, overrides: [ { diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx index e8b82ceecd..c32efd2a6b 100644 --- a/packages/cli/templates/default-app/packages/app/src/App.tsx +++ b/packages/cli/templates/default-app/packages/app/src/App.tsx @@ -1,6 +1,5 @@ -import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import { createApp } from '@backstage/core'; -import { lightTheme } from '@backstage/theme'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import * as plugins from './plugins'; @@ -34,13 +33,9 @@ const App: FC<{}> = () => { useStyles(); return ( - - - - - - - + + + ); }; diff --git a/packages/core/src/api/apis/ApiAggregator.test.ts b/packages/core/src/api/apis/ApiAggregator.test.ts new file mode 100644 index 0000000000..a230e0cfff --- /dev/null +++ b/packages/core/src/api/apis/ApiAggregator.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { ApiAggregator } from './ApiAggregator'; +import { ApiRef } from './ApiRef'; +import { ApiRegistry } from './ApiRegistry'; + +describe('ApiAggregator', () => { + const apiARef = new ApiRef({ id: 'a', description: '' }); + const apiBRef = new ApiRef({ id: 'b', description: '' }); + + it('should forward implementations', () => { + const agg = new ApiAggregator( + ApiRegistry.from([ + [apiARef, 5], + [apiBRef, 10], + ]), + ); + expect(agg.get(apiARef)).toBe(5); + expect(agg.get(apiBRef)).toBe(10); + }); + + it('should return the first implementation', () => { + const agg = new ApiAggregator( + ApiRegistry.from([ + [apiARef, 1], + [apiARef, 2], + ]), + ); + expect(agg.get(apiARef)).toBe(2); + expect(agg.get(apiBRef)).toBe(undefined); + }); +}); diff --git a/packages/core/src/api/apis/ApiAggregator.ts b/packages/core/src/api/apis/ApiAggregator.ts new file mode 100644 index 0000000000..da49ee6488 --- /dev/null +++ b/packages/core/src/api/apis/ApiAggregator.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { ApiRef } from './ApiRef'; +import { ApiHolder } from './types'; + +/** + * An ApiHolder that queries multiple other holders from for + * an Api implementation, returning the first one encountered.. + */ +export class ApiAggregator implements ApiHolder { + private readonly holders: ApiHolder[]; + + constructor(...holders: ApiHolder[]) { + this.holders = holders; + } + + get(apiRef: ApiRef): T | undefined { + for (const holder of this.holders) { + const api = holder.get(apiRef); + if (api) { + return api; + } + } + return undefined; + } +} diff --git a/packages/core/src/api/apis/definitions/AppThemeApi.ts b/packages/core/src/api/apis/definitions/AppThemeApi.ts new file mode 100644 index 0000000000..738bae8a9f --- /dev/null +++ b/packages/core/src/api/apis/definitions/AppThemeApi.ts @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { ApiRef } from '../ApiRef'; +import { BackstageTheme } from '@backstage/theme'; +import { Observable } from '../../types'; + +/** + * Describes a theme provided by the app. + */ +export type AppTheme = { + /** + * ID used to remember theme selections. + */ + id: string; + + /** + * Title of the theme + */ + title: string; + + /** + * Theme variant + */ + variant: 'light' | 'dark'; + + /** + * The specialized MaterialUI theme instance. + */ + theme: BackstageTheme; +}; + +/** + * The AppThemeApi gives access to the current app theme, and allows switching + * to other options that have been registered as a part of the App. + */ +export type AppThemeApi = { + /** + * Get a list of available themes. + */ + getInstalledThemes(): AppTheme[]; + + /** + * Observe the currently selected theme. A value of undefined means no specific theme has been selected. + */ + activeThemeId$(): Observable; + + /** + * Get the current theme ID. Returns undefined if no specific theme is selected. + */ + getActiveThemeId(): string | undefined; + + /** + * Set a specific theme to use in the app, overriding the default theme selection. + * + * Clear the selection by passing in undefined. + */ + setActiveThemeId(themeId?: string): void; +}; + +export const appThemeApiRef = new ApiRef({ + id: 'core.apptheme', + description: 'API Used to configure the app theme, and enumerate options', +}); diff --git a/packages/core/src/api/apis/definitions/index.ts b/packages/core/src/api/apis/definitions/index.ts index 9e7f7534b9..2bb365b2ab 100644 --- a/packages/core/src/api/apis/definitions/index.ts +++ b/packages/core/src/api/apis/definitions/index.ts @@ -21,6 +21,7 @@ // If you think some API definition is missing, please open an Issue or send a PR! export * from './AlertApi'; +export * from './AppThemeApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.test.ts b/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.test.ts new file mode 100644 index 0000000000..0c23fe5219 --- /dev/null +++ b/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { AppTheme } from '../../definitions'; +import { AppThemeSelector } from './AppThemeSelector'; + +describe('AppThemeSelector', () => { + it('should should select new themes', async () => { + const selector = new AppThemeSelector([]); + + expect(selector.getInstalledThemes()).toEqual([]); + + const subFn = jest.fn(); + selector.activeThemeId$().subscribe(subFn); + expect(selector.getActiveThemeId()).toBe(undefined); + await 'wait a tick'; + expect(subFn).toHaveBeenLastCalledWith(undefined); + + selector.setActiveThemeId('x'); + expect(subFn).toHaveBeenLastCalledWith('x'); + expect(selector.getActiveThemeId()).toBe('x'); + + selector.setActiveThemeId(undefined); + expect(subFn).toHaveBeenLastCalledWith(undefined); + expect(selector.getActiveThemeId()).toBe(undefined); + }); + + it('should return a new array of themes', () => { + const themes = new Array(); + const selector = new AppThemeSelector(themes); + + expect(selector.getInstalledThemes()).toEqual(themes); + expect(selector.getInstalledThemes()).not.toBe(themes); + }); + + it('should store theme in local storage', async () => { + expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe( + undefined, + ); + localStorage.setItem('theme', 'x'); + expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe('x'); + localStorage.removeItem('theme'); + expect(AppThemeSelector.createWithStorage([]).getActiveThemeId()).toBe( + undefined, + ); + + const addListenerSpy = jest.spyOn(window, 'addEventListener'); + const selector = AppThemeSelector.createWithStorage([]); + + expect(addListenerSpy).toHaveBeenCalledTimes(1); + expect(addListenerSpy).toHaveBeenCalledWith( + 'storage', + expect.any(Function), + ); + + selector.setActiveThemeId('y'); + await 'wait a tick'; + expect(localStorage.getItem('theme')).toBe('y'); + + selector.setActiveThemeId(undefined); + await 'wait a tick'; + expect(localStorage.getItem('theme')).toBe(null); + + localStorage.setItem('theme', 'z'); + expect(selector.getActiveThemeId()).toBe(undefined); + + const listener = addListenerSpy.mock.calls[0][1] as EventListener; + listener({ key: 'theme' } as StorageEvent); + + expect(selector.getActiveThemeId()).toBe('z'); + }); +}); diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts b/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts new file mode 100644 index 0000000000..cc1146b336 --- /dev/null +++ b/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 Observable from 'zen-observable'; +import { AppThemeApi, AppTheme } from '../../definitions'; + +const STORAGE_KEY = 'theme'; + +export class AppThemeSelector implements AppThemeApi { + static createWithStorage(themes: AppTheme[]) { + const selector = new AppThemeSelector(themes); + + if (!window.localStorage) { + return selector; + } + + const initialThemeId = + window.localStorage.getItem(STORAGE_KEY) ?? undefined; + + selector.setActiveThemeId(initialThemeId); + + selector.activeThemeId$().subscribe((themeId) => { + if (themeId) { + window.localStorage.setItem(STORAGE_KEY, themeId); + } else { + window.localStorage.removeItem(STORAGE_KEY); + } + }); + + window.addEventListener('storage', (event) => { + if (event.key === STORAGE_KEY) { + const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined; + selector.setActiveThemeId(themeId); + } + }); + + return selector; + } + + private readonly themes: AppTheme[]; + + private activeThemeId: string | undefined; + + private readonly observable: Observable; + private readonly subscribers = new Set< + ZenObservable.SubscriptionObserver + >(); + + constructor(themes: AppTheme[]) { + this.themes = themes; + + this.observable = new Observable((subscriber) => { + subscriber.next(this.activeThemeId); + + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); + } + + getInstalledThemes(): AppTheme[] { + return this.themes.slice(); + } + + activeThemeId$(): Observable { + return this.observable; + } + + getActiveThemeId(): string | undefined { + return this.activeThemeId; + } + + setActiveThemeId(themeId?: string): void { + this.activeThemeId = themeId; + this.subscribers.forEach((subscriber) => subscriber.next(themeId)); + } +} diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/index.ts b/packages/core/src/api/apis/implementations/AppThemeSelector/index.ts new file mode 100644 index 0000000000..cb42c0f875 --- /dev/null +++ b/packages/core/src/api/apis/implementations/AppThemeSelector/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export * from './AppThemeSelector'; diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core/src/api/apis/implementations/index.ts index ebd89b54c6..8334b068fc 100644 --- a/packages/core/src/api/apis/implementations/index.ts +++ b/packages/core/src/api/apis/implementations/index.ts @@ -18,6 +18,7 @@ // // Plugins should rely on these APIs for functionality as much as possible. +export * from './AppThemeSelector'; export * from './AlertApiForwarder'; export * from './ErrorApiForwarder'; export * from './OAuthRequestManager'; diff --git a/packages/core/src/api/app/App.tsx b/packages/core/src/api/app/App.tsx index c44454b10e..128f978139 100644 --- a/packages/core/src/api/app/App.tsx +++ b/packages/core/src/api/app/App.tsx @@ -22,6 +22,7 @@ import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef } from '../apis/definitions'; import ErrorPage from '../../layout/ErrorPage'; +import { AppThemeProvider } from './AppThemeProvider'; import { IconComponent, @@ -29,14 +30,24 @@ import { SystemIconKey, defaultSystemIcons, } from '../../icons'; -import { ApiHolder, ApiProvider, ApiRegistry } from '../apis'; +import { + ApiHolder, + ApiProvider, + ApiRegistry, + AppTheme, + AppThemeSelector, + appThemeApiRef, +} from '../apis'; import LoginPage from './LoginPage'; +import { lightTheme, darkTheme } from '@backstage/theme'; +import { ApiAggregator } from '../apis/ApiAggregator'; type FullAppOptions = { apis: ApiHolder; icons: SystemIcons; plugins: BackstagePlugin[]; components: AppComponents; + themes: AppTheme[]; }; class AppImpl implements BackstageApp { @@ -44,12 +55,14 @@ class AppImpl implements BackstageApp { private readonly icons: SystemIcons; private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; + private readonly themes: AppTheme[]; constructor(options: FullAppOptions) { this.apis = options.apis; this.icons = options.icons; this.plugins = options.plugins; this.components = options.components; + this.themes = options.themes; } getApis(): ApiHolder { @@ -129,23 +142,28 @@ class AppImpl implements BackstageApp { , ); - let rendered = ( + const rendered = ( {routes} ); - if (this.apis) { - rendered = ; - } - return () => rendered; } getProvider(): ComponentType<{}> { + const appApis = ApiRegistry.from([ + [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)], + ]); + const apis = new ApiAggregator(this.apis, appApis); + const Provider: FC<{}> = ({ children }) => ( - {children} + + + {children} + + ); return Provider; } @@ -178,8 +196,22 @@ export function createApp(options?: AppOptions) { NotFoundErrorPage: DefaultNotFoundPage, ...options?.components, }; + const themes = options?.themes ?? [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + theme: lightTheme, + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + theme: darkTheme, + }, + ]; - const app = new AppImpl({ apis, icons, plugins, components }); + const app = new AppImpl({ apis, icons, plugins, components, themes }); app.verify(); diff --git a/packages/core/src/api/app/AppThemeProvider.tsx b/packages/core/src/api/app/AppThemeProvider.tsx new file mode 100644 index 0000000000..6e68fe9ba0 --- /dev/null +++ b/packages/core/src/api/app/AppThemeProvider.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC, useMemo, useEffect, useState } from 'react'; +import { ThemeProvider, CssBaseline } from '@material-ui/core'; +import { useApi, appThemeApiRef, AppTheme } from '../apis'; +import { useObservable } from 'react-use'; + +// This tries to find the most accurate match, but also falls back to less +// accurate results in order to avoid errors. +function resolveTheme( + themeId: string | undefined, + shouldPreferDark: boolean, + themes: AppTheme[], +) { + if (themeId !== undefined) { + const selectedTheme = themes.find((theme) => theme.id === themeId); + if (selectedTheme) { + return selectedTheme; + } + } + + if (shouldPreferDark) { + const darkTheme = themes.find((theme) => theme.variant === 'dark'); + if (darkTheme) { + return darkTheme; + } + } + + const lightTheme = themes.find((theme) => theme.variant === 'light'); + if (lightTheme) { + return lightTheme; + } + + return themes[0]; +} + +const useShouldPreferDarkTheme = () => { + if (!window.matchMedia) { + return false; + } + + const mediaQuery = useMemo( + () => window.matchMedia('(prefers-color-scheme: dark)'), + [], + ); + const [shouldPreferDark, setPrefersDark] = useState(mediaQuery.matches); + + useEffect(() => { + const listener = (event: MediaQueryListEvent) => { + setPrefersDark(event.matches); + }; + mediaQuery.addListener(listener); + return () => { + mediaQuery.removeListener(listener); + }; + }, [mediaQuery]); + + return shouldPreferDark; +}; + +export const AppThemeProvider: FC<{}> = ({ children }) => { + const appThemeApi = useApi(appThemeApiRef); + const shouldPreferDark = useShouldPreferDarkTheme(); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + const appTheme = resolveTheme( + themeId, + shouldPreferDark, + appThemeApi.getInstalledThemes(), + ); + if (!appTheme) { + throw new Error('App has no themes'); + } + + return ( + + {children} + + ); +}; diff --git a/packages/core/src/api/app/LoginPage/LoginPage.tsx b/packages/core/src/api/app/LoginPage/LoginPage.tsx index 07160e750c..cdf0b55fd4 100644 --- a/packages/core/src/api/app/LoginPage/LoginPage.tsx +++ b/packages/core/src/api/app/LoginPage/LoginPage.tsx @@ -15,7 +15,7 @@ */ import React, { FC, useState } from 'react'; -import { GitHub as GitHubIcon } from '@material-ui/icons'; +import GitHubIcon from '@material-ui/icons/GitHub'; import Page from '../../../layout/Page'; import Header from '../../../layout/Header'; import Content from '../../../layout/Content/Content'; diff --git a/packages/core/src/api/app/types.ts b/packages/core/src/api/app/types.ts index e8d597e120..75a750ce6e 100644 --- a/packages/core/src/api/app/types.ts +++ b/packages/core/src/api/app/types.ts @@ -18,6 +18,7 @@ import { ComponentType } from 'react'; import { IconComponent, SystemIconKey, SystemIcons } from '../../icons'; import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; +import { AppTheme } from '../apis/definitions'; export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; @@ -45,6 +46,28 @@ export type AppOptions = { * Supply components to the app to override the default ones. */ components?: Partial; + + /** + * Themes provided as a part of the app. By default two themes are included, one + * light variant of the default backstage theme, and one dark. + * + * This is the default config: + * + * ``` + * [{ + * id: 'light', + * title: 'Light Theme', + * variant: 'light', + * theme: lightTheme, + * }, { + * id: 'dark', + * title: 'Dark Theme', + * variant: 'dark', + * theme: darkTheme, + * }] + * ``` + */ + themes?: AppTheme[]; }; export type BackstageApp = { diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx index 52db02177e..1695b5d8c3 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.stories.tsx @@ -72,7 +72,7 @@ export const Overflow = () => ( ); -export const Laguages = () => ( +export const Languages = () => ( <> diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index b7d0849736..44d3788443 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -27,23 +27,21 @@ import { BackstageTheme } from '@backstage/theme'; import { makeStyles, useTheme } from '@material-ui/core'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 -import { - AddBox, - ArrowUpward, - Check, - ChevronLeft, - ChevronRight, - Clear, - DeleteOutline, - Edit, - FilterList, - FirstPage, - LastPage, - Remove, - SaveAlt, - Search, - ViewColumn, -} from '@material-ui/icons'; +import AddBox from '@material-ui/icons/AddBox'; +import ArrowUpward from '@material-ui/icons/ArrowUpward'; +import Check from '@material-ui/icons/Check'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import DeleteOutline from '@material-ui/icons/DeleteOutline'; +import Edit from '@material-ui/icons/Edit'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import Remove from '@material-ui/icons/Remove'; +import SaveAlt from '@material-ui/icons/SaveAlt'; +import Search from '@material-ui/icons/Search'; +import ViewColumn from '@material-ui/icons/ViewColumn'; const tableIcons = { Add: forwardRef((props, ref: React.Ref) => ( diff --git a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx new file mode 100644 index 0000000000..7f4c8890f9 --- /dev/null +++ b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC } from 'react'; +import { useObservable } from 'react-use'; +import LightIcon from '@material-ui/icons/WbSunny'; +import DarkIcon from '@material-ui/icons/Brightness2'; +import AutoIcon from '@material-ui/icons/BrightnessAuto'; +import { appThemeApiRef, useApi } from '../../api'; +import { SidebarItem } from './Items'; + +export const SidebarThemeToggle: FC<{}> = () => { + const appThemeApi = useApi(appThemeApiRef); + const themeId = useObservable( + appThemeApi.activeThemeId$(), + appThemeApi.getActiveThemeId(), + ); + + let text = 'Auto'; + let icon = AutoIcon; + switch (themeId) { + case 'dark': + text = 'Dark mode'; + icon = DarkIcon; + break; + case 'light': + text = 'Light mode'; + icon = LightIcon; + break; + default: + break; + } + + const handleToggle = () => { + if (!themeId) { + appThemeApi.setActiveThemeId('light'); + } else if (themeId === 'light') { + appThemeApi.setActiveThemeId('dark'); + } else { + appThemeApi.setActiveThemeId(undefined); + } + }; + + return ; +}; diff --git a/packages/core/src/layout/Sidebar/index.ts b/packages/core/src/layout/Sidebar/index.ts index e4e0a2d991..f3f1a8872b 100644 --- a/packages/core/src/layout/Sidebar/index.ts +++ b/packages/core/src/layout/Sidebar/index.ts @@ -18,3 +18,4 @@ export * from './Bar'; export * from './Page'; export * from './Items'; export * from './config'; +export { SidebarThemeToggle } from './SidebarThemeToggle'; diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 3a69faa18f..90a0847b4d 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -18,7 +18,6 @@ import React, { FC, ComponentType } from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom'; import BookmarkIcon from '@material-ui/icons/Bookmark'; -import { ThemeProvider, CssBaseline } from '@material-ui/core'; import { createApp, SidebarPage, @@ -30,7 +29,6 @@ import { ApiTestRegistry, ApiHolder, } from '@backstage/core'; -import { lightTheme } from '@backstage/theme'; import * as defaultApiFactories from './apiFactories'; // TODO(rugvip): export proper plugin type from core that isn't the plugin class @@ -78,16 +76,12 @@ class DevAppBuilder { const DevApp: FC<{}> = () => { return ( - - - - - {sidebar} - - - - - + + + {sidebar} + + + ); }; diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 4a2216546f..bac67f4e47 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -21,12 +21,17 @@ import { Overrides } from '@material-ui/core/styles/overrides'; import { BackstageTheme, BackstageThemeOptions, - BackstagePaletteOptions, + SimpleThemeOptions, } from './types'; +const DEFAULT_FONT_FAMILY = + '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif'; + export function createThemeOptions( - palette: BackstagePaletteOptions, + options: SimpleThemeOptions, ): BackstageThemeOptions { + const { palette, fontFamily = DEFAULT_FONT_FAMILY } = options; + return { palette, props: { @@ -38,7 +43,7 @@ export function createThemeOptions( }, }, typography: { - fontFamily: '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif', + fontFamily, h5: { fontWeight: 700, }, @@ -205,8 +210,8 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { // Creates a Backstage MUI theme using a palette. // The theme is created with the common Backstage options and component styles. -export function createTheme(palette: BackstagePaletteOptions): BackstageTheme { - const themeOptions = createThemeOptions(palette); +export function createTheme(options: SimpleThemeOptions): BackstageTheme { + const themeOptions = createThemeOptions(options); const baseTheme = createMuiTheme(themeOptions) as BackstageTheme; const overrides = createThemeOverrides(baseTheme); const theme = { ...baseTheme, overrides }; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 33cefe3283..ff86cca330 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -18,81 +18,85 @@ import { createTheme } from './baseTheme'; import { blue, yellow } from '@material-ui/core/colors'; export const lightTheme = createTheme({ - type: 'light', - background: { - default: '#F8F8F8', - }, - status: { - ok: '#1DB954', - warning: '#FF9800', - error: '#E22134', - running: '#2E77D0', - pending: '#FFED51', - aborted: '#757575', - }, - bursts: { - fontColor: '#FEFEFE', - slackChannelText: '#ddd', - backgroundColor: { - default: '#7C3699', + palette: { + type: 'light', + background: { + default: '#F8F8F8', }, + status: { + ok: '#1DB954', + warning: '#FF9800', + error: '#E22134', + running: '#2E77D0', + pending: '#FFED51', + aborted: '#757575', + }, + bursts: { + fontColor: '#FEFEFE', + slackChannelText: '#ddd', + backgroundColor: { + default: '#7C3699', + }, + }, + primary: { + main: blue[500], + }, + border: '#E6E6E6', + textContrast: '#000000', + textVerySubtle: '#DDD', + textSubtle: '#6E6E6E', + highlight: '#FFFBCC', + errorBackground: '#FFEBEE', + warningBackground: '#F59B23', + infoBackground: '#ebf5ff', + errorText: '#CA001B', + infoText: '#004e8a', + warningText: '#FEFEFE', + linkHover: '#2196F3', + link: '#0A6EBE', + gold: yellow.A700, + sidebar: '#171717', }, - primary: { - main: blue[500], - }, - border: '#E6E6E6', - textContrast: '#000000', - textVerySubtle: '#DDD', - textSubtle: '#6E6E6E', - highlight: '#FFFBCC', - errorBackground: '#FFEBEE', - warningBackground: '#F59B23', - infoBackground: '#ebf5ff', - errorText: '#CA001B', - infoText: '#004e8a', - warningText: '#FEFEFE', - linkHover: '#2196F3', - link: '#0A6EBE', - gold: yellow.A700, - sidebar: '#171717', }); export const darkTheme = createTheme({ - type: 'dark', - background: { - default: '#333333', - }, - status: { - ok: '#1DB954', - warning: '#FF9800', - error: '#E22134', - running: '#2E77D0', - pending: '#FFED51', - aborted: '#757575', - }, - bursts: { - fontColor: '#FEFEFE', - slackChannelText: '#ddd', - backgroundColor: { - default: '#7C3699', + palette: { + type: 'dark', + background: { + default: '#333333', }, + status: { + ok: '#1DB954', + warning: '#FF9800', + error: '#E22134', + running: '#2E77D0', + pending: '#FFED51', + aborted: '#757575', + }, + bursts: { + fontColor: '#FEFEFE', + slackChannelText: '#ddd', + backgroundColor: { + default: '#7C3699', + }, + }, + primary: { + main: blue[500], + }, + border: '#E6E6E6', + textContrast: '#FFFFFF', + textVerySubtle: '#DDD', + textSubtle: '#EEEEEE', + highlight: '#FFFBCC', + errorBackground: '#FFEBEE', + warningBackground: '#F59B23', + infoBackground: '#ebf5ff', + errorText: '#CA001B', + infoText: '#004e8a', + warningText: '#FEFEFE', + linkHover: '#2196F3', + link: '#0A6EBE', + gold: yellow.A700, + sidebar: '#424242', }, - primary: { - main: blue[500], - }, - border: '#E6E6E6', - textContrast: '#FFFFFF', - textVerySubtle: '#DDD', - textSubtle: '#EEEEEE', - highlight: '#FFFBCC', - errorBackground: '#FFEBEE', - warningBackground: '#F59B23', - infoBackground: '#ebf5ff', - errorText: '#CA001B', - infoText: '#004e8a', - warningText: '#FEFEFE', - linkHover: '#2196F3', - link: '#0A6EBE', - gold: yellow.A700, - sidebar: '#424242', }); diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 82c717e433..d11b07d3fe 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -63,3 +63,11 @@ export interface BackstageTheme extends Theme { export interface BackstageThemeOptions extends ThemeOptions { palette: BackstagePaletteOptions; } + +/** + * A simpler configuration for creating a new theme that just tweaks some parts of the backstage one. + */ +export type SimpleThemeOptions = { + palette: BackstagePaletteOptions; + fontFamily?: string; +}; diff --git a/plugins/auth-backend/.eslintrc.js b/plugins/auth-backend/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/auth-backend/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md new file mode 100644 index 0000000000..f450332e8d --- /dev/null +++ b/plugins/auth-backend/README.md @@ -0,0 +1,12 @@ +# Auth Backend + +WORK IN PROGRESS + +This is the backend part of the auth plugin. + +It responds to auth requests from the frontend, and fulfills them by delegating +to the appropriate provider in the backend. + +## Links + +- (The Backstage homepage)[https://backstage.io] diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json new file mode 100644 index 0000000000..99998dc6ba --- /dev/null +++ b/plugins/auth-backend/package.json @@ -0,0 +1,37 @@ +{ + "name": "@backstage/plugin-auth-backend", + "version": "0.1.1-alpha.5", + "main": "dist", + "license": "Apache-2.0", + "private": true, + "scripts": { + "start": "tsc-watch --onFirstSuccess \"cross-env NODE_ENV=development nodemon dist/run.js\"", + "build": "tsc", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^0.1.1-alpha.5", + "compression": "^1.7.4", + "cors": "^2.8.5", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "fs-extra": "^9.0.0", + "helmet": "^3.22.0", + "morgan": "^1.10.0", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.5", + "jest-fetch-mock": "^3.0.3", + "tsc-watch": "^4.2.3" + }, + "files": [ + "dist" + ], + "nodemonConfig": { + "watch": "./dist" + } +} diff --git a/plugins/auth-backend/src/index.test.ts b/plugins/auth-backend/src/index.test.ts new file mode 100644 index 0000000000..b3e2f19771 --- /dev/null +++ b/plugins/auth-backend/src/index.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +describe('test', () => { + it('unbreaks the test runner', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/plugins/auth-backend/src/index.ts b/plugins/auth-backend/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/plugins/auth-backend/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +export * from './service/router'; diff --git a/plugins/auth-backend/src/run.ts b/plugins/auth-backend/src/run.ts new file mode 100644 index 0000000000..0a684c379e --- /dev/null +++ b/plugins/auth-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 yn from 'yn'; +import { getRootLogger } from '@backstage/backend-common'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch((err) => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts new file mode 100644 index 0000000000..6d0282738a --- /dev/null +++ b/plugins/auth-backend/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const logger = options.logger.child({ plugin: 'auth' }); + const router = Router(); + + router.get('/ping', async (req, res) => { + res.status(200).send('pong'); + }); + + const app = express(); + app.set('logger', logger); + app.use(router); + + return app; +} diff --git a/plugins/auth-backend/src/service/standaloneApplication.ts b/plugins/auth-backend/src/service/standaloneApplication.ts new file mode 100644 index 0000000000..5eea4fb8f5 --- /dev/null +++ b/plugins/auth-backend/src/service/standaloneApplication.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { + errorHandler, + notFoundHandler, + requestLoggingHandler, +} from '@backstage/backend-common'; +import compression from 'compression'; +import cors from 'cors'; +import express from 'express'; +import helmet from 'helmet'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ApplicationOptions { + enableCors: boolean; + logger: Logger; +} + +export async function createStandaloneApplication( + options: ApplicationOptions, +): Promise { + const { enableCors, logger } = options; + const app = express(); + + app.use(helmet()); + if (enableCors) { + app.use(cors()); + } + app.use(compression()); + app.use(express.json()); + app.use(requestLoggingHandler()); + app.use('/', await createRouter({ logger })); + app.use(notFoundHandler()); + app.use(errorHandler()); + + return app; +} diff --git a/plugins/auth-backend/src/service/standaloneServer.ts b/plugins/auth-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..be1021d604 --- /dev/null +++ b/plugins/auth-backend/src/service/standaloneServer.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 { Server } from 'http'; +import { Logger } from 'winston'; +import { createStandaloneApplication } from './standaloneApplication'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'auth-backend' }); + + logger.debug('Creating application...'); + const app = await createStandaloneApplication({ + enableCors: options.enableCors, + logger, + }); + + logger.debug('Starting application server...'); + return await new Promise((resolve, reject) => { + const server = app.listen(options.port, (err?: Error) => { + if (err) { + reject(err); + return; + } + + logger.info(`Listening on port ${options.port}`); + resolve(server); + }); + }); +} diff --git a/plugins/auth-backend/src/setupTests.ts b/plugins/auth-backend/src/setupTests.ts new file mode 100644 index 0000000000..3fa7cb04b4 --- /dev/null +++ b/plugins/auth-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * 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. + */ + +require('jest-fetch-mock').enableMocks(); diff --git a/plugins/auth-backend/tsconfig.json b/plugins/auth-backend/tsconfig.json new file mode 100644 index 0000000000..015a967f76 --- /dev/null +++ b/plugins/auth-backend/tsconfig.json @@ -0,0 +1,15 @@ +{ + "include": ["src"], + "compilerOptions": { + "outDir": "dist", + "incremental": true, + "sourceMap": true, + "declaration": true, + "strict": true, + "target": "es2019", + "module": "commonjs", + "esModuleInterop": true, + "lib": ["es2019"], + "types": ["node", "jest"] + } +} diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 1987dea1f5..5cdb6b715e 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.5", "@backstage/dev-utils": "^0.1.1-alpha.5", + "@backstage/test-utils": "^0.1.1-alpha.5", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 01d821e175..d018d59837 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -19,12 +19,19 @@ import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; +import { ComponentFactory } from '../../data/component'; + +const testComponentFactory: ComponentFactory = { + getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), + getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), + removeComponentByName: jest.fn(() => Promise.resolve(true)), +}; describe('CatalogPage', () => { it('should render', async () => { const rendered = render( - + , ); expect(await rendered.findByText('Your components')).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index 9972b9d71f..7889c5e0c6 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -18,12 +18,12 @@ import React, { FC } from 'react'; import { Content, Header, Page, pageTheme } from '@backstage/core'; import { useAsync } from 'react-use'; import { ComponentFactory } from '../../data/component'; -import { MockComponentFactory } from '../../data/mock-factory'; import CatalogTable from '../CatalogTable/CatalogTable'; -const componentFactory: ComponentFactory = MockComponentFactory; - -const CatalogPage: FC<{}> = () => { +type CatalogPageProps = { + componentFactory: ComponentFactory; +}; +const CatalogPage: FC = ({ componentFactory }) => { const { value, error, loading } = useAsync(componentFactory.getAllComponents); return ( diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 31f15fe5ca..ea23281262 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -16,13 +16,16 @@ import React, { FC } from 'react'; import { Component } from '../../data/component'; import { InfoCard, Progress, Table, TableColumn } from '@backstage/core'; -import { Typography } from '@material-ui/core'; +import { Typography, Link } from '@material-ui/core'; const columns: TableColumn[] = [ { title: 'Name', field: 'name', highlight: true, + render: (componentData: any) => ( + {componentData.name} + ), }, ]; diff --git a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.test.tsx b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.test.tsx new file mode 100644 index 0000000000..cd9bb95b23 --- /dev/null +++ b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.test.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 ComponentContextMenu from './ComponentContextMenu'; +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { act } from 'react-dom/test-utils'; + +describe('ComponentContextMenu', () => { + it('should call onUnregisterComponent on button click', async () => { + await act(async () => { + const mockCallback = jest.fn(); + const menu = await render( + , + ); + const button = await menu.findByTestId('menu-button'); + button.click(); + const unregister = await menu.findByText('Unregister component'); + expect(unregister).toBeInTheDOM(); + }); + }); +}); diff --git a/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx new file mode 100644 index 0000000000..674d979f83 --- /dev/null +++ b/plugins/catalog/src/components/ComponentContextMenu/ComponentContextMenu.tsx @@ -0,0 +1,90 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC, useEffect, useRef, useState } from 'react'; +import { + IconButton, + ListItemIcon, + Menu, + MenuItem, + Typography, +} from '@material-ui/core'; +import Cancel from '@material-ui/icons/Cancel'; +import MoreVert from '@material-ui/icons/MoreVert'; +import SwapHoriz from '@material-ui/icons/SwapHoriz'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + menu: { + marginTop: 52, + }, +}); + +type ComponentContextMenuProps = { + onUnregisterComponent: () => void; +}; + +const ComponentContextMenu: FC = ({ + onUnregisterComponent, +}) => { + const [menuOpen, setMenuOpen] = useState(false); + const menuAnchor = useRef(null); + const classes = useStyles(); + + useEffect(() => { + const globalCloseHandler = (event: any) => { + const menu = menuAnchor.current; + if (menu !== null && !menu.contains(event.target)) { + setMenuOpen(false); + } + }; + + window.addEventListener('click', globalCloseHandler); + return () => window.removeEventListener('click', globalCloseHandler); + }, [menuOpen]); + + return ( +
+ setMenuOpen(!menuOpen)} + data-testid="menu-button" + > + + + + + + + + Unregister component + + + + + + More repository + + +
+ ); +}; +export default ComponentContextMenu; diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx new file mode 100644 index 0000000000..e190ee6994 --- /dev/null +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.test.tsx @@ -0,0 +1,37 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 ComponentMetadataCard from './ComponentMetadataCard'; +import { Component } from '../../data/component'; +import { render } from '@testing-library/react'; + +describe('ComponentMetadataCard component', () => { + it('should display component name if provided', async () => { + const testComponent: Component = { + name: 'test', + }; + const rendered = await render( + , + ); + expect(await rendered.findByText('test')).toBeInTheDOM(); + }); + it('should display loader when loading is set to true', async () => { + const rendered = await render( + , + ); + expect(await rendered.findByRole('progressbar')).toBeInTheDOM(); + }); +}); diff --git a/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx new file mode 100644 index 0000000000..9d606c5839 --- /dev/null +++ b/plugins/catalog/src/components/ComponentMetadataCard/ComponentMetadataCard.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC } from 'react'; +import { Component } from '../../data/component'; +import { Progress, InfoCard, StructuredMetadataTable } from '@backstage/core'; + +type ComponentMetadataCardProps = { + loading: boolean; + component: Component | undefined; +}; +const ComponentMetadataCard: FC = ({ + loading, + component, +}) => { + if (loading) { + return ; + } + if (!component) { + return null; + } + return ( + + + + ); +}; +export default ComponentMetadataCard; diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx new file mode 100644 index 0000000000..369b80de80 --- /dev/null +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 ComponentPage from './ComponentPage'; +import { render } from '@testing-library/react'; +import * as React from 'react'; +import { wrapInTheme } from '@backstage/test-utils'; +import { act } from 'react-dom/test-utils'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; + +const getTestProps = (componentName: string) => { + return { + match: { + params: { + name: componentName, + }, + }, + history: { + push: jest.fn(), + }, + componentFactory: { + getAllComponents: jest.fn(() => Promise.resolve([{ name: 'test' }])), + getComponentByName: jest.fn(() => Promise.resolve({ name: 'test' })), + removeComponentByName: jest.fn(() => Promise.resolve(true)), + }, + }; +}; + +const errorApi = { post: () => {} }; + +describe('ComponentPage', () => { + it('should redirect to component table page when name is not provided', async () => { + const props = getTestProps(''); + await render( + wrapInTheme( + + + , + ), + ); + expect(props.history.push).toHaveBeenCalledWith('/catalog'); + }); + it('should use factory to fetch component by name and display it', async () => { + await act(async () => { + const props = getTestProps('test'); + await render( + wrapInTheme( + + + , + ), + ); + expect(props.componentFactory.getComponentByName).toHaveBeenCalledWith( + 'test', + ); + }); + }); +}); diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx new file mode 100644 index 0000000000..6348e88c4f --- /dev/null +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC, useEffect, useState } from 'react'; +import { useAsync } from 'react-use'; +import { ComponentFactory } from '../../data/component'; +import ComponentMetadataCard from '../ComponentMetadataCard/ComponentMetadataCard'; +import { + Content, + Header, + pageTheme, + Page, + useApi, + ErrorApi, + errorApiRef, +} from '@backstage/core'; +import ComponentContextMenu from '../ComponentContextMenu/ComponentContextMenu'; +import ComponentRemovalDialog from '../ComponentRemovalDialog/ComponentRemovalDialog'; + +const REDIRECT_DELAY = 1000; + +type ComponentPageProps = { + componentFactory: ComponentFactory; + match: { + params: { + name: string; + }; + }; + history: { + push: (url: string) => void; + }; +}; + +const ComponentPage: FC = ({ + match, + history, + componentFactory, +}) => { + const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false); + const [removingPending, setRemovingPending] = useState(false); + const showRemovalDialog = () => setConfirmationDialogOpen(true); + const hideRemovalDialog = () => setConfirmationDialogOpen(false); + const componentName = match.params.name; + const errorApi = useApi(errorApiRef); + + if (componentName === '') { + history.push('/catalog'); + return null; + } + + const catalogRequest = useAsync(() => + componentFactory.getComponentByName(match.params.name), + ); + + useEffect(() => { + if (catalogRequest.error) { + errorApi.post(new Error('Component not found!')); + setTimeout(() => { + history.push('/catalog'); + }, REDIRECT_DELAY); + } + }, [catalogRequest.error]); + + const removeComponent = async () => { + setConfirmationDialogOpen(false); + setRemovingPending(true); + await componentFactory.removeComponentByName(componentName); + history.push('/catalog'); + }; + + return ( + +
+ +
+ {confirmationDialogOpen && catalogRequest.value && ( + + )} + + + +
+ ); +}; +export default ComponentPage; diff --git a/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx new file mode 100644 index 0000000000..b5a863cef8 --- /dev/null +++ b/plugins/catalog/src/components/ComponentRemovalDialog/ComponentRemovalDialog.tsx @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Spotify AB + * + * 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, { FC } from 'react'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + useMediaQuery, + useTheme, +} from '@material-ui/core'; +import { Component } from '../../data/component'; + +type ComponentRemovalDialogProps = { + onConfirm: () => any; + onCancel: () => any; + onClose: () => any; + component: Component; +}; +const ComponentRemovalDialog: FC = ({ + onConfirm, + onCancel, + onClose, + component, +}) => { + const theme = useTheme(); + const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); + return ( + + + Are you sure you want to unregister this component? + + + + This action will unregister {component.name}. To undo, just + re-register the component in Backstage. + + + + + + + + ); +}; +export default ComponentRemovalDialog; diff --git a/plugins/catalog/src/data/component.ts b/plugins/catalog/src/data/component.ts index 5b67b647e4..ec6e2368b8 100644 --- a/plugins/catalog/src/data/component.ts +++ b/plugins/catalog/src/data/component.ts @@ -20,4 +20,5 @@ export type Component = { export interface ComponentFactory { getAllComponents(): Promise; getComponentByName(name: string): Promise; + removeComponentByName(name: string): Promise; } diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts index 010036d732..bf39fa436c 100644 --- a/plugins/catalog/src/data/mock-factory.ts +++ b/plugins/catalog/src/data/mock-factory.ts @@ -16,16 +16,33 @@ import { Component, ComponentFactory } from './component'; import mock from './mock-factory-data.json'; +const ARTIFICIAL_TIMEOUT = 800; +let inMemoryStore = [...mock]; export const MockComponentFactory: ComponentFactory = { getAllComponents(): Promise { - return new Promise((resolve) => setTimeout(() => resolve(mock), 2000)); + return new Promise((resolve) => + setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), + ); }, getComponentByName(name: string): Promise { + return new Promise((resolve, reject) => + setTimeout(() => { + const mockComponent = inMemoryStore.find( + (component) => component.name === name, + ); + if (mockComponent) return resolve(mockComponent); + return reject({ code: 'Component not found!' }); + }, ARTIFICIAL_TIMEOUT), + ); + }, + removeComponentByName(name: string): Promise { return new Promise((resolve) => - setTimeout( - () => resolve(mock.find((component) => component.name === name)), - 2000, - ), + setTimeout(() => { + inMemoryStore = inMemoryStore.filter( + (component) => component.name !== name, + ); + resolve(true); + }, ARTIFICIAL_TIMEOUT), ); }, }; diff --git a/plugins/catalog/src/data/with-mock-store.tsx b/plugins/catalog/src/data/with-mock-store.tsx new file mode 100644 index 0000000000..2e5425d03e --- /dev/null +++ b/plugins/catalog/src/data/with-mock-store.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2020 Spotify AB + * + * 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 * as React from 'react'; +import { ComponentFactory } from './component'; +import { MockComponentFactory } from './mock-factory'; + +const componentFactory: ComponentFactory = MockComponentFactory; + +export const withMockStore = (Component: React.ElementType) => { + return (props: any) => ( + + ); +}; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index ff3a53ce5b..456a6d2446 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -16,10 +16,13 @@ import { createPlugin } from '@backstage/core'; import CatalogPage from './components/CatalogPage'; +import ComponentPage from './components/ComponentPage/ComponentPage'; +import { withMockStore } from './data/with-mock-store'; export const plugin = createPlugin({ id: 'catalog', register({ router }) { - router.registerRoute('/catalog', CatalogPage); + router.registerRoute('/catalog', withMockStore(CatalogPage)); + router.registerRoute('/catalog/:name/', withMockStore(ComponentPage)); }, }); diff --git a/yarn.lock b/yarn.lock index adf2cb670f..3e60308ab5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4358,9 +4358,9 @@ immutable ">=3.8.2" "@types/react-router-dom@^5.1.3": - version "5.1.3" - resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196" - integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA== + version "5.1.5" + resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090" + integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw== dependencies: "@types/history" "*" "@types/react" "*"