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:
@@ -1,6 +1,9 @@
|
||||
app:
|
||||
title: Backstage Example App
|
||||
baseUrl: http://localhost:3000
|
||||
experimental:
|
||||
packages: all # ✨
|
||||
|
||||
#datadogRum:
|
||||
# clientToken: '123456789'
|
||||
# applicationId: qwerty
|
||||
|
||||
@@ -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:^",
|
||||
|
||||
@@ -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>
|
||||
</>,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user