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 0000000..4bbd9f3 Binary files /dev/null and b/packages/tech-radar-editor-backend/tech-radar-editor-backend-1.0.0.tgz differ diff --git a/packages/tech-radar-editor-backend/tsconfig.json b/packages/tech-radar-editor-backend/tsconfig.json new file mode 100644 index 0000000..94f1182 --- /dev/null +++ b/packages/tech-radar-editor-backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "module": "CommonJS", + "moduleResolution": "node" + }, + "include": ["src"] +} diff --git a/packages/tech-radar-editor-backstage/package.json b/packages/tech-radar-editor-backstage/package.json new file mode 100644 index 0000000..abb747e --- /dev/null +++ b/packages/tech-radar-editor-backstage/package.json @@ -0,0 +1,37 @@ +{ + "name": "tech-radar-editor-backstage", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://github.com/jcreek/Tech-Radar-Editor.git", + "directory": "packages/tech-radar-editor-backstage" + }, + "author": "Josh Creek", + "license": "GPL-3.0", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "tech-radar-editor": "^1.2.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "@backstage/core-plugin-api": "^1.x.x", + "@backstage/core-components": ">=0.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 0000000..2ed1e20 Binary files /dev/null and b/packages/tech-radar-editor-backstage/tech-radar-editor-backstage-1.0.0.tgz differ diff --git a/packages/tech-radar-editor-backstage/tsconfig.json b/packages/tech-radar-editor-backstage/tsconfig.json new file mode 100644 index 0000000..fc8e2b1 --- /dev/null +++ b/packages/tech-radar-editor-backstage/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "module": "CommonJS", + "moduleResolution": "node", + "jsx": "react-jsx", + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} diff --git a/packages/tech-radar-editor/index.html b/packages/tech-radar-editor/index.html new file mode 100644 index 0000000..38ec847 --- /dev/null +++ b/packages/tech-radar-editor/index.html @@ -0,0 +1,23 @@ + + + + + + + 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 @@ - +