diff --git a/plugins/app-visualizer/src/components/CopyTreeButton.tsx b/plugins/app-visualizer/src/components/CopyTreeButton.tsx new file mode 100644 index 0000000000..4b430e65dc --- /dev/null +++ b/plugins/app-visualizer/src/components/CopyTreeButton.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2025 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 { useState } from 'react'; +import { + useApi, + appTreeApiRef, + type AppNode, +} from '@backstage/frontend-plugin-api'; +import { Button } from '@backstage/ui'; +import { RiFileCopyLine, RiCheckLine } from '@remixicon/react'; + +function nodeToJson(node: AppNode): object { + const attachments: Record = {}; + for (const [input, children] of node.edges.attachments) { + attachments[input] = children.map(nodeToJson); + } + + return { + id: node.spec.id, + plugin: node.spec.plugin.pluginId, + disabled: node.spec.disabled || undefined, + ...(Object.keys(attachments).length > 0 ? { attachments } : {}), + }; +} + +export function CopyTreeButton() { + const appTreeApi = useApi(appTreeApiRef); + const [copied, setCopied] = useState(false); + + const handlePress = () => { + const { tree } = appTreeApi.getTree(); + const json = JSON.stringify(nodeToJson(tree.root), null, 2); + window.navigator.clipboard.writeText(json).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( + + ); +} diff --git a/plugins/app-visualizer/src/plugin.tsx b/plugins/app-visualizer/src/plugin.tsx index cdd429287a..205a1ad968 100644 --- a/plugins/app-visualizer/src/plugin.tsx +++ b/plugins/app-visualizer/src/plugin.tsx @@ -19,6 +19,7 @@ import { createRouteRef, NavItemBlueprint, PageBlueprint, + HeaderActionBlueprint, SubPageBlueprint, } from '@backstage/frontend-plugin-api'; import { RiEyeLine } from '@remixicon/react'; @@ -113,6 +114,14 @@ const appVisualizerTextPage = SubPageBlueprint.make({ }, }); +const copyTreeAsJson = HeaderActionBlueprint.make({ + params: defineParams => + defineParams({ + loader: () => + import('./components/CopyTreeButton').then(m => ), + }), +}); + export const appVisualizerNavItem = NavItemBlueprint.make({ params: { title: 'Visualizer', @@ -134,5 +143,6 @@ export const visualizerPlugin = createFrontendPlugin({ appVisualizerDetailedPage, appVisualizerTextPage, appVisualizerNavItem, + copyTreeAsJson, ], });