From 0433fe5c2c06443874f18f7641222296e112f5c6 Mon Sep 17 00:00:00 2001 From: Joshua Creek <8179928+jcreek@users.noreply.github.com> Date: Thu, 5 Mar 2026 18:32:45 +0000 Subject: [PATCH] feat(*): Enable loading the file and creating a PR in Azure Devops --- README.md | 214 +- index.html | 13 - package.json | 50 +- .../tech-radar-editor-backend/package.json | 35 + .../tech-radar-editor-backend/src/index.ts | 1 + .../tech-radar-editor-backend/src/plugin.ts | 22 + .../tech-radar-editor-backend/src/router.ts | 242 + .../tech-radar-editor-backend/src/types.ts | 20 + .../tech-radar-editor-backend-1.0.0.tgz | Bin 0 -> 17693 bytes .../tech-radar-editor-backend/tsconfig.json | 10 + .../tech-radar-editor-backstage/package.json | 37 + .../src/TechRadarEditorPage.tsx | 263 + .../tech-radar-editor-backstage/src/index.ts | 1 + .../tech-radar-editor-backstage-1.0.0.tgz | Bin 0 -> 17464 bytes .../tech-radar-editor-backstage/tsconfig.json | 12 + packages/tech-radar-editor/index.html | 23 + packages/tech-radar-editor/package.json | 50 + .../tech-radar-editor/postcss.config.js | 0 .../tech-radar-editor/public}/vite.svg | 0 .../src}/TechRadarEditor.svelte | 168 +- .../tech-radar-editor/src}/assets/svelte.svg | 0 packages/tech-radar-editor/src/main.ts | 3 + packages/tech-radar-editor/src/types.ts | 40 + .../tech-radar-editor/src}/vite-env.d.ts | 0 .../tech-radar-editor/svelte.config.js | 0 .../tech-radar-editor/tailwind.config.cjs | 0 .../tech-radar-editor-1.2.0.tgz | Bin 0 -> 40497 bytes .../tech-radar-editor/tsconfig.build.json | 0 .../tech-radar-editor/tsconfig.json | 0 .../tech-radar-editor/tsconfig.node.json | 0 .../tech-radar-editor/vite.config.ts | 0 pnpm-lock.yaml | 5056 +++++++++++++++-- pnpm-workspace.yaml | 2 + src/main.ts | 1 - tsconfig.base.json | 16 + 35 files changed, 5503 insertions(+), 776 deletions(-) delete mode 100644 index.html create mode 100644 packages/tech-radar-editor-backend/package.json create mode 100644 packages/tech-radar-editor-backend/src/index.ts create mode 100644 packages/tech-radar-editor-backend/src/plugin.ts create mode 100644 packages/tech-radar-editor-backend/src/router.ts create mode 100644 packages/tech-radar-editor-backend/src/types.ts create mode 100644 packages/tech-radar-editor-backend/tech-radar-editor-backend-1.0.0.tgz create mode 100644 packages/tech-radar-editor-backend/tsconfig.json create mode 100644 packages/tech-radar-editor-backstage/package.json create mode 100644 packages/tech-radar-editor-backstage/src/TechRadarEditorPage.tsx create mode 100644 packages/tech-radar-editor-backstage/src/index.ts create mode 100644 packages/tech-radar-editor-backstage/tech-radar-editor-backstage-1.0.0.tgz create mode 100644 packages/tech-radar-editor-backstage/tsconfig.json create mode 100644 packages/tech-radar-editor/index.html create mode 100644 packages/tech-radar-editor/package.json rename postcss.config.js => packages/tech-radar-editor/postcss.config.js (100%) rename {public => packages/tech-radar-editor/public}/vite.svg (100%) rename {src => packages/tech-radar-editor/src}/TechRadarEditor.svelte (76%) rename {src => packages/tech-radar-editor/src}/assets/svelte.svg (100%) create mode 100644 packages/tech-radar-editor/src/main.ts create mode 100644 packages/tech-radar-editor/src/types.ts rename {src => packages/tech-radar-editor/src}/vite-env.d.ts (100%) rename svelte.config.js => packages/tech-radar-editor/svelte.config.js (100%) rename tailwind.config.cjs => packages/tech-radar-editor/tailwind.config.cjs (100%) create mode 100644 packages/tech-radar-editor/tech-radar-editor-1.2.0.tgz rename tsconfig.build.json => packages/tech-radar-editor/tsconfig.build.json (100%) rename tsconfig.json => packages/tech-radar-editor/tsconfig.json (100%) rename tsconfig.node.json => packages/tech-radar-editor/tsconfig.node.json (100%) rename vite.config.ts => packages/tech-radar-editor/vite.config.ts (100%) create mode 100644 pnpm-workspace.yaml delete mode 100644 src/main.ts create mode 100644 tsconfig.base.json diff --git a/README.md b/README.md index 80469f1..4131fbc 100644 --- a/README.md +++ b/README.md @@ -19,109 +19,59 @@ Zalando has a fantastic description [on their website](https://opensource.zaland It serves and scales well for teams and companies of all sizes that want to have alignment across dozens of technologies and visualize it in a simple way. +## Packages + +This is a monorepo containing three packages: + +| Package | Description | +|---------|-------------| +| [`tech-radar-editor`](./packages/tech-radar-editor) | The core web component (``) | +| [`tech-radar-editor-backend`](./packages/tech-radar-editor-backend) | Backstage backend plugin for Azure DevOps integration | +| [`tech-radar-editor-backstage`](./packages/tech-radar-editor-backstage) | Backstage frontend wrapper component | + ## Include the Component in Spotify Backstage -### Install +### Quick Start -For either simple or advanced installations, you'll need to add the dependency using Yarn: - -From your Backstage root directory: +1. Install packages: ```bash -yarn --cwd packages/app add tech-radar-editor +# From your Backstage root directory +yarn --cwd packages/app add tech-radar-editor-backstage +yarn --cwd packages/backend add tech-radar-editor-backend ``` -### Configuration - -Modify your app routes to include the Router component exported from the tech radar, for example: - -```tsx -// In packages/app/src/App.tsx -import 'tech-radar-editor'; - -const routes = ( - - {/* ...other routes */} - } - /> -``` - -#### How to Use This Component in Backstage with a Config-Based URL - -##### Configure the URL in app-config.yaml - -In your Backstage `app-config.yaml`, add a config key for the editor’s JSON URL. For example: +2. Add config to `app-config.yaml`: ```yaml -techRadar: - url: "https://example.org/path/to/tech-radar.json" +techRadarEditor: + organization: my-org + project: my-project + repository: my-repo + filePath: /tech-radar.json # optional, defaults to /tech-radar.json + targetBranch: main # optional, defaults to main ``` -Adjust the key and value to suit your environment. +3. Register the backend plugin in `packages/backend/src/index.ts`: -##### Set up the type for the config value - -In your `package.json`, add: - -```json -{ - // ... - "files": [ - // ... - "config.d.ts" - ], - "configSchema": "config.d.ts" -} +```ts +backend.add(import('tech-radar-editor-backend')); ``` -You'll need to create a type definition for the config value in your Backstage app. In your `config.d.ts` file, add: - -````ts -export interface Config { - techRadar: { - /** - * Frontend URL - * @visibility frontend - */ - url: string; - }; -} -``` - -##### Create a small React wrapper in your Backstage app - -Somewhere in your Backstage app (for example, in `App.tsx` or in a separate `TechRadarEditorWrapper.tsx` file), create a tiny wrapper component to read the config and pass that URL to the `` web component as an attribute: +4. Add the route in your `packages/app/src/App.tsx`: ```tsx -import React from "react"; -import { useApi, configApiRef } from "@backstage/core-plugin-api"; +import { TechRadarEditorPage } from 'tech-radar-editor-backstage'; -// Import the custom element so that React recognizes -import "tech-radar-editor"; - -export const TechRadarEditorWrapper = () => { - const configApi = useApi(configApiRef); - const dataUrl = configApi.getOptionalString("techRadar.url") ?? ""; - - return ; -}; -```` - -##### Add a route for your Tech Radar Editor - -In your Backstage `App.tsx` (or wherever you define your routes), import that wrapper and point a route to it: - -```tsx -} /> +// In your routes: +} /> ``` -Now when you visit `/tech-radar-editor`, the `` component will automatically fetch the JSON from your configured URL. +That's it! The editor will fetch data from your Azure DevOps repository and allow users to submit changes as pull requests. -## Consume the Component in Plain JavaScript +## Use the Web Component Standalone -Include the component in an HTML file: +### In Plain HTML ```html @@ -137,64 +87,76 @@ Include the component in an HTML file: ``` -## Consume the Component in React +### In React -In a React project, you can use the web component as you would use any custom HTML element. - -First, ensure that the component is imported or included: - -```js -// index.js -import React from "react"; -import ReactDOM from "react-dom"; -import "./index.css"; - -// Import the component -import "tech-radar-editor"; +```tsx +import 'tech-radar-editor'; function App() { - return ( -
- -
- ); -} - -ReactDOM.render(, document.getElementById("root")); -``` - -Note: In React, you might need to instruct TypeScript about the custom element. Create a react-app-env.d.ts or a global.d.ts file: - -```ts -// react-app-env.d.ts -declare namespace JSX { - interface IntrinsicElements { - "tech-radar-editor": any; - } + return ; } ``` -## Building the Component +### With a Data URL -To build the component, you need to have Node.js installed. Clone the repository and run the following commands: +Load JSON from a URL on mount: + +```html + +``` + +### With Inline JSON Data + +Pass JSON directly via attribute: + +```html + +``` + +### Web Component Props + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `data-url` | `string` | `''` | URL to fetch JSON data from on mount | +| `data-json` | `string` | `''` | JSON string to load directly (bypasses URL fetch) | +| `hide-title` | `boolean\|string` | `false` | Hide the h1 heading | +| `hide-export` | `boolean\|string` | `false` | Hide the "Export JSON" button | +| `hide-json-input` | `boolean\|string` | `false` | Hide the textarea + "Load JSON" button | +| `hide-json-preview` | `boolean\|string` | `false` | Hide the "JSON Preview" section | +| `auto-expand-entries` | `boolean\|string` | `false` | Auto-expand the Entries section on load | + +### Custom Events + +| Event | Detail | Description | +|-------|--------|-------------| +| `radar-data-loaded` | `{ data: TechRadarData }` | Fired once when data is first loaded | +| `radar-data-change` | `{ data: TechRadarData }` | Fired whenever the data changes | + +```js +const editor = document.querySelector('tech-radar-editor'); +editor.addEventListener('radar-data-change', (e) => { + console.log('Updated data:', e.detail.data); +}); +``` + +## Building ```bash pnpm install pnpm build ``` -To get it ready to add to NPM or a CDN, run: +To develop the web component with hot reload: + +```bash +pnpm dev +``` + +To publish the packages to npm: ```bash pnpm build -pnpm pack -``` - -You can then test the package locally. - -To publish the component to NPM, run: - -```bash -npm login -npm publish +cd packages/tech-radar-editor && npm publish +cd packages/tech-radar-editor-backend && npm publish +cd packages/tech-radar-editor-backstage && npm publish ``` diff --git a/index.html b/index.html deleted file mode 100644 index 5e6dab5..0000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Tech Radar Editor - - - - - - diff --git a/package.json b/package.json index 4917b9e..b8c238a 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,10 @@ { - "name": "tech-radar-editor", - "version": "1.1.0", - "type": "module", - "repository": { - "type": "git", - "url": "https://github.com/jcreek/Tech-Radar-Editor.git" - }, - "keywords": [ - "tech-radar", - "editor", - "visualization", - "web component", - "svelte", - "tech trends", - "technology radar", - "opensource", - "thoughtworks", - "zalando" - ], - "author": "Josh Creek", - "license": "GPL-3.0", - "main": "dist/tech-radar-editor.umd.js", - "module": "dist/tech-radar-editor.es.js", - "types": "dist/main.d.ts", - "files": [ - "dist" - ], + "private": true, "scripts": { - "dev": "vite", - "build": "vite build && tsc -p tsconfig.build.json", - "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + "build": "pnpm -r build", + "dev": "pnpm --filter tech-radar-editor dev" }, - "dependencies": { - "svelte": "^4.2.18" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.1.1", - "@tsconfig/svelte": "^5.0.4", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.47", - "postcss-load-config": "^6.0.1", - "svelte-check": "^3.8.5", - "tailwindcss": "^3.4.13", - "tslib": "^2.6.3", - "typescript": "^5.5.3", - "vite": "^5.4.1" + "engines": { + "node": ">=18" } } diff --git a/packages/tech-radar-editor-backend/package.json b/packages/tech-radar-editor-backend/package.json new file mode 100644 index 0000000..d53451b --- /dev/null +++ b/packages/tech-radar-editor-backend/package.json @@ -0,0 +1,35 @@ +{ + "name": "tech-radar-editor-backend", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "backstage": { + "role": "backend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/jcreek/Tech-Radar-Editor.git", + "directory": "packages/tech-radar-editor-backend" + }, + "author": "Josh Creek", + "license": "GPL-3.0", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@backstage/backend-plugin-api": "^1.3.1", + "@backstage/config": "^1.3.6", + "express": "^4.17.1", + "express-promise-router": "^4.1.0" + }, + "devDependencies": { + "@types/express": "^4.17.6", + "typescript": "^5.5.3" + }, + "peerDependencies": { + "@backstage/backend-plugin-api": "^1.x.x" + } +} diff --git a/packages/tech-radar-editor-backend/src/index.ts b/packages/tech-radar-editor-backend/src/index.ts new file mode 100644 index 0000000..e70422b --- /dev/null +++ b/packages/tech-radar-editor-backend/src/index.ts @@ -0,0 +1 @@ +export { techRadarEditorPlugin as default } from './plugin'; diff --git a/packages/tech-radar-editor-backend/src/plugin.ts b/packages/tech-radar-editor-backend/src/plugin.ts new file mode 100644 index 0000000..aae0489 --- /dev/null +++ b/packages/tech-radar-editor-backend/src/plugin.ts @@ -0,0 +1,22 @@ +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './router'; + +export const techRadarEditorPlugin = createBackendPlugin({ + pluginId: 'tech-radar-editor', + register(env) { + env.registerInit({ + deps: { + httpRouter: coreServices.httpRouter, + logger: coreServices.logger, + config: coreServices.rootConfig, + }, + async init({ httpRouter, logger, config }) { + const router = await createRouter({ logger, config }); + httpRouter.use(router); + }, + }); + }, +}); diff --git a/packages/tech-radar-editor-backend/src/router.ts b/packages/tech-radar-editor-backend/src/router.ts new file mode 100644 index 0000000..80422fb --- /dev/null +++ b/packages/tech-radar-editor-backend/src/router.ts @@ -0,0 +1,242 @@ +import { RootConfigService } from '@backstage/backend-plugin-api'; +import express from 'express'; +import Router from 'express-promise-router'; +import { RouterOptions, AzureCredentials, AzureDevOpsTarget } from './types'; + +function getAzureCredentials(config: RootConfigService): AzureCredentials { + const integrations = config.getConfigArray('integrations.azure'); + const azureIntegration = integrations[0]; + const credentials = azureIntegration.getConfigArray('credentials'); + const cred = credentials[0]; + + return { + clientId: cred.getString('clientId'), + clientSecret: cred.getString('clientSecret'), + tenantId: cred.getString('tenantId'), + }; +} + +function getTarget(config: RootConfigService): AzureDevOpsTarget { + return { + organization: config.getString('techRadarEditor.organization'), + project: config.getString('techRadarEditor.project'), + repository: config.getString('techRadarEditor.repository'), + filePath: config.getOptionalString('techRadarEditor.filePath') ?? '/tech-radar.json', + targetBranch: config.getOptionalString('techRadarEditor.targetBranch') ?? 'main', + }; +} + +async function getAzureToken(credentials: AzureCredentials): Promise { + const tokenUrl = `https://login.microsoftonline.com/${credentials.tenantId}/oauth2/v2.0/token`; + const body = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: credentials.clientId, + client_secret: credentials.clientSecret, + scope: '499b84ac-1321-427f-aa17-267ca6975798/.default', + }); + + const response = await fetch(tokenUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Failed to get Azure token: ${response.status} ${text}`); + } + + const data = await response.json() as { access_token: string }; + return data.access_token; +} + +async function azureDevOpsApi( + token: string, + url: string, + options?: { method?: string; body?: unknown }, +): Promise { + const response = await fetch(url, { + method: options?.method ?? 'GET', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Azure DevOps API error: ${response.status} ${text}`); + } + + return response.json(); +} + +export async function createRouter(options: RouterOptions): Promise { + const { logger, config } = options; + const router = Router(); + router.use(express.json({ limit: '1mb' })); + + const credentials = getAzureCredentials(config); + const target = getTarget(config); + + // Proxy endpoint for the web component to fetch tech radar data without auth + router.get('/data', async (_req, res) => { + try { + const token = await getAzureToken(credentials); + const apiUrl = + `https://dev.azure.com/${target.organization}/${target.project}` + + `/_apis/git/repositories/${target.repository}/items` + + `?path=${encodeURIComponent(target.filePath)}` + + `&versionDescriptor.version=${target.targetBranch}` + + `&versionDescriptor.versionType=branch` + + `&api-version=7.1`; + + const response = await fetch(apiUrl, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + const text = await response.text(); + logger.error(`Failed to fetch tech radar data: ${response.status} ${text}`); + res.status(502).json({ message: 'Failed to fetch tech radar data' }); + return; + } + + const data = await response.json(); + res.json(data); + } catch (error) { + logger.error(`Error fetching tech radar data: ${error}`); + res.status(500).json({ message: 'Internal server error' }); + } + }); + + // Save endpoint: creates a branch and PR with the updated tech radar JSON + router.post('/save', async (req, res) => { + const { data, title, description, userToken } = req.body as { + data: string; + title?: string; + description?: string; + userToken?: string; + }; + + if (!data) { + res.status(400).json({ message: 'Missing "data" field in request body' }); + return; + } + + // Validate JSON + try { + JSON.parse(data); + } catch { + res.status(400).json({ message: 'Invalid JSON data' }); + return; + } + + try { + // Use the user's token if provided (PR will be authored as the user), + // otherwise fall back to the service principal token. + const token = userToken || await getAzureToken(credentials); + const baseApiUrl = + `https://dev.azure.com/${target.organization}/${target.project}` + + `/_apis/git/repositories/${target.repository}`; + + // 1. Get the latest commit SHA on the target branch + const refsResult = await azureDevOpsApi( + token, + `${baseApiUrl}/refs?filter=heads/${target.targetBranch}&api-version=7.1`, + ) as { value: Array<{ objectId: string }> }; + + if (!refsResult.value?.length) { + res.status(500).json({ message: `Branch '${target.targetBranch}' not found` }); + return; + } + + const mainBranchCommitId = refsResult.value[0].objectId; + const branchName = `tech-radar-update-${Date.now()}`; + + // 2. Create a new branch from the target branch HEAD + await azureDevOpsApi( + token, + `${baseApiUrl}/refs?api-version=7.1`, + { + method: 'POST', + body: [ + { + name: `refs/heads/${branchName}`, + oldObjectId: '0000000000000000000000000000000000000000', + newObjectId: mainBranchCommitId, + }, + ], + }, + ); + + // 3. Push a commit to the new branch with the updated file + await azureDevOpsApi( + token, + `${baseApiUrl}/pushes?api-version=7.1`, + { + method: 'POST', + body: { + refUpdates: [ + { + name: `refs/heads/${branchName}`, + oldObjectId: mainBranchCommitId, + }, + ], + commits: [ + { + comment: title ?? 'Update tech radar data', + changes: [ + { + changeType: 'edit', + item: { path: target.filePath }, + newContent: { + content: data, + contentType: 'rawtext', + }, + }, + ], + }, + ], + }, + }, + ); + + logger.info(`Created branch '${branchName}' with updated tech radar data`); + + // 4. Create a pull request + const prResult = await azureDevOpsApi( + token, + `${baseApiUrl}/pullrequests?api-version=7.1`, + { + method: 'POST', + body: { + sourceRefName: `refs/heads/${branchName}`, + targetRefName: `refs/heads/${target.targetBranch}`, + title: title ?? 'Update tech radar data', + description: + description ?? + 'This PR was created from the Tech Radar Editor in Backstage.', + }, + }, + ) as { pullRequestId: number; url: string }; + + const prUrl = + `https://dev.azure.com/${target.organization}/${target.project}` + + `/_git/${target.repository}/pullrequest/${prResult.pullRequestId}`; + + logger.info(`Created PR #${prResult.pullRequestId}: ${prUrl}`); + + res.json({ + pullRequestId: prResult.pullRequestId, + pullRequestUrl: prUrl, + }); + } catch (error) { + logger.error(`Error creating tech radar PR: ${error}`); + res.status(500).json({ message: `Failed to create PR: ${error}` }); + } + }); + + return router; +} diff --git a/packages/tech-radar-editor-backend/src/types.ts b/packages/tech-radar-editor-backend/src/types.ts new file mode 100644 index 0000000..1795a4b --- /dev/null +++ b/packages/tech-radar-editor-backend/src/types.ts @@ -0,0 +1,20 @@ +import { LoggerService, RootConfigService } from '@backstage/backend-plugin-api'; + +export interface RouterOptions { + logger: LoggerService; + config: RootConfigService; +} + +export interface AzureCredentials { + clientId: string; + clientSecret: string; + tenantId: string; +} + +export interface AzureDevOpsTarget { + organization: string; + project: string; + repository: string; + filePath: string; + targetBranch: string; +} diff --git a/packages/tech-radar-editor-backend/tech-radar-editor-backend-1.0.0.tgz b/packages/tech-radar-editor-backend/tech-radar-editor-backend-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..4bbd9f3aadd9cea4a3e276dbade05e8b5e03ade8 GIT binary patch literal 17693 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.16.0", + "@material-ui/core": "^4.x.x" + }, + "devDependencies": { + "@types/react": "^18.2.0", + "@backstage/core-plugin-api": "^1.9.4", + "@backstage/core-components": "^0.16.2", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", + "react": "^18.2.0", + "typescript": "^5.5.3" + } +} diff --git a/packages/tech-radar-editor-backstage/src/TechRadarEditorPage.tsx b/packages/tech-radar-editor-backstage/src/TechRadarEditorPage.tsx new file mode 100644 index 0000000..ecef6e9 --- /dev/null +++ b/packages/tech-radar-editor-backstage/src/TechRadarEditorPage.tsx @@ -0,0 +1,263 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + discoveryApiRef, + fetchApiRef, + microsoftAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { + Content, + Header, + HeaderLabel, + Page, +} from '@backstage/core-components'; +import Button from '@material-ui/core/Button'; +import Typography from '@material-ui/core/Typography'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Snackbar from '@material-ui/core/Snackbar'; +import SnackbarContent from '@material-ui/core/SnackbarContent'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import { makeStyles } from '@material-ui/core/styles'; + +import 'tech-radar-editor'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + interface IntrinsicElements { + 'tech-radar-editor': React.DetailedHTMLProps< + React.HTMLAttributes & { + 'data-json'?: string; + 'hide-title'?: string; + 'hide-export'?: string; + 'hide-json-input'?: string; + 'hide-json-preview'?: string; + 'auto-expand-entries'?: string; + }, + HTMLElement + >; + } + } +} + +const useStyles = makeStyles(theme => ({ + editorContainer: { + '& tech-radar-editor': { + display: 'block', + width: '100%', + }, + }, + stickyFooter: { + position: 'sticky', + bottom: 0, + zIndex: 100, + display: 'flex', + alignItems: 'center', + gap: theme.spacing(2), + padding: theme.spacing(2, 3), + backgroundColor: theme.palette.background.paper, + borderTop: `1px solid ${theme.palette.divider}`, + boxShadow: theme.shadows[4], + marginTop: theme.spacing(3), + }, + successSnackbar: { + backgroundColor: theme.palette.success?.main ?? '#4caf50', + }, + errorSnackbar: { + backgroundColor: theme.palette.error.main, + }, +})); + +export const TechRadarEditorPage = () => { + const classes = useStyles(); + const discoveryApi = useApi(discoveryApiRef); + const fetchApi = useApi(fetchApiRef); + const microsoftAuth = useApi(microsoftAuthApiRef); + const editorRef = useRef(null); + const [loading, setLoading] = useState(true); + const [initialJson, setInitialJson] = useState(null); + const [saving, setSaving] = useState(false); + const [currentJson, setCurrentJson] = useState(null); + const [result, setResult] = useState<{ + type: 'success' | 'error'; + message: string; + } | null>(null); + + // Fetch the current tech radar data from the backend (with Backstage auth) + useEffect(() => { + (async () => { + try { + const baseUrl = await discoveryApi.getBaseUrl('tech-radar-editor'); + const response = await fetchApi.fetch(`${baseUrl}/data`); + if (response.ok) { + const data = await response.json(); + setInitialJson(JSON.stringify(data, null, 2)); + } else { + console.error('Failed to fetch tech radar data:', response.status); + } + } catch (error) { + console.error('Error fetching tech radar data:', error); + } finally { + setLoading(false); + } + })(); + }, [discoveryApi, fetchApi]); + + // Listen for radar-data-change events from the web component + useEffect(() => { + const editor = editorRef.current; + if (!editor) return undefined; + + const handleDataChange = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (detail?.data) { + setCurrentJson(JSON.stringify(detail.data, null, 2)); + } + }; + + editor.addEventListener('radar-data-change', handleDataChange); + return () => editor.removeEventListener('radar-data-change', handleDataChange); + }, [loading]); + + const handleSave = useCallback(async () => { + const jsonData = currentJson; + if (!jsonData) { + setResult({ + type: 'error', + message: + 'Could not read valid JSON from the editor. Make sure you have loaded data first.', + }); + return; + } + + setSaving(true); + setResult(null); + + try { + const userAzureToken = await microsoftAuth.getAccessToken( + '499b84ac-1321-427f-aa17-267ca6975798/user_impersonation', + ); + + const baseUrl = await discoveryApi.getBaseUrl('tech-radar-editor'); + const response = await fetchApi.fetch(`${baseUrl}/save`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: jsonData, userToken: userAzureToken }), + }); + + const body = await response.json(); + + if (response.ok) { + setResult({ + type: 'success', + message: `Pull request created successfully! View it here: ${body.pullRequestUrl}`, + }); + } else { + setResult({ + type: 'error', + message: body.message || 'Failed to create pull request.', + }); + } + } catch (error) { + setResult({ + type: 'error', + message: `Error creating pull request: ${error}`, + }); + } finally { + setSaving(false); + } + }, [discoveryApi, fetchApi, microsoftAuth, currentJson]); + + if (loading) { + return ( + +
+ + + + + ); + } + + return ( + +
+ +
+ +
+ { + editorRef.current = el; + }} + {...(initialJson ? { 'data-json': initialJson } : {})} + hide-title="true" + hide-export="true" + hide-json-input={initialJson ? 'true' : undefined} + hide-json-preview="true" + auto-expand-entries="true" + /> +
+
+ + + This will create a pull request with your changes for review. + +
+ setResult(null)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + > + {result ? ( + + {result.message.split('View it here: ')[0]} + {result.message.includes('View it here: ') && ( + + View the pull request + + )} + + ) : ( + result.message + ) + } + action={ + setResult(null)} + > + + + } + /> + ) : undefined} + +
+
+ ); +}; diff --git a/packages/tech-radar-editor-backstage/src/index.ts b/packages/tech-radar-editor-backstage/src/index.ts new file mode 100644 index 0000000..4f161cb --- /dev/null +++ b/packages/tech-radar-editor-backstage/src/index.ts @@ -0,0 +1 @@ +export { TechRadarEditorPage } from './TechRadarEditorPage'; diff --git a/packages/tech-radar-editor-backstage/tech-radar-editor-backstage-1.0.0.tgz b/packages/tech-radar-editor-backstage/tech-radar-editor-backstage-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..2ed1e20d82f137654847988c3e23672a1d602e1a GIT binary patch literal 17464 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 + + + + + + Tech Radar Editor + + + + + + + + diff --git a/packages/tech-radar-editor/package.json b/packages/tech-radar-editor/package.json new file mode 100644 index 0000000..38f746d --- /dev/null +++ b/packages/tech-radar-editor/package.json @@ -0,0 +1,50 @@ +{ + "name": "tech-radar-editor", + "version": "1.2.0", + "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/jcreek/Tech-Radar-Editor.git" + }, + "keywords": [ + "tech-radar", + "editor", + "visualization", + "web component", + "svelte", + "tech trends", + "technology radar", + "opensource", + "thoughtworks", + "zalando" + ], + "author": "Josh Creek", + "license": "GPL-3.0", + "main": "dist/tech-radar-editor.umd.js", + "module": "dist/tech-radar-editor.es.js", + "types": "dist/main.d.ts", + "files": [ + "dist" + ], + "scripts": { + "dev": "vite", + "build": "vite build && tsc -p tsconfig.build.json", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json && tsc -p tsconfig.node.json" + }, + "dependencies": { + "svelte": "^4.2.18" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "@tsconfig/svelte": "^5.0.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.47", + "postcss-load-config": "^6.0.1", + "svelte-check": "^3.8.5", + "tailwindcss": "^3.4.13", + "tslib": "^2.6.3", + "typescript": "^5.5.3", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.js b/packages/tech-radar-editor/postcss.config.js similarity index 100% rename from postcss.config.js rename to packages/tech-radar-editor/postcss.config.js diff --git a/public/vite.svg b/packages/tech-radar-editor/public/vite.svg similarity index 100% rename from public/vite.svg rename to packages/tech-radar-editor/public/vite.svg diff --git a/src/TechRadarEditor.svelte b/packages/tech-radar-editor/src/TechRadarEditor.svelte similarity index 76% rename from src/TechRadarEditor.svelte rename to packages/tech-radar-editor/src/TechRadarEditor.svelte index b26a2ad..ecbb628 100644 --- a/src/TechRadarEditor.svelte +++ b/packages/tech-radar-editor/src/TechRadarEditor.svelte @@ -1,4 +1,17 @@ - +