diff --git a/.changeset/mean-planes-smoke.md b/.changeset/mean-planes-smoke.md
new file mode 100644
index 0000000000..b082023cde
--- /dev/null
+++ b/.changeset/mean-planes-smoke.md
@@ -0,0 +1,5 @@
+---
+'@backstage/core-app-api': patch
+---
+
+Internal refactor
diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index c94d900b21..a97c52f40f 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -2,6 +2,8 @@ app:
experimental:
packages: 'all' # ✨
+ extensions:
+ - apis.plugin.graphiql.browse.gitlab: true
# scmAuthExtension: >-
# createScmAuthExtension({
diff --git a/packages/app-next/src/App.test.tsx b/packages/app-next/src/App.test.tsx
index 047c9821b5..b3cef58fe1 100644
--- a/packages/app-next/src/App.test.tsx
+++ b/packages/app-next/src/App.test.tsx
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-import React from 'react';
import { renderWithEffects } from '@backstage/test-utils';
+jest.mock('@backstage/plugin-graphiql', () => ({
+ ...jest.requireActual('@backstage/plugin-graphiql'),
+ GraphiQLIcon: () => null,
+}));
+
describe('App', () => {
it('should render', async () => {
process.env = {
@@ -41,8 +45,8 @@ describe('App', () => {
] as any,
};
- const { default: App } = await import('./App');
- const rendered = await renderWithEffects();
+ const { default: app } = await import('./App');
+ const rendered = await renderWithEffects(app);
expect(rendered.baseElement).toBeInTheDocument();
});
});
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index 0452cc3709..aa9bb985c2 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -14,8 +14,6 @@
* limitations under the License.
*/
-import { graphiqlPlugin as legacyGraphiqlPlugin } from '@backstage/plugin-graphiql';
-import { createApp as createLegacyApp } from '@backstage/app-defaults';
import { createApp } from '@backstage/frontend-app-api';
import { pagesPlugin } from './examples/pagesPlugin';
import graphiqlPlugin from '@backstage/plugin-graphiql/alpha';
@@ -61,9 +59,9 @@ const app = createApp({
// },
});
-const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] });
+// const legacyApp = createLegacyApp({ plugins: [legacyGraphiqlPlugin] });
-export default legacyApp.createRoot(app.createRoot());
+export default app.createRoot();
// const routes = (
//
diff --git a/packages/app-next/src/index.tsx b/packages/app-next/src/index.tsx
index b15bc4c102..3c354b06d0 100644
--- a/packages/app-next/src/index.tsx
+++ b/packages/app-next/src/index.tsx
@@ -15,8 +15,7 @@
*/
import '@backstage/cli/asset-types';
-import React from 'react';
import ReactDOM from 'react-dom';
-import App from './App';
+import app from './App';
-ReactDOM.render(, document.getElementById('root'));
+ReactDOM.render(app, document.getElementById('root'));
diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx
index de078c0ffc..0bd495c03b 100644
--- a/packages/core-app-api/src/app/AppManager.tsx
+++ b/packages/core-app-api/src/app/AppManager.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { AppConfig, Config } from '@backstage/config';
+import { Config } from '@backstage/config';
import React, {
ComponentType,
PropsWithChildren,
@@ -81,6 +81,7 @@ import { InternalAppContext } from './InternalAppContext';
import { AppRouter, getBasePath } from './AppRouter';
import { AppTranslationProvider } from './AppTranslationProvider';
import { AppTranslationApiImpl } from '../apis/implementations/AppTranslationApi';
+import { overrideBaseUrlConfigs } from './overrideBaseUrlConfigs';
type CompatiblePlugin =
| BackstagePlugin
@@ -88,17 +89,6 @@ type CompatiblePlugin =
output(): Array<{ type: 'feature-flag'; name: string }>;
});
-/**
- * Creates a base URL that uses to the current document origin.
- */
-function createLocalBaseUrl(fullUrl: string): string {
- const url = new URL(fullUrl);
- url.protocol = document.location.protocol;
- url.hostname = document.location.hostname;
- url.port = document.location.port;
- return url.toString().replace(/\/$/, '');
-}
-
function useConfigLoader(
configLoader: AppConfigLoader | undefined,
components: AppComponents,
@@ -131,52 +121,9 @@ function useConfigLoader(
};
}
- let configReader;
- /**
- * config.value can be undefined or empty. If it's either, don't bother overriding anything.
- */
- if (config.value?.length) {
- const urlConfigReader = ConfigReader.fromConfigs(config.value);
-
- /**
- * Test configs may not define `app.baseUrl` or `backend.baseUrl` and we
- * don't want to enforce here.
- */
- const appBaseUrl = urlConfigReader.getOptionalString('app.baseUrl');
- const backendBaseUrl = urlConfigReader.getOptionalString('backend.baseUrl');
-
- let configs = config.value;
- const relativeResolverConfig: AppConfig = {
- data: {},
- context: 'relative-resolver',
- };
- if (appBaseUrl && backendBaseUrl) {
- const appOrigin = new URL(appBaseUrl).origin;
- const backendOrigin = new URL(backendBaseUrl).origin;
-
- if (appOrigin === backendOrigin) {
- const newBackendBaseUrl = createLocalBaseUrl(backendBaseUrl);
- if (backendBaseUrl !== newBackendBaseUrl) {
- relativeResolverConfig.data.backend = { baseUrl: newBackendBaseUrl };
- }
- }
- }
- if (appBaseUrl) {
- const newAppBaseUrl = createLocalBaseUrl(appBaseUrl);
- if (appBaseUrl !== newAppBaseUrl) {
- relativeResolverConfig.data.app = { baseUrl: newAppBaseUrl };
- }
- }
- /**
- * Only add the relative config if there is actually data to add.
- */
- if (Object.keys(relativeResolverConfig.data).length) {
- configs = configs.concat([relativeResolverConfig]);
- }
- configReader = ConfigReader.fromConfigs(configs);
- } else {
- configReader = ConfigReader.fromConfigs([]);
- }
+ const configReader = ConfigReader.fromConfigs(
+ config.value?.length ? overrideBaseUrlConfigs(config.value) : [],
+ );
return { api: configReader };
}
diff --git a/packages/core-app-api/src/app/defaultConfigLoader.test.ts b/packages/core-app-api/src/app/defaultConfigLoader.test.ts
index aebba92c43..7579e53dd2 100644
--- a/packages/core-app-api/src/app/defaultConfigLoader.test.ts
+++ b/packages/core-app-api/src/app/defaultConfigLoader.test.ts
@@ -14,40 +14,38 @@
* limitations under the License.
*/
-import { defaultConfigLoader } from './defaultConfigLoader';
+import { defaultConfigLoaderSync } from './defaultConfigLoader';
(process as any).env = { NODE_ENV: 'test' };
const anyEnv = process.env as any;
const anyWindow = window as any;
-describe('defaultConfigLoader', () => {
+describe('defaultConfigLoaderSync', () => {
afterEach(() => {
delete anyEnv.APP_CONFIG;
delete anyWindow.__APP_CONFIG__;
});
- it('loads static config', async () => {
+ it('loads static config', () => {
anyEnv.APP_CONFIG = [
{ data: { my: 'config' }, context: 'a' },
{ data: { my: 'override-config' }, context: 'b' },
];
- const configs = await defaultConfigLoader();
+ const configs = defaultConfigLoaderSync();
expect(configs).toEqual([
{ data: { my: 'config' }, context: 'a' },
{ data: { my: 'override-config' }, context: 'b' },
]);
});
- it('loads runtime config', async () => {
+ it('loads runtime config', () => {
anyEnv.APP_CONFIG = [
{ data: { my: 'override-config' }, context: 'a' },
{ data: { my: 'config' }, context: 'b' },
];
- const configs = await (defaultConfigLoader as any)(
- '{"my":"runtime-config"}',
- );
+ const configs = (defaultConfigLoaderSync as any)('{"my":"runtime-config"}');
expect(configs).toEqual([
{ data: { my: 'override-config' }, context: 'a' },
{ data: { my: 'config' }, context: 'b' },
@@ -55,28 +53,28 @@ describe('defaultConfigLoader', () => {
]);
});
- it('fails to load invalid missing config', async () => {
- await expect(defaultConfigLoader()).rejects.toThrow(
+ it('fails to load invalid missing config', () => {
+ expect(() => defaultConfigLoaderSync()).toThrow(
'No static configuration provided',
);
});
- it('fails to load invalid static config', async () => {
+ it('fails to load invalid static config', () => {
anyEnv.APP_CONFIG = { my: 'invalid-config' };
- await expect(defaultConfigLoader()).rejects.toThrow(
+ expect(() => defaultConfigLoaderSync()).toThrow(
'Static configuration has invalid format',
);
});
- it('fails to load bad runtime config', async () => {
+ it('fails to load bad runtime config', () => {
anyEnv.APP_CONFIG = [{ data: { my: 'config' }, context: 'a' }];
- await expect((defaultConfigLoader as any)('}')).rejects.toThrow(
+ expect(() => defaultConfigLoaderSync('}')).toThrow(
'Failed to load runtime configuration, SyntaxError: Unexpected token } in JSON at position 0',
);
});
- it('loads config from window.__APP_CONFIG__', async () => {
+ it('loads config from window.__APP_CONFIG__', () => {
anyEnv.APP_CONFIG = [
{ data: { my: 'config' }, context: 'a' },
{ data: { my: 'override-config' }, context: 'b' },
@@ -84,7 +82,7 @@ describe('defaultConfigLoader', () => {
const windowConfig = { app: { configKey: 'config-value' } };
anyWindow.__APP_CONFIG__ = windowConfig;
- const configs = await defaultConfigLoader();
+ const configs = defaultConfigLoaderSync();
expect(configs).toEqual([
...anyEnv.APP_CONFIG,
diff --git a/packages/core-app-api/src/app/defaultConfigLoader.ts b/packages/core-app-api/src/app/defaultConfigLoader.ts
index 00aee2d367..301b64a865 100644
--- a/packages/core-app-api/src/app/defaultConfigLoader.ts
+++ b/packages/core-app-api/src/app/defaultConfigLoader.ts
@@ -30,12 +30,16 @@ import { AppConfigLoader } from './types';
*
* @public
*/
-export const defaultConfigLoader: AppConfigLoader = async (
+export const defaultConfigLoader: AppConfigLoader = async () =>
+ defaultConfigLoaderSync();
+
+/** @internal */
+export function defaultConfigLoaderSync(
// 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');
@@ -70,4 +74,4 @@ export const defaultConfigLoader: AppConfigLoader = async (
});
}
return configs;
-};
+}
diff --git a/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts b/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts
new file mode 100644
index 0000000000..5dab6db127
--- /dev/null
+++ b/packages/core-app-api/src/app/overrideBaseUrlConfigs.ts
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { AppConfig, ConfigReader } from '@backstage/config';
+
+/**
+ * Creates a base URL that uses to the current document origin.
+ */
+function createLocalBaseUrl(fullUrl: string): string {
+ const url = new URL(fullUrl);
+ url.protocol = document.location.protocol;
+ url.hostname = document.location.hostname;
+ url.port = document.location.port;
+ return url.toString().replace(/\/$/, '');
+}
+
+/**
+ * If we are able to override the app and backend base URLs to values that
+ * match the origin of the current location, then this function returns a
+ * new array of app configs that contain the overrides.
+ *
+ * @internal
+ */
+export function overrideBaseUrlConfigs(inputConfigs: AppConfig[]): AppConfig[] {
+ const urlConfigReader = ConfigReader.fromConfigs(inputConfigs);
+
+ // In tests we may not have `app.baseUrl` or `backend.baseUrl`, to keep them optional
+ const appBaseUrl = urlConfigReader.getOptionalString('app.baseUrl');
+ const backendBaseUrl = urlConfigReader.getOptionalString('backend.baseUrl');
+
+ let configs = inputConfigs;
+
+ let newBackendBaseUrl: string | undefined = undefined;
+ let newAppBaseUrl: string | undefined = undefined;
+
+ if (appBaseUrl && backendBaseUrl) {
+ const appOrigin = new URL(appBaseUrl).origin;
+ const backendOrigin = new URL(backendBaseUrl).origin;
+
+ if (appOrigin === backendOrigin) {
+ const maybeNewBackendBaseUrl = createLocalBaseUrl(backendBaseUrl);
+ if (backendBaseUrl !== maybeNewBackendBaseUrl) {
+ newBackendBaseUrl = maybeNewBackendBaseUrl;
+ }
+ }
+ }
+
+ if (appBaseUrl) {
+ const maybeNewAppBaseUrl = createLocalBaseUrl(appBaseUrl);
+ if (appBaseUrl !== maybeNewAppBaseUrl) {
+ newAppBaseUrl = maybeNewAppBaseUrl;
+ }
+ }
+
+ // Only add the relative config if there is actually data to add.
+ if (newAppBaseUrl || newBackendBaseUrl) {
+ configs = configs.concat({
+ data: {
+ app: newAppBaseUrl && {
+ baseUrl: newAppBaseUrl,
+ },
+ backend: newBackendBaseUrl && {
+ baseUrl: newBackendBaseUrl,
+ },
+ },
+ context: 'relative-resolver',
+ });
+ }
+
+ return configs;
+}
diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md
index bf7db4451b..e16e4d4d97 100644
--- a/packages/frontend-app-api/api-report.md
+++ b/packages/frontend-app-api/api-report.md
@@ -6,9 +6,13 @@
///
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
+import { ConfigApi } from '@backstage/core-plugin-api';
// @public (undocumented)
-export function createApp(options: { plugins: BackstagePlugin[] }): {
+export function createApp(options: {
+ plugins: BackstagePlugin[];
+ config?: ConfigApi;
+}): {
createRoot(): JSX.Element;
};
```
diff --git a/packages/frontend-app-api/package.json b/packages/frontend-app-api/package.json
index 451a1c0c5a..463a10fcae 100644
--- a/packages/frontend-app-api/package.json
+++ b/packages/frontend-app-api/package.json
@@ -34,10 +34,13 @@
],
"dependencies": {
"@backstage/config": "workspace:^",
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-graphiql": "workspace:^",
"@backstage/types": "workspace:^",
+ "@material-ui/core": "^4.12.4",
"lodash": "^4.17.21"
},
"peerDependencies": {
diff --git a/packages/frontend-app-api/src/createApp.tsx b/packages/frontend-app-api/src/createApp.tsx
index c50b507642..4882191206 100644
--- a/packages/frontend-app-api/src/createApp.tsx
+++ b/packages/frontend-app-api/src/createApp.tsx
@@ -20,7 +20,10 @@ import {
BackstagePlugin,
coreExtensionData,
} from '@backstage/frontend-plugin-api';
-import { CoreRouter } from './extensions/CoreRouter';
+import { Core } from './extensions/Core';
+import { CoreRoutes } from './extensions/CoreRoutes';
+import { CoreLayout } from './extensions/CoreLayout';
+import { CoreNav } from './extensions/CoreNav';
import {
createExtensionInstance,
ExtensionInstance,
@@ -31,22 +34,66 @@ import {
readAppExtensionParameters,
} from './wiring/parameters';
import { RoutingProvider } from './routing/RoutingContext';
-import { RouteRef } from '@backstage/core-plugin-api';
+import {
+ AnyApiFactory,
+ ApiHolder,
+ AppComponents,
+ AppContext,
+ appThemeApiRef,
+ ConfigApi,
+ configApiRef,
+ IconComponent,
+ RouteRef,
+ BackstagePlugin as LegacyBackstagePlugin,
+ featureFlagsApiRef,
+} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './wiring/discovery';
+import {
+ ApiFactoryRegistry,
+ ApiProvider,
+ ApiResolver,
+ AppThemeSelector,
+} from '@backstage/core-app-api';
+
+// TODO: Get rid of all of these
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { AppThemeProvider } from '../../core-app-api/src/app/AppThemeProvider';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { AppContextProvider } from '../../core-app-api/src/app/AppContext';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { LocalStorageFeatureFlags } from '../../core-app-api/src/apis/implementations/FeatureFlagsApi/LocalStorageFeatureFlags';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { defaultConfigLoaderSync } from '../../core-app-api/src/app/defaultConfigLoader';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { overrideBaseUrlConfigs } from '../../core-app-api/src/app/overrideBaseUrlConfigs';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import {
+ apis as defaultApis,
+ components as defaultComponents,
+ icons as defaultIcons,
+ themes as defaultThemes,
+} from '../../app-defaults/src/defaults';
+import { BrowserRouter } from 'react-router-dom';
/** @public */
-export function createApp(options: { plugins: BackstagePlugin[] }): {
+export function createApp(options: {
+ plugins: BackstagePlugin[];
+ config?: ConfigApi;
+}): {
createRoot(): JSX.Element;
} {
- const appConfig = ConfigReader.fromConfigs(process.env.APP_CONFIG as any);
+ const appConfig =
+ options?.config ??
+ ConfigReader.fromConfigs(overrideBaseUrlConfigs(defaultConfigLoaderSync()));
- const builtinExtensions = [CoreRouter];
+ const builtinExtensions = [Core, CoreRoutes, CoreNav, CoreLayout];
const discoveredPlugins = getAvailablePlugins();
+ const allPlugins = [...discoveredPlugins, ...options.plugins];
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionParams = mergeExtensionParameters({
- sources: [...options.plugins, ...discoveredPlugins],
+ sources: allPlugins,
builtinExtensions,
parameters: readAppExtensionParameters(appConfig),
});
@@ -115,25 +162,144 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
const routePaths = extractRouteInfoFromInstanceTree(rootInstances);
+ const coreInstance = rootInstances.find(({ id }) => id === 'core');
+ if (!coreInstance) {
+ throw Error('Unable to find core extension instance');
+ }
+
+ const apiHolder = createApiHolder(coreInstance, appConfig);
+
+ const appContext = createLegacyAppContext(allPlugins);
+
return {
createRoot() {
- const rootComponents = rootInstances.map(
- e =>
- e.data.get(
- coreExtensionData.reactComponent.id,
- ) as typeof coreExtensionData.reactComponent.T,
- );
+ const rootComponents = rootInstances
+ .map(
+ e =>
+ e.data.get(
+ coreExtensionData.reactComponent.id,
+ ) as typeof coreExtensionData.reactComponent.T,
+ )
+ .filter(Boolean);
return (
-
- {rootComponents.map((Component, i) => (
-
- ))}
-
+
+
+
+
+ {/* TODO: set base path using the logic from AppRouter */}
+
+ {rootComponents.map((Component, i) => (
+
+ ))}
+
+
+
+
+
);
},
};
}
+function toLegacyPlugin(plugin: BackstagePlugin): LegacyBackstagePlugin {
+ const errorMsg = 'Not implemented in legacy plugin compatibility layer';
+ const notImplemented = () => {
+ throw new Error(errorMsg);
+ };
+ return {
+ getId(): string {
+ return plugin.id;
+ },
+ get routes(): never {
+ throw new Error(errorMsg);
+ },
+ get externalRoutes(): never {
+ throw new Error(errorMsg);
+ },
+ getApis: notImplemented,
+ getFeatureFlags: notImplemented,
+ provide: notImplemented,
+ __experimentalReconfigure: notImplemented,
+ };
+}
+
+function createLegacyAppContext(plugins: BackstagePlugin[]): AppContext {
+ return {
+ getPlugins(): LegacyBackstagePlugin[] {
+ return plugins.map(toLegacyPlugin);
+ },
+
+ getSystemIcon(key: string): IconComponent | undefined {
+ return key in defaultIcons
+ ? defaultIcons[key as keyof typeof defaultIcons]
+ : undefined;
+ },
+
+ getSystemIcons(): Record {
+ return defaultIcons;
+ },
+
+ getComponents(): AppComponents {
+ return defaultComponents;
+ },
+ };
+}
+
+function createApiHolder(
+ coreExtension: ExtensionInstance,
+ configApi: ConfigApi,
+): ApiHolder {
+ const factoryRegistry = new ApiFactoryRegistry();
+
+ const apiFactories =
+ coreExtension.attachments
+ .get('apis')
+ ?.map(
+ e =>
+ e.data.get(
+ coreExtensionData.apiFactory.id,
+ ) as typeof coreExtensionData.apiFactory.T,
+ )
+ .filter(Boolean) ?? [];
+
+ for (const factory of apiFactories) {
+ factoryRegistry.register('default', factory);
+ }
+
+ // TODO: properly discovery feature flags, maybe rework the whole thing
+ factoryRegistry.register('default', {
+ api: featureFlagsApiRef,
+ deps: {},
+ factory: () => new LocalStorageFeatureFlags(),
+ });
+
+ factoryRegistry.register('static', {
+ api: appThemeApiRef,
+ deps: {},
+ // TODO: add extension for registering themes
+ factory: () => AppThemeSelector.createWithStorage(defaultThemes),
+ });
+
+ factoryRegistry.register('static', {
+ api: configApiRef,
+ deps: {},
+ factory: () => configApi,
+ });
+
+ // TODO: ship these as default extensions instead
+ for (const factory of defaultApis as AnyApiFactory[]) {
+ if (!factoryRegistry.register('app', factory)) {
+ throw new Error(
+ `Duplicate or forbidden API factory for ${factory.api} in app`,
+ );
+ }
+ }
+
+ ApiResolver.validateFactories(factoryRegistry, factoryRegistry.getAllApis());
+
+ return new ApiResolver(factoryRegistry);
+}
+
/** @internal */
export function extractRouteInfoFromInstanceTree(
roots: ExtensionInstance[],
diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx
new file mode 100644
index 0000000000..cb93b2888f
--- /dev/null
+++ b/packages/frontend-app-api/src/extensions/Core.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ coreExtensionData,
+ createExtension,
+} from '@backstage/frontend-plugin-api';
+
+export const Core = createExtension({
+ id: 'core',
+ at: 'root',
+ inputs: {
+ apis: {
+ extensionData: {
+ api: coreExtensionData.apiFactory,
+ },
+ },
+ },
+ output: {},
+ factory() {},
+});
diff --git a/packages/frontend-app-api/src/extensions/CoreLayout.tsx b/packages/frontend-app-api/src/extensions/CoreLayout.tsx
new file mode 100644
index 0000000000..845bc8cf5b
--- /dev/null
+++ b/packages/frontend-app-api/src/extensions/CoreLayout.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import {
+ createExtension,
+ coreExtensionData,
+} from '@backstage/frontend-plugin-api';
+import { SidebarPage } from '@backstage/core-components';
+
+export const CoreLayout = createExtension({
+ id: 'core.layout',
+ at: 'root',
+ inputs: {
+ nav: {
+ extensionData: {
+ component: coreExtensionData.reactComponent,
+ },
+ },
+ content: {
+ extensionData: {
+ component: coreExtensionData.reactComponent,
+ },
+ },
+ },
+ output: {
+ component: coreExtensionData.reactComponent,
+ },
+ factory({ bind, inputs }) {
+ // TODO: Support this as part of the core system
+ if (inputs.nav.length !== 1) {
+ throw Error(
+ `Extension 'core.layout' did not receive exactly one 'nav' input, got ${inputs.nav.length}`,
+ );
+ }
+ const Nav = inputs.nav[0].component;
+
+ if (inputs.content.length !== 1) {
+ throw Error(
+ `Extension 'core.layout' did not receive exactly one 'content' input, got ${inputs.content.length}`,
+ );
+ }
+ const Content = inputs.content[0].component;
+
+ bind({
+ // TODO: set base path using the logic from AppRouter
+ component: () => (
+
+
+
+
+ ),
+ });
+ },
+});
diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx
new file mode 100644
index 0000000000..57f458cec7
--- /dev/null
+++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import {
+ createExtension,
+ coreExtensionData,
+} from '@backstage/frontend-plugin-api';
+import { makeStyles } from '@material-ui/core';
+import {
+ Sidebar,
+ useSidebarOpenState,
+ Link,
+ sidebarConfig,
+ SidebarDivider,
+ SidebarItem,
+} from '@backstage/core-components';
+import { GraphiQLIcon } from '@backstage/plugin-graphiql';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import LogoIcon from '../../../app/src/components/Root/LogoIcon';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import LogoFull from '../../../app/src/components/Root/LogoFull';
+
+const useSidebarLogoStyles = makeStyles({
+ root: {
+ width: sidebarConfig.drawerWidthClosed,
+ height: 3 * sidebarConfig.logoHeight,
+ display: 'flex',
+ flexFlow: 'row nowrap',
+ alignItems: 'center',
+ marginBottom: -14,
+ },
+ link: {
+ width: sidebarConfig.drawerWidthClosed,
+ marginLeft: 24,
+ },
+});
+
+const SidebarLogo = () => {
+ const classes = useSidebarLogoStyles();
+ const { isOpen } = useSidebarOpenState();
+
+ return (
+
+
+ {isOpen ? : }
+
+
+ );
+};
+
+export const CoreNav = createExtension({
+ id: 'core.nav',
+ at: 'core.layout/nav',
+ inputs: {},
+ output: {
+ component: coreExtensionData.reactComponent,
+ },
+ factory({ bind }) {
+ bind({
+ // TODO: set base path using the logic from AppRouter
+ component: () => (
+
+
+
+
+
+ ),
+ });
+ },
+});
diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx
similarity index 83%
rename from packages/frontend-app-api/src/extensions/CoreRouter.tsx
rename to packages/frontend-app-api/src/extensions/CoreRoutes.tsx
index 05f84aba6d..5b48149fbf 100644
--- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx
@@ -19,11 +19,11 @@ import {
createExtension,
coreExtensionData,
} from '@backstage/frontend-plugin-api';
-import { BrowserRouter, useRoutes } from 'react-router-dom';
+import { useRoutes } from 'react-router-dom';
-export const CoreRouter = createExtension({
- id: 'core.router',
- at: 'root',
+export const CoreRoutes = createExtension({
+ id: 'core.routes',
+ at: 'core.layout/content',
inputs: {
routes: {
extensionData: {
@@ -48,11 +48,7 @@ export const CoreRouter = createExtension({
return element;
};
bind({
- component: () => (
-
-
-
- ),
+ component: () => ,
});
},
});
diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json
index fda7bd8a05..5fed19a46a 100644
--- a/packages/frontend-plugin-api/package.json
+++ b/packages/frontend-plugin-api/package.json
@@ -25,6 +25,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3"
},
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
index db389ae4b1..71f6c87374 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx
@@ -35,7 +35,7 @@ describe('createPageExtension', () => {
).toEqual({
$$type: 'extension',
id: 'test',
- at: 'core.router/routes',
+ at: 'core.routes/routes',
configSchema: expect.anything(),
disabled: false,
inputs: {},
@@ -88,7 +88,7 @@ describe('createPageExtension', () => {
).toEqual({
$$type: 'extension',
id: 'test',
- at: 'core.router/routes',
+ at: 'core.routes/routes',
configSchema: expect.anything(),
disabled: false,
inputs: {},
diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
index 167dd397dd..9a82aa3f37 100644
--- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
+++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx
@@ -63,7 +63,7 @@ export function createPageExtension<
return createExtension({
id: options.id,
- at: options.at ?? 'core.router/routes',
+ at: options.at ?? 'core.routes/routes',
disabled: options.disabled,
output: {
component: coreExtensionData.reactComponent,
diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
index 724dad5143..a8ec7b3471 100644
--- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
+++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts
@@ -23,6 +23,7 @@ import { JsonObject } from '@backstage/types';
import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { coreExtensionData } from './coreExtensionData';
+import { MockConfigApi } from '@backstage/test-utils';
const nameExtensionDataRef = createExtensionDataRef('name');
@@ -113,17 +114,10 @@ function createTestAppRoot({
plugins: BackstagePlugin[];
config: JsonObject;
}) {
- Object.defineProperty(process.env, 'APP_CONFIG', {
- value: [
- {
- data: config,
- context: 'test',
- },
- ],
- configurable: true,
- });
-
- return createApp({ plugins: plugins }).createRoot();
+ return createApp({
+ plugins: plugins,
+ config: new MockConfigApi(config),
+ }).createRoot();
}
describe('createPlugin', () => {
@@ -143,7 +137,7 @@ describe('createPlugin', () => {
render(
createTestAppRoot({
plugins: [plugin],
- config: { app: { extensions: [{ 'core.router': false }] } },
+ config: { app: { extensions: [{ 'core.layout': false }] } },
}),
);
@@ -169,7 +163,7 @@ describe('createPlugin', () => {
config: {
app: {
extensions: [
- { 'core.router': false },
+ { 'core.layout': false },
{
'plugin.catalog.page': {
config: { name: 'CatalogRenamed' },
diff --git a/plugins/graphiql/alpha-api-report.md b/plugins/graphiql/alpha-api-report.md
index 31bad223b1..665bf1464e 100644
--- a/plugins/graphiql/alpha-api-report.md
+++ b/plugins/graphiql/alpha-api-report.md
@@ -12,6 +12,7 @@ import { PortableSchema } from '@backstage/frontend-plugin-api';
export function createEndpointExtension(options: {
id: string;
configSchema?: PortableSchema;
+ disabled?: boolean;
factory: (options: { config: TConfig }) => {
endpoint: GraphQLEndpoint;
};
diff --git a/plugins/graphiql/src/alpha.tsx b/plugins/graphiql/src/alpha.tsx
index ded517e3c7..f0333c8c6c 100644
--- a/plugins/graphiql/src/alpha.tsx
+++ b/plugins/graphiql/src/alpha.tsx
@@ -65,13 +65,14 @@ export const graphiqlBrowseApi = createApiExtension({
export function createEndpointExtension(options: {
id: string;
configSchema?: PortableSchema;
+ disabled?: boolean;
factory: (options: { config: TConfig }) => { endpoint: GraphQLEndpoint };
}) {
return createExtension({
id: `apis.plugin.graphiql.browse.${options.id}`,
at: 'apis.plugin.graphiql.browse/endpoints',
configSchema: options.configSchema,
- disabled: true,
+ disabled: options.disabled ?? false,
output: {
endpoint: endpointDataRef,
},
@@ -86,6 +87,7 @@ export function createEndpointExtension(options: {
/** @alpha */
const gitlabGraphiQLBrowseExtension = createEndpointExtension({
id: 'gitlab',
+ disabled: true,
configSchema: createSchemaFromZod(z =>
z
.object({
diff --git a/yarn.lock b/yarn.lock
index 6efa788884..c62fc28103 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4317,10 +4317,13 @@ __metadata:
dependencies:
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
+ "@backstage/core-app-api": "workspace:^"
+ "@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-graphiql": "workspace:^"
"@backstage/types": "workspace:^"
+ "@material-ui/core": ^4.12.4
"@testing-library/jest-dom": ^5.10.1
"@types/react": ^16.13.1 || ^17.0.0
lodash: ^4.17.21
@@ -4337,6 +4340,7 @@ __metadata:
"@backstage/cli": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
+ "@backstage/test-utils": "workspace:^"
"@backstage/types": "workspace:^"
"@testing-library/jest-dom": ^5.10.1
"@testing-library/react": ^12.1.3