From 1807df9dbd9bf33c5bf19513ffd48e29492a1082 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 13 Sep 2023 14:55:51 +0200 Subject: [PATCH] Add support for Pages and Sidebar items in existing app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Patrik Oldsberg Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Johan Haals --- app-config.yaml | 3 ++ packages/app/package.json | 1 + packages/app/src/App.tsx | 21 ++++++-- packages/app/src/components/Root/Root.tsx | 11 ++-- .../src/extensions/CoreNav.tsx | 8 ++- .../frontend-app-api/src/wiring/createApp.tsx | 52 ++++++++++++++++++- .../src/extensions/createNavItemExtension.tsx | 49 +++++++++++++++++ .../src/extensions/index.ts | 1 + .../src/wiring/coreExtensionData.ts | 14 ++++- .../frontend-plugin-api/src/wiring/index.ts | 2 +- plugins/graphiql/api-report.md | 5 ++ plugins/graphiql/src/alpha.tsx | 25 ++++++++- yarn.lock | 1 + 13 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx diff --git a/app-config.yaml b/app-config.yaml index 37cb61f26e..9e153e5763 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -1,6 +1,9 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 + experimental: + packages: all # ✨ + #datadogRum: # clientToken: '123456789' # applicationId: qwerty diff --git a/packages/app/package.json b/packages/app/package.json index f019199bff..a4fa62b216 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index df54f37dc6..7f761500c5 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -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 = ( } /> @@ -271,7 +282,9 @@ const routes = ( path="/tech-radar" element={} /> - } /> + { + /* HIGHLY EXPERIMENTAL. DO NOT USE THIS IN YOUR APP */ extensionTree.getRootRoutes() + } } /> } /> } /> @@ -310,7 +323,7 @@ export default app.createRoot( - {routes} + {routes} , ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index ffaf72296a..bfa2263aa5 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -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 }>) => ( @@ -163,7 +166,9 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( to="cost-insights" text="Cost Insights" /> - + { + /* HIGHLY EXPERIMENTAL. DO NOT USE THIS IN YOUR APP */ extensionTree.getSidebarItems() + } diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx index 4dc3636a29..da709d1eb3 100644 --- a/packages/frontend-app-api/src/extensions/CoreNav.tsx +++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx @@ -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, }, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 23b7b4ca11..44d534f702 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -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 } />; + }); + }, + getSidebarItems(): JSX.Element[] { + const RoutedSidebarItem = (props: { + title: string; + routeRef: RouteRef; + icon: IconComponent; + }): React.JSX.Element => { + const location = useRouteRef(props.routeRef); + return ( + + ); + }; + + return this.getExtensionAttachments('core.nav', 'items') + .map((node, index) => { + const target = node.getData(coreExtensionData.navTarget); + if (!target) { + return null; + } + return ( + + ); + }) + .filter((x): x is JSX.Element => !!x); + }, }; } diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx new file mode 100644 index 0000000000..d3eaa53c66 --- /dev/null +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -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, + }, + }); + }, + }); +} diff --git a/packages/frontend-plugin-api/src/extensions/index.ts b/packages/frontend-plugin-api/src/extensions/index.ts index fa6e60894f..0ec0cdf652 100644 --- a/packages/frontend-plugin-api/src/extensions/index.ts +++ b/packages/frontend-plugin-api/src/extensions/index.ts @@ -16,3 +16,4 @@ export { createApiExtension } from './createApiExtension'; export { createPageExtension } from './createPageExtension'; +export { createNavItemExtension } from './createNavItemExtension'; diff --git a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts index 0f370855e1..079d5bcb18 100644 --- a/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts +++ b/packages/frontend-plugin-api/src/wiring/coreExtensionData.ts @@ -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('core.reactElement'), routePath: createExtensionDataRef('core.routing.path'), apiFactory: createExtensionDataRef('core.api.factory'), routeRef: createExtensionDataRef('core.routing.ref'), + navTarget: createExtensionDataRef('core.nav.target'), }; diff --git a/packages/frontend-plugin-api/src/wiring/index.ts b/packages/frontend-plugin-api/src/wiring/index.ts index 175200cf0c..4177ffbfcc 100644 --- a/packages/frontend-plugin-api/src/wiring/index.ts +++ b/packages/frontend-plugin-api/src/wiring/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -export { coreExtensionData } from './coreExtensionData'; +export { coreExtensionData, type NavTarget } from './coreExtensionData'; export { createExtension, type CreateExtensionOptions, diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md index cbecc37006..e6c6ebe193 100644 --- a/plugins/graphiql/api-report.md +++ b/plugins/graphiql/api-report.md @@ -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; diff --git a/plugins/graphiql/src/alpha.tsx b/plugins/graphiql/src/alpha.tsx index 4d2abc12e7..1b095c32c9 100644 --- a/plugins/graphiql/src/alpha.tsx +++ b/plugins/graphiql/src/alpha.tsx @@ -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 => ), }); +export const graphiqlPageSidebarItem = createNavItemExtension({ + id: 'plugin.graphiql.nav.index', + title: 'GraphiQL', + icon: GraphiQLIcon as IconComponent, + routeRef: graphiqlRouteRef, +}); + /** @internal */ const endpointDataRef = createExtensionDataRef( '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, + ], }); diff --git a/yarn.lock b/yarn.lock index 0176682861..548b7d1ee5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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:^"