app-defaults: refactor things into folders and move things closer to home
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/core-components": "^0.7.1",
|
||||
"@backstage/config": "^0.1.10",
|
||||
"@backstage/core-app-api": "^0.1.11",
|
||||
"@backstage/core-plugin-api": "^0.1.11",
|
||||
"@backstage/theme": "^0.2.11",
|
||||
"@backstage/types": "^0.1.1",
|
||||
|
||||
@@ -18,84 +18,7 @@ import { screen } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { defaultConfigLoader, createApp } from './createApp';
|
||||
|
||||
(process as any).env = { NODE_ENV: 'test' };
|
||||
const anyEnv = process.env as any;
|
||||
const anyWindow = window as any;
|
||||
|
||||
describe('defaultConfigLoader', () => {
|
||||
afterEach(() => {
|
||||
delete anyEnv.APP_CONFIG;
|
||||
delete anyWindow.__APP_CONFIG__;
|
||||
});
|
||||
|
||||
it('loads static config', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
];
|
||||
|
||||
const configs = await defaultConfigLoader();
|
||||
expect(configs).toEqual([
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads runtime config', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'override-config' }, context: 'a' },
|
||||
{ data: { my: 'config' }, context: 'b' },
|
||||
];
|
||||
|
||||
const configs = await (defaultConfigLoader as any)(
|
||||
'{"my":"runtime-config"}',
|
||||
);
|
||||
expect(configs).toEqual([
|
||||
{ data: { my: 'override-config' }, context: 'a' },
|
||||
{ data: { my: 'config' }, context: 'b' },
|
||||
{ data: { my: 'runtime-config' }, context: 'env' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails to load invalid missing config', async () => {
|
||||
await expect(defaultConfigLoader()).rejects.toThrow(
|
||||
'No static configuration provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load invalid static config', async () => {
|
||||
anyEnv.APP_CONFIG = { my: 'invalid-config' };
|
||||
await expect(defaultConfigLoader()).rejects.toThrow(
|
||||
'Static configuration has invalid format',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load bad runtime config', async () => {
|
||||
anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }];
|
||||
|
||||
await expect((defaultConfigLoader as any)('}')).rejects.toThrow(
|
||||
'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
|
||||
);
|
||||
});
|
||||
|
||||
it('loads config from window.__APP_CONFIG__', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
];
|
||||
const windowConfig = { app: { configKey: 'config-value' } };
|
||||
anyWindow.__APP_CONFIG__ = windowConfig;
|
||||
|
||||
const configs = await defaultConfigLoader();
|
||||
|
||||
expect(configs).toEqual([
|
||||
...anyEnv.APP_CONFIG,
|
||||
{ context: 'window', data: windowConfig },
|
||||
]);
|
||||
});
|
||||
});
|
||||
import { createApp } from './createApp';
|
||||
|
||||
describe('Optional ThemeProvider', () => {
|
||||
it('should render app with user-provided ThemeProvider', async () => {
|
||||
|
||||
@@ -14,132 +14,81 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { withDefaults } from '@backstage/core-components';
|
||||
import { PrivateAppImpl } from './App';
|
||||
import { AppComponents, AppConfigLoader, AppOptions } from './types';
|
||||
import { defaultApis } from './defaultApis';
|
||||
import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
const REQUIRED_APP_COMPONENTS: Array<keyof AppComponents> = [
|
||||
'Progress',
|
||||
'Router',
|
||||
'NotFoundErrorPage',
|
||||
'BootErrorPage',
|
||||
'ErrorBoundaryFallback',
|
||||
];
|
||||
import { apis, components, configLoader, icons, themes } from './defaults';
|
||||
import {
|
||||
AppTheme,
|
||||
BackstagePlugin,
|
||||
IconComponent,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
AppComponents,
|
||||
AppOptions,
|
||||
AppIcons,
|
||||
PrivateAppImpl,
|
||||
} from '@backstage/core-app-api';
|
||||
|
||||
/**
|
||||
* The default config loader, which expects that config is available at compile-time
|
||||
* in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as
|
||||
* returned by the config loader.
|
||||
*
|
||||
* It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,
|
||||
* which can be rewritten at runtime to contain an additional JSON config object.
|
||||
* If runtime config is present, it will be placed first in the config array, overriding
|
||||
* other config values.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const defaultConfigLoader: AppConfigLoader = async (
|
||||
// This string may be replaced at runtime to provide additional config.
|
||||
// It should be replaced by a JSON-serialized config object.
|
||||
// It's a param so we can test it, but at runtime this will always fall back to default.
|
||||
runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',
|
||||
) => {
|
||||
const appConfig = process.env.APP_CONFIG;
|
||||
if (!appConfig) {
|
||||
throw new Error('No static configuration provided');
|
||||
}
|
||||
if (!Array.isArray(appConfig)) {
|
||||
throw new Error('Static configuration has invalid format');
|
||||
}
|
||||
const configs = appConfig.slice() as unknown as AppConfig[];
|
||||
|
||||
// Avoiding this string also being replaced at runtime
|
||||
if (
|
||||
runtimeConfigJson !==
|
||||
'__app_injected_runtime_config__'.toLocaleUpperCase('en-US')
|
||||
) {
|
||||
try {
|
||||
const data = JSON.parse(runtimeConfigJson) as JsonObject;
|
||||
if (Array.isArray(data)) {
|
||||
configs.push(...data);
|
||||
} else {
|
||||
configs.push({ data, context: 'env' });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load runtime configuration, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const windowAppConfig = (window as any).__APP_CONFIG__;
|
||||
if (windowAppConfig) {
|
||||
configs.push({
|
||||
context: 'window',
|
||||
data: windowAppConfig,
|
||||
});
|
||||
}
|
||||
return configs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new Backstage App.
|
||||
* Creates a new Backstage App using a default set of components, icons and themes unless
|
||||
* they are explicitly provided.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function createApp(options?: AppOptions) {
|
||||
const optionsWithDefaults = withDefaults(options);
|
||||
|
||||
const missingRequiredComponents = REQUIRED_APP_COMPONENTS.filter(
|
||||
name => !options?.components?.[name],
|
||||
);
|
||||
if (missingRequiredComponents.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'DEPRECATION WARNING: The createApp options will soon require a minimal set of components to ' +
|
||||
'be provided. You can use the default components by using withDefaults from @backstage/core-components ' +
|
||||
'like this: createApp(withDefaults({ ... })), or you can provide the components yourself. ' +
|
||||
`The following components are missing: ${missingRequiredComponents.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const providedIconKeys = Object.keys(options?.icons ?? {});
|
||||
const missingIconKeys = Object.keys(optionsWithDefaults.icons!).filter(
|
||||
key => !providedIconKeys.includes(key),
|
||||
);
|
||||
if (missingIconKeys.length > 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'DEPRECATION WARNING: The createApp options will soon require a minimal set of icons to ' +
|
||||
'be provided. You can use the default icons by using withDefaults from @backstage/core-components ' +
|
||||
'like this: createApp(withDefaults({ ... })), or you can provide the icons yourself. ' +
|
||||
`The following icons are missing: ${missingIconKeys.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!options?.themes) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'DEPRECATION WARNING: The createApp options will soon require the themes to be provided. ' +
|
||||
'You can use the default themes by using withDefaults from @backstage/core-components ' +
|
||||
'like this: createApp(withDefaults({ ... })), or you can provide the themes yourself. ',
|
||||
);
|
||||
}
|
||||
|
||||
const { icons, themes, components } = optionsWithDefaults;
|
||||
|
||||
return new PrivateAppImpl({
|
||||
icons: icons!,
|
||||
themes: themes!,
|
||||
components: components! as AppComponents,
|
||||
defaultApis,
|
||||
...options,
|
||||
apis: options?.apis ?? [],
|
||||
bindRoutes: options?.bindRoutes,
|
||||
components: {
|
||||
...components,
|
||||
...options?.components,
|
||||
},
|
||||
configLoader: options?.configLoader ?? configLoader,
|
||||
defaultApis: apis,
|
||||
icons: {
|
||||
...icons,
|
||||
...options?.icons,
|
||||
},
|
||||
plugins: (options?.plugins as BackstagePlugin<any, any>[]) ?? [],
|
||||
configLoader: options?.configLoader ?? defaultConfigLoader,
|
||||
themes: options?.themes ?? themes,
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: we don't re-export any of the types imported from core-app-api, as we
|
||||
// want them to be imported from there rather than core-components.
|
||||
|
||||
/**
|
||||
* The set of app options that will be populated by {@link withDefaults} if they
|
||||
* are not passed in explicitly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OptionalAppOptions {
|
||||
/**
|
||||
* A set of icons to override the default icons with.
|
||||
*
|
||||
* The override is applied for each icon individually.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
icons?: Partial<AppIcons> & {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of themes that override all of the default app themes.
|
||||
*
|
||||
* If this option is provided none of the default themes will be used.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[]; // TODO: simplify once AppTheme is updated
|
||||
|
||||
/**
|
||||
* A set of components to override the default components with.
|
||||
*
|
||||
* The override is applied for each icon individually.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
components?: Partial<AppComponents>;
|
||||
}
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ import {
|
||||
OneLoginAuth,
|
||||
UnhandledErrorForwarder,
|
||||
AtlassianAuth,
|
||||
} from '../apis';
|
||||
} from '@backstage/core-app-api';
|
||||
|
||||
import {
|
||||
createApiFactory,
|
||||
@@ -61,7 +61,7 @@ import {
|
||||
|
||||
import OAuth2Icon from '@material-ui/icons/AcUnit';
|
||||
|
||||
export const defaultApis = [
|
||||
export const apis = [
|
||||
createApiFactory({
|
||||
api: discoveryApiRef,
|
||||
deps: { configApi: configApiRef },
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { OptionallyWrapInRouter } from './defaultAppComponents';
|
||||
import { OptionallyWrapInRouter } from './components';
|
||||
|
||||
describe('OptionallyWrapInRouter', () => {
|
||||
it('should wrap with router if not yet inside a router', async () => {
|
||||
+16
-15
@@ -16,16 +16,18 @@
|
||||
|
||||
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 { ErrorPanel, Progress, ErrorPage } from '@backstage/core-components';
|
||||
import {
|
||||
MemoryRouter,
|
||||
useInRouterContext,
|
||||
BrowserRouter,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
AppComponents,
|
||||
BootErrorPageProps,
|
||||
ErrorBoundaryFallbackProps,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppThemeProvider } from './AppThemeProvider';
|
||||
import { AppThemeProvider } from '../components/AppThemeProvider';
|
||||
|
||||
export function OptionallyWrapInRouter({ children }: { children: ReactNode }) {
|
||||
if (useInRouterContext()) {
|
||||
@@ -52,6 +54,7 @@ const DefaultBootErrorPage = ({ step, error }: BootErrorPageProps) => {
|
||||
</OptionallyWrapInRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const DefaultErrorBoundaryFallback = ({
|
||||
error,
|
||||
resetError,
|
||||
@@ -75,13 +78,11 @@ const DefaultErrorBoundaryFallback = ({
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function defaultAppComponents(): AppComponents {
|
||||
return {
|
||||
Progress,
|
||||
Router: BrowserRouter,
|
||||
ThemeProvider: AppThemeProvider,
|
||||
NotFoundErrorPage: DefaultNotFoundPage,
|
||||
BootErrorPage: DefaultBootErrorPage,
|
||||
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
|
||||
};
|
||||
}
|
||||
export const components: AppComponents = {
|
||||
Progress,
|
||||
Router: BrowserRouter,
|
||||
ThemeProvider: AppThemeProvider,
|
||||
NotFoundErrorPage: DefaultNotFoundPage,
|
||||
BootErrorPage: DefaultBootErrorPage,
|
||||
ErrorBoundaryFallback: DefaultErrorBoundaryFallback,
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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 { configLoader } from './configLoader';
|
||||
|
||||
(process as any).env = { NODE_ENV: 'test' };
|
||||
const anyEnv = process.env as any;
|
||||
const anyWindow = window as any;
|
||||
|
||||
describe('configLoader', () => {
|
||||
afterEach(() => {
|
||||
delete anyEnv.APP_CONFIG;
|
||||
delete anyWindow.__APP_CONFIG__;
|
||||
});
|
||||
|
||||
it('loads static config', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
];
|
||||
|
||||
const configs = await configLoader();
|
||||
expect(configs).toEqual([
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('loads runtime config', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'override-config' }, context: 'a' },
|
||||
{ data: { my: 'config' }, context: 'b' },
|
||||
];
|
||||
|
||||
const configs = await (configLoader as any)('{"my":"runtime-config"}');
|
||||
expect(configs).toEqual([
|
||||
{ data: { my: 'override-config' }, context: 'a' },
|
||||
{ data: { my: 'config' }, context: 'b' },
|
||||
{ data: { my: 'runtime-config' }, context: 'env' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('fails to load invalid missing config', async () => {
|
||||
await expect(configLoader()).rejects.toThrow(
|
||||
'No static configuration provided',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load invalid static config', async () => {
|
||||
anyEnv.APP_CONFIG = { my: 'invalid-config' };
|
||||
await expect(configLoader()).rejects.toThrow(
|
||||
'Static configuration has invalid format',
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to load bad runtime config', async () => {
|
||||
anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }];
|
||||
|
||||
await expect((configLoader as any)('}')).rejects.toThrow(
|
||||
'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
|
||||
);
|
||||
});
|
||||
|
||||
it('loads config from window.__APP_CONFIG__', async () => {
|
||||
anyEnv.APP_CONFIG = [
|
||||
{ data: { my: 'config' }, context: 'a' },
|
||||
{ data: { my: 'override-config' }, context: 'b' },
|
||||
];
|
||||
const windowConfig = { app: { configKey: 'config-value' } };
|
||||
anyWindow.__APP_CONFIG__ = windowConfig;
|
||||
|
||||
const configs = await configLoader();
|
||||
|
||||
expect(configs).toEqual([
|
||||
...anyEnv.APP_CONFIG,
|
||||
{ context: 'window', data: windowConfig },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { AppConfig } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { AppConfigLoader } from '@backstage/core-app-api';
|
||||
|
||||
/**
|
||||
* The default config loader, which expects that config is available at compile-time
|
||||
* in `process.env.APP_CONFIG`. APP_CONFIG should be an array of config objects as
|
||||
* returned by the config loader.
|
||||
*
|
||||
* It will also load runtime config from the __APP_INJECTED_RUNTIME_CONFIG__ string,
|
||||
* which can be rewritten at runtime to contain an additional JSON config object.
|
||||
* If runtime config is present, it will be placed first in the config array, overriding
|
||||
* other config values.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const configLoader: AppConfigLoader = async (
|
||||
// This string may be replaced at runtime to provide additional config.
|
||||
// It should be replaced by a JSON-serialized config object.
|
||||
// It's a param so we can test it, but at runtime this will always fall back to default.
|
||||
runtimeConfigJson: string = '__APP_INJECTED_RUNTIME_CONFIG__',
|
||||
) => {
|
||||
const appConfig = process.env.APP_CONFIG;
|
||||
if (!appConfig) {
|
||||
throw new Error('No static configuration provided');
|
||||
}
|
||||
if (!Array.isArray(appConfig)) {
|
||||
throw new Error('Static configuration has invalid format');
|
||||
}
|
||||
const configs = appConfig.slice() as unknown as AppConfig[];
|
||||
|
||||
// Avoiding this string also being replaced at runtime
|
||||
if (
|
||||
runtimeConfigJson !==
|
||||
'__app_injected_runtime_config__'.toLocaleUpperCase('en-US')
|
||||
) {
|
||||
try {
|
||||
const data = JSON.parse(runtimeConfigJson) as JsonObject;
|
||||
if (Array.isArray(data)) {
|
||||
configs.push(...data);
|
||||
} else {
|
||||
configs.push({ data, context: 'env' });
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to load runtime configuration, ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
const windowAppConfig = (window as any).__APP_CONFIG__;
|
||||
if (windowAppConfig) {
|
||||
configs.push({
|
||||
context: 'window',
|
||||
data: windowAppConfig,
|
||||
});
|
||||
}
|
||||
return configs;
|
||||
};
|
||||
+1
-1
@@ -32,7 +32,7 @@ import MuiPeopleIcon from '@material-ui/icons/People';
|
||||
import MuiPersonIcon from '@material-ui/icons/Person';
|
||||
import MuiWarningIcon from '@material-ui/icons/Warning';
|
||||
|
||||
export const defaultAppIcons = () => ({
|
||||
export const icons = () => ({
|
||||
brokenImage: MuiBrokenImageIcon as IconComponent,
|
||||
// To be confirmed: see https://github.com/backstage/backstage/issues/4970
|
||||
catalog: MuiMenuBookIcon as IconComponent,
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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 { apis } from './apis';
|
||||
export { components } from './components';
|
||||
export { configLoader } from './configLoader';
|
||||
export { icons } from './icons';
|
||||
export { themes } from './themes';
|
||||
+26
-28
@@ -22,31 +22,29 @@ import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
||||
import { AppTheme } from '@backstage/core-plugin-api';
|
||||
|
||||
export function defaultAppThemes(): AppTheme[] {
|
||||
return [
|
||||
{
|
||||
id: 'light',
|
||||
title: 'Light Theme',
|
||||
variant: 'light',
|
||||
icon: <LightIcon />,
|
||||
theme: lightTheme,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
title: 'Dark Theme',
|
||||
variant: 'dark',
|
||||
icon: <DarkIcon />,
|
||||
theme: darkTheme,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
export const themes: AppTheme[] = [
|
||||
{
|
||||
id: 'light',
|
||||
title: 'Light Theme',
|
||||
variant: 'light',
|
||||
icon: <LightIcon />,
|
||||
theme: lightTheme,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
title: 'Dark Theme',
|
||||
variant: 'dark',
|
||||
icon: <DarkIcon />,
|
||||
theme: darkTheme,
|
||||
Provider: ({ children }) => (
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<CssBaseline>{children}</CssBaseline>
|
||||
</ThemeProvider>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* 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 { IconComponent } from '@backstage/core-plugin-api';
|
||||
import MuiApartmentIcon from '@material-ui/icons/Apartment';
|
||||
import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage';
|
||||
import MuiCategoryIcon from '@material-ui/icons/Category';
|
||||
import MuiChatIcon from '@material-ui/icons/Chat';
|
||||
import MuiDashboardIcon from '@material-ui/icons/Dashboard';
|
||||
import MuiDocsIcon from '@material-ui/icons/Description';
|
||||
import MuiEmailIcon from '@material-ui/icons/Email';
|
||||
import MuiExtensionIcon from '@material-ui/icons/Extension';
|
||||
import MuiGitHubIcon from '@material-ui/icons/GitHub';
|
||||
import MuiHelpIcon from '@material-ui/icons/Help';
|
||||
import MuiLocationOnIcon from '@material-ui/icons/LocationOn';
|
||||
import MuiMemoryIcon from '@material-ui/icons/Memory';
|
||||
import MuiMenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import MuiPeopleIcon from '@material-ui/icons/People';
|
||||
import MuiPersonIcon from '@material-ui/icons/Person';
|
||||
import MuiWarningIcon from '@material-ui/icons/Warning';
|
||||
|
||||
/** @public */
|
||||
export type AppIcons = {
|
||||
'kind:api': IconComponent;
|
||||
'kind:component': IconComponent;
|
||||
'kind:domain': IconComponent;
|
||||
'kind:group': IconComponent;
|
||||
'kind:location': IconComponent;
|
||||
'kind:system': IconComponent;
|
||||
'kind:user': IconComponent;
|
||||
|
||||
brokenImage: IconComponent;
|
||||
catalog: IconComponent;
|
||||
chat: IconComponent;
|
||||
dashboard: IconComponent;
|
||||
docs: IconComponent;
|
||||
email: IconComponent;
|
||||
github: IconComponent;
|
||||
group: IconComponent;
|
||||
help: IconComponent;
|
||||
user: IconComponent;
|
||||
warning: IconComponent;
|
||||
};
|
||||
|
||||
export const defaultAppIcons: AppIcons = {
|
||||
brokenImage: MuiBrokenImageIcon,
|
||||
// To be confirmed: see https://github.com/backstage/backstage/issues/4970
|
||||
catalog: MuiMenuBookIcon,
|
||||
chat: MuiChatIcon,
|
||||
dashboard: MuiDashboardIcon,
|
||||
docs: MuiDocsIcon,
|
||||
email: MuiEmailIcon,
|
||||
github: MuiGitHubIcon,
|
||||
group: MuiPeopleIcon,
|
||||
help: MuiHelpIcon,
|
||||
'kind:api': MuiExtensionIcon,
|
||||
'kind:component': MuiMemoryIcon,
|
||||
'kind:domain': MuiApartmentIcon,
|
||||
'kind:group': MuiPeopleIcon,
|
||||
'kind:location': MuiLocationOnIcon,
|
||||
'kind:system': MuiCategoryIcon,
|
||||
'kind:user': MuiPersonIcon,
|
||||
user: MuiPersonIcon,
|
||||
warning: MuiWarningIcon,
|
||||
};
|
||||
@@ -14,6 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createApp, defaultConfigLoader } from './createApp';
|
||||
export type { AppIcons } from './icons';
|
||||
export * from './types';
|
||||
export { createApp } from './createApp';
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
/*
|
||||
* 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 { ComponentType } from 'react';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
AppTheme,
|
||||
ProfileInfo,
|
||||
IconComponent,
|
||||
BackstagePlugin,
|
||||
RouteRef,
|
||||
SubRouteRef,
|
||||
ExternalRouteRef,
|
||||
PluginOutput,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { AppIcons } from './icons';
|
||||
|
||||
/**
|
||||
* Props for the `BootErrorPage` component of {@link AppComponents}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BootErrorPageProps = {
|
||||
step: 'load-config' | 'load-chunk';
|
||||
error: Error;
|
||||
};
|
||||
|
||||
/**
|
||||
* The outcome of signing in on the sign-in page.
|
||||
*
|
||||
* @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` component of {@link AppComponents}.
|
||||
*
|
||||
* @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 fallback error boundary.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ErrorBoundaryFallbackProps = {
|
||||
plugin?: BackstagePlugin;
|
||||
error: Error;
|
||||
resetError: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A set of replaceable core components that are part of every Backstage app.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppComponents = {
|
||||
NotFoundErrorPage: ComponentType<{}>;
|
||||
BootErrorPage: ComponentType<BootErrorPageProps>;
|
||||
Progress: ComponentType<{}>;
|
||||
Router: ComponentType<{}>;
|
||||
ErrorBoundaryFallback: ComponentType<ErrorBoundaryFallbackProps>;
|
||||
ThemeProvider: ComponentType<{}>;
|
||||
|
||||
/**
|
||||
* 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>;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that loads in the App config that will be accessible via the ConfigApi.
|
||||
*
|
||||
* If multiple config objects are returned in the array, values in the earlier configs
|
||||
* will override later ones.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppConfigLoader = () => Promise<AppConfig[]>;
|
||||
|
||||
/**
|
||||
* Extracts a union of the keys in a map whose value extends the given type
|
||||
*/
|
||||
type KeysWithType<Obj extends { [key in string]: any }, Type> = {
|
||||
[key in keyof Obj]: Obj[key] extends Type ? key : never;
|
||||
}[keyof Obj];
|
||||
|
||||
/**
|
||||
* Takes a map Map required values and makes all keys matching Keys optional
|
||||
*/
|
||||
type PartialKeys<
|
||||
Map extends { [name in string]: any },
|
||||
Keys extends keyof Map,
|
||||
> = Partial<Pick<Map, Keys>> & Required<Omit<Map, Keys>>;
|
||||
|
||||
/**
|
||||
* Creates a map of target routes with matching parameters based on a map of external routes.
|
||||
*/
|
||||
type TargetRouteMap<
|
||||
ExternalRoutes extends { [name: string]: ExternalRouteRef },
|
||||
> = {
|
||||
[name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
|
||||
infer Params,
|
||||
any
|
||||
>
|
||||
? RouteRef<Params> | SubRouteRef<Params>
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function that can bind from external routes of a given plugin, to concrete
|
||||
* routes of other plugins. See {@link createApp}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppRouteBinder = <
|
||||
ExternalRoutes extends { [name: string]: ExternalRouteRef },
|
||||
>(
|
||||
externalRoutes: ExternalRoutes,
|
||||
targetRoutes: PartialKeys<
|
||||
TargetRouteMap<ExternalRoutes>,
|
||||
KeysWithType<ExternalRoutes, ExternalRouteRef<any, true>>
|
||||
>,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Internal helper type that represents a plugin with any type of output.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* The `type: string` type is there to handle output from newer or older plugin
|
||||
* API versions that might not be supported by this version of the app API, but
|
||||
* we don't want to break at the type checking level. We only use this more
|
||||
* permissive type for the `createApp` options, as we otherwise want to stick
|
||||
* to using the type for the outputs that we know about in this version of the
|
||||
* app api.
|
||||
*
|
||||
* TODO(freben): This should be marked internal but that's not supported by the api report generation tools yet
|
||||
*/
|
||||
export type BackstagePluginWithAnyOutput = Omit<
|
||||
BackstagePlugin<any, any>,
|
||||
'output'
|
||||
> & {
|
||||
output(): (PluginOutput | { type: string })[];
|
||||
};
|
||||
|
||||
/**
|
||||
* The options accepted by {@link createApp}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type AppOptions = {
|
||||
/**
|
||||
* A collection of ApiFactories to register in the application to either
|
||||
* add add new ones, or override factories provided by default or by plugins.
|
||||
*/
|
||||
apis?: Iterable<AnyApiFactory>;
|
||||
|
||||
/**
|
||||
* Supply icons to override the default ones.
|
||||
*/
|
||||
icons?: Partial<AppIcons> & { [key in string]: IconComponent };
|
||||
|
||||
/**
|
||||
* A list of all plugins to include in the app.
|
||||
*/
|
||||
plugins?: BackstagePluginWithAnyOutput[];
|
||||
|
||||
/**
|
||||
* Supply components to the app to override the default ones.
|
||||
*/
|
||||
components?: Partial<AppComponents>;
|
||||
|
||||
/**
|
||||
* 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',
|
||||
* icon: <LightIcon />,
|
||||
* Provider: ({ children }) => (
|
||||
* <ThemeProvider theme={lightTheme}>
|
||||
* <CssBaseline>{children}</CssBaseline>
|
||||
* </ThemeProvider>
|
||||
* ),
|
||||
* }, {
|
||||
* id: 'dark',
|
||||
* title: 'Dark Theme',
|
||||
* variant: 'dark',
|
||||
* icon: <DarkIcon />,
|
||||
* Provider: ({ children }) => (
|
||||
* <ThemeProvider theme={darkTheme}>
|
||||
* <CssBaseline>{children}</CssBaseline>
|
||||
* </ThemeProvider>
|
||||
* ),
|
||||
* }]
|
||||
* ```
|
||||
*/
|
||||
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[];
|
||||
|
||||
/**
|
||||
* A function that loads in App configuration that will be accessible via
|
||||
* the ConfigApi.
|
||||
*
|
||||
* Defaults to an empty config.
|
||||
*
|
||||
* TODO(Rugvip): Omitting this should instead default to loading in configuration
|
||||
* that was packaged by the backstage-cli and default docker container boot script.
|
||||
*/
|
||||
configLoader?: AppConfigLoader;
|
||||
|
||||
/**
|
||||
* A function that is used to register associations between cross-plugin route
|
||||
* references, enabling plugins to navigate between each other.
|
||||
*
|
||||
* The `bind` function that is passed in should be used to bind all external
|
||||
* routes of all used plugins.
|
||||
*
|
||||
* ```ts
|
||||
* bindRoutes({ bind }) {
|
||||
* bind(docsPlugin.externalRoutes, {
|
||||
* homePage: managePlugin.routes.managePage,
|
||||
* })
|
||||
* bind(homePagePlugin.externalRoutes, {
|
||||
* settingsPage: settingsPlugin.routes.settingsPage,
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
bindRoutes?(context: { bind: AppRouteBinder }): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* The public API of the output of {@link createApp}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type BackstageApp = {
|
||||
/**
|
||||
* Returns all plugins registered for the app.
|
||||
*/
|
||||
getPlugins(): BackstagePlugin<any, any>[];
|
||||
|
||||
/**
|
||||
* Get a common or custom icon for this app.
|
||||
*/
|
||||
getSystemIcon(key: string): IconComponent | undefined;
|
||||
|
||||
/**
|
||||
* Provider component that should wrap the Router created with getRouter()
|
||||
* and any other components that need to be within the app context.
|
||||
*/
|
||||
getProvider(): ComponentType<{}>;
|
||||
|
||||
/**
|
||||
* Router component that should wrap the App Routes create with getRoutes()
|
||||
* and any other components that should only be available while signed in.
|
||||
*/
|
||||
getRouter(): ComponentType<{}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The central context providing runtime app specific state that plugin views
|
||||
* want to consume.
|
||||
*
|
||||
* @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;
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
// NOTE: we don't re-export any of the types imported from core-app-api, as we
|
||||
// want them to be imported from there rather than core-components.
|
||||
|
||||
/**
|
||||
* The set of app options that will be populated by {@link withDefaults} if they
|
||||
* are not passed in explicitly.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface OptionalAppOptions {
|
||||
icons?: Partial<AppIcons> & {
|
||||
[key in string]: IconComponent;
|
||||
};
|
||||
themes?: (Partial<AppTheme> & Omit<AppTheme, 'theme'>)[]; // TODO: simplify once AppTheme is updated
|
||||
components?: Partial<AppComponents>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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?: Omit<AppOptions, keyof OptionalAppOptions> & OptionalAppOptions,
|
||||
): AppOptions {
|
||||
const { themes, icons, components } = options ?? {};
|
||||
|
||||
return {
|
||||
...options,
|
||||
themes: themes ?? defaultAppThemes(),
|
||||
icons: { ...defaultAppIcons(), ...icons },
|
||||
components: { ...defaultAppComponents(), ...components },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user