Add support for Pages and Sidebar items in existing app

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Co-authored-by: Camila Belo <camilaibs@gmail.com>
Co-authored-by: Philipp Hugenroth <philipph@spotify.com>
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Johan Haals
2023-09-13 14:55:51 +02:00
parent 474cd71ab8
commit 1807df9dbd
13 changed files with 180 additions and 13 deletions
+3
View File
@@ -1,6 +1,9 @@
app:
title: Backstage Example App
baseUrl: http://localhost:3000
experimental:
packages: all # ✨
#datadogRum:
# clientToken: '123456789'
# applicationId: qwerty
+1
View File
@@ -20,6 +20,7 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-adr": "workspace:^",
"@backstage/plugin-airbrake": "workspace:^",
+17 -4
View File
@@ -27,7 +27,13 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { createApp } from '@backstage/app-defaults';
import { AppRouter, FeatureFlagged, FlatRoutes } from '@backstage/core-app-api';
import {
AppRouter,
ConfigReader,
defaultConfigLoader,
FeatureFlagged,
FlatRoutes,
} from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -55,7 +61,6 @@ import {
import { orgPlugin } from '@backstage/plugin-org';
import { ExplorePage } from '@backstage/plugin-explore';
import { GcpProjectsPage } from '@backstage/plugin-gcp-projects';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
import { HomepageCompositionRoot } from '@backstage/plugin-home';
import { LighthousePage } from '@backstage/plugin-lighthouse';
import { NewRelicPage } from '@backstage/plugin-newrelic';
@@ -112,6 +117,7 @@ import { PuppetDbPage } from '@backstage/plugin-puppetdb';
import { DevToolsPage } from '@backstage/plugin-devtools';
import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
import { CatalogUnprocessedEntitiesPage } from '@backstage/plugin-catalog-unprocessed-entities';
import { createExtensionTree } from '@backstage/frontend-app-api';
const app = createApp({
apis,
@@ -158,6 +164,11 @@ const app = createApp({
},
});
/* HIGHLY EXPERIMENTAL. DO NOT USE THIS IN YOUR APP */
const extensionTree = createExtensionTree({
config: ConfigReader.fromConfigs(await defaultConfigLoader()),
});
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="catalog" />} />
@@ -271,7 +282,9 @@ const routes = (
path="/tech-radar"
element={<TechRadarPage width={1500} height={800} />}
/>
<Route path="/graphiql" element={<GraphiQLPage />} />
{
/* HIGHLY EXPERIMENTAL. DO NOT USE THIS IN YOUR APP */ extensionTree.getRootRoutes()
}
<Route path="/lighthouse" element={<LighthousePage />} />
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route path="/gcp-projects" element={<GcpProjectsPage />} />
@@ -310,7 +323,7 @@ export default app.createRoot(
<AlertDisplay transientTimeoutMs={2500} />
<OAuthRequestDialog />
<AppRouter>
<Root>{routes}</Root>
<Root extensionTree={extensionTree}>{routes}</Root>
</AppRouter>
</>,
);
+8 -3
View File
@@ -27,7 +27,6 @@ import MenuIcon from '@material-ui/icons/Menu';
import MoneyIcon from '@material-ui/icons/MonetizationOn';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
import {
Settings as SidebarSettings,
UserSettingsSignInAvatar,
@@ -53,6 +52,7 @@ import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import { useApp } from '@backstage/core-plugin-api';
import BuildIcon from '@material-ui/icons/Build';
import { ExtensionTree } from '@backstage/frontend-app-api';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -82,7 +82,10 @@ const SidebarLogo = () => {
);
};
export const Root = ({ children }: PropsWithChildren<{}>) => (
export const Root = ({
children,
extensionTree,
}: PropsWithChildren<{ extensionTree: ExtensionTree }>) => (
<SidebarPage>
<Sidebar>
<SidebarLogo />
@@ -163,7 +166,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
to="cost-insights"
text="Cost Insights"
/>
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
{
/* HIGHLY EXPERIMENTAL. DO NOT USE THIS IN YOUR APP */ extensionTree.getSidebarItems()
}
<SidebarItem icon={Score} to="score-board" text="Score board" />
</SidebarScrollWrapper>
<SidebarDivider />
@@ -65,7 +65,13 @@ const SidebarLogo = () => {
export const CoreNav = createExtension({
id: 'core.nav',
at: 'core.layout/nav',
inputs: {},
inputs: {
targets: {
extensionData: {
path: coreExtensionData.navTarget,
},
},
},
output: {
element: coreExtensionData.reactElement,
},
@@ -47,6 +47,8 @@ import {
RouteRef,
BackstagePlugin as LegacyBackstagePlugin,
featureFlagsApiRef,
attachComponentData,
useRouteRef,
} from '@backstage/core-plugin-api';
import { getAvailablePlugins } from './discovery';
import {
@@ -74,7 +76,8 @@ import {
icons as defaultIcons,
themes as defaultThemes,
} from '../../../app-defaults/src/defaults';
import { BrowserRouter } from 'react-router-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { SidebarItem } from '@backstage/core-components';
/** @public */
export interface ExtensionTreeNode {
@@ -86,6 +89,8 @@ export interface ExtensionTreeNode {
export interface ExtensionTree {
getExtension(id: string): ExtensionTreeNode | undefined;
getExtensionAttachments(id: string, inputName: string): ExtensionTreeNode[];
getRootRoutes(): JSX.Element[];
getSidebarItems(): JSX.Element[];
}
/** @public */
@@ -108,6 +113,51 @@ export function createExtensionTree(options: {
): ExtensionTreeNode[] {
return instances.get(id)?.attachments.get(inputName) ?? [];
},
getRootRoutes(): JSX.Element[] {
return this.getExtensionAttachments('core.routes', 'routes').map(node => {
const path = node.getData(coreExtensionData.routePath);
const element = node.getData(coreExtensionData.reactElement);
const routeRef = node.getData(coreExtensionData.routeRef);
if (!path || !element) {
throw new Error(`Invalid route extension: ${node.id}`);
}
const Component = () => {
return element;
};
attachComponentData(Component, 'core.mountPoint', routeRef);
return <Route path={path} element={<Component />} />;
});
},
getSidebarItems(): JSX.Element[] {
const RoutedSidebarItem = (props: {
title: string;
routeRef: RouteRef;
icon: IconComponent;
}): React.JSX.Element => {
const location = useRouteRef(props.routeRef);
return (
<SidebarItem icon={props.icon} to={location()} text={props.title} />
);
};
return this.getExtensionAttachments('core.nav', 'items')
.map((node, index) => {
const target = node.getData(coreExtensionData.navTarget);
if (!target) {
return null;
}
return (
<RoutedSidebarItem
key={index}
title={target.title}
icon={target.icon}
routeRef={target.routeRef}
/>
);
})
.filter((x): x is JSX.Element => !!x);
},
};
}
@@ -0,0 +1,49 @@
/*
* 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 { IconComponent, RouteRef } from '@backstage/core-plugin-api';
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
import { coreExtensionData, createExtension } from '../wiring';
export function createNavItemExtension(options: {
id: string;
routeRef: RouteRef;
title: string;
icon: IconComponent;
}) {
const { id, routeRef, title, icon } = options;
return createExtension({
id,
at: 'core.nav/items',
configSchema: createSchemaFromZod(z =>
z.object({
title: z.string().default(title),
}),
),
output: {
navTarget: coreExtensionData.navTarget,
},
factory: ({ bind, config }) => {
bind({
navTarget: {
title: config.title,
icon,
routeRef,
},
});
},
});
}
@@ -16,3 +16,4 @@
export { createApiExtension } from './createApiExtension';
export { createPageExtension } from './createPageExtension';
export { createNavItemExtension } from './createNavItemExtension';
@@ -14,14 +14,26 @@
* limitations under the License.
*/
import { AnyApiFactory, RouteRef } from '@backstage/core-plugin-api';
import { JSX } from 'react';
import {
AnyApiFactory,
IconComponent,
RouteRef,
} from '@backstage/core-plugin-api';
import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
export type NavTarget = {
title: string;
icon: IconComponent;
routeRef: RouteRef<{}>;
};
/** @public */
export const coreExtensionData = {
reactElement: createExtensionDataRef<JSX.Element>('core.reactElement'),
routePath: createExtensionDataRef<string>('core.routing.path'),
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
navTarget: createExtensionDataRef<NavTarget>('core.nav.target'),
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export { coreExtensionData } from './coreExtensionData';
export { coreExtensionData, type NavTarget } from './coreExtensionData';
export {
createExtension,
type CreateExtensionOptions,
+5
View File
@@ -7,6 +7,7 @@
import { ApiRef } from '@backstage/core-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/frontend-plugin-api';
import { ErrorApi } from '@backstage/core-plugin-api';
import { FetchApi } from '@backstage/core-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
@@ -15,6 +16,10 @@ import { OAuthApi } from '@backstage/core-plugin-api';
import { default as React_2 } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
// @alpha (undocumented)
const _default: BackstagePlugin_2;
export default _default;
// @public
export type EndpointConfig = {
id: string;
+23 -2
View File
@@ -19,6 +19,7 @@ import {
createApiExtension,
createExtension,
createExtensionDataRef,
createNavItemExtension,
createPageExtension,
createPlugin,
createSchemaFromZod,
@@ -29,15 +30,30 @@ import {
GraphQLEndpoints,
GraphQLEndpoint,
} from '@backstage/plugin-graphiql';
import { createApiFactory } from '@backstage/core-plugin-api';
import GraphiQLIcon from './assets/graphiql.icon.svg';
import {
createApiFactory,
createRouteRef,
IconComponent,
} from '@backstage/core-plugin-api';
const graphiqlRouteRef = createRouteRef({ id: 'graphiql' });
/** @alpha */
export const GraphiqlPage = createPageExtension({
id: 'plugin.graphiql.page',
defaultPath: '/graphiql',
routeRef: graphiqlRouteRef,
loader: () => import('./components').then(m => <m.GraphiQLPage />),
});
export const graphiqlPageSidebarItem = createNavItemExtension({
id: 'plugin.graphiql.nav.index',
title: 'GraphiQL',
icon: GraphiQLIcon as IconComponent,
routeRef: graphiqlRouteRef,
});
/** @internal */
const endpointDataRef = createExtensionDataRef<GraphQLEndpoint>(
'plugin.graphiql.endpoint',
@@ -103,5 +119,10 @@ const gitlabGraphiQLBrowseExtension = createEndpointExtension({
/** @alpha */
export default createPlugin({
id: 'graphiql',
extensions: [GraphiqlPage, graphiqlBrowseApi, gitlabGraphiQLBrowseExtension],
extensions: [
GraphiqlPage,
graphiqlBrowseApi,
gitlabGraphiQLBrowseExtension,
graphiqlPageSidebarItem,
],
});
+1
View File
@@ -25847,6 +25847,7 @@ __metadata:
"@backstage/core-app-api": "workspace:^"
"@backstage/core-components": "workspace:^"
"@backstage/core-plugin-api": "workspace:^"
"@backstage/frontend-app-api": "workspace:^"
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-adr": "workspace:^"
"@backstage/plugin-airbrake": "workspace:^"