diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 56faf4402b..c18169ae03 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -14,14 +14,9 @@
* limitations under the License.
*/
-import {
- createApp,
- AlertDisplay,
- OAuthRequestDialog,
- LoginPage,
-} from '@backstage/core';
+import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core';
import React, { FC } from 'react';
-import { BrowserRouter as Router, Route } from 'react-router-dom';
+import { BrowserRouter as Router } from 'react-router-dom';
import Root from './components/Root';
import * as plugins from './plugins';
import apis from './apis';
@@ -29,6 +24,18 @@ import apis from './apis';
const app = createApp({
apis,
plugins: Object.values(plugins),
+ configLoader: async () => ({
+ app: {
+ title: 'Backstage Example App',
+ baseUrl: 'http://localhost:3000',
+ },
+ backend: {
+ baseUrl: 'http://localhost:7000',
+ },
+ organization: {
+ name: 'Spotify',
+ },
+ }),
});
const AppProvider = app.getProvider();
@@ -40,7 +47,6 @@ const App: FC<{}> = () => (
-
diff --git a/packages/core-api/src/apis/definitions/ConfigApi.ts b/packages/core-api/src/apis/definitions/ConfigApi.ts
new file mode 100644
index 0000000000..20676df899
--- /dev/null
+++ b/packages/core-api/src/apis/definitions/ConfigApi.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 { createApiRef } from '../ApiRef';
+
+export type Config = {
+ getConfig(key: string): Config;
+
+ getConfigArray(key: string): Config[];
+
+ getNumber(key: string): number | undefined;
+
+ getBoolean(key: string): boolean | undefined;
+
+ getString(key: string): string | undefined;
+
+ getStringArray(key: string): string[] | undefined;
+};
+
+// Using interface to make the ConfigApi name show up in docs
+export interface ConfigApi extends Config {}
+
+export const configApiRef = createApiRef({
+ id: 'core.config',
+ description: 'Used to access runtime configuration',
+});
diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts
index 2e008965cd..2e9db325dc 100644
--- a/packages/core-api/src/apis/definitions/index.ts
+++ b/packages/core-api/src/apis/definitions/index.ts
@@ -24,6 +24,7 @@ export * from './auth';
export * from './AlertApi';
export * from './AppThemeApi';
+export * from './ConfigApi';
export * from './ErrorApi';
export * from './FeatureFlagsApi';
export * from './OAuthRequestApi';
diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts
new file mode 100644
index 0000000000..68c1fd5353
--- /dev/null
+++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.test.ts
@@ -0,0 +1,226 @@
+/*
+ * 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 { ConfigReader } from './ConfigReader';
+
+const DATA = {
+ zero: 0,
+ one: 1,
+ true: true,
+ false: false,
+ null: null,
+ string: 'string',
+ emptyString: '',
+ strings: ['string1', 'string2'],
+ badStrings: ['string1', ''],
+ worseStrings: ['string1', 3] as string[],
+ worstStrings: ['string1', 'string2', {}] as string[],
+ nested: {
+ one: 1,
+ string: 'string',
+ strings: ['string1', 'string2'],
+ },
+ nestlings: [{ boolean: true }, { string: 'string' }, { number: 42 }] as {}[],
+};
+
+function expectValidValues(config: ConfigReader) {
+ expect(config.getNumber('zero')).toBe(0);
+ expect(config.getNumber('one')).toBe(1);
+ expect(config.getBoolean('true')).toBe(true);
+ expect(config.getBoolean('false')).toBe(false);
+ expect(config.getString('string')).toBe('string');
+ expect(config.getStringArray('strings')).toEqual(['string1', 'string2']);
+ expect(config.getConfig('nested').getNumber('one')).toBe(1);
+ expect(config.getConfig('nested').getString('string')).toBe('string');
+ expect(config.getConfig('nested').getStringArray('strings')).toEqual([
+ 'string1',
+ 'string2',
+ ]);
+
+ const [config1, config2, config3] = config.getConfigArray('nestlings');
+ expect(config1.getBoolean('boolean')).toBe(true);
+ expect(config2.getString('string')).toBe('string');
+ expect(config3.getNumber('number')).toBe(42);
+}
+
+function expectInvalidValues(config: ConfigReader) {
+ expect(() => config.getNumber('string')).toThrow(
+ 'Invalid type in config for key string, got string, wanted number',
+ );
+ expect(() => config.getString('one')).toThrow(
+ 'Invalid type in config for key one, got number, wanted string',
+ );
+ expect(() => config.getNumber('true')).toThrow(
+ 'Invalid type in config for key true, got boolean, wanted number',
+ );
+ expect(() => config.getStringArray('null')).toThrow(
+ 'Invalid type in config for key null, got null, wanted string-array',
+ );
+ expect(() => config.getString('emptyString')).toThrow(
+ 'Invalid type in config for key emptyString, got empty-string, wanted string',
+ );
+ expect(() => config.getStringArray('badStrings')).toThrow(
+ 'Invalid type in config for key badStrings[1], got empty-string, wanted string',
+ );
+ expect(() => config.getStringArray('worseStrings')).toThrow(
+ 'Invalid type in config for key worseStrings[1], got number, wanted string',
+ );
+ expect(() => config.getStringArray('worstStrings')).toThrow(
+ 'Invalid type in config for key worstStrings[2], got object, wanted string',
+ );
+ expect(() => config.getConfig('one')).toThrow(
+ 'Invalid type in config for key one, got number, wanted object',
+ );
+ expect(() => config.getConfigArray('one')).toThrow(
+ 'Invalid type in config for key one, got number, wanted object-array',
+ );
+}
+
+describe('ConfigReader', () => {
+ it('should read empty config with valid keys', () => {
+ const config = new ConfigReader({});
+ expect(config.getString('x')).toBeUndefined();
+ expect(config.getString('x_x')).toBeUndefined();
+ expect(config.getString('x-X')).toBeUndefined();
+ expect(config.getString('x0')).toBeUndefined();
+ expect(config.getString('X-x2')).toBeUndefined();
+ expect(config.getString('x0_x0')).toBeUndefined();
+ expect(config.getString('x_x-x_x')).toBeUndefined();
+ });
+
+ it('should throw on invalid keys', () => {
+ const config = new ConfigReader({});
+
+ expect(() => config.getString('.')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('0')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('(')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('z-_')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('-')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('.a')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('0.a')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('0a')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a.0a')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a..a')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a.')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a...')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a.a.a.a.')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a._')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a.-.a')).toThrow(/^Invalid config key/);
+ });
+
+ it('should read valid values', () => {
+ const config = new ConfigReader(DATA);
+ expectValidValues(config);
+ });
+
+ it('should fail to read invalid values', () => {
+ const config = new ConfigReader(DATA);
+ expectInvalidValues(config);
+ });
+});
+
+describe('ConfigReader with fallback', () => {
+ it('should behave as if without fallback', () => {
+ const config = new ConfigReader({}, new ConfigReader(DATA));
+ expect(config.getString('x')).toBeUndefined();
+ expect(() => config.getString('.')).toThrow(/^Invalid config key/);
+ expect(() => config.getString('a.')).toThrow(/^Invalid config key/);
+ });
+
+ it('should read values from itself', () => {
+ const config = new ConfigReader(DATA, new ConfigReader({}));
+ expectValidValues(config);
+ expectInvalidValues(config);
+ });
+
+ it('should read values from a fallback', () => {
+ const config = new ConfigReader({}, new ConfigReader(DATA));
+ expectValidValues(config);
+ expectInvalidValues(config);
+ });
+
+ it('should read values from multiple levels of fallbacks', () => {
+ const config = new ConfigReader(
+ {},
+ new ConfigReader({}, new ConfigReader({}, new ConfigReader(DATA))),
+ );
+ expectValidValues(config);
+ expectInvalidValues(config);
+ });
+
+ it('should read merged objects', () => {
+ const a = {
+ merged: {
+ x: 'x',
+ z: 'z1',
+ arr: ['a', 'b'],
+ config: { d: 'd' },
+ configs: [{ a: 'a' }],
+ },
+ };
+ const b = {
+ merged: {
+ y: 'y',
+ z: 'z2',
+ arr: ['c'],
+ config: { e: 'e' },
+ configs: [{ b: 'b' }],
+ },
+ };
+
+ const config = new ConfigReader(a, new ConfigReader(b));
+
+ expect(config.getString('merged.x')).toBe('x');
+ expect(config.getString('merged.y')).toBe('y');
+ expect(config.getString('merged.z')).toBe('z1');
+ expect(config.getConfig('merged').getString('x')).toBe('x');
+ expect(config.getConfig('merged').getString('y')).toBe('y');
+ expect(config.getConfig('merged').getString('z')).toBe('z1');
+ expect(config.getString('merged.config.d')).toBe('d');
+ expect(config.getString('merged.config.e')).toBe('e');
+ expect(config.getConfig('merged').getString('config.d')).toBe('d');
+ expect(config.getConfig('merged').getString('config.e')).toBe('e');
+ expect(config.getConfig('merged').getConfig('config').getString('d')).toBe(
+ 'd',
+ );
+ expect(config.getConfig('merged').getConfig('config').getString('e')).toBe(
+ 'e',
+ );
+
+ // Arrays are not merged
+ expect(config.getStringArray('merged.arr')).toEqual(['a', 'b']);
+ expect(config.getConfig('merged').getStringArray('arr')).toEqual([
+ 'a',
+ 'b',
+ ]);
+
+ // Config arrays aren't merged either
+ expect(config.getConfigArray('merged.configs').length).toBe(1);
+ expect(config.getConfigArray('merged.configs')[0].getString('a')).toBe('a');
+ expect(
+ config.getConfigArray('merged.configs')[0].getString('b'),
+ ).toBeUndefined();
+
+ // Config arrays aren't merged either
+ expect(config.getConfig('merged').getConfigArray('configs').length).toBe(1);
+ expect(
+ config.getConfig('merged').getConfigArray('configs')[0].getString('a'),
+ ).toBe('a');
+ expect(
+ config.getConfig('merged').getConfigArray('configs')[0].getString('b'),
+ ).toBeUndefined();
+ });
+});
diff --git a/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts
new file mode 100644
index 0000000000..7a5cf3b185
--- /dev/null
+++ b/packages/core-api/src/apis/implementations/ConfigApi/ConfigReader.ts
@@ -0,0 +1,168 @@
+/*
+ * 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 { ConfigApi, Config } from '../../definitions/ConfigApi';
+
+const CONFIG_KEY_PART_PATTERN = /^[a-z][a-z0-9]*(?:[-_][a-z][a-z0-9]*)*$/i;
+
+type JsonObject = { [key in string]: JsonValue };
+type JsonArray = JsonValue[];
+type JsonValue = JsonObject | JsonArray | number | string | boolean | null;
+
+function isObject(value: JsonValue | undefined): value is JsonObject {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function typeOf(value: JsonValue | undefined): string {
+ if (value === null) {
+ return 'null';
+ } else if (Array.isArray(value)) {
+ return 'array';
+ }
+ const type = typeof value;
+ if (type === 'number' && isNaN(value as number)) {
+ return 'nan';
+ }
+ return type;
+}
+
+function typeErrorMessage(key: string, got: string, wanted: string) {
+ return `Invalid type in config for key ${key}, got ${got}, wanted ${wanted}`;
+}
+
+function validateString(
+ key: string,
+ value: JsonValue | undefined,
+): value is string {
+ if (typeof value === 'string' && value.length > 0) {
+ return true;
+ }
+ if (value === '') {
+ throw new TypeError(typeErrorMessage(key, 'empty-string', 'string'));
+ }
+ if (value !== undefined) {
+ throw new TypeError(typeErrorMessage(key, typeOf(value), 'string'));
+ }
+ return false;
+}
+
+export class ConfigReader implements ConfigApi {
+ static nullReader = new ConfigReader({});
+
+ constructor(
+ private readonly data: JsonObject,
+ private readonly fallback?: ConfigApi,
+ ) {}
+
+ getConfig(key: string): Config {
+ const value = this.readValue(key);
+ const fallbackConfig = this.fallback?.getConfig(key);
+ if (isObject(value)) {
+ return new ConfigReader(value, fallbackConfig);
+ }
+ if (value !== undefined) {
+ throw new TypeError(typeErrorMessage(key, typeOf(value), 'object'));
+ }
+ return fallbackConfig ?? ConfigReader.nullReader;
+ }
+
+ getConfigArray(key: string): Config[] {
+ const values = this.readValue(key);
+ if (Array.isArray(values)) {
+ return values.map((value, index) => {
+ if (isObject(value)) {
+ return new ConfigReader(value);
+ }
+ throw new TypeError(
+ typeErrorMessage(`${key}[${index}]`, typeOf(value), 'object'),
+ );
+ });
+ }
+ if (values !== undefined) {
+ throw new TypeError(
+ typeErrorMessage(key, typeOf(values), 'object-array'),
+ );
+ }
+ return this.fallback?.getConfigArray(key) ?? [];
+ }
+
+ getNumber(key: string): number | undefined {
+ const value = this.readValue(key);
+ if (typeof value === 'number' && !isNaN(value)) {
+ return value;
+ }
+ if (value !== undefined) {
+ throw new TypeError(typeErrorMessage(key, typeOf(value), 'number'));
+ }
+ return this.fallback?.getNumber(key);
+ }
+
+ getBoolean(key: string): boolean | undefined {
+ const value = this.readValue(key);
+ if (typeof value === 'boolean') {
+ return value;
+ }
+ if (value !== undefined) {
+ throw new TypeError(typeErrorMessage(key, typeOf(value), 'boolean'));
+ }
+ return this.fallback?.getBoolean(key);
+ }
+
+ getString(key: string): string | undefined {
+ const value = this.readValue(key);
+ if (validateString(key, value)) {
+ return value;
+ }
+ return this.fallback?.getString(key);
+ }
+
+ getStringArray(key: string): string[] | undefined {
+ const values = this.readValue(key);
+ if (Array.isArray(values)) {
+ for (const [index, value] of values.entries()) {
+ const iKey = `${key}[${index}]`;
+ if (!validateString(iKey, value)) {
+ throw new TypeError(typeErrorMessage(iKey, typeOf(value), 'string'));
+ }
+ }
+ return values as string[];
+ }
+ if (values !== undefined) {
+ throw new TypeError(
+ typeErrorMessage(key, typeOf(values), 'string-array'),
+ );
+ }
+ return this.fallback?.getStringArray(key);
+ }
+
+ private readValue(key: string): JsonValue | undefined {
+ const parts = key.split('.');
+
+ let value: JsonValue | undefined = this.data;
+ for (const part of parts) {
+ if (!CONFIG_KEY_PART_PATTERN.test(part)) {
+ throw new TypeError(`Invalid config key '${key}'`);
+ }
+ if (isObject(value)) {
+ value = value[part];
+ } else {
+ value = undefined;
+ }
+ }
+
+ return value;
+ }
+}
diff --git a/packages/core/src/layout/LoginPage/index.ts b/packages/core-api/src/apis/implementations/ConfigApi/index.ts
similarity index 92%
rename from packages/core/src/layout/LoginPage/index.ts
rename to packages/core-api/src/apis/implementations/ConfigApi/index.ts
index caa94bd6d7..8839cb948e 100644
--- a/packages/core/src/layout/LoginPage/index.ts
+++ b/packages/core-api/src/apis/implementations/ConfigApi/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
-export { LoginPage } from './LoginPage';
+export { ConfigReader } from './ConfigReader';
diff --git a/packages/core-api/src/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts
index bb77cf5bd3..b5cc250ae4 100644
--- a/packages/core-api/src/apis/implementations/index.ts
+++ b/packages/core-api/src/apis/implementations/index.ts
@@ -22,5 +22,6 @@ export * from './auth';
export * from './AlertApi';
export * from './AppThemeApi';
+export * from './ConfigApi';
export * from './ErrorApi';
export * from './OAuthRequestApi';
diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx
index 844c38ecce..49d2291acd 100644
--- a/packages/core-api/src/app/App.tsx
+++ b/packages/core-api/src/app/App.tsx
@@ -17,7 +17,7 @@
import React, { ComponentType, FC } from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { AppContextProvider } from './AppContext';
-import { BackstageApp, AppComponents } from './types';
+import { BackstageApp, AppComponents, AppConfigLoader } from './types';
import { BackstagePlugin } from '../plugin';
import { FeatureFlagsRegistryItem } from './FeatureFlags';
import { featureFlagsApiRef } from '../apis/definitions';
@@ -31,8 +31,11 @@ import {
AppTheme,
AppThemeSelector,
appThemeApiRef,
+ configApiRef,
+ ConfigReader,
} from '../apis';
import { ApiAggregator } from '../apis/ApiAggregator';
+import { useAsync } from 'react-use';
type FullAppOptions = {
apis: ApiHolder;
@@ -40,6 +43,7 @@ type FullAppOptions = {
plugins: BackstagePlugin[];
components: AppComponents;
themes: AppTheme[];
+ configLoader: AppConfigLoader;
};
export class PrivateAppImpl implements BackstageApp {
@@ -48,6 +52,7 @@ export class PrivateAppImpl implements BackstageApp {
private readonly plugins: BackstagePlugin[];
private readonly components: AppComponents;
private readonly themes: AppTheme[];
+ private readonly configLoader: AppConfigLoader;
constructor(options: FullAppOptions) {
this.apis = options.apis;
@@ -55,6 +60,7 @@ export class PrivateAppImpl implements BackstageApp {
this.plugins = options.plugins;
this.components = options.components;
this.themes = options.themes;
+ this.configLoader = options.configLoader;
}
getApis(): ApiHolder {
@@ -141,18 +147,33 @@ export class PrivateAppImpl implements BackstageApp {
}
getProvider(): ComponentType<{}> {
- const appApis = ApiRegistry.from([
- [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)],
- ]);
- const apis = new ApiAggregator(this.apis, appApis);
+ const Provider: FC<{}> = ({ children }) => {
+ const config = useAsync(this.configLoader);
- const Provider: FC<{}> = ({ children }) => (
-
-
- {children}
-
-
- );
+ let childNode = children;
+
+ if (config.loading) {
+ const { Progress } = this.components;
+ childNode = ;
+ } else if (config.error) {
+ const { BootErrorPage } = this.components;
+ childNode = ;
+ }
+
+ const appApis = ApiRegistry.from([
+ [appThemeApiRef, AppThemeSelector.createWithStorage(this.themes)],
+ [configApiRef, new ConfigReader(config.value ?? {})],
+ ]);
+ const apis = new ApiAggregator(this.apis, appApis);
+
+ return (
+
+
+ {childNode}
+
+
+ );
+ };
return Provider;
}
diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts
index ea3a812557..defc155a82 100644
--- a/packages/core-api/src/app/types.ts
+++ b/packages/core-api/src/app/types.ts
@@ -20,10 +20,27 @@ import { BackstagePlugin } from '../plugin';
import { ApiHolder } from '../apis';
import { AppTheme } from '../apis/definitions';
+export type BootErrorPageProps = {
+ step: 'load-config';
+ error: Error;
+};
+
export type AppComponents = {
NotFoundErrorPage: ComponentType<{}>;
+ BootErrorPage: ComponentType;
+ Progress: ComponentType<{}>;
};
+/**
+ * TBD
+ */
+export type AppConfig = any;
+
+/**
+ * A function that loads in the App config that will be accessible via the ConfigApi.
+ */
+export type AppConfigLoader = () => Promise;
+
export type AppOptions = {
/**
* A holder of all APIs available in the app.
@@ -68,6 +85,17 @@ export type AppOptions = {
* ```
*/
themes?: AppTheme[];
+
+ /**
+ * 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;
};
export type BackstageApp = {
diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx
index 03adc41ec6..7c605c4e18 100644
--- a/packages/core/src/api-wrappers/createApp.tsx
+++ b/packages/core/src/api-wrappers/createApp.tsx
@@ -14,14 +14,17 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { FC } from 'react';
import privateExports, {
AppOptions,
ApiRegistry,
defaultSystemIcons,
+ BootErrorPageProps,
} from '@backstage/core-api';
+import { BrowserRouter as Router } from 'react-router-dom';
import { ErrorPage } from '../layout/ErrorPage';
+import Progress from '../components/Progress';
import { lightTheme, darkTheme } from '@backstage/theme';
const { PrivateAppImpl } = privateExports;
@@ -38,12 +41,26 @@ export function createApp(options?: AppOptions) {
const DefaultNotFoundPage = () => (
);
+ const DefaultBootErrorPage: FC = ({ step, error }) => {
+ let message = '';
+ if (step === 'load-config') {
+ message = `The configuration failed to load, someone should have a look at this error: ${error.message}`;
+ }
+ // TODO: figure out a nicer way to handle routing on the error page, when it can be done.
+ return (
+
+
+
+ );
+ };
const apis = options?.apis ?? ApiRegistry.from([]);
const icons = { ...defaultSystemIcons, ...options?.icons };
const plugins = options?.plugins ?? [];
const components = {
NotFoundErrorPage: DefaultNotFoundPage,
+ BootErrorPage: DefaultBootErrorPage,
+ Progress: Progress,
...options?.components,
};
const themes = options?.themes ?? [
@@ -60,8 +77,16 @@ export function createApp(options?: AppOptions) {
theme: darkTheme,
},
];
+ const configLoader = options?.configLoader ?? (async () => ({}));
- const app = new PrivateAppImpl({ apis, icons, plugins, components, themes });
+ const app = new PrivateAppImpl({
+ apis,
+ icons,
+ plugins,
+ components,
+ themes,
+ configLoader,
+ });
app.verify();
diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx
index 7190ee8725..b7a36c8b98 100644
--- a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx
+++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx
@@ -31,6 +31,7 @@ const useStyles = makeStyles((theme: Theme) => ({
marginTop: -theme.spacing(3),
display: 'flex',
flexFlow: 'row nowrap',
+ zIndex: 'unset',
},
icon: {
fontSize: 20,
diff --git a/packages/core/src/layout/LoginPage/LoginPage.tsx b/packages/core/src/layout/LoginPage/LoginPage.tsx
deleted file mode 100644
index 7bf14dba01..0000000000
--- a/packages/core/src/layout/LoginPage/LoginPage.tsx
+++ /dev/null
@@ -1,178 +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, useState } from 'react';
-import GitHubIcon from '@material-ui/icons/GitHub';
-import { Page } from '../Page';
-import { Header } from '../Header';
-import { Content } from '../Content';
-import { ContentHeader } from '../ContentHeader';
-import { InfoCard } from '../InfoCard/InfoCard';
-import {
- Grid,
- Typography,
- Button,
- TextField,
- List,
- ListItem,
- Link,
-} from '@material-ui/core';
-
-enum AuthType {
- GitHub,
-}
-
-export const LoginPage: FC<{}> = () => {
- const [githubUsername, setGithubUsername] = useState(String);
- const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState(
- String,
- );
- const [loginDetails, setLoginDetails] = useState(Object);
-
- const saveGithubInfo = (info: {}) => {
- localStorage.setItem('githubLoginDetails', JSON.stringify(info));
- setLoginDetails(info);
- };
-
- const deleteGithubInfo = () => {
- localStorage.removeItem('githubLoginDetails');
- setLoginDetails(undefined);
- };
-
- const handleTokenRegistration = (event: any) => {
- switch (event.target.name) {
- case 'github-username-tf':
- setGithubUsername(event.target.value);
- break;
- case 'github-auth-tf':
- setGithubPersonalAuthToken(event.target.value);
- break;
- default:
- break;
- }
- };
-
- const fetchGitHubToken = (username: String, token: String) => {
- fetch('https://api.github.com/user', {
- headers: new Headers({
- Authorization: `Basic ${btoa(`${username}:${token}`)}`,
- 'Content-Type': 'application/x-www-form-urlencoded',
- }),
- })
- .then(response => {
- if (response.status === 200) return response.json();
- throw Error(`${response.status} ${response.statusText}`);
- })
- .then(data => {
- const info = {
- username: username,
- token: token,
- name: data.name || data.login,
- };
- saveGithubInfo(info);
- })
- .catch(() => {});
- };
-
- const validateUsernameAndToken = (username: String, token: String) => {
- if (username === undefined || username === null || username === '')
- return false;
-
- if (token === undefined || token === null || token === '') return false;
-
- return true;
- };
-
- const authenticate = (type: AuthType) => {
- switch (type) {
- case AuthType.GitHub:
- {
- const username = githubUsername;
- const token = githubPersonalAuthToken;
- if (validateUsernameAndToken(username, token))
- fetchGitHubToken(username, token);
- }
- break;
- default:
- break;
- }
- };
-
- const LoginIndicator = () => {
- const ls = localStorage.getItem('githubLoginDetails');
- if (ls !== null) {
- const obj = ls || loginDetails ? JSON.parse(ls) : loginDetails;
- return (
-
- {`Welcome, ${obj.name}!`}
-
- Logout
-
- );
- }
- return (
-
- Welcome, guest!
-
- );
- };
-
- return (
-
-
-
-
-
-
-
-
- GitHub
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts
index c9dbae9ca2..e8341e1124 100644
--- a/packages/core/src/layout/index.ts
+++ b/packages/core/src/layout/index.ts
@@ -21,7 +21,6 @@ export * from './Header';
export * from './HeaderLabel';
export * from './HomepageTimer';
export * from './InfoCard';
-export * from './LoginPage';
export * from './Page';
export * from './Sidebar';
export * from './TabbedCard';
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index cd8548bdde..0cdc5d2618 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -19,6 +19,7 @@
"dependencies": {
"@backstage/core": "^0.1.1-alpha.6",
"@backstage/theme": "^0.1.1-alpha.6",
+ "@backstage/catalog-model": "^0.1.1-alpha.6",
"@material-ui/core": "^4.9.1",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx
index 9cba76fddc..b639da9d50 100644
--- a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx
+++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx
@@ -19,14 +19,23 @@ import { render } from '@testing-library/react';
import WelcomePage from './WelcomePage';
import { ThemeProvider } from '@material-ui/core';
import { lightTheme } from '@backstage/theme';
-import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core';
+import {
+ ApiProvider,
+ ApiRegistry,
+ errorApiRef,
+ configApiRef,
+ ConfigReader,
+} from '@backstage/core';
describe('WelcomePage', () => {
it('should render', () => {
// TODO: use common test app with mock implementations of all core APIs
const rendered = render(
diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
index 1036096ae5..6a1b0e0008 100644
--- a/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
+++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.tsx
@@ -34,15 +34,18 @@ import {
ContentHeader,
SupportButton,
WarningPanel,
+ useApi,
+ configApiRef,
} from '@backstage/core';
const WelcomePage: FC<{}> = () => {
+ const appTitle = useApi(configApiRef).getString('app.title') ?? 'Backstage';
const profile = { givenName: '' };
return (
diff --git a/yarn.lock b/yarn.lock
index b34d5505d2..b115b0bf9f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6871,9 +6871,9 @@ clone@^1.0.2:
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0:
- version "1.1.0"
- resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
- integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
+ integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
cmd-shim@^3.0.0, cmd-shim@^3.0.3:
version "3.0.3"
@@ -7866,9 +7866,9 @@ cyclist@^1.0.1:
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@*, cypress@^4.2.0:
- version "4.6.0"
- resolved "https://registry.npmjs.org/cypress/-/cypress-4.6.0.tgz#ac76786500580df1347a0a50be63e5c59ffbef59"
- integrity sha512-vIPXAceRP+Nxvnm/O9ruY9EQaRGmVVybtk9F1sfC9mH3067YbitrdBTynaaLuHFj90p9e0U2ZCV7OkX4x4V/Wg==
+ version "4.7.0"
+ resolved "https://registry.npmjs.org/cypress/-/cypress-4.7.0.tgz#3ea29bddaf9a1faeaa5b8d54b60a84ed1cafa83d"
+ integrity sha512-Vav6wUFhPRlImIND/2lOQlUnAWzgCC/iXyJlJjX9nJOJul5LC1vUpf/m8Oiae870PFPyT0ZLLwPHKTXZNdXmHw==
dependencies:
"@cypress/listr-verbose-renderer" "0.4.1"
"@cypress/request" "2.88.5"
@@ -18353,9 +18353,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
inherits "^2.0.1"
rollup-plugin-dts@^1.4.6:
- version "1.4.6"
- resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.6.tgz#26e3da11ec647cfffee9658b63fa41d67e7840b9"
- integrity sha512-1o5+eI97Ne8zXJrgdasn/xGi0xKuovCQwZRtPI2Lfl/c6qa9jQTFbn60NwOx3gWJ89K265/6kpDuahnBbplyWA==
+ version "1.4.7"
+ resolved "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-1.4.7.tgz#6255147ac777314c0725a1efcb42df10fe282243"
+ integrity sha512-QkunbJ96yUNkW95k/Vd6SdTjCbWSG0rMVUtpHSCwfg078Z7vbDaBnfz/gkSqR5h8WFMxoccBT4aodHm6387Jvg==
optionalDependencies:
"@babel/code-frame" "^7.8.3"
@@ -19328,9 +19328,9 @@ stacktrace-js@^2.0.0:
stacktrace-gps "^3.0.4"
start-server-and-test@^1.10.11:
- version "1.10.11"
- resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.10.11.tgz#24290ee8a5ed15f4a34e9bb45a5d6ff93c93c83e"
- integrity sha512-CZilaj293uQWdD4vgOxTOuzlCWxOyBm6bzmH1r6OGLG/q5zcBmGYevLfOimkg0kSn9jLHwYSXLuoKG/DDQJhww==
+ version "1.11.0"
+ resolved "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-1.11.0.tgz#1b1a83d062b0028ee6e296bb4e0231f2d8b2f4af"
+ integrity sha512-FhkJFYL/lvbd0tKWvbxWNWjtFtq3Zpa09QDjA8EUH88AsgNL4hkAAKYNmbac+fFM8/GIZoJ1Mj4mm3SMI0X1bA==
dependencies:
bluebird "3.7.2"
check-more-types "2.24.0"