core-components: provide withDefaults and work around type duplication
Co-authored-by: Johan Haals <johan.haals@gmail.com> Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+40
-42
@@ -31,8 +31,7 @@ import {
|
||||
AlertDisplay,
|
||||
OAuthRequestDialog,
|
||||
SignInPage,
|
||||
defaultAppIcons,
|
||||
defaultAppComponents,
|
||||
withDefaults,
|
||||
} from '@backstage/core-components';
|
||||
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
|
||||
import {
|
||||
@@ -88,47 +87,46 @@ import * as plugins from './plugins';
|
||||
|
||||
import { techDocsPage } from './components/techdocs/TechDocsPage';
|
||||
|
||||
const app = createApp({
|
||||
apis,
|
||||
plugins: Object.values(plugins),
|
||||
icons: {
|
||||
...defaultAppIcons(),
|
||||
// Custom icon example
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
|
||||
components: {
|
||||
...defaultAppComponents(),
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
/>
|
||||
);
|
||||
const app = createApp(
|
||||
withDefaults({
|
||||
apis,
|
||||
plugins: Object.values(plugins),
|
||||
icons: {
|
||||
// Custom icon example
|
||||
alert: AlarmIcon,
|
||||
},
|
||||
},
|
||||
bindRoutes({ bind }) {
|
||||
bind(catalogPlugin.externalRoutes, {
|
||||
createComponent: scaffolderPlugin.routes.root,
|
||||
viewTechDoc: techdocsPlugin.routes.docRoot,
|
||||
});
|
||||
bind(catalogGraphPlugin.externalRoutes, {
|
||||
catalogEntity: catalogPlugin.routes.catalogEntity,
|
||||
});
|
||||
bind(apiDocsPlugin.externalRoutes, {
|
||||
createComponent: scaffolderPlugin.routes.root,
|
||||
});
|
||||
bind(explorePlugin.externalRoutes, {
|
||||
catalogEntity: catalogPlugin.routes.catalogEntity,
|
||||
});
|
||||
bind(scaffolderPlugin.externalRoutes, {
|
||||
registerComponent: catalogImportPlugin.routes.importPage,
|
||||
});
|
||||
},
|
||||
});
|
||||
components: {
|
||||
SignInPage: props => {
|
||||
return (
|
||||
<SignInPage
|
||||
{...props}
|
||||
providers={['guest', 'custom', ...providers]}
|
||||
title="Select a sign-in method"
|
||||
align="center"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
bindRoutes({ bind }) {
|
||||
bind(catalogPlugin.externalRoutes, {
|
||||
createComponent: scaffolderPlugin.routes.root,
|
||||
viewTechDoc: techdocsPlugin.routes.docRoot,
|
||||
});
|
||||
bind(catalogGraphPlugin.externalRoutes, {
|
||||
catalogEntity: catalogPlugin.routes.catalogEntity,
|
||||
});
|
||||
bind(apiDocsPlugin.externalRoutes, {
|
||||
createComponent: scaffolderPlugin.routes.root,
|
||||
});
|
||||
bind(explorePlugin.externalRoutes, {
|
||||
catalogEntity: catalogPlugin.routes.catalogEntity,
|
||||
});
|
||||
bind(scaffolderPlugin.externalRoutes, {
|
||||
registerComponent: catalogImportPlugin.routes.importPage,
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
|
||||
@@ -16,20 +16,15 @@
|
||||
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import {
|
||||
defaultAppComponents,
|
||||
defaultAppIcons,
|
||||
defaultAppThemes,
|
||||
} from '@backstage/core-components';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { withDefaults } from '@backstage/core-components';
|
||||
import { PrivateAppImpl } from './App';
|
||||
import { AppThemeProvider } from './AppThemeProvider';
|
||||
import { AppComponents, AppConfigLoader, AppOptions } from './types';
|
||||
import { defaultApis } from './defaultApis';
|
||||
import { AppConfigLoader, AppOptions } from './types';
|
||||
import { AppComponents, BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
const REQUIRED_APP_COMPONENTS: Array<keyof AppComponents> = [
|
||||
'Progress',
|
||||
'Router',
|
||||
'NotFoundErrorPage',
|
||||
'BootErrorPage',
|
||||
'ErrorBoundaryFallback',
|
||||
@@ -95,6 +90,8 @@ export const defaultConfigLoader: AppConfigLoader = async (
|
||||
* @public
|
||||
*/
|
||||
export function createApp(options?: AppOptions) {
|
||||
const optionsWithDefaults = withDefaults(options);
|
||||
|
||||
const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter(
|
||||
name => !options?.components?.[name],
|
||||
);
|
||||
@@ -111,9 +108,8 @@ export function createApp(options?: AppOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
const appIcons = defaultAppIcons();
|
||||
const providedIconKeys = Object.keys(options?.icons ?? {});
|
||||
const missingIconKeys = Object.keys(appIcons).filter(
|
||||
const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter(
|
||||
key => !providedIconKeys.includes(key),
|
||||
);
|
||||
if (missingIconKeys.length > 0) {
|
||||
@@ -135,24 +131,16 @@ export function createApp(options?: AppOptions) {
|
||||
);
|
||||
}
|
||||
|
||||
const apis = options?.apis ?? [];
|
||||
const plugins = options?.plugins ?? [];
|
||||
const components = {
|
||||
...defaultAppComponents(),
|
||||
Router: BrowserRouter,
|
||||
ThemeProvider: AppThemeProvider,
|
||||
...options?.components,
|
||||
};
|
||||
const configLoader = options?.configLoader ?? defaultConfigLoader;
|
||||
const { icons, themes, components } = optionsWithDefaults;
|
||||
|
||||
return new PrivateAppImpl({
|
||||
apis,
|
||||
icons: { ...appIcons, ...options?.icons },
|
||||
plugins: plugins as BackstagePlugin<any, any>[],
|
||||
components,
|
||||
themes: options?.themes ?? defaultAppThemes(),
|
||||
configLoader,
|
||||
icons: icons!,
|
||||
themes: themes!,
|
||||
components: components! as AppComponents,
|
||||
defaultApis,
|
||||
apis: options?.apis ?? [],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
plugins: (options?.plugins as BackstagePlugin<any, any>[]) ?? [],
|
||||
configLoader: options?.configLoader ?? defaultConfigLoader,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2020 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, { useMemo, useEffect, useState, PropsWithChildren } from 'react';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { useApi, appThemeApiRef, AppTheme } from '@backstage/core-plugin-api';
|
||||
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 = () => {
|
||||
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 function AppThemeProvider({ children }: PropsWithChildren<{}>) {
|
||||
const appThemeApi = useApi(appThemeApiRef);
|
||||
const themeId = useObservable(
|
||||
appThemeApi.activeThemeId$(),
|
||||
appThemeApi.getActiveThemeId(),
|
||||
);
|
||||
|
||||
// Browser feature detection won't change over time, so ignore lint rule
|
||||
const shouldPreferDark = Boolean(window.matchMedia)
|
||||
? useShouldPreferDarkTheme() // eslint-disable-line react-hooks/rules-of-hooks
|
||||
: false;
|
||||
|
||||
const appTheme = resolveTheme(
|
||||
themeId,
|
||||
shouldPreferDark,
|
||||
appThemeApi.getInstalledThemes(),
|
||||
);
|
||||
if (!appTheme) {
|
||||
throw new Error('App has no themes');
|
||||
}
|
||||
|
||||
if (appTheme.Provider) {
|
||||
return <appTheme.Provider children={children} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
"DEPRECATION WARNING: A provided app theme is using the deprecated 'theme' property " +
|
||||
'and should be migrated to use a Provider instead. ' +
|
||||
'See https://backstage.io/docs/deprecations/TODO for more info.',
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={appTheme.theme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
+8
-4
@@ -16,14 +16,16 @@
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { ErrorPanel, Progress } from '../components';
|
||||
import { ErrorPage } from '../layout';
|
||||
import { MemoryRouter, useInRouterContext } from 'react-router';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import {
|
||||
AppComponents,
|
||||
BootErrorPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ErrorPanel, Progress } from './components';
|
||||
import { ErrorPage } from './layout';
|
||||
import { MemoryRouter, useInRouterContext } from 'react-router';
|
||||
import { AppThemeProvider } from './AppThemeProvider';
|
||||
|
||||
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
|
||||
if (useInRouterContext()) {
|
||||
@@ -73,9 +75,11 @@ const DefaultErrorBoundaryFallback = ({
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function defaultAppComponents(): Omit<AppComponents, 'Router'> {
|
||||
export function defaultAppComponents(): AppComponents {
|
||||
return {
|
||||
Progress,
|
||||
Router: BrowserRouter,
|
||||
ThemeProvider: AppThemeProvider,
|
||||
NotFoundErrorPage: DefaultNotFoundPage,
|
||||
BootErrorPage: DefaultBootErrorPage,
|
||||
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export { withDefaults } from './withDefaults';
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2021 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 { AppTheme, IconComponent } from '@backstage/core-plugin-api';
|
||||
// This is a bit of a hack that we use to avoid having to redeclare these types
|
||||
// within this package or have an explicit dependency on core-app-api.
|
||||
// These types end up being inlined and duplicated into this package at build time.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
AppIcons,
|
||||
AppComponents,
|
||||
AppOptions,
|
||||
} from '../../../core-app-api/src/app';
|
||||
import { defaultAppComponents } from './defaultAppComponents';
|
||||
import { defaultAppIcons } from './defaultAppIcons';
|
||||
import { defaultAppThemes } from './defaultAppThemes';
|
||||
|
||||
interface OptionalAppOptions {
|
||||
icons?: Partial<AppIcons> & {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
|
||||
components?: Partial<AppComponents>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options required by {@link @backstage/core-app-api#createApp}, but with
|
||||
* many of the fields being optional
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
type DefaultAppOptions = Omit<AppOptions, keyof OptionalAppOptions> &
|
||||
OptionalAppOptions;
|
||||
|
||||
/**
|
||||
* Provides a set of default App options with the ability to override specific options.
|
||||
*
|
||||
* These options populate the theme, icons and components options of {@link @backstage/core-app-api#AppOptions}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function withDefaults(options?: DefaultAppOptions): AppOptions {
|
||||
const { themes, icons, components } = options ?? {};
|
||||
|
||||
return {
|
||||
...options,
|
||||
themes: themes ?? defaultAppThemes(),
|
||||
icons: { ...defaultAppIcons(), ...icons },
|
||||
components: { ...defaultAppComponents(), ...components },
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,4 @@ export * from './hooks';
|
||||
export * from './icons';
|
||||
export * from './layout';
|
||||
export * from './overridableComponents';
|
||||
export { defaultAppComponents } from './defaultAppComponents';
|
||||
export { defaultAppIcons } from './defaultAppIcons';
|
||||
export { defaultAppThemes } from './defaultAppThemes';
|
||||
export * from './appDefaults';
|
||||
|
||||
@@ -14,110 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentType } from 'react';
|
||||
import { ProfileInfo } from '../apis/definitions';
|
||||
import { IconComponent } from '../icons';
|
||||
import { BackstagePlugin } from '../plugin/types';
|
||||
|
||||
/**
|
||||
* Props for the BootErrorPage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BootErrorPageProps = {
|
||||
step: 'load-config' | 'load-chunk';
|
||||
error: Error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Data and handlers associated with the user sign in event.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SignInResult = {
|
||||
/**
|
||||
* User ID that will be returned by the IdentityApi
|
||||
*/
|
||||
userId: string;
|
||||
|
||||
profile: ProfileInfo;
|
||||
|
||||
/**
|
||||
* Function used to retrieve an ID token for the signed in user.
|
||||
*/
|
||||
getIdToken?: () => Promise<string>;
|
||||
|
||||
/**
|
||||
* Sign out handler that will be called if the user requests to sign out.
|
||||
*/
|
||||
signOut?: () => Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the SignInPage.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SignInPageProps = {
|
||||
/**
|
||||
* Set the sign-in result for the app. This should only be called once.
|
||||
*/
|
||||
onResult(result: SignInResult): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for the ErrorBoundaryFallback.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ErrorBoundaryFallbackProps = {
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic app components.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppComponents = {
|
||||
NotFoundErrorPage: ComponentType<{}>;
|
||||
BootErrorPage: ComponentType<BootErrorPageProps>;
|
||||
Progress: ComponentType<{}>;
|
||||
Router: ComponentType<{}>;
|
||||
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
|
||||
|
||||
/**
|
||||
* An optional sign-in page that will be rendered instead of the AppRouter at startup.
|
||||
*
|
||||
* If a sign-in page is set, it will always be shown before the app, and it is up
|
||||
* to the sign-in page to handle e.g. saving of login methods for subsequent visits.
|
||||
*
|
||||
* The sign-in page will be displayed until it has passed up a result to the parent,
|
||||
* and which point the AppRouter and all of its children will be rendered instead.
|
||||
*/
|
||||
SignInPage?: ComponentType<SignInPageProps>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Provides plugins and components registered in the app.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppContext = {
|
||||
/**
|
||||
* Get a list of all plugins that are installed in the app.
|
||||
*/
|
||||
getPlugins(): BackstagePlugin<any, any>[];
|
||||
|
||||
/**
|
||||
* Get a common or custom icon for this app.
|
||||
*/
|
||||
getSystemIcon(key: string): IconComponent | undefined;
|
||||
|
||||
/**
|
||||
* Get the components registered for various purposes in the app.
|
||||
*/
|
||||
getComponents(): AppComponents;
|
||||
};
|
||||
// This is a bit of a hack that we use to avoid having to redeclare these types
|
||||
// within this package or have an explicit dependency on core-app-api.
|
||||
// These types end up being inlined and duplicated into this package at build time.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
export type {
|
||||
BootErrorPageProps,
|
||||
SignInResult,
|
||||
SignInPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
AppComponents,
|
||||
AppContext,
|
||||
} from '../../../core-app-api/src/app/types';
|
||||
|
||||
Reference in New Issue
Block a user