frontend-app-api: implement core layout and initial sidebar

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-09-08 16:35:59 +02:00
parent 70169897f5
commit c55a1a4a93
9 changed files with 187 additions and 20 deletions
+2
View File
@@ -35,10 +35,12 @@
"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": {
+21 -5
View File
@@ -20,8 +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,
@@ -43,6 +45,7 @@ import {
IconComponent,
RouteRef,
BackstagePlugin as LegacyBackstagePlugin,
featureFlagsApiRef,
} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './wiring/discovery';
import {
@@ -58,6 +61,8 @@ 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';
@@ -68,6 +73,7 @@ import {
icons as defaultIcons,
themes as defaultThemes,
} from '../../app-defaults/src/defaults';
import { BrowserRouter } from 'react-router-dom';
/** @public */
export function createApp(options: {
@@ -80,7 +86,7 @@ export function createApp(options: {
options?.config ??
ConfigReader.fromConfigs(overrideBaseUrlConfigs(defaultConfigLoaderSync()));
const builtinExtensions = [CoreRouter, Core];
const builtinExtensions = [Core, CoreRoutes, CoreNav, CoreLayout];
const discoveredPlugins = getAvailablePlugins();
const allPlugins = [...discoveredPlugins, ...options.plugins];
@@ -180,9 +186,12 @@ export function createApp(options: {
<AppContextProvider appContext={appContext}>
<AppThemeProvider>
<RoutingProvider routePaths={routePaths}>
{rootComponents.map((Component, i) => (
<Component key={i} />
))}
{/* TODO: set base path using the logic from AppRouter */}
<BrowserRouter>
{rootComponents.map((Component, i) => (
<Component key={i} />
))}
</BrowserRouter>
</RoutingProvider>
</AppThemeProvider>
</AppContextProvider>
@@ -257,6 +266,13 @@ function createApiHolder(
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: {},
@@ -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: () => (
<SidebarPage>
<Nav />
<Content />
</SidebarPage>
),
});
},
});
@@ -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 (
<div className={classes.root}>
<Link to="/" underline="none" className={classes.link} aria-label="Home">
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
};
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: () => (
<Sidebar>
<SidebarLogo />
<SidebarDivider />
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
</Sidebar>
),
});
},
});
@@ -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,12 +48,7 @@ export const CoreRouter = createExtension({
return element;
};
bind({
// TODO: set base path using the logic from AppRouter
component: () => (
<BrowserRouter>
<Routes />
</BrowserRouter>
),
component: () => <Routes />,
});
},
});
@@ -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: {},
@@ -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,
@@ -143,7 +143,7 @@ describe('createPlugin', () => {
render(
createTestAppRoot({
plugins: [plugin],
config: { app: { extensions: [{ 'core.router': false }] } },
config: { app: { extensions: [{ 'core.routes': false }] } },
}),
);
@@ -169,7 +169,7 @@ describe('createPlugin', () => {
config: {
app: {
extensions: [
{ 'core.router': false },
{ 'core.routes': false },
{
'plugin.catalog.page': {
config: { name: 'CatalogRenamed' },
+2
View File
@@ -4318,10 +4318,12 @@ __metadata:
"@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
lodash: ^4.17.21
peerDependencies: