From 35df801e094064a153b746662335f3ed00fe86b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Oct 2023 14:10:26 +0200 Subject: [PATCH 01/24] app-next: initial app visualizer plugin Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 2 ++ .../components/AppVisualizerPage.tsx | 21 ++++++++++++ .../src/plugins/app-visualizer/index.ts | 17 ++++++++++ .../src/plugins/app-visualizer/plugin.tsx | 33 +++++++++++++++++++ packages/app-next/src/plugins/index.ts | 19 +++++++++++ 5 files changed, 92 insertions(+) create mode 100644 packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx create mode 100644 packages/app-next/src/plugins/app-visualizer/index.ts create mode 100644 packages/app-next/src/plugins/app-visualizer/plugin.tsx create mode 100644 packages/app-next/src/plugins/index.ts diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index 0eca65fdda..09f454f302 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 { plugins } from './plugins'; 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, + ...plugins, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx new file mode 100644 index 0000000000..759b3870b9 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx @@ -0,0 +1,21 @@ +/* + * 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'; + +export function AppVisualizerPage() { + return

Hello world

; +} diff --git a/packages/app-next/src/plugins/app-visualizer/index.ts b/packages/app-next/src/plugins/app-visualizer/index.ts new file mode 100644 index 0000000000..63fd16c520 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/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 { appVisualizerPlugin } from './plugin'; diff --git a/packages/app-next/src/plugins/app-visualizer/plugin.tsx b/packages/app-next/src/plugins/app-visualizer/plugin.tsx new file mode 100644 index 0000000000..c39cb9a6c3 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/plugin.tsx @@ -0,0 +1,33 @@ +/* + * 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 { + createPageExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import React from 'react'; + +const appVisualizerPage = createPageExtension({ + id: 'plugin.app-visualizer.page', + defaultPath: '/visualizer', + loader: () => + import('./components/AppVisualizerPage').then(m => ), +}); + +export const appVisualizerPlugin = createPlugin({ + id: 'app-visualizer', + extensions: [appVisualizerPage], +}); diff --git a/packages/app-next/src/plugins/index.ts b/packages/app-next/src/plugins/index.ts new file mode 100644 index 0000000000..baafcde889 --- /dev/null +++ b/packages/app-next/src/plugins/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { appVisualizerPlugin } from './app-visualizer'; + +export const plugins = [appVisualizerPlugin]; From d03fc971d65359b58b481f3ea42ee00b639bd1cf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 28 Oct 2023 14:32:21 +0200 Subject: [PATCH 02/24] app-next: text tab for visualizer Signed-off-by: Patrik Oldsberg --- .../components/AppVisualizerPage.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx index 759b3870b9..08e95305a2 100644 --- a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx +++ b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx @@ -14,8 +14,32 @@ * limitations under the License. */ +import { + Header, + Page, + TabbedLayout, + CodeSnippet, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { appTreeApiRef } from '@backstage/frontend-plugin-api'; import React from 'react'; export function AppVisualizerPage() { - return

Hello world

; + const appTreeApi = useApi(appTreeApiRef); + const { tree } = appTreeApi.getTree(); + + return ( + +
+ + + + + + + ); } From 42ac760b44374cc0207b363c5d2b99a7af9bdaa6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Oct 2023 11:18:39 +0100 Subject: [PATCH 03/24] app-next: add graph visualizer Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage.tsx | 33 +-- .../AppVisualizerPage/GraphVisualizer.tsx | 266 ++++++++++++++++++ .../AppVisualizerPage/TextVisualizer.tsx | 116 ++++++++ .../components/AppVisualizerPage/index.ts | 17 ++ 4 files changed, 416 insertions(+), 16 deletions(-) rename packages/app-next/src/plugins/app-visualizer/components/{ => AppVisualizerPage}/AppVisualizerPage.tsx (56%) create mode 100644 packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx create mode 100644 packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/TextVisualizer.tsx create mode 100644 packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/index.ts diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/AppVisualizerPage.tsx similarity index 56% rename from packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx rename to packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/AppVisualizerPage.tsx index 08e95305a2..ec6145a1af 100644 --- a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage.tsx +++ b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -14,32 +14,33 @@ * limitations under the License. */ -import { - Header, - Page, - TabbedLayout, - CodeSnippet, -} from '@backstage/core-components'; +import { Content, Header, HeaderTabs, Page } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; import { appTreeApiRef } from '@backstage/frontend-plugin-api'; -import React from 'react'; +import Box from '@material-ui/core/Box'; +import React, { useState } from 'react'; +import { GraphVisualizer } from './GraphVisualizer'; +import { TextVisualizer } from './TextVisualizer'; export function AppVisualizerPage() { const appTreeApi = useApi(appTreeApiRef); const { tree } = appTreeApi.getTree(); + const [tab, setTab] = useState(0); + + const tabs = [ + { id: 'graph', label: 'Graph', element: }, + { id: 'text', label: 'Text', element: }, + ]; return (
- - - - - + + + + {tabs[tab].element} + + ); } diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx new file mode 100644 index 0000000000..cd41c45875 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx @@ -0,0 +1,266 @@ +/* + * 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 Tooltip from '@material-ui/core/Tooltip'; +import * as colors from '@material-ui/core/colors'; +import { Theme, makeStyles } from '@material-ui/core/styles'; +import InputIcon from '@material-ui/icons/InputSharp'; +import React from 'react'; + +function createOutputColorGenerator(availableColors: string[]) { + const map = new Map(); + let i = 0; + + return function getOutputColor(id: string) { + 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([ + colors.green[500], + colors.blue[500], + colors.yellow[500], + colors.purple[500], + colors.orange[500], + colors.red[500], + colors.lime[500], + colors.green[200], + colors.blue[200], + colors.yellow[200], + colors.purple[200], + colors.orange[200], + colors.red[200], + colors.lime[200], +]); + +interface StyleProps { + enabled: boolean; +} + +function mainColor(theme: Theme) { + return ({ enabled }: StyleProps) => + enabled ? theme.palette.primary.main : colors.grey[600]; +} + +function hoverColor(theme: Theme) { + return ({ enabled }: StyleProps) => + enabled ? theme.palette.primary.dark : colors.grey[500]; +} + +const useStyles = makeStyles(theme => ({ + extension: { + borderLeftWidth: theme.spacing(1), + borderLeftStyle: 'solid', + borderLeftColor: mainColor(theme), + marginBottom: theme.spacing(0.5), + cursor: 'pointer', + fontSize: theme.typography.h6.fontSize, + + '&:hover': { + borderLeftColor: hoverColor(theme), + }, + '&:hover $extensionHeader': { + background: hoverColor(theme), + }, + }, + extensionHeader: { + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + width: 'fit-content', + padding: theme.spacing(0.5, 1), + background: mainColor(theme), + }, + extensionHeaderId: { + flex: '0 0 auto', + color: theme.palette.primary.contrastText, + }, + extensionHeaderOutputs: { + margin: theme.spacing(0, 0.5), + marginTop: 1, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + }, + extensionTooltip: { + fontSize: theme.typography.h6.fontSize, + maxWidth: 'unset', + }, + output: { + marginLeft: theme.spacing(1), + width: theme.spacing(2.3), + height: theme.spacing(2.3), + borderRadius: '50%', + }, + outputId: { + maxWidth: 'unset', + padding: theme.spacing(1), + fontSize: theme.typography.h6.fontSize, + }, + extensionDisabledIcon: { + marginLeft: theme.spacing(1), + }, + attachments: {}, + attachmentsInput: () => ({ + marginBottom: theme.spacing(1), + + '&:not(:first-child) $attachmentsInputTitle': { + borderTopWidth: theme.spacing(1), + borderTopStyle: 'solid', + borderTopColor: mainColor(theme), + }, + '&:not(:first-child):hover $attachmentsInputTitle': { + borderTopColor: hoverColor(theme), + }, + }), + attachmentsInputTitle: () => ({ + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + width: 'fit-content', + padding: theme.spacing(1, 2), + }), + attachmentsInputIcon: {}, + attachmentsInputName: { + marginLeft: theme.spacing(1), + color: theme.palette.text.primary, + }, + attachmentsInputChildren: { + marginLeft: theme.spacing(1), + }, +})); + +function Output(props: { id: string; enabled: boolean }) { + const { id, enabled } = props; + const classes = useStyles({ enabled }); + + return ( + +
+ + ); +} + +function Attachments(props: { + attachments: ReadonlyMap; + enabled: boolean; +}) { + const { attachments, enabled } = props; + + const classes = useStyles({ enabled }); + + if (attachments.size === 0) { + return null; + } + + return ( +
+ {[...attachments.entries()] + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, v]) => { + const children = v.sort((a, b) => a.spec.id.localeCompare(b.spec.id)); + + return ( +
+
+ +
{key}
+
+
+ {children.map(node => ( + + ))} +
+
+ ); + })} +
+ ); +} + +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 }) { + const { node } = props; + + const enabled = Boolean(node.instance); + const classes = useStyles({ enabled }); + + const dataRefIds = + node.instance && [...node.instance.getDataRefs()].map(r => r.id); + + return ( +
+
+ } + classes={{ tooltip: classes.extensionTooltip }} + > +
{node.spec.id}
+
+
+ {dataRefIds && + dataRefIds.length > 0 && + [...dataRefIds] + .sort() + .map(id => )} +
+
+ +
+ ); +} + +export function GraphVisualizer({ tree }: { tree: AppTree }) { + return ( + + + + ); +} diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/TextVisualizer.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/TextVisualizer.tsx new file mode 100644 index 0000000000..50b1f61484 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/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/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/index.ts b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/index.ts new file mode 100644 index 0000000000..971bc6ccb2 --- /dev/null +++ b/packages/app-next/src/plugins/app-visualizer/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'; From 59d60e853b52a40be3755aa07361f7da760906ca Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 30 Oct 2023 12:12:28 +0100 Subject: [PATCH 04/24] app-next: app graph visualizer cleanup Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/GraphVisualizer.tsx | 152 ++++++++---------- 1 file changed, 66 insertions(+), 86 deletions(-) diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx index cd41c45875..f8b9ae2d72 100644 --- a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx @@ -17,9 +17,11 @@ import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; import * as colors from '@material-ui/core/colors'; import { Theme, makeStyles } from '@material-ui/core/styles'; import InputIcon from '@material-ui/icons/InputSharp'; +import DisabledIcon from '@material-ui/icons/NotInterestedSharp'; import React from 'react'; function createOutputColorGenerator(availableColors: string[]) { @@ -62,30 +64,25 @@ interface StyleProps { enabled: boolean; } -function mainColor(theme: Theme) { +function borderColor(theme: Theme) { return ({ enabled }: StyleProps) => - enabled ? theme.palette.primary.main : colors.grey[600]; + enabled ? theme.palette.primary.main : theme.palette.divider; } -function hoverColor(theme: Theme) { - return ({ enabled }: StyleProps) => - enabled ? theme.palette.primary.dark : colors.grey[500]; -} +const config = { + borderWidth: 0.75, +}; const useStyles = makeStyles(theme => ({ extension: { - borderLeftWidth: theme.spacing(1), + borderLeftWidth: theme.spacing(config.borderWidth), borderLeftStyle: 'solid', - borderLeftColor: mainColor(theme), - marginBottom: theme.spacing(0.5), + borderLeftColor: borderColor(theme), cursor: 'pointer', - fontSize: theme.typography.h6.fontSize, - '&:hover': { - borderLeftColor: hoverColor(theme), - }, '&:hover $extensionHeader': { - background: hoverColor(theme), + color: ({ enabled }: StyleProps) => + enabled ? theme.palette.primary.main : theme.palette.text.secondary, }, }, extensionHeader: { @@ -93,77 +90,62 @@ const useStyles = makeStyles(theme => ({ flexFlow: 'row nowrap', alignItems: 'center', width: 'fit-content', + padding: theme.spacing(0.5, 1), - background: mainColor(theme), - }, - extensionHeaderId: { - flex: '0 0 auto', - color: theme.palette.primary.contrastText, + 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, }, extensionHeaderOutputs: { - margin: theme.spacing(0, 0.5), - marginTop: 1, display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', - }, - extensionTooltip: { - fontSize: theme.typography.h6.fontSize, - maxWidth: 'unset', - }, - output: { - marginLeft: theme.spacing(1), - width: theme.spacing(2.3), - height: theme.spacing(2.3), - borderRadius: '50%', - }, - outputId: { - maxWidth: 'unset', - padding: theme.spacing(1), - fontSize: theme.typography.h6.fontSize, - }, - extensionDisabledIcon: { marginLeft: theme.spacing(1), + gap: theme.spacing(1), }, attachments: {}, - attachmentsInput: () => ({ - marginBottom: theme.spacing(1), - - '&:not(:first-child) $attachmentsInputTitle': { - borderTopWidth: theme.spacing(1), - borderTopStyle: 'solid', - borderTopColor: mainColor(theme), + attachmentsInput: { + '&:first-child $attachmentsInputTitle': { + borderTop: 0, }, - '&:not(:first-child):hover $attachmentsInputTitle': { - borderTopColor: hoverColor(theme), - }, - }), - attachmentsInputTitle: () => ({ + }, + attachmentsInputTitle: { display: 'flex', flexFlow: 'row nowrap', alignItems: 'center', width: 'fit-content', - padding: theme.spacing(1, 2), - }), - attachmentsInputIcon: {}, + padding: theme.spacing(1), + + borderTopWidth: theme.spacing(config.borderWidth), + borderTopStyle: 'solid', + borderTopColor: borderColor(theme), + }, attachmentsInputName: { marginLeft: theme.spacing(1), - color: theme.palette.text.primary, }, attachmentsInputChildren: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'flex-start', + gap: theme.spacing(0.5), marginLeft: theme.spacing(1), + marginBottom: theme.spacing(1), }, })); -function Output(props: { id: string; enabled: boolean }) { - const { id, enabled } = props; - const classes = useStyles({ enabled }); +function Output(props: { id: string }) { + const { id } = props; return ( - -
{id}}> + ); @@ -182,27 +164,29 @@ function Attachments(props: { } return ( -
+ {[...attachments.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([key, v]) => { const children = v.sort((a, b) => a.spec.id.localeCompare(b.spec.id)); return ( -
-
- -
{key}
-
-
+ + + + + {key} + + + {children.map(node => ( ))} -
-
+
+ ); })} -
+
); } @@ -220,7 +204,7 @@ function ExtensionTooltip(props: { node: AppNode }) { return ( <> {parts.map(part => ( -
{part}
+ {part} ))} ); @@ -236,24 +220,20 @@ function Extension(props: { node: AppNode }) { node.instance && [...node.instance.getDataRefs()].map(r => r.id); return ( -
-
- } - classes={{ tooltip: classes.extensionTooltip }} - > -
{node.spec.id}
+ + + }> + {node.spec.id} -
+ {dataRefIds && dataRefIds.length > 0 && - [...dataRefIds] - .sort() - .map(id => )} -
-
+ [...dataRefIds].sort().map(id => )} + {!enabled && } + + -
+ ); } From 899530efb52bf9b00eedb26ab0ab72a8eeb9c12b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 1 Nov 2023 18:18:42 +0100 Subject: [PATCH 05/24] plugins: add visualizer Signed-off-by: Patrik Oldsberg --- packages/app-next/package.json | 1 + packages/app-next/src/App.tsx | 4 +- packages/app-next/src/plugins/index.ts | 19 -------- plugins/visualizer/.eslintrc.js | 1 + plugins/visualizer/README.md | 3 ++ plugins/visualizer/api-report.md | 13 +++++ plugins/visualizer/catalog-info.yaml | 10 ++++ plugins/visualizer/package.json | 47 +++++++++++++++++++ .../AppVisualizerPage/AppVisualizerPage.tsx | 0 .../AppVisualizerPage/GraphVisualizer.tsx | 0 .../AppVisualizerPage/TextVisualizer.tsx | 0 .../components/AppVisualizerPage/index.ts | 0 .../visualizer/src}/index.ts | 2 +- .../visualizer/src}/plugin.tsx | 0 yarn.lock | 22 +++++++++ 15 files changed, 100 insertions(+), 22 deletions(-) delete mode 100644 packages/app-next/src/plugins/index.ts create mode 100644 plugins/visualizer/.eslintrc.js create mode 100644 plugins/visualizer/README.md create mode 100644 plugins/visualizer/api-report.md create mode 100644 plugins/visualizer/catalog-info.yaml create mode 100644 plugins/visualizer/package.json rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/components/AppVisualizerPage/AppVisualizerPage.tsx (100%) rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/components/AppVisualizerPage/GraphVisualizer.tsx (100%) rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/components/AppVisualizerPage/TextVisualizer.tsx (100%) rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/components/AppVisualizerPage/index.ts (100%) rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/index.ts (91%) rename {packages/app-next/src/plugins/app-visualizer => plugins/visualizer/src}/plugin.tsx (100%) 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 09f454f302..112532cee2 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -32,7 +32,7 @@ import { createExtensionOverrides, } from '@backstage/frontend-plugin-api'; import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; -import { plugins } from './plugins'; +import appVisualizerPlugin from '@backstage/plugin-visualizer'; import { homePage } from './HomePage'; import { convertLegacyApp } from '@backstage/core-compat-api'; import { FlatRoutes } from '@backstage/core-app-api'; @@ -124,7 +124,7 @@ const app = createApp({ techdocsPlugin, userSettingsPlugin, homePlugin, - ...plugins, + appVisualizerPlugin, ...collectedLegacyPlugins, createExtensionOverrides({ extensions: [ diff --git a/packages/app-next/src/plugins/index.ts b/packages/app-next/src/plugins/index.ts deleted file mode 100644 index baafcde889..0000000000 --- a/packages/app-next/src/plugins/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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 { appVisualizerPlugin } from './app-visualizer'; - -export const plugins = [appVisualizerPlugin]; 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/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx similarity index 100% rename from packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/AppVisualizerPage.tsx rename to plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx similarity index 100% rename from packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/GraphVisualizer.tsx rename to plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/TextVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx similarity index 100% rename from packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/TextVisualizer.tsx rename to plugins/visualizer/src/components/AppVisualizerPage/TextVisualizer.tsx diff --git a/packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/index.ts b/plugins/visualizer/src/components/AppVisualizerPage/index.ts similarity index 100% rename from packages/app-next/src/plugins/app-visualizer/components/AppVisualizerPage/index.ts rename to plugins/visualizer/src/components/AppVisualizerPage/index.ts diff --git a/packages/app-next/src/plugins/app-visualizer/index.ts b/plugins/visualizer/src/index.ts similarity index 91% rename from packages/app-next/src/plugins/app-visualizer/index.ts rename to plugins/visualizer/src/index.ts index 63fd16c520..60e8ced455 100644 --- a/packages/app-next/src/plugins/app-visualizer/index.ts +++ b/plugins/visualizer/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { appVisualizerPlugin } from './plugin'; +export { appVisualizerPlugin as default } from './plugin'; diff --git a/packages/app-next/src/plugins/app-visualizer/plugin.tsx b/plugins/visualizer/src/plugin.tsx similarity index 100% rename from packages/app-next/src/plugins/app-visualizer/plugin.tsx rename to plugins/visualizer/src/plugin.tsx diff --git a/yarn.lock b/yarn.lock index 4719742e16..624293289b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10068,6 +10068,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" @@ -26658,6 +26679,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 From dacdfa401a90195aa14877cbf6fd961d9943f10c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Nov 2023 01:00:11 +0100 Subject: [PATCH 06/24] visualizer: add nav item Signed-off-by: Patrik Oldsberg --- plugins/visualizer/src/index.ts | 2 +- plugins/visualizer/src/plugin.tsx | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/plugins/visualizer/src/index.ts b/plugins/visualizer/src/index.ts index 60e8ced455..b91a06c569 100644 --- a/plugins/visualizer/src/index.ts +++ b/plugins/visualizer/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { appVisualizerPlugin as default } from './plugin'; +export { visualizerPlugin as default } from './plugin'; diff --git a/plugins/visualizer/src/plugin.tsx b/plugins/visualizer/src/plugin.tsx index c39cb9a6c3..216c1f8ee2 100644 --- a/plugins/visualizer/src/plugin.tsx +++ b/plugins/visualizer/src/plugin.tsx @@ -15,19 +15,32 @@ */ import { + createNavItemExtension, createPageExtension, createPlugin, + createRouteRef, } from '@backstage/frontend-plugin-api'; +import VisualizerIcon from '@material-ui/icons/Visibility'; import React from 'react'; -const appVisualizerPage = createPageExtension({ - id: 'plugin.app-visualizer.page', +const rootRouteRef = createRouteRef(); + +const visualizerPage = createPageExtension({ + id: 'plugin.visualizer.page', defaultPath: '/visualizer', + routeRef: rootRouteRef, loader: () => import('./components/AppVisualizerPage').then(m => ), }); -export const appVisualizerPlugin = createPlugin({ - id: 'app-visualizer', - extensions: [appVisualizerPage], +export const visualizerPageSidebarItem = createNavItemExtension({ + id: 'plugin.visualizer.nav.index', + title: 'Visualizer', + icon: VisualizerIcon, + routeRef: rootRouteRef, +}); + +export const visualizerPlugin = createPlugin({ + id: 'visualizer', + extensions: [visualizerPage, visualizerPageSidebarItem], }); From 1300459235020645deedf5f4a36f73e94bbcec46 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Nov 2023 14:15:33 +0100 Subject: [PATCH 07/24] visualizer: always select entire extension ID Signed-off-by: Patrik Oldsberg --- .../src/components/AppVisualizerPage/GraphVisualizer.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index f8b9ae2d72..4f98e890f5 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -99,6 +99,9 @@ const useStyles = makeStyles(theme => ({ borderTopRightRadius: theme.shape.borderRadius, borderBottomRightRadius: theme.shape.borderRadius, }, + extensionHeaderId: { + userSelect: 'all', + }, extensionHeaderOutputs: { display: 'flex', flexFlow: 'row nowrap', @@ -223,7 +226,9 @@ function Extension(props: { node: AppNode }) { }> - {node.spec.id} + + {node.spec.id} + {dataRefIds && From 29ad231a22e8f322b8cd91122aca005d8b1f6f34 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Nov 2023 14:16:01 +0100 Subject: [PATCH 08/24] visualizer: do not reorder extensions Signed-off-by: Patrik Oldsberg --- .../src/components/AppVisualizerPage/GraphVisualizer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index 4f98e890f5..ae78611d2e 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -170,9 +170,7 @@ function Attachments(props: { {[...attachments.entries()] .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, v]) => { - const children = v.sort((a, b) => a.spec.id.localeCompare(b.spec.id)); - + .map(([key, children]) => { return ( From 5b290176493ecd1d346f80b7903a16252ecb0ec2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 2 Nov 2023 14:43:12 +0100 Subject: [PATCH 09/24] visualizer: render route paths and links Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/GraphVisualizer.tsx | 95 ++++++++++++++++--- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index ae78611d2e..de043d71ca 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -14,7 +14,14 @@ * limitations under the License. */ -import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; +import { + AppNode, + AppTree, + ExtensionDataRef, + RouteRef, + coreExtensionData, + useRouteRef, +} from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; @@ -23,6 +30,7 @@ import { Theme, 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(availableColors: string[]) { const map = new Map(); @@ -139,17 +147,75 @@ const useStyles = makeStyles(theme => ({ }, })); -function Output(props: { id: string }) { - const { id } = props; +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 { + 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}}> - + ); } @@ -217,8 +283,7 @@ function Extension(props: { node: AppNode }) { const enabled = Boolean(node.instance); const classes = useStyles({ enabled }); - const dataRefIds = - node.instance && [...node.instance.getDataRefs()].map(r => r.id); + const dataRefs = node.instance && [...node.instance.getDataRefs()]; return ( @@ -229,9 +294,11 @@ function Extension(props: { node: AppNode }) { - {dataRefIds && - dataRefIds.length > 0 && - [...dataRefIds].sort().map(id => )} + {dataRefs && + dataRefs.length > 0 && + dataRefs + .sort((a, b) => a.id.localeCompare(b.id)) + .map(ref => )} {!enabled && } From defd9b3a2858003cbc6cb13c9d4feb59603c5c7a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Nov 2023 10:25:33 +0100 Subject: [PATCH 10/24] visualizer: use fixed colors for core data types Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/GraphVisualizer.tsx | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index de043d71ca..e047bc498f 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -32,11 +32,17 @@ import DisabledIcon from '@material-ui/icons/NotInterestedSharp'; import React from 'react'; import { Link } from 'react-router-dom'; -function createOutputColorGenerator(availableColors: string[]) { +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; @@ -51,22 +57,25 @@ function createOutputColorGenerator(availableColors: string[]) { }; } -const getOutputColor = createOutputColorGenerator([ - colors.green[500], - colors.blue[500], - colors.yellow[500], - colors.purple[500], - colors.orange[500], - colors.red[500], - colors.lime[500], - colors.green[200], - colors.blue[200], - colors.yellow[200], - colors.purple[200], - colors.orange[200], - colors.red[200], - colors.lime[200], -]); +const getOutputColor = createOutputColorGenerator( + { + [coreExtensionData.reactElement.id]: colors.green[500], + [coreExtensionData.routePath.id]: colors.yellow[500], + [coreExtensionData.routeRef.id]: colors.purple[500], + [coreExtensionData.apiFactory.id]: colors.blue[500], + [coreExtensionData.theme.id]: colors.lime[500], + [coreExtensionData.navTarget.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; From 94295786267b87b677e067a1564a523dc1f90618 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Nov 2023 10:28:02 +0100 Subject: [PATCH 11/24] visualizer: styles cleanup Signed-off-by: Patrik Oldsberg --- .../src/components/AppVisualizerPage/GraphVisualizer.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index e047bc498f..f4945a0534 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -104,7 +104,6 @@ const useStyles = makeStyles(theme => ({ }, extensionHeader: { display: 'flex', - flexFlow: 'row nowrap', alignItems: 'center', width: 'fit-content', @@ -121,7 +120,6 @@ const useStyles = makeStyles(theme => ({ }, extensionHeaderOutputs: { display: 'flex', - flexFlow: 'row nowrap', alignItems: 'center', marginLeft: theme.spacing(1), gap: theme.spacing(1), @@ -134,7 +132,6 @@ const useStyles = makeStyles(theme => ({ }, attachmentsInputTitle: { display: 'flex', - flexFlow: 'row nowrap', alignItems: 'center', width: 'fit-content', padding: theme.spacing(1), @@ -148,7 +145,7 @@ const useStyles = makeStyles(theme => ({ }, attachmentsInputChildren: { display: 'flex', - flexFlow: 'column nowrap', + flexDirection: 'column', alignItems: 'flex-start', gap: theme.spacing(0.5), marginLeft: theme.spacing(1), From 8c71296cc48acc7e059a86a074a79f46a1f5d1ab Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 3 Nov 2023 10:56:37 +0100 Subject: [PATCH 12/24] visualizer: add legend Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/GraphVisualizer.tsx | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index f4945a0534..327c2d226b 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -23,6 +23,7 @@ import { 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'; @@ -177,10 +178,10 @@ function getFullPath(node?: AppNode): string { function OutputLink(props: { dataRef: ExtensionDataRef; - node: AppNode; + node?: AppNode; className: string; }) { - const routeRef = props.node.instance?.getData(coreExtensionData.routeRef); + const routeRef = props.node?.instance?.getData(coreExtensionData.routeRef); let link: string | undefined = undefined; try { @@ -198,10 +199,10 @@ function OutputLink(props: { ); } -function Output(props: { dataRef: ExtensionDataRef; node: AppNode }) { +function Output(props: { dataRef: ExtensionDataRef; node?: AppNode }) { const { dataRef, node } = props; const { id } = dataRef; - const instance = node.instance!; + const instance = node?.instance; const classes = useOutputStyles({ color: getOutputColor(id) }); @@ -209,7 +210,7 @@ function Output(props: { dataRef: ExtensionDataRef; node: AppNode }) { return ( {getFullPath(node)}}> - {String(instance.getData(dataRef))} + {String(instance?.getData(dataRef) ?? '')} ); @@ -313,10 +314,51 @@ function Extension(props: { node: AppNode }) { ); } -export function GraphVisualizer({ tree }: { tree: AppTree }) { +const legendMap = { + 'React Element': coreExtensionData.reactElement, + 'Utility API': coreExtensionData.apiFactory, + 'Route Path': coreExtensionData.routePath, + 'Route Ref': coreExtensionData.routeRef, + 'Nav Target': coreExtensionData.navTarget, + Theme: coreExtensionData.theme, +}; + +function Legend() { return ( - - + + {Object.entries(legendMap).map(([label, dataRef]) => ( + + + {label} + + ))} + + ); +} + +export function GraphVisualizer({ tree }: { tree: AppTree }) { + return ( + + + + + + + + ); } From 32f03b69760886035a40ff241cc5a08a8b1267b6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 14:38:43 +0100 Subject: [PATCH 13/24] visualizer: depth based borders Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/GraphVisualizer.tsx | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index 327c2d226b..89112b2f4c 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -27,7 +27,7 @@ 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 { Theme, makeStyles } from '@material-ui/core/styles'; +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'; @@ -80,11 +80,7 @@ const getOutputColor = createOutputColorGenerator( interface StyleProps { enabled: boolean; -} - -function borderColor(theme: Theme) { - return ({ enabled }: StyleProps) => - enabled ? theme.palette.primary.main : theme.palette.divider; + depth: number; } const config = { @@ -95,7 +91,8 @@ const useStyles = makeStyles(theme => ({ extension: { borderLeftWidth: theme.spacing(config.borderWidth), borderLeftStyle: 'solid', - borderLeftColor: borderColor(theme), + borderLeftColor: ({ depth }: StyleProps) => + colors.grey[(700 - (depth % 6) * 100) as keyof typeof colors.grey], cursor: 'pointer', '&:hover $extensionHeader': { @@ -139,7 +136,8 @@ const useStyles = makeStyles(theme => ({ borderTopWidth: theme.spacing(config.borderWidth), borderTopStyle: 'solid', - borderTopColor: borderColor(theme), + borderTopColor: ({ depth }: StyleProps) => + colors.grey[(700 - (depth % 6) * 100) as keyof typeof colors.grey], }, attachmentsInputName: { marginLeft: theme.spacing(1), @@ -230,10 +228,11 @@ function Output(props: { dataRef: ExtensionDataRef; node?: AppNode }) { function Attachments(props: { attachments: ReadonlyMap; enabled: boolean; + depth: number; }) { - const { attachments, enabled } = props; + const { attachments, enabled, depth } = props; - const classes = useStyles({ enabled }); + const classes = useStyles({ enabled, depth }); if (attachments.size === 0) { return null; @@ -254,7 +253,7 @@ function Attachments(props: { {children.map(node => ( - + ))} @@ -284,11 +283,11 @@ function ExtensionTooltip(props: { node: AppNode }) { ); } -function Extension(props: { node: AppNode }) { - const { node } = props; +function Extension(props: { node: AppNode; depth: number }) { + const { node, depth } = props; const enabled = Boolean(node.instance); - const classes = useStyles({ enabled }); + const classes = useStyles({ enabled, depth }); const dataRefs = node.instance && [...node.instance.getDataRefs()]; @@ -309,7 +308,11 @@ function Extension(props: { node: AppNode }) { {!enabled && } - + ); } @@ -353,7 +356,7 @@ export function GraphVisualizer({ tree }: { tree: AppTree }) { return ( - + From d7a24cb4f9f0aa5aaa5ca38f81beec300016b9a8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 20:02:49 +0100 Subject: [PATCH 14/24] visualizer: tweak spacing a bit Signed-off-by: Patrik Oldsberg --- .../src/components/AppVisualizerPage/GraphVisualizer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index 89112b2f4c..3b6f2c9c90 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -122,7 +122,11 @@ const useStyles = makeStyles(theme => ({ marginLeft: theme.spacing(1), gap: theme.spacing(1), }, - attachments: {}, + attachments: { + gap: theme.spacing(2), + display: 'flex', + flexDirection: 'column', + }, attachmentsInput: { '&:first-child $attachmentsInputTitle': { borderTop: 0, From db79a2026d89aec500e627a5e2fbcb6158936259 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 20:58:27 +0100 Subject: [PATCH 15/24] visualizer: pass full node to attachments Signed-off-by: Patrik Oldsberg --- .../components/AppVisualizerPage/GraphVisualizer.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx index 3b6f2c9c90..52d090af48 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx @@ -230,11 +230,12 @@ function Output(props: { dataRef: ExtensionDataRef; node?: AppNode }) { } function Attachments(props: { - attachments: ReadonlyMap; + node: AppNode; enabled: boolean; depth: number; }) { - const { attachments, enabled, depth } = props; + const { node, enabled, depth } = props; + const { attachments } = node.edges; const classes = useStyles({ enabled, depth }); @@ -312,11 +313,7 @@ function Extension(props: { node: AppNode; depth: number }) { {!enabled && } - + ); } From 4b3d0ec9e7aaa21d251cf4ad92a1679501e96faf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 21:23:06 +0100 Subject: [PATCH 16/24] visualizer: initial tree visualizer Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/AppVisualizerPage.tsx | 2 + .../AppVisualizerPage/TreeVisualizer.tsx | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index ec6145a1af..78ace296b4 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -21,6 +21,7 @@ import Box from '@material-ui/core/Box'; import React, { useState } from 'react'; import { GraphVisualizer } from './GraphVisualizer'; import { TextVisualizer } from './TextVisualizer'; +import { TreeVisualizer } from './TreeVisualizer'; export function AppVisualizerPage() { const appTreeApi = useApi(appTreeApiRef); @@ -29,6 +30,7 @@ export function AppVisualizerPage() { const tabs = [ { id: 'graph', label: 'Graph', element: }, + { id: 'tree', label: 'Tree', element: }, { id: 'text', label: 'Text', element: }, ]; diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx new file mode 100644 index 0000000000..1260da3c32 --- /dev/null +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -0,0 +1,57 @@ +/* + * 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 { + DependencyGraph, + DependencyGraphTypes, +} from '@backstage/core-components'; +import { AppTree } from '@backstage/frontend-plugin-api'; +import Box from '@material-ui/core/Box'; +import { useMemo } from 'react'; + +function resolveGraphData(tree: AppTree) { + const nodes = [...tree.nodes.values()] + .filter(n => n.instance) + .map(n => ({ ...n, id: n.spec.id })); + return { + nodes, + edges: nodes + .filter(n => n.edges.attachedTo) + .map(n => ({ + from: n.spec.id, + to: n.edges.attachedTo!.node.spec.id, + label: n.edges.attachedTo!.input, + })), + }; +} + +export function TreeVisualizer({ tree }: { tree: AppTree }) { + const graphData = useMemo(() => resolveGraphData(tree), [tree]); + + return ( + + + + ); +} From 5ad1a062e32ca76ee7d9fe184d2a1903a2641e03 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 21:49:16 +0100 Subject: [PATCH 17/24] visualizer: split inputs to separate nodes in tree Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/TreeVisualizer.tsx | 120 ++++++++++++++++-- 1 file changed, 106 insertions(+), 14 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index 1260da3c32..d405e1e164 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -18,26 +18,117 @@ import { DependencyGraph, DependencyGraphTypes, } from '@backstage/core-components'; -import { AppTree } from '@backstage/frontend-plugin-api'; +import { AppNode, AppTree } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; -import { useMemo } from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import { useLayoutEffect, useMemo, useRef, useState } from 'react'; -function resolveGraphData(tree: AppTree) { +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 resolveGraphData(tree: AppTree): { + nodes: NodeType[]; + edges: { from: string; to: string }[]; +} { const nodes = [...tree.nodes.values()] - .filter(n => n.instance) - .map(n => ({ ...n, id: n.spec.id })); + .filter(node => node.instance) + .map(node => ({ ...node, id: node.spec.id, type: 'node' as const })); + return { - nodes, - edges: nodes - .filter(n => n.edges.attachedTo) - .map(n => ({ - from: n.spec.id, - to: n.edges.attachedTo!.node.spec.id, - label: n.edges.attachedTo!.input, - })), + 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: theme.palette.primary.light, + }, + 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' ? node.id : node.name} + + + ); +} + export function TreeVisualizer({ tree }: { tree: AppTree }) { const graphData = useMemo(() => resolveGraphData(tree), [tree]); @@ -50,7 +141,8 @@ export function TreeVisualizer({ tree }: { tree: AppTree }) { nodeMargin={10} edgeMargin={30} rankMargin={100} - direction={DependencyGraphTypes.Direction.BOTTOM_TOP} + renderNode={Node} + direction={DependencyGraphTypes.Direction.TOP_BOTTOM} /> ); From 389e61a49803dd6ffe22247822c82ba6b979e7e9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 22:09:22 +0100 Subject: [PATCH 18/24] visualizer: cleaner tree Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/TreeVisualizer.tsx | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index d405e1e164..f38acf1379 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -31,6 +31,23 @@ 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 }[]; @@ -73,7 +90,10 @@ const useStyles = makeStyles(theme => ({ node.type === 'node' ? theme.palette.primary.light : theme.palette.grey[500], - stroke: theme.palette.primary.light, + stroke: (node: NodeType) => + node.type === 'node' + ? theme.palette.primary.main + : theme.palette.grey[600], }, text: { fill: theme.palette.primary.contrastText, @@ -123,7 +143,7 @@ export function Node(props: { node: NodeType }) { textAnchor="middle" alignmentBaseline="middle" > - {node.type === 'node' ? node.id : node.name} + {node.type === 'node' ? trimNodeId(node.id) : node.name} ); @@ -139,9 +159,11 @@ export function TreeVisualizer({ tree }: { tree: AppTree }) { style={{ height: '100%', width: '100%' }} {...graphData} nodeMargin={10} - edgeMargin={30} - rankMargin={100} + rankMargin={50} + paddingX={50} renderNode={Node} + align={DependencyGraphTypes.Alignment.DOWN_RIGHT} + ranker={DependencyGraphTypes.Ranker.TIGHT_TREE} direction={DependencyGraphTypes.Direction.TOP_BOTTOM} /> From cbd836aef45e7c2cd97a38c1308d04ba0f8945bc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 22:19:54 +0100 Subject: [PATCH 19/24] visualizer: switch to routed tabs Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/AppVisualizerPage.tsx | 70 ++++++++++++++++--- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index 78ace296b4..cc7dca24f4 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -18,29 +18,81 @@ 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, { useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { GraphVisualizer } from './GraphVisualizer'; 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 [tab, setTab] = useState(0); - const tabs = [ - { id: 'graph', label: 'Graph', element: }, - { id: 'tree', label: 'Tree', element: }, - { id: 'text', label: 'Text', element: }, - ]; + const tabs = useMemo( + () => [ + { + id: 'graph', + path: 'graph', + label: 'Graph', + element: , + }, + { + id: 'tree', + path: 'tree', + label: 'Tree', + 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 (
- - {tabs[tab].element} + + {element} From f0fb3fac14001119060952b00e33733bc01d8897 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 22:29:53 +0100 Subject: [PATCH 20/24] visualizer: make tree visualizer the default Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/AppVisualizerPage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index cc7dca24f4..1f0b038c87 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -36,18 +36,18 @@ export function AppVisualizerPage() { const tabs = useMemo( () => [ - { - id: 'graph', - path: 'graph', - label: 'Graph', - element: , - }, { id: 'tree', path: 'tree', label: 'Tree', element: , }, + { + id: 'graph', + path: 'graph', + label: 'Graph', + element: , + }, { id: 'text', path: 'text', From 73a5ce31f4f02d4434afc89ac40f0cbc2a6f6dc4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 4 Nov 2023 22:31:19 +0100 Subject: [PATCH 21/24] visualizer: rename GraphVisualizer -> DetailedVisualizer Signed-off-by: Patrik Oldsberg --- .../components/AppVisualizerPage/AppVisualizerPage.tsx | 10 +++++----- .../{GraphVisualizer.tsx => DetailedVisualizer.tsx} | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) rename plugins/visualizer/src/components/AppVisualizerPage/{GraphVisualizer.tsx => DetailedVisualizer.tsx} (99%) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx index 1f0b038c87..b984f8ac59 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/AppVisualizerPage.tsx @@ -19,7 +19,7 @@ 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 { GraphVisualizer } from './GraphVisualizer'; +import { DetailedVisualizer } from './DetailedVisualizer'; import { TextVisualizer } from './TextVisualizer'; import { TreeVisualizer } from './TreeVisualizer'; import { @@ -43,10 +43,10 @@ export function AppVisualizerPage() { element: , }, { - id: 'graph', - path: 'graph', - label: 'Graph', - element: , + id: 'detailed', + path: 'detailed', + label: 'Detailed', + element: , }, { id: 'text', diff --git a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx similarity index 99% rename from plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx rename to plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index 52d090af48..f3a76ff726 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/GraphVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -353,7 +353,7 @@ function Legend() { ); } -export function GraphVisualizer({ tree }: { tree: AppTree }) { +export function DetailedVisualizer({ tree }: { tree: AppTree }) { return ( From e1071afe41f1fff8a00d50094930e214df1ae6d1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 5 Nov 2023 12:41:56 -0600 Subject: [PATCH 22/24] visualizer: square extensions in tree Signed-off-by: Patrik Oldsberg --- .../src/components/AppVisualizerPage/TreeVisualizer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index f38acf1379..8532168bc0 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -133,7 +133,7 @@ export function Node(props: { node: NodeType }) { className={classes.node} width={paddedWidth} height={paddedHeight} - rx={10} + rx={node.type === 'node' ? 0 : 20} /> Date: Wed, 13 Dec 2023 19:47:16 +0100 Subject: [PATCH 23/24] visualizer: update naming patterns Signed-off-by: Patrik Oldsberg --- plugins/visualizer/src/plugin.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/visualizer/src/plugin.tsx b/plugins/visualizer/src/plugin.tsx index 216c1f8ee2..bb541bf4af 100644 --- a/plugins/visualizer/src/plugin.tsx +++ b/plugins/visualizer/src/plugin.tsx @@ -26,15 +26,13 @@ import React from 'react'; const rootRouteRef = createRouteRef(); const visualizerPage = createPageExtension({ - id: 'plugin.visualizer.page', defaultPath: '/visualizer', routeRef: rootRouteRef, loader: () => import('./components/AppVisualizerPage').then(m => ), }); -export const visualizerPageSidebarItem = createNavItemExtension({ - id: 'plugin.visualizer.nav.index', +export const visualizerNavItem = createNavItemExtension({ title: 'Visualizer', icon: VisualizerIcon, routeRef: rootRouteRef, @@ -42,5 +40,5 @@ export const visualizerPageSidebarItem = createNavItemExtension({ export const visualizerPlugin = createPlugin({ id: 'visualizer', - extensions: [visualizerPage, visualizerPageSidebarItem], + extensions: [visualizerPage, visualizerNavItem], }); From 3fddd065fc0ec7d3112ad459505d0df7729d5cb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 13 Dec 2023 20:00:30 +0100 Subject: [PATCH 24/24] visualizer: fixes for backstage/backstage Signed-off-by: Patrik Oldsberg --- .../AppVisualizerPage/DetailedVisualizer.tsx | 25 +++++++++++++------ .../AppVisualizerPage/TreeVisualizer.tsx | 1 + plugins/visualizer/src/plugin.tsx | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx index f3a76ff726..2356f38cfa 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/DetailedVisualizer.tsx @@ -20,6 +20,9 @@ import { ExtensionDataRef, RouteRef, coreExtensionData, + createApiExtension, + createNavItemExtension, + createThemeExtension, useRouteRef, } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; @@ -63,10 +66,11 @@ const getOutputColor = createOutputColorGenerator( [coreExtensionData.reactElement.id]: colors.green[500], [coreExtensionData.routePath.id]: colors.yellow[500], [coreExtensionData.routeRef.id]: colors.purple[500], - [coreExtensionData.apiFactory.id]: colors.blue[500], - [coreExtensionData.theme.id]: colors.lime[500], - [coreExtensionData.navTarget.id]: colors.orange[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], @@ -187,6 +191,7 @@ function OutputLink(props: { let link: string | undefined = undefined; try { + // eslint-disable-next-line react-hooks/rules-of-hooks link = useRouteRef(routeRef as RouteRef)(); } catch { /* ignore */ @@ -257,8 +262,12 @@ function Attachments(props: { - {children.map(node => ( - + {children.map(childNode => ( + ))} @@ -320,11 +329,11 @@ function Extension(props: { node: AppNode; depth: number }) { const legendMap = { 'React Element': coreExtensionData.reactElement, - 'Utility API': coreExtensionData.apiFactory, + 'Utility API': createApiExtension.factoryDataRef, 'Route Path': coreExtensionData.routePath, 'Route Ref': coreExtensionData.routeRef, - 'Nav Target': coreExtensionData.navTarget, - Theme: coreExtensionData.theme, + 'Nav Target': createNavItemExtension.targetDataRef, + Theme: createThemeExtension.themeDataRef, }; function Legend() { diff --git a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx index 8532168bc0..eac7c4e203 100644 --- a/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx +++ b/plugins/visualizer/src/components/AppVisualizerPage/TreeVisualizer.tsx @@ -14,6 +14,7 @@ * limitations under the License. */ +import React from 'react'; import { DependencyGraph, DependencyGraphTypes, diff --git a/plugins/visualizer/src/plugin.tsx b/plugins/visualizer/src/plugin.tsx index bb541bf4af..0aa8053604 100644 --- a/plugins/visualizer/src/plugin.tsx +++ b/plugins/visualizer/src/plugin.tsx @@ -38,6 +38,7 @@ export const visualizerNavItem = createNavItemExtension({ routeRef: rootRouteRef, }); +/** @public */ export const visualizerPlugin = createPlugin({ id: 'visualizer', extensions: [visualizerPage, visualizerNavItem],