From 722e2df20a6243d67e528846a6ed1aeb93924ebe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 5 Nov 2025 01:35:51 +0100 Subject: [PATCH] app-visualizer: migrate to use BUI Signed-off-by: Patrik Oldsberg --- .changeset/sharp-dragons-relate.md | 5 + plugins/app-visualizer/package.json | 4 +- .../AppVisualizerPage/AppVisualizerPage.tsx | 6 +- .../AppVisualizerPage/DetailedVisualizer.tsx | 370 ++++++++---------- .../AppVisualizerPage/TextVisualizer.tsx | 46 +-- .../AppVisualizerPage/TreeVisualizer.tsx | 45 +-- plugins/app-visualizer/src/plugin.tsx | 2 +- 7 files changed, 218 insertions(+), 260 deletions(-) create mode 100644 .changeset/sharp-dragons-relate.md diff --git a/.changeset/sharp-dragons-relate.md b/.changeset/sharp-dragons-relate.md new file mode 100644 index 0000000000..1f7e5e0d4a --- /dev/null +++ b/.changeset/sharp-dragons-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-visualizer': patch +--- + +Migrated to use `@backstage/ui`. diff --git a/plugins/app-visualizer/package.json b/plugins/app-visualizer/package.json index f7bfdb8296..3e6a0c7d85 100644 --- a/plugins/app-visualizer/package.json +++ b/plugins/app-visualizer/package.json @@ -37,8 +37,8 @@ "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", - "@material-ui/core": "^4.12.2", - "@material-ui/icons": "^4.9.1" + "@backstage/ui": "workspace:^", + "@remixicon/react": "^4.6.0" }, "devDependencies": { "@backstage/cli": "workspace:^", diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index 6410e20965..c005ccb18f 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -17,7 +17,7 @@ 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 { Flex } from '@backstage/ui'; import { useCallback, useEffect, useMemo } from 'react'; import { DetailedVisualizer } from './DetailedVisualizer'; import { TextVisualizer } from './TextVisualizer'; @@ -86,14 +86,14 @@ export function AppVisualizerPage() {
- + {element} - + ); diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index ba93507ae2..df9308ce96 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -25,150 +25,93 @@ import { ThemeBlueprint, 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 { Box, Flex } from '@backstage/ui'; import { Link } from 'react-router-dom'; +import { + RiInputField as InputIcon, + RiCloseCircleLine as DisabledIcon, +} from '@remixicon/react'; + +function getContrastColor(bgColor: string): string { + const hex = bgColor.replace('#', ''); + const r = parseInt(hex.substr(0, 2), 16); + const g = parseInt(hex.substr(2, 2), 16); + const b = parseInt(hex.substr(4, 2), 16); + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 128 ? '#000000' : '#ffffff'; +} function createOutputColorGenerator( colorMap: { [extDataId: string]: string }, availableColors: string[], ) { - const map = new Map(); + const map = new Map(); let i = 0; return function getOutputColor(id: string) { + let backgroundColor: string; if (id in colorMap) { - return colorMap[id]; + backgroundColor = colorMap[id]; + } else { + const cached = map.get(id); + if (cached) { + return cached; + } + backgroundColor = availableColors[i]; + i += 1; + if (i >= availableColors.length) { + i = 0; + } } - 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 result = { + backgroundColor, + color: getContrastColor(backgroundColor), + }; + map.set(id, result); + return result; }; } +// Color palette for output visualization +const colorPalette = { + green: { 500: '#4caf50', 200: '#a5d6a7' }, + yellow: { 500: '#ffeb3b', 200: '#fff59d' }, + purple: { 500: '#9c27b0', 200: '#ce93d8' }, + blue: { 500: '#2196f3', 200: '#90caf9' }, + lime: { 500: '#cddc39', 200: '#e6ee9c' }, + orange: { 500: '#ff9800', 200: '#ffcc80' }, + red: { 200: '#ef9a9a' }, +}; + const getOutputColor = createOutputColorGenerator( { - [coreExtensionData.reactElement.id]: colors.green[500], - [coreExtensionData.routePath.id]: colors.yellow[500], - [coreExtensionData.routeRef.id]: colors.purple[500], - [ApiBlueprint.dataRefs.factory.id]: colors.blue[500], - [ThemeBlueprint.dataRefs.theme.id]: colors.lime[500], - [NavItemBlueprint.dataRefs.target.id]: colors.orange[500], + [coreExtensionData.reactElement.id]: colorPalette.green[500], + [coreExtensionData.routePath.id]: colorPalette.yellow[500], + [coreExtensionData.routeRef.id]: colorPalette.purple[500], + [ApiBlueprint.dataRefs.factory.id]: colorPalette.blue[500], + [ThemeBlueprint.dataRefs.theme.id]: colorPalette.lime[500], + [NavItemBlueprint.dataRefs.target.id]: colorPalette.orange[500], }, [ - colors.blue[200], - colors.orange[200], - colors.green[200], - colors.red[200], - colors.yellow[200], - colors.purple[200], - colors.lime[200], + colorPalette.blue[200], + colorPalette.orange[200], + colorPalette.green[200], + colorPalette.red[200], + colorPalette.yellow[200], + colorPalette.purple[200], + colorPalette.lime[200], ], ); -interface StyleProps { - enabled: boolean; - depth: number; +// Helper function to get border color based on depth +function getBorderColor(depth: number): string { + const greyLevels = [8, 7, 6, 5]; // darker levels that contrast well with background + const index = depth % greyLevels.length; + const level = greyLevels[index]; + return `var(--bui-gray-${level})`; } -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 ''; @@ -184,7 +127,7 @@ function getFullPath(node?: AppNode): string { function OutputLink(props: { dataRef: ExtensionDataRef; node?: AppNode; - className: string; + style: React.CSSProperties; }) { const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); @@ -192,11 +135,11 @@ function OutputLink(props: { const link = useRouteRef(routeRef as RouteRef); return ( - {props.dataRef.id}}> - +
+ {link ? link : null} - - + +
); } catch (ex) { // eslint-disable-next-line no-console @@ -215,26 +158,39 @@ function Output(props: { dataRef: ExtensionDataRef; node?: AppNode }) { const { id } = dataRef; const instance = node?.instance; - const classes = useOutputStyles({ color: getOutputColor(id) }); + const { backgroundColor, color } = getOutputColor(id); + + const chipStyle: React.CSSProperties = { + padding: '0 var(--bui-space-2_5, 10px)', + height: 20, + borderRadius: 'var(--bui-radius-full)', + color, + backgroundColor, + }; if (id === coreExtensionData.routePath.id) { return ( - {getFullPath(node)}}> - +
+ {String(instance?.getData(dataRef) ?? '')} - - + +
); } - if (id === coreExtensionData.routeRef.id) { - return ; + if (id === coreExtensionData.routeRef.id && node) { + return ( + + ); } return ( - {id}}> - - +
+ +
); } @@ -243,29 +199,35 @@ function Attachments(props: { enabled: boolean; depth: number; }) { - const { node, enabled, depth } = props; + const { node, 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]) => { + .map(([key, children], idx) => { return ( - - - - - {key} - - - + + + +
{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} - ))} - + ); } @@ -305,27 +247,54 @@ 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()]; + // Build tooltip text + const tooltipParts = []; + let currentNode = node; + tooltipParts.push(currentNode.spec.id); + while (currentNode.edges.attachedTo) { + const input = currentNode.edges.attachedTo.input; + currentNode = currentNode.edges.attachedTo.node; + tooltipParts.push(`${currentNode.spec.id} [${input}]`); + } + tooltipParts.reverse(); + const tooltipText = tooltipParts.join(' → '); + return ( - - - }> - - {node.spec.id} - - - + + +
+ {node.spec.id} +
+ {dataRefs && dataRefs.length > 0 && dataRefs .sort((a, b) => a.id.localeCompare(b.id)) .map(ref => )} - {!enabled && } -
-
+ {!enabled && } + +
); @@ -343,24 +312,19 @@ const legendMap = { function Legend() { return ( {Object.entries(legendMap).map(([label, dataRef]) => ( - + - {label} - +
{label}
+ ))}
); @@ -368,14 +332,22 @@ function Legend() { export function DetailedVisualizer({ tree }: { tree: AppTree }) { return ( - - + + - + - + ); } diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx index 4385ba0938..82fd913bf2 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx @@ -15,10 +15,7 @@ */ 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 { Box, Checkbox } from '@backstage/ui'; import { ReactNode, useState } from 'react'; function mkDiv( @@ -30,7 +27,7 @@ function mkDiv( key={options?.key} style={{ color: options?.color, - marginLeft: options?.indent ? 16 : undefined, + marginLeft: options?.indent ? 'var(--bui-space-4)' : undefined, }} > {children} @@ -87,30 +84,25 @@ export function TextVisualizer({ tree }: { tree: AppTree }) { return ( <> -
+ {nodeToText(tree.root, { showOutputs, showDisabled })} -
+
+
+ + + Show Outputs + + + Show Disabled + - - setShowOutputs(value)} - /> - } - label="Show Outputs" - /> - setShowDisabled(value)} - /> - } - label="Show Disabled" - /> - ); } diff --git a/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index b9058cb87b..d3a9145bb3 100644 --- a/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/app-visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -19,8 +19,7 @@ import { 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 { Flex } from '@backstage/ui'; import { useLayoutEffect, useMemo, useRef, useState } from 'react'; type NodeType = @@ -84,26 +83,9 @@ function resolveGraphData(tree: AppTree): { }; } -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); @@ -127,17 +109,23 @@ export function Node(props: { node: NodeType }) { const paddedWidth = width + padding * 2; const paddedHeight = height + padding * 2; + // Simple inline styles for SVG elements + const nodeFill = node.type === 'node' ? '#90caf9' : '#9e9e9e'; + const nodeStroke = node.type === 'node' ? '#2196f3' : '#757575'; + const textFill = '#000000'; + return ( resolveGraphData(tree), [tree]); return ( - - + ); } diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index 58e1b2effa..3d30df16e6 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -20,7 +20,7 @@ import { NavItemBlueprint, PageBlueprint, } from '@backstage/frontend-plugin-api'; -import VisualizerIcon from '@material-ui/icons/Visibility'; +import { RiEyeLine as VisualizerIcon } from '@remixicon/react'; const rootRouteRef = createRouteRef();