diff --git a/packages/app-next/package.json b/packages/app-next/package.json index 4bbc4fe5be..d6145b5286 100644 --- a/packages/app-next/package.json +++ b/packages/app-next/package.json @@ -75,6 +75,7 @@ "@backstage/plugin-techdocs-react": "workspace:^", "@backstage/plugin-todo": "workspace:^", "@backstage/plugin-user-settings": "workspace:^", + "@backstage/plugin-visualizer": "workspace:^", "@backstage/theme": "workspace:^", "@circleci/backstage-plugin": "^0.1.1", "@material-ui/core": "^4.12.2", diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 0eca65fdda..112532cee2 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -32,6 +32,7 @@ import { createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; +import appVisualizerPlugin from '@backstage/plugin-visualizer'; import { homePage } from './HomePage'; import { convertLegacyApp } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -123,6 +124,7 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, + appVisualizerPlugin, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/plugins/visualizer/.eslintrc.js b/plugins/visualizer/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/visualizer/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/visualizer/README.md b/plugins/visualizer/README.md new file mode 100644 index 0000000000..dd6f081a18 --- /dev/null +++ b/plugins/visualizer/README.md @@ -0,0 +1,3 @@ +# @backstage/plugin-visualizer + +A plugin to help visualize the structure of your Backstage app. diff --git a/plugins/visualizer/api-report.md b/plugins/visualizer/api-report.md new file mode 100644 index 0000000000..0cd9eface0 --- /dev/null +++ b/plugins/visualizer/api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-visualizer" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackstagePlugin } from '@backstage/frontend-plugin-api'; + +// @public (undocumented) +const visualizerPlugin: BackstagePlugin<{}, {}>; +export default visualizerPlugin; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/visualizer/catalog-info.yaml b/plugins/visualizer/catalog-info.yaml new file mode 100644 index 0000000000..f9cedddf87 --- /dev/null +++ b/plugins/visualizer/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-visualizer + title: '@backstage/plugin-visualizer' + description: Visualizes the Backstage app structure +spec: + lifecycle: experimental + type: backstage-frontend-plugin + owner: maintainers diff --git a/plugins/visualizer/package.json b/plugins/visualizer/package.json new file mode 100644 index 0000000000..afcbd81951 --- /dev/null +++ b/plugins/visualizer/package.json @@ -0,0 +1,47 @@ +{ + "name": "@backstage/plugin-visualizer", + "description": "Visualizes the Backstage app structure", + "private": true, + "version": "0.0.0", + "publishConfig": { + "access": "public" + }, + "backstage": { + "role": "frontend-plugin" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "sideEffects": false, + "scripts": { + "build": "backstage-cli package build", + "start": "backstage-cli package start", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "clean": "backstage-cli package clean" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.61", + "@types/react": "^16.13.1 || ^17.0.0", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx new file mode 100644 index 0000000000..b984f8ac59 --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -0,0 +1,100 @@ +/* + * 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 { Content, Header, HeaderTabs, Page } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { appTreeApiRef } from '@backstage/frontend-plugin-api'; +import Box from '@material-ui/core/Box'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { DetailedVisualizer } from './DetailedVisualizer'; +import { TextVisualizer } from './TextVisualizer'; +import { TreeVisualizer } from './TreeVisualizer'; +import { + matchRoutes, + useLocation, + useNavigate, + useParams, + useRoutes, +} from 'react-router-dom'; + +export function AppVisualizerPage() { + const appTreeApi = useApi(appTreeApiRef); + const { tree } = appTreeApi.getTree(); + + const tabs = useMemo( + () => [ + { + id: 'tree', + path: 'tree', + label: 'Tree', + element: , + }, + { + id: 'detailed', + path: 'detailed', + label: 'Detailed', + element: , + }, + { + id: 'text', + path: 'text', + label: 'Text', + element: , + }, + ], + [tree], + ); + + const location = useLocation(); + const element = useRoutes(tabs, location); + + const currentPath = `/${useParams()['*']}`; + const [matchedRoute] = matchRoutes(tabs, currentPath) ?? []; + + const currentTabIndex = matchedRoute + ? tabs.findIndex(t => t.path === matchedRoute.route.path) + : 0; + + const navigate = useNavigate(); + const handleTabChange = useCallback( + (index: number) => { + navigate(tabs[index].id); + }, + [navigate, tabs], + ); + + useEffect(() => { + if (!element) { + navigate(tabs[0].path); + } + }, [element, navigate, tabs]); + + return ( + +
+ + + + {element} + + + + ); +} diff --git a/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx new file mode 100644 index 0000000000..2356f38cfa --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -0,0 +1,377 @@ +/* + * 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 { + AppNode, + AppTree, + ExtensionDataRef, + RouteRef, + coreExtensionData, + createApiExtension, + createNavItemExtension, + createThemeExtension, + useRouteRef, +} from '@backstage/frontend-plugin-api'; +import Box from '@material-ui/core/Box'; +import Paper from '@material-ui/core/Paper'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import * as colors from '@material-ui/core/colors'; +import { makeStyles } from '@material-ui/core/styles'; +import InputIcon from '@material-ui/icons/InputSharp'; +import DisabledIcon from '@material-ui/icons/NotInterestedSharp'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +function createOutputColorGenerator( + colorMap: { [extDataId: string]: string }, + availableColors: string[], +) { + const map = new Map(); + let i = 0; + + return function getOutputColor(id: string) { + if (id in colorMap) { + return colorMap[id]; + } + let color = map.get(id); + if (color) { + return color; + } + color = availableColors[i]; + i += 1; + if (i >= availableColors.length) { + i = 0; + } + map.set(id, color); + return color; + }; +} + +const getOutputColor = createOutputColorGenerator( + { + [coreExtensionData.reactElement.id]: colors.green[500], + [coreExtensionData.routePath.id]: colors.yellow[500], + [coreExtensionData.routeRef.id]: colors.purple[500], + [createApiExtension.factoryDataRef.id]: colors.blue[500], + [createThemeExtension.themeDataRef.id]: colors.lime[500], + [createNavItemExtension.targetDataRef.id]: colors.orange[500], + }, + + [ + colors.blue[200], + colors.orange[200], + colors.green[200], + colors.red[200], + colors.yellow[200], + colors.purple[200], + colors.lime[200], + ], +); + +interface StyleProps { + enabled: boolean; + depth: number; +} + +const config = { + borderWidth: 0.75, +}; + +const useStyles = makeStyles(theme => ({ + extension: { + borderLeftWidth: theme.spacing(config.borderWidth), + borderLeftStyle: 'solid', + borderLeftColor: ({ depth }: StyleProps) => + colors.grey[(700 - (depth % 6) * 100) as keyof typeof colors.grey], + cursor: 'pointer', + + '&:hover $extensionHeader': { + color: ({ enabled }: StyleProps) => + enabled ? theme.palette.primary.main : theme.palette.text.secondary, + }, + }, + extensionHeader: { + display: 'flex', + alignItems: 'center', + width: 'fit-content', + + padding: theme.spacing(0.5, 1), + color: ({ enabled }: StyleProps) => + enabled ? theme.palette.text.primary : theme.palette.text.disabled, + background: theme.palette.background.paper, + + borderTopRightRadius: theme.shape.borderRadius, + borderBottomRightRadius: theme.shape.borderRadius, + }, + extensionHeaderId: { + userSelect: 'all', + }, + extensionHeaderOutputs: { + display: 'flex', + alignItems: 'center', + marginLeft: theme.spacing(1), + gap: theme.spacing(1), + }, + attachments: { + gap: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }, + attachmentsInput: { + '&:first-child $attachmentsInputTitle': { + borderTop: 0, + }, + }, + attachmentsInputTitle: { + display: 'flex', + alignItems: 'center', + width: 'fit-content', + padding: theme.spacing(1), + + borderTopWidth: theme.spacing(config.borderWidth), + borderTopStyle: 'solid', + borderTopColor: ({ depth }: StyleProps) => + colors.grey[(700 - (depth % 6) * 100) as keyof typeof colors.grey], + }, + attachmentsInputName: { + marginLeft: theme.spacing(1), + }, + attachmentsInputChildren: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + gap: theme.spacing(0.5), + marginLeft: theme.spacing(1), + marginBottom: theme.spacing(1), + }, +})); + +const useOutputStyles = makeStyles(theme => ({ + output: ({ color }: { color: string }) => ({ + padding: `0 10px`, + height: 20, + borderRadius: 10, + color: theme.palette.getContrastText(color), + backgroundColor: color, + }), +})); + +function getFullPath(node?: AppNode): string { + if (!node) { + return ''; + } + const parent = node.edges.attachedTo?.node; + const part = node.instance?.getData(coreExtensionData.routePath); + if (!part) { + return getFullPath(parent); + } + return getFullPath(parent) + part; +} + +function OutputLink(props: { + dataRef: ExtensionDataRef; + node?: AppNode; + className: string; +}) { + const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); + + let link: string | undefined = undefined; + try { + // eslint-disable-next-line react-hooks/rules-of-hooks + link = useRouteRef(routeRef as RouteRef)(); + } catch { + /* ignore */ + } + + return ( + {props.dataRef.id}}> + + {link ? link : null} + + + ); +} + +function Output(props: { dataRef: ExtensionDataRef; node?: AppNode }) { + const { dataRef, node } = props; + const { id } = dataRef; + const instance = node?.instance; + + const classes = useOutputStyles({ color: getOutputColor(id) }); + + if (id === coreExtensionData.routePath.id) { + return ( + {getFullPath(node)}}> + + {String(instance?.getData(dataRef) ?? '')} + + + ); + } + + if (id === coreExtensionData.routeRef.id) { + return ; + } + + return ( + {id}}> + + + ); +} + +function Attachments(props: { + node: AppNode; + enabled: boolean; + depth: number; +}) { + const { node, enabled, depth } = props; + const { attachments } = node.edges; + + const classes = useStyles({ enabled, depth }); + + if (attachments.size === 0) { + return null; + } + + return ( + + {[...attachments.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, children]) => { + return ( + + + + + {key} + + + + {children.map(childNode => ( + + ))} + + + ); + })} + + ); +} + +function ExtensionTooltip(props: { node: AppNode }) { + const parts = []; + let node = props.node; + parts.push(node.spec.id); + while (node.edges.attachedTo) { + const input = node.edges.attachedTo.input; + node = node.edges.attachedTo.node; + parts.push(`${node.spec.id} [${input}]`); + } + parts.reverse(); + + return ( + <> + {parts.map(part => ( + {part} + ))} + + ); +} + +function Extension(props: { node: AppNode; depth: number }) { + const { node, depth } = props; + + const enabled = Boolean(node.instance); + const classes = useStyles({ enabled, depth }); + + const dataRefs = node.instance && [...node.instance.getDataRefs()]; + + return ( + + + }> + + {node.spec.id} + + + + {dataRefs && + dataRefs.length > 0 && + dataRefs + .sort((a, b) => a.id.localeCompare(b.id)) + .map(ref => )} + {!enabled && } + + + + + ); +} + +const legendMap = { + 'React Element': coreExtensionData.reactElement, + 'Utility API': createApiExtension.factoryDataRef, + 'Route Path': coreExtensionData.routePath, + 'Route Ref': coreExtensionData.routeRef, + 'Nav Target': createNavItemExtension.targetDataRef, + Theme: createThemeExtension.themeDataRef, +}; + +function Legend() { + return ( + + {Object.entries(legendMap).map(([label, dataRef]) => ( + + + {label} + + ))} + + ); +} + +export function DetailedVisualizer({ tree }: { tree: AppTree }) { + return ( + + + + + + + + + + ); +} diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx new file mode 100644 index 0000000000..50b1f61484 --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -0,0 +1,116 @@ +/* + * 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 { AppNode, AppTree } from '@backstage/frontend-plugin-api'; +import Box from '@material-ui/core/Box'; +import Checkbox from '@material-ui/core/Checkbox'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Paper from '@material-ui/core/Paper'; +import React, { ReactNode, useState } from 'react'; + +function mkDiv( + children: ReactNode, + options?: { indent?: boolean; key?: string | number; color?: string }, +) { + return ( +
+ {children} +
+ ); +} + +function nodeToText( + node: AppNode, + options?: { showOutputs?: boolean; showDisabled?: boolean }, +): ReactNode { + const dataRefIds = + node.instance && [...node.instance.getDataRefs()].map(r => r.id); + const out = + options?.showOutputs && dataRefIds && dataRefIds.length > 0 + ? ` out="${[...dataRefIds].sort().join(', ')}"` + : ''; + const color = node.instance ? undefined : 'gray'; + + if (node.edges.attachments.size === 0) { + return mkDiv(`<${node.spec.id}${out}/>`, { color }); + } + + return mkDiv([ + mkDiv(`<${node.spec.id}${out}>`, { key: 'start', color }), + ...[...node.edges.attachments.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, v]) => { + const children = v + .filter(e => options?.showDisabled || e.instance) + .sort((a, b) => a.spec.id.localeCompare(b.spec.id)); + if (children.length === 0) { + return mkDiv(`${key} []`, { indent: true }); + } + return mkDiv( + [ + mkDiv(`${key} [`), + ...children.map(e => + mkDiv(nodeToText(e, options), { indent: true }), + ), + mkDiv(']'), + ], + { key, indent: true }, + ); + }), + mkDiv(``, { key: 'end', color }), + ]); +} + +export function TextVisualizer({ tree }: { tree: AppTree }) { + const [showOutputs, setShowOutputs] = useState(false); + const [showDisabled, setShowDisabled] = useState(false); + + return ( + <> + +
+ {nodeToText(tree.root, { showOutputs, showDisabled })} +
+
+ + setShowOutputs(value)} + /> + } + label="Show Outputs" + /> + setShowDisabled(value)} + /> + } + label="Show Disabled" + /> + + + ); +} diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx new file mode 100644 index 0000000000..eac7c4e203 --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -0,0 +1,172 @@ +/* + * 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 { + DependencyGraph, + DependencyGraphTypes, +} from '@backstage/core-components'; +import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; +import Box from '@material-ui/core/Box'; +import { makeStyles } from '@material-ui/core/styles'; +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; + +type NodeType = + | ({ type: 'node'; id: string } & AppNode) + | { type: 'input'; id: string; name: string }; + +function inputId({ node, input }: { node: AppNode; input: string }) { + return `${node.spec.id}$$${input}`; +} + +function trimNodeId(id: string) { + let newId = id; + if (newId.startsWith('apis.')) { + newId = newId.slice('apis.'.length); + } + if (newId.startsWith('plugin.')) { + newId = newId.slice('plugin.'.length); + } + if (newId.startsWith('catalog.filter.entity.')) { + newId = newId.slice('catalog.filter.entity.'.length); + } + if (newId.endsWith('.nav.index')) { + newId = newId.slice(0, -'.nav.index'.length); + } + return newId; +} + +function resolveGraphData(tree: AppTree): { + nodes: NodeType[]; + edges: { from: string; to: string }[]; +} { + const nodes = [...tree.nodes.values()] + .filter(node => node.instance) + .map(node => ({ ...node, id: node.spec.id, type: 'node' as const })); + + return { + nodes: [ + ...nodes, + ...nodes.flatMap(node => + [...node.edges.attachments.keys()].map(input => ({ + id: inputId({ node, input }), + type: 'input' as const, + name: input, + })), + ), + ], + edges: [ + ...nodes + .filter(node => node.edges.attachedTo) + .map(node => ({ + from: inputId(node.edges.attachedTo!), + to: node.spec.id, + })), + ...nodes.flatMap(node => + [...node.edges.attachments.keys()].map(input => ({ + from: node.spec.id, + to: inputId({ node, input }), + })), + ), + ], + }; +} + +const useStyles = makeStyles(theme => ({ + node: { + fill: (node: NodeType) => + node.type === 'node' + ? theme.palette.primary.light + : theme.palette.grey[500], + stroke: (node: NodeType) => + node.type === 'node' + ? theme.palette.primary.main + : theme.palette.grey[600], + }, + text: { + fill: theme.palette.primary.contrastText, + }, +})); + +/** @public */ +export function Node(props: { node: NodeType }) { + const { node } = props; + const classes = useStyles(node); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + const idRef = useRef(null); + + useLayoutEffect(() => { + // set the width to the length of the ID + if (idRef.current) { + let { height: renderedHeight, width: renderedWidth } = + idRef.current.getBBox(); + renderedHeight = Math.round(renderedHeight); + renderedWidth = Math.round(renderedWidth); + + if (renderedHeight !== height || renderedWidth !== width) { + setWidth(renderedWidth); + setHeight(renderedHeight); + } + } + }, [width, height]); + + const padding = 10; + const paddedWidth = width + padding * 2; + const paddedHeight = height + padding * 2; + + return ( + + + + {node.type === 'node' ? trimNodeId(node.id) : node.name} + + + ); +} + +export function TreeVisualizer({ tree }: { tree: AppTree }) { + const graphData = useMemo(() => resolveGraphData(tree), [tree]); + + return ( + + + + ); +} diff --git a/plugins/visualizer/src/components/AppVisualizerPage/index.ts b/plugins/visualizer/src/components/AppVisualizerPage/index.ts new file mode 100644 index 0000000000..971bc6ccb2 --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { AppVisualizerPage } from './AppVisualizerPage'; diff --git a/plugins/visualizer/src/index.ts b/plugins/visualizer/src/index.ts new file mode 100644 index 0000000000..b91a06c569 --- /dev/null +++ b/plugins/visualizer/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export { visualizerPlugin as default } from './plugin'; diff --git a/plugins/visualizer/src/plugin.tsx b/plugins/visualizer/src/plugin.tsx new file mode 100644 index 0000000000..0aa8053604 --- /dev/null +++ b/plugins/visualizer/src/plugin.tsx @@ -0,0 +1,45 @@ +/* + * 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 { + createNavItemExtension, + createPageExtension, + createPlugin, + createRouteRef, +} from '@backstage/frontend-plugin-api'; +import VisualizerIcon from '@material-ui/icons/Visibility'; +import React from 'react'; + +const rootRouteRef = createRouteRef(); + +const visualizerPage = createPageExtension({ + defaultPath: '/visualizer', + routeRef: rootRouteRef, + loader: () => + import('./components/AppVisualizerPage').then(m => ), +}); + +export const visualizerNavItem = createNavItemExtension({ + title: 'Visualizer', + icon: VisualizerIcon, + routeRef: rootRouteRef, +}); + +/** @public */ +export const visualizerPlugin = createPlugin({ + id: 'visualizer', + extensions: [visualizerPage, visualizerNavItem], +}); diff --git a/yarn.lock b/yarn.lock index 7a9560091f..a2bc0b8eed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10072,6 +10072,27 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-visualizer@workspace:^, @backstage/plugin-visualizer@workspace:plugins/visualizer": + version: 0.0.0-use.local + resolution: "@backstage/plugin-visualizer@workspace:plugins/visualizer" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.61 + "@types/react": ^16.13.1 || ^17.0.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + "@backstage/plugin-xcmetrics@workspace:plugins/xcmetrics": version: 0.0.0-use.local resolution: "@backstage/plugin-xcmetrics@workspace:plugins/xcmetrics" @@ -26662,6 +26683,7 @@ __metadata: "@backstage/plugin-techdocs-react": "workspace:^" "@backstage/plugin-todo": "workspace:^" "@backstage/plugin-user-settings": "workspace:^" + "@backstage/plugin-visualizer": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@circleci/backstage-plugin": ^0.1.1