From e12c08354ee1826b639935eb1f0ed66601c901fc Mon Sep 17 00:00:00 2001 From: Wesley Date: Sun, 11 Sep 2022 11:33:24 +0200 Subject: [PATCH] +plugin-azure-functions +plugin-azure-functions-backend Signed-off-by: Wesley --- .changeset/pre.json | 2 + microsite/data/plugins/azure-functions.yaml | 9 + microsite/static/img/azurefunctions-icon.svg | 1 + packages/app/package.json | 1 + packages/backend/package.json | 1 + plugins/azure-functions-backend/.eslintrc.js | 1 + plugins/azure-functions-backend/README.md | 82 +++ plugins/azure-functions-backend/package.json | 46 ++ plugins/azure-functions-backend/schema.d.ts | 19 + .../src/api/AzureWebManagementApi.ts | 85 +++ .../azure-functions-backend/src/api/index.ts | 18 + .../azure-functions-backend/src/api/types.ts | 15 + plugins/azure-functions-backend/src/index.ts | 18 + plugins/azure-functions-backend/src/run.ts | 33 + .../src/service/router.ts | 46 ++ .../src/service/standaloneServer.ts | 53 ++ .../azure-functions-backend/src/setupTests.ts | 17 + plugins/azure-functions/.eslintrc.js | 1 + plugins/azure-functions/README.md | 74 +++ plugins/azure-functions/dev/index.tsx | 6 + .../azure-functions/docs/functions-table.png | Bin 0 -> 58539 bytes plugins/azure-functions/package.json | 53 ++ .../src/api/AzureFunctionsApi.ts | 30 + .../src/api/AzureFunctionsBackendClient.ts | 53 ++ plugins/azure-functions/src/api/index.ts | 19 + plugins/azure-functions/src/api/types.ts | 10 + .../AzureFunctionsOverview.test.tsx | 91 +++ .../AzureFunctionsOverview.tsx | 154 +++++ .../src/components/ErrorBoundary.tsx | 58 ++ .../azure-functions/src/hooks/useFunctions.ts | 71 +++ .../src/hooks/useServiceEntityAnnotations.ts | 30 + plugins/azure-functions/src/index.ts | 3 + plugins/azure-functions/src/mocks/mocks.ts | 243 ++++++++ plugins/azure-functions/src/plugin.test.ts | 7 + plugins/azure-functions/src/plugin.ts | 43 ++ plugins/azure-functions/src/routes.ts | 5 + plugins/azure-functions/src/setupTests.ts | 2 + yarn.lock | 585 +++++++++++++++++- 38 files changed, 1975 insertions(+), 10 deletions(-) create mode 100644 microsite/data/plugins/azure-functions.yaml create mode 100644 microsite/static/img/azurefunctions-icon.svg create mode 100644 plugins/azure-functions-backend/.eslintrc.js create mode 100644 plugins/azure-functions-backend/README.md create mode 100644 plugins/azure-functions-backend/package.json create mode 100644 plugins/azure-functions-backend/schema.d.ts create mode 100644 plugins/azure-functions-backend/src/api/AzureWebManagementApi.ts create mode 100644 plugins/azure-functions-backend/src/api/index.ts create mode 100644 plugins/azure-functions-backend/src/api/types.ts create mode 100644 plugins/azure-functions-backend/src/index.ts create mode 100644 plugins/azure-functions-backend/src/run.ts create mode 100644 plugins/azure-functions-backend/src/service/router.ts create mode 100644 plugins/azure-functions-backend/src/service/standaloneServer.ts create mode 100644 plugins/azure-functions-backend/src/setupTests.ts create mode 100644 plugins/azure-functions/.eslintrc.js create mode 100644 plugins/azure-functions/README.md create mode 100644 plugins/azure-functions/dev/index.tsx create mode 100644 plugins/azure-functions/docs/functions-table.png create mode 100644 plugins/azure-functions/package.json create mode 100644 plugins/azure-functions/src/api/AzureFunctionsApi.ts create mode 100644 plugins/azure-functions/src/api/AzureFunctionsBackendClient.ts create mode 100644 plugins/azure-functions/src/api/index.ts create mode 100644 plugins/azure-functions/src/api/types.ts create mode 100644 plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.test.tsx create mode 100644 plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.tsx create mode 100644 plugins/azure-functions/src/components/ErrorBoundary.tsx create mode 100644 plugins/azure-functions/src/hooks/useFunctions.ts create mode 100644 plugins/azure-functions/src/hooks/useServiceEntityAnnotations.ts create mode 100644 plugins/azure-functions/src/index.ts create mode 100644 plugins/azure-functions/src/mocks/mocks.ts create mode 100644 plugins/azure-functions/src/plugin.test.ts create mode 100644 plugins/azure-functions/src/plugin.ts create mode 100644 plugins/azure-functions/src/routes.ts create mode 100644 plugins/azure-functions/src/setupTests.ts diff --git a/.changeset/pre.json b/.changeset/pre.json index 96664632fd..552d1f96d3 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -52,6 +52,8 @@ "@backstage/plugin-azure-devops": "0.1.24", "@backstage/plugin-azure-devops-backend": "0.3.14", "@backstage/plugin-azure-devops-common": "0.2.4", + "@backstage/plugin-azure-functions": "0.1.0", + "@backstage/plugin-azure-functions-backend": "0.1.0", "@backstage/plugin-badges": "0.2.32", "@backstage/plugin-badges-backend": "0.1.29", "@backstage/plugin-bazaar": "0.1.23", diff --git a/microsite/data/plugins/azure-functions.yaml b/microsite/data/plugins/azure-functions.yaml new file mode 100644 index 0000000000..883a893418 --- /dev/null +++ b/microsite/data/plugins/azure-functions.yaml @@ -0,0 +1,9 @@ +--- +title: Azure Functions +author: FRISS +authorUrl: https://friss.com +category: Infrastructure +description: View Azure Functions for your components in Backstage. +documentation: https://github.com/backstage/backstage/tree/master/plugins/azure-functions +iconUrl: img/azurefunctions-icon.svg +npmPackageName: '@backstage/plugin-azure-functions' diff --git a/microsite/static/img/azurefunctions-icon.svg b/microsite/static/img/azurefunctions-icon.svg new file mode 100644 index 0000000000..0c2435bf42 --- /dev/null +++ b/microsite/static/img/azurefunctions-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index e9c9632d81..1872b728a9 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -19,6 +19,7 @@ "@backstage/plugin-apache-airflow": "^0.2.2-next.2", "@backstage/plugin-api-docs": "^0.8.9-next.2", "@backstage/plugin-azure-devops": "^0.2.0-next.2", + "@backstage/plugin-azure-functions": "^0.1.0", "@backstage/plugin-badges": "^0.2.33-next.2", "@backstage/plugin-catalog-common": "^1.0.6-next.0", "@backstage/plugin-catalog-graph": "^0.2.21-next.1", diff --git a/packages/backend/package.json b/packages/backend/package.json index 5518b9d586..23a63226cf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -36,6 +36,7 @@ "@backstage/plugin-auth-backend": "^0.16.0-next.2", "@backstage/plugin-auth-node": "^0.2.5-next.2", "@backstage/plugin-azure-devops-backend": "^0.3.15-next.1", + "@backstage/plugin-azure-functions-backend": "^0.1.0", "@backstage/plugin-badges-backend": "^0.1.30-next.0", "@backstage/plugin-catalog-backend": "^1.4.0-next.2", "@backstage/plugin-code-coverage-backend": "^0.2.2-next.1", diff --git a/plugins/azure-functions-backend/.eslintrc.js b/plugins/azure-functions-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/azure-functions-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-functions-backend/README.md b/plugins/azure-functions-backend/README.md new file mode 100644 index 0000000000..5bfc6a1062 --- /dev/null +++ b/plugins/azure-functions-backend/README.md @@ -0,0 +1,82 @@ +# Azure Functions Backend + +Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries. + +*Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)* + +## Setup + +The following sections will help you get the Azure Functions Backend plugin setup and running. + +### Configuration + +The Azure Functions plugin requires the following YAML to be added to your app-config.yaml: + +```yaml +azureFunctions: + domain: + tenantId: + clientId: + clientSecret: + allowedSubscriptions: + - id: + name: +``` + +Configuration Details: + +- `domain` can be found by visiting the [Directories + Subscriptions settings page](https://portal.azure.com/#settings/directory). Alternatively you can inspect the [Azure home](https://portal.azure.com/#home) URL - `https://portal.azure.com/#@/`. +- `tenantId` can be found by visiting [Azure Directory Overview page](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade). +- `clientId` and `clientSecret` can be the same values you used for [Azure DevOps Backend](https://github.com/backstage/backstage/tree/master/plugins/azure-devops-backend) or [Azure Integration](https://backstage.io/docs/integrations/azure/org#app-registration) as long as this App Registration has permissions to read your function apps. +- `allowedSubscriptions` is an array of `id` and `name` that will be used to iterate over and look for the specified functions' app. `id` and `name` can be found the [Subscriptions page](https://portal.azure.com/#view/Microsoft_Azure_Billing/SubscriptionsBlade). + +### Integrating + +Here's how to get the backend plugin up and running: + +1. First we need to add the `@backstage/plugin-azure-functions-backend` package to your backend: + + ```sh + # From the Backstage root directory + cd packages/backend + yarn add @backstage/plugin-azure-functions-backend + ``` + +2. Then we will create a new file named `packages/backend/src/plugins/azure-functions.ts`, and add the following to it: + + ```ts + import { createRouter, AzureWebManagementApi } from '@backstage/plugin-azure-functions-backend'; + import { Router } from 'express'; + import { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + return await createRouter({ + logger: env.logger, + azureWebManagementApi: AzureWebManagementApi.fromConfig(env.config) + }); + } + ``` + +3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: + + ```ts + import azureFunctions from './plugins/azure-functions'; + + // Removed for clairty... + + async function main() { + // ... + // Add this line under the other lines that follow the useHotMemoize pattern + const azureFunctionsEnv = useHotMemoize(module, () => createEnv('azureFunctions')); + + // ... + // Insert this line under the other lines that add their routers to apiRouter in the same way + apiRouter.use('/azure-functions', await azureFunctions(azureFunctionsEnv)); + } + ``` + +4. Now run `yarn start-backend` from the repo root. + +5. Finally, open `http://localhost:7007/api/azure-functions/health` in a browser, it should return `{"status":"ok"}`. \ No newline at end of file diff --git a/plugins/azure-functions-backend/package.json b/plugins/azure-functions-backend/package.json new file mode 100644 index 0000000000..3a1b83ec34 --- /dev/null +++ b/plugins/azure-functions-backend/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-azure-functions-backend", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@azure/arm-appservice": "^13.0.0", + "@azure/identity": "^2.1.0", + "@backstage/backend-common": "^0.15.0", + "@backstage/config": "^1.0.1", + "@types/express": "*", + "express": "^4.17.1", + "express-promise-router": "^4.1.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.18.0", + "@types/supertest": "^2.0.8", + "msw": "^0.44.0", + "supertest": "^4.0.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-functions-backend/schema.d.ts b/plugins/azure-functions-backend/schema.d.ts new file mode 100644 index 0000000000..a86aaad1a3 --- /dev/null +++ b/plugins/azure-functions-backend/schema.d.ts @@ -0,0 +1,19 @@ +export interface Config { + azureFunctions: { + /** @visibility backend */ + tenantId: string; + /** @visibility backend */ + clientId: string; + /** @visibility secret */ + clientSecret: string; + /** @visibility backend */ + domain: string; + /** @visibility backend */ + allowedSubscriptions: [{ + /** @visibility backend */ + name: string; + /** @visibility backend */ + id: string; + }] + }; +} \ No newline at end of file diff --git a/plugins/azure-functions-backend/src/api/AzureWebManagementApi.ts b/plugins/azure-functions-backend/src/api/AzureWebManagementApi.ts new file mode 100644 index 0000000000..bc9dbbce12 --- /dev/null +++ b/plugins/azure-functions-backend/src/api/AzureWebManagementApi.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2022 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 { Config } from '@backstage/config'; +import { ClientSecretCredential } from '@azure/identity'; +import { WebSiteManagementClient } from '@azure/arm-appservice'; +import { AzureFunctionsAllowedSubscriptionsConfig, FunctionsData } from './types'; + +export class AzureFunctionsConfig { + constructor(public readonly tenantId: string, public readonly clientId: string, public readonly clientSecret: string, public readonly domain: string, public readonly allowedSubscriptions: AzureFunctionsAllowedSubscriptionsConfig[]) { } + + static fromConfig(config: Config): AzureFunctionsConfig { + const azfConfig = config.getConfig('azureFunctions'); + + return new AzureFunctionsConfig( + azfConfig.getString('tenantId'), + azfConfig.getString('clientId'), + azfConfig.getString('clientSecret'), + azfConfig.getString('domain'), + azfConfig.getConfigArray('allowedSubscriptions').map(as => ({ id: as.getString('id'), name: as.getString('name') })) + ) + } +} + +export class AzureWebManagementApi { + private readonly baseHref = (domain: string) => `https://portal.azure.com/#@${domain}/resource`; + private readonly clients: WebSiteManagementClient[] = []; + + constructor(private readonly config: AzureFunctionsConfig) { + const creds = new ClientSecretCredential(config.tenantId, config.clientId, config.clientSecret); + for (const subscription of config.allowedSubscriptions) { + if (!this.clients.some(c => c.subscriptionId == subscription.id)) { + this.clients.push(new WebSiteManagementClient(creds, subscription.id)); + } + } + } + + static fromConfig(config: Config): AzureWebManagementApi { + return new AzureWebManagementApi(AzureFunctionsConfig.fromConfig(config)); + } + + async list({ + functionName, + }: { + functionName: string; + }): Promise { + const results = []; + for (const client of this.clients) { + try { + for await (const webApp of client.webApps.list()) { + if (!webApp.name!.startsWith(functionName)) { + continue; + } + const v = webApp!; + results.push({ + href: `${this.baseHref(this.config.domain)}${v.id!}`, + logstreamHref: `${this.baseHref(this.config.domain)}${v.id!}/logStream`, + functionName: v.name!, + location: v.location!, + lastModifiedDate: v.lastModifiedTimeUtc!, + usageState: v.usageState!, + state: v.state!, + containerSize: v.containerSize! + }) + } + } catch (ex) { + console.log(ex); + } + } + return results; + } +} \ No newline at end of file diff --git a/plugins/azure-functions-backend/src/api/index.ts b/plugins/azure-functions-backend/src/api/index.ts new file mode 100644 index 0000000000..db2bb3de27 --- /dev/null +++ b/plugins/azure-functions-backend/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 { AzureWebManagementApi, AzureFunctionsConfig } from './AzureWebManagementApi' +export * from './types' \ No newline at end of file diff --git a/plugins/azure-functions-backend/src/api/types.ts b/plugins/azure-functions-backend/src/api/types.ts new file mode 100644 index 0000000000..6f6963a0af --- /dev/null +++ b/plugins/azure-functions-backend/src/api/types.ts @@ -0,0 +1,15 @@ +export interface AzureFunctionsAllowedSubscriptionsConfig { + name: string; + id: string; +} + +export type FunctionsData = { + href: string; + logstreamHref: string; + functionName: string; + location: string; + state: string; + usageState: string; + containerSize: number; + lastModifiedDate: Date; +}; \ No newline at end of file diff --git a/plugins/azure-functions-backend/src/index.ts b/plugins/azure-functions-backend/src/index.ts new file mode 100644 index 0000000000..0e8cedac61 --- /dev/null +++ b/plugins/azure-functions-backend/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 * from './service/router'; +export * from './api'; \ No newline at end of file diff --git a/plugins/azure-functions-backend/src/run.ts b/plugins/azure-functions-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/azure-functions-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/azure-functions-backend/src/service/router.ts b/plugins/azure-functions-backend/src/service/router.ts new file mode 100644 index 0000000000..ac07579a7c --- /dev/null +++ b/plugins/azure-functions-backend/src/service/router.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +import { AzureWebManagementApi } from '../api'; + +export interface RouterOptions { + logger: Logger; + azureWebManagementApi: AzureWebManagementApi; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger, azureWebManagementApi } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.get('/get', async (request, response) => { + response.json(await azureWebManagementApi.list({ functionName: request.query['functionName']!.toString() })) + }); + router.use(errorHandler()); + return router; +} diff --git a/plugins/azure-functions-backend/src/service/standaloneServer.ts b/plugins/azure-functions-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..12a76b2d44 --- /dev/null +++ b/plugins/azure-functions-backend/src/service/standaloneServer.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 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 { createServiceBuilder, loadBackendConfig } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { AzureWebManagementApi } from '../api'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'azure-functions-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }) + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + azureWebManagementApi: AzureWebManagementApi.fromConfig(config) + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/azure-functions-backend', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/azure-functions-backend/src/setupTests.ts b/plugins/azure-functions-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/azure-functions-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 {}; diff --git a/plugins/azure-functions/.eslintrc.js b/plugins/azure-functions/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/azure-functions/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/azure-functions/README.md b/plugins/azure-functions/README.md new file mode 100644 index 0000000000..77d08ee5cf --- /dev/null +++ b/plugins/azure-functions/README.md @@ -0,0 +1,74 @@ +# Azure Functions Plugin + +![preview of Azure Functions table](docs/functions-table.png) + +*Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)* + +## Features + +- Azure Functions overview table + +## Plugin Setup + +The following sections will help you get the Azure Functions plugin setup and running + +### Azure Functions Backend + +You need to set up the Azure Functions backend plugin before you move forward with any of these steps if you haven't already. + +### Entity Annotation + +To be able to use the Azure Functions plugin you need to add the following annotation to any entities you want to use it with: + +```yaml +portal.azure.com/functions-name: +``` + +`` can be an exact or partial name for the functions' app. When using a partial name, it's important that the value here matches the **start** of the functions name. + +Example of Partial Matching: + +Let's say you have a number of functions apps, spread out over different regions (and possibly different subscriptions), and they follow a naming convention: + +``` +func-testapp-eu +func-testapp-ca +func-testapp-us +``` + +The annotation you will use to have the three functions' app appear in the overview table would look like this: + +```yaml +portal.azure.com/functions-name: func-testapp +``` + +### Install the component + +1. Install the plugin in the `packages/app` directory + +```sh +yarn add @backstage/plugin-azure-functions +``` + +2. Add widget component to your Backstage instance: + +```ts +// In packages/app/src/components/catalog/EntityPage.tsx +import { EntityAzureFunctionsOverviewCard, isAzureFunctionsAvailable } from '@backstage/plugin-azure-functions'; + +... + +const serviceEntityPage = ( + + //... + Boolean(isAzureFunctionsAvailable(e))} path="/azure-functions" title="Azure Functions"> + + + //... + +); +``` + +## Roadmap + +- [ ] Metrics \ No newline at end of file diff --git a/plugins/azure-functions/dev/index.tsx b/plugins/azure-functions/dev/index.tsx new file mode 100644 index 0000000000..a46aa4c67b --- /dev/null +++ b/plugins/azure-functions/dev/index.tsx @@ -0,0 +1,6 @@ +import { createDevApp } from '@backstage/dev-utils'; +import { azureFunctionsPlugin } from '../src/plugin'; + +createDevApp() + .registerPlugin(azureFunctionsPlugin) + .render(); diff --git a/plugins/azure-functions/docs/functions-table.png b/plugins/azure-functions/docs/functions-table.png new file mode 100644 index 0000000000000000000000000000000000000000..d8603e8fa765cf5c05b35a3c0cba7c3eb155f102 GIT binary patch literal 58539 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 o{jPTvzopr07v4>8vp({ + id: 'plugin.azurefunctions.service', +}); + +export type AzureFunctionsApi = { + list: ({ + functionName, + }: { + functionName: string; + }) => Promise; +}; diff --git a/plugins/azure-functions/src/api/AzureFunctionsBackendClient.ts b/plugins/azure-functions/src/api/AzureFunctionsBackendClient.ts new file mode 100644 index 0000000000..e7b5506c44 --- /dev/null +++ b/plugins/azure-functions/src/api/AzureFunctionsBackendClient.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 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 { AzureFunctionsApi } from './AzureFunctionsApi'; +import { FunctionsData } from './types'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; + +export class AzureFunctionsBackendClient implements AzureFunctionsApi { + + private readonly identityApi: IdentityApi; + private readonly discoveryApi: DiscoveryApi; + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + async list({ + functionName, + }: { + functionName: string; + }): Promise { + try { + const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/get?functionName=${functionName}`; + const { token: idToken } = await this.identityApi.getCredentials(); + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + ...(idToken && { Authorization: `Bearer ${idToken}` }), + } + }); + return await response.json(); + } catch (e: any) { + throw new Error('MissingAzureBackendException') + } + } +} diff --git a/plugins/azure-functions/src/api/index.ts b/plugins/azure-functions/src/api/index.ts new file mode 100644 index 0000000000..14bb548361 --- /dev/null +++ b/plugins/azure-functions/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2022 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 * from './AzureFunctionsApi'; +export * from './AzureFunctionsBackendClient'; +export * from './types'; \ No newline at end of file diff --git a/plugins/azure-functions/src/api/types.ts b/plugins/azure-functions/src/api/types.ts new file mode 100644 index 0000000000..b3c2a2bd26 --- /dev/null +++ b/plugins/azure-functions/src/api/types.ts @@ -0,0 +1,10 @@ +export type FunctionsData = { + href: string; + logstreamHref: string; + functionName: string; + location: string; + state: string; + usageState: string; + containerSize: number; + lastModifiedDate: Date; +}; \ No newline at end of file diff --git a/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.test.tsx b/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.test.tsx new file mode 100644 index 0000000000..aef10a113e --- /dev/null +++ b/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright 2022 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'; +import { render } from '@testing-library/react'; +import { + AnyApiRef, + configApiRef, + errorApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { rest } from 'msw'; +import { + setupRequestMockHandlers, + TestApiProvider, +} from '@backstage/test-utils'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { setupServer } from 'msw/node'; +import { + entityMock, + functionResponseMock, +} from '../../mocks/mocks'; +import { azureFunctionsApiRef, AzureFunctionsOverviewWidget } from '../..'; + +const errorApiMock = { post: jest.fn(), error$: jest.fn() }; +const identityApiMock = (getCredentials: any) => ({ + signOut: jest.fn(), + getProfileInfo: jest.fn(), + getBackstageIdentity: jest.fn(), + getCredentials, +}); +const azureFunctionsApiMock = {}; + +const config = { + getString: (_: string) => 'https://test-url', +}; + +const apis: [AnyApiRef, Partial][] = [ + [errorApiRef, errorApiMock], + [configApiRef, config], + [azureFunctionsApiRef, azureFunctionsApiMock], + [identityApiRef, identityApiMock], +]; + +describe('AzureFunctionsOverviewWidget', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(() => { + worker.use( + rest.get( + 'https://portal.azure.com/#@test/resource/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mock_rg/providers/Microsoft.Web/sites/func-mock', + (_, res, ctx) => res(ctx.json(functionResponseMock)), + ), + ); + }); + + it('should display an overview table with the data from the requests', async () => { + const rendered = render( + + + + + , + ); + expect( + await rendered.findByText(functionResponseMock.name), + ).toBeInTheDocument(); + expect( + await rendered.findByText(functionResponseMock.properties.state), + ).toBeInTheDocument(); + expect( + await rendered.findByText( + new Date(functionResponseMock.properties.lastModifiedTimeUtc).toUTCString(), + ), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.tsx b/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.tsx new file mode 100644 index 0000000000..17d190e1a9 --- /dev/null +++ b/plugins/azure-functions/src/components/AzureFunctionsOverview/AzureFunctionsOverview.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2022 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'; +import { + Box, + Card, + Link, + LinearProgress +} from '@material-ui/core'; +import { Entity } from '@backstage/catalog-model'; +import { useFunctions } from '../../hooks/useFunctions'; +import { FunctionsData } from '../../api/types'; +import { + AZURE_FUNCTIONS_ANNOTATION, + useServiceEntityAnnotations, +} from '../../hooks/useServiceEntityAnnotations'; +import { MissingAnnotationEmptyState, Table, TableColumn } from '@backstage/core-components'; +import { FlashOn } from '@material-ui/icons' +import ErrorBoundary from '../ErrorBoundary'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +type States = 'Waiting' | 'Running' | 'Paused' | 'Failed'; + +const State = ({ value }: { value: States }) => { + const colorMap = { + Waiting: '#dcbc21', + Running: 'green', + Paused: 'black', + Failed: 'red', + }; + return ( + + + {value} + + ); +}; + + + +type FunctionTableProps = { + data: FunctionsData[]; + loading: boolean; +}; + +const DEFAULT_COLUMNS: TableColumn[] = [ + { + title: 'name', + highlight: true, + render: (func: FunctionsData) => { + return ({func.functionName}) + }, + }, + { + title: 'location', + render: (func: FunctionsData) => func.location ?? 'unknown', + }, + { + title: 'status', + render: (func: FunctionsData) => , + }, + { + title: 'last modified', + render: (func: FunctionsData) => new Date(func.lastModifiedDate).toUTCString(), + }, + { + title: 'logs', + align: 'right', + render: (func: FunctionsData) => { + return (View Logs) + }, + }, +]; + +const OverviewComponent = ({ data, loading }: FunctionTableProps) => { + const columns: TableColumn[] = [...DEFAULT_COLUMNS]; + const tableStyle = { + minWidth: '0', + width: '100%', + }; + + return ( + + + + + Azure Functions + + } + options={{ paging: true, search: false, pageSize: 10 }} + data={data} + emptyContent={ + + } + isLoading={loading} + columns={columns} + /> + + ); +}; + +export const isAzureFunctionsAvailable = (entity: Entity) => + entity?.metadata.annotations?.[AZURE_FUNCTIONS_ANNOTATION]; + +const AzureFunctionsOverview = ({ entity }: { entity: Entity }) => { + const { functionsName } = useServiceEntityAnnotations(entity); + + const [functionsData] = useFunctions({ + functionsName + }); + + return ( + <>{} + ); +}; + +export const AzureFunctionsOverviewWidget = () => { + const { entity } = useEntity(); + + if (!isAzureFunctionsAvailable(entity)) { + return (); + } + + return ( + + + + ) +}; diff --git a/plugins/azure-functions/src/components/ErrorBoundary.tsx b/plugins/azure-functions/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..01aede226b --- /dev/null +++ b/plugins/azure-functions/src/components/ErrorBoundary.tsx @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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, { Component } from 'react'; +import Alert from '@material-ui/lab/Alert'; + +interface Props {} +interface MyProps {} + +interface MyState { + hasError: boolean; +} + +export default class ErrorBoundary extends Component { + static getDerivedStateFromError() { + // Update state so the next render will show the fallback UI. + return { hasError: true }; + } + + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + render() { + if (this.state.hasError) { + // You can render any custom fallback UI + return ( + + Something went wrong. Please make sure that you installed: + + + @backstage/plugin-azure-functions-backend + + + + ); + } + + return this.props.children; + } +} diff --git a/plugins/azure-functions/src/hooks/useFunctions.ts b/plugins/azure-functions/src/hooks/useFunctions.ts new file mode 100644 index 0000000000..a5a9c11f98 --- /dev/null +++ b/plugins/azure-functions/src/hooks/useFunctions.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2022 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 { useAsyncRetry } from 'react-use'; +import { + useApi, + errorApiRef, +} from '@backstage/core-plugin-api'; +import { FunctionsData } from '../api/types'; +import { azureFunctionsApiRef } from '../api'; +import { useCallback } from 'react'; + +export function useFunctions({ + functionsName +}: { + functionsName: string; +}) { + const azureFunctionsApi = useApi(azureFunctionsApiRef); + const errorApi = useApi(errorApiRef); + + const list = useCallback( + async () => { + return await azureFunctionsApi.list({ + functionName: functionsName, + }); + }, + [functionsName], // eslint-disable-line react-hooks/exhaustive-deps + ); + const { + loading, + value: data, + error, + retry, + } = useAsyncRetry(async () => { + try { + const azureFunction = await list(); + return azureFunction; + } catch (e) { + if (e instanceof Error) { + if (e?.message === 'MissingAzureBackendException') { + errorApi.post(new Error('Please add azure-functions-backend plugin')); + return null; + } + errorApi.post(e); + } + return null; + } + }, []); + + return [ + { + loading, + data, + error, + retry, + }, + ] as const; +} diff --git a/plugins/azure-functions/src/hooks/useServiceEntityAnnotations.ts b/plugins/azure-functions/src/hooks/useServiceEntityAnnotations.ts new file mode 100644 index 0000000000..6bbe7f5f70 --- /dev/null +++ b/plugins/azure-functions/src/hooks/useServiceEntityAnnotations.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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 { Entity } from '@backstage/catalog-model'; + +export const AZURE_FUNCTIONS_ANNOTATION = 'portal.azure.com/functions-name'; +export const useServiceEntityAnnotations = (entity: Entity) => { + const functionsName = + entity?.metadata.annotations?.[AZURE_FUNCTIONS_ANNOTATION] ?? ''; + const projectName = + entity?.metadata.annotations?.['dev.azure.com/project-repo'] ?? ''; + + return { + projectName, + functionsName + }; +}; diff --git a/plugins/azure-functions/src/index.ts b/plugins/azure-functions/src/index.ts new file mode 100644 index 0000000000..6b7079ad36 --- /dev/null +++ b/plugins/azure-functions/src/index.ts @@ -0,0 +1,3 @@ +export * from './plugin'; +export * from './api'; +export * from './components/AzureFunctionsOverview/AzureFunctionsOverview'; \ No newline at end of file diff --git a/plugins/azure-functions/src/mocks/mocks.ts b/plugins/azure-functions/src/mocks/mocks.ts new file mode 100644 index 0000000000..487a4b1f5d --- /dev/null +++ b/plugins/azure-functions/src/mocks/mocks.ts @@ -0,0 +1,243 @@ +export const entityMock = { + metadata: { + namespace: 'default', + annotations: { + 'portal.azure.com/functions-name': 'func-mock', + }, + name: 'sample-azure-function-service', + description: + 'A service for testing Backstage functionality.', + uid: 'c009b513-d053-4b3f-9429-8433a145e943', + }, + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + spec: { + type: 'service', + owner: 'dev_group@example.com', + lifecycle: 'experimental', + }, +}; + +// https://management.azure.com/subscriptions/{{subscriptionId}}/resourceGroups/{{resourceGroup}}/providers/Microsoft.Web/sites/{{functionsName}}?api-version=2022-03-01 +export const functionResponseMock = { + id: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg_mock/providers/Microsoft.Web/sites/func-mock", + name: "func-mock", + type: "Microsoft.Web/sites", + kind: "functionapp", + location: "West Europe", + tags: {}, + properties: { + name: "func-mock", + state: "Running", + hostNames: [ + "func-mock.azurewebsites.net" + ], + webSpace: "rg_mock-WestEuropewebspace", + selfLink: "https://mockurl.api.azurewebsites.windows.net:454/subscriptions/00000000-0000-0000-0000-000000000000/webspaces/rg_mock-WestEuropewebspace/sites/func-mock", + repositorySiteName: "func-mock", + owner: null, + usageState: "Normal", + enabled: true, + adminEnabled: true, + enabledHostNames: [ + "func-mock.azurewebsites.net", + "func-mock.scm.azurewebsites.net" + ], + siteProperties: { + metadata: null, + properties: [ + { + name: "LinuxFxVersion", + value: "" + }, + { + name: "WindowsFxVersion", + value: null + } + ], + appSettings: null + }, + availabilityState: "Normal", + sslCertificates: null, + csrs: [], + cers: null, + siteMode: null, + hostNameSslStates: [ + { + name: "func-mock.azurewebsites.net", + sslState: "Disabled", + ipBasedSslResult: null, + virtualIP: null, + thumbprint: null, + toUpdate: null, + toUpdateIpBasedSsl: null, + ipBasedSslState: "NotConfigured", + hostType: "Standard" + }, + { + name: "func-mock.scm.azurewebsites.net", + sslState: "Disabled", + ipBasedSslResult: null, + virtualIP: null, + thumbprint: null, + toUpdate: null, + toUpdateIpBasedSsl: null, + ipBasedSslState: "NotConfigured", + hostType: "Repository" + } + ], + computeMode: null, + serverFarm: null, + serverFarmId: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg_mock/providers/Microsoft.Web/serverfarms/plan-mock", + reserved: false, + isXenon: false, + hyperV: false, + lastModifiedTimeUtc: "2022-09-02T11:09:58.9033333", + storageRecoveryDefaultState: "Running", + contentAvailabilityState: "Normal", + runtimeAvailabilityState: "Normal", + secretsCollection: [], + vnetRouteAllEnabled: false, + containerAllocationSubnet: null, + useContainerLocalhostBindings: null, + vnetImagePullEnabled: false, + vnetContentShareEnabled: false, + siteConfig: { + numberOfWorkers: 1, + defaultDocuments: null, + netFrameworkVersion: null, + phpVersion: null, + pythonVersion: null, + nodeVersion: null, + powerShellVersion: null, + linuxFxVersion: "", + windowsFxVersion: null, + requestTracingEnabled: null, + remoteDebuggingEnabled: null, + remoteDebuggingVersion: null, + httpLoggingEnabled: null, + azureMonitorLogCategories: null, + acrUseManagedIdentityCreds: false, + acrUserManagedIdentityID: null, + logsDirectorySizeLimit: null, + detailedErrorLoggingEnabled: null, + publishingUsername: null, + publishingPassword: null, + appSettings: null, + metadata: null, + connectionStrings: null, + machineKey: null, + handlerMappings: null, + documentRoot: null, + scmType: null, + use32BitWorkerProcess: null, + webSocketsEnabled: null, + alwaysOn: true, + javaVersion: null, + javaContainer: null, + javaContainerVersion: null, + appCommandLine: null, + managedPipelineMode: null, + virtualApplications: null, + winAuthAdminState: null, + winAuthTenantState: null, + customAppPoolIdentityAdminState: null, + customAppPoolIdentityTenantState: null, + runtimeADUser: null, + runtimeADUserPassword: null, + loadBalancing: null, + routingRules: null, + experiments: null, + limits: null, + autoHealEnabled: null, + autoHealRules: null, + tracingOptions: null, + vnetName: null, + vnetRouteAllEnabled: null, + vnetPrivatePortsCount: null, + publicNetworkAccess: null, + cors: null, + push: null, + apiDefinition: null, + apiManagementConfig: null, + autoSwapSlotName: null, + localMySqlEnabled: null, + managedServiceIdentityId: null, + xManagedServiceIdentityId: null, + keyVaultReferenceIdentity: null, + ipSecurityRestrictions: null, + ipSecurityRestrictionsDefaultAction: null, + scmIpSecurityRestrictions: null, + scmIpSecurityRestrictionsDefaultAction: null, + scmIpSecurityRestrictionsUseMain: null, + http20Enabled: false, + minTlsVersion: null, + minTlsCipherSuite: null, + supportedTlsCipherSuites: null, + scmMinTlsVersion: null, + ftpsState: null, + preWarmedInstanceCount: null, + functionAppScaleLimit: 0, + elasticWebAppScaleLimit: null, + healthCheckPath: null, + fileChangeAuditEnabled: null, + functionsRuntimeScaleMonitoringEnabled: null, + websiteTimeZone: null, + minimumElasticInstanceCount: 1, + azureStorageAccounts: null, + http20ProxyFlag: null, + sitePort: null, + antivirusScanEnabled: null, + storageType: null + }, + deploymentId: "func-mock", + slotName: null, + trafficManagerHostNames: null, + sku: "Standard", + scmSiteAlsoStopped: false, + targetSwapSlot: null, + hostingEnvironment: null, + hostingEnvironmentProfile: null, + clientAffinityEnabled: false, + clientCertEnabled: false, + clientCertMode: "Required", + clientCertExclusionPaths: null, + hostNamesDisabled: false, + domainVerificationIdentifiers: null, + customDomainVerificationId: "", + kind: "functionapp", + inboundIpAddress: "", + possibleInboundIpAddresses: "", + ftpUsername: "func-mock\\$func-mock", + ftpsHostName: "ftps://mockurl.ftp.azurewebsites.windows.net/site/wwwroot", + outboundIpAddresses: "", + possibleOutboundIpAddresses: "", + containerSize: 1536, + dailyMemoryTimeQuota: 0, + suspendedTill: null, + siteDisabledReason: 0, + functionExecutionUnitsCache: null, + maxNumberOfWorkers: null, + homeStamp: "mockurl", + cloningInfo: null, + hostingEnvironmentId: null, + tags: {}, + resourceGroup: "rg_mock", + defaultHostName: "func-mock.azurewebsites.net", + slotSwapStatus: null, + httpsOnly: false, + redundancyMode: "None", + inProgressOperationId: null, + geoDistributions: null, + privateEndpointConnections: [], + publicNetworkAccess: null, + buildVersion: null, + targetBuildVersion: null, + migrationState: null, + eligibleLogCategories: "FunctionAppLogs", + storageAccountRequired: false, + virtualNetworkSubnetId: null, + keyVaultReferenceIdentity: "SystemAssigned", + privateLinkIdentifiers: null + } +}; diff --git a/plugins/azure-functions/src/plugin.test.ts b/plugins/azure-functions/src/plugin.test.ts new file mode 100644 index 0000000000..84dce9eed8 --- /dev/null +++ b/plugins/azure-functions/src/plugin.test.ts @@ -0,0 +1,7 @@ +import { azureFunctionsPlugin } from './plugin'; + +describe('azure-functions', () => { + it('should export plugin', () => { + expect(azureFunctionsPlugin).toBeDefined(); + }); +}); diff --git a/plugins/azure-functions/src/plugin.ts b/plugins/azure-functions/src/plugin.ts new file mode 100644 index 0000000000..e796eebbfc --- /dev/null +++ b/plugins/azure-functions/src/plugin.ts @@ -0,0 +1,43 @@ +import { + createApiFactory, + createComponentExtension, + createPlugin, + createRouteRef, + discoveryApiRef, + identityApiRef +} from '@backstage/core-plugin-api'; +import { azureFunctionsApiRef, AzureFunctionsBackendClient } from './api'; + +export const entityContentRouteRef = createRouteRef({ + id: 'Azure Functions Entity Content', +}); + +export const azureFunctionsPlugin = createPlugin({ + id: 'azure-functions', + apis: [ + createApiFactory({ + api: azureFunctionsApiRef, + deps: { + discoveryApi: discoveryApiRef, + identityApi: identityApiRef, + }, + factory: ({ discoveryApi, identityApi }) => + new AzureFunctionsBackendClient({ discoveryApi, identityApi }), + }) + ], + routes: { + entityContent: entityContentRouteRef, + }, +}); + +export const EntityAzureFunctionsOverviewCard = azureFunctionsPlugin.provide( + createComponentExtension({ + name: 'EntityAzureFunctionsOverviewCard', + component: { + lazy: () => + import('./components/AzureFunctionsOverview/AzureFunctionsOverview').then( + m => m.AzureFunctionsOverviewWidget, + ), + }, + }), +); \ No newline at end of file diff --git a/plugins/azure-functions/src/routes.ts b/plugins/azure-functions/src/routes.ts new file mode 100644 index 0000000000..d775ceb7b0 --- /dev/null +++ b/plugins/azure-functions/src/routes.ts @@ -0,0 +1,5 @@ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'azure-functions', +}); diff --git a/plugins/azure-functions/src/setupTests.ts b/plugins/azure-functions/src/setupTests.ts new file mode 100644 index 0000000000..48c09b5346 --- /dev/null +++ b/plugins/azure-functions/src/setupTests.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 5939d370a2..5f77f67dd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -337,6 +337,21 @@ __metadata: languageName: node linkType: hard +"@azure/arm-appservice@npm:^13.0.0": + version: 13.0.0 + resolution: "@azure/arm-appservice@npm:13.0.0" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.3.0 + "@azure/core-client": ^1.5.0 + "@azure/core-lro": ^2.2.0 + "@azure/core-paging": ^1.2.0 + "@azure/core-rest-pipeline": ^1.8.0 + tslib: ^2.2.0 + checksum: 0a21b4d462ac0ba5b46561ef90559bfc2b1f5a2a8ea551ae13f95205b4aafee5b5bd5d03f52fced7687af8d5f37d5451b48b4edc8acabf977b534af1b2790dcd + languageName: node + linkType: hard + "@azure/core-asynciterator-polyfill@npm:^1.0.0": version: 1.0.0 resolution: "@azure/core-asynciterator-polyfill@npm:1.0.0" @@ -354,6 +369,16 @@ __metadata: languageName: node linkType: hard +"@azure/core-auth@npm:^1.4.0": + version: 1.4.0 + resolution: "@azure/core-auth@npm:1.4.0" + dependencies: + "@azure/abort-controller": ^1.0.0 + tslib: ^2.2.0 + checksum: 1c76c296fe911ad39fc780b033c25a92c41c5a15f011b816d42c13584f627a4dd153dfb4334900ec93eb5b006e14bdda37e8412a7697c5a9636a0abaccffad39 + languageName: node + linkType: hard + "@azure/core-client@npm:^1.4.0": version: 1.5.0 resolution: "@azure/core-client@npm:1.5.0" @@ -369,6 +394,21 @@ __metadata: languageName: node linkType: hard +"@azure/core-client@npm:^1.5.0": + version: 1.6.1 + resolution: "@azure/core-client@npm:1.6.1" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-rest-pipeline": ^1.9.1 + "@azure/core-tracing": ^1.0.0 + "@azure/core-util": ^1.0.0 + "@azure/logger": ^1.0.0 + tslib: ^2.2.0 + checksum: 400890a9b5f0c8801ff92005c3cba7bb5124634e321735406731bef33688a79f23d28b49fccdfab90814dade41a13f3d3cb99f80151a2bff17628416e811afb6 + languageName: node + linkType: hard + "@azure/core-http@npm:^2.0.0": version: 2.2.4 resolution: "@azure/core-http@npm:2.2.4" @@ -413,6 +453,15 @@ __metadata: languageName: node linkType: hard +"@azure/core-paging@npm:^1.2.0": + version: 1.3.0 + resolution: "@azure/core-paging@npm:1.3.0" + dependencies: + tslib: ^2.2.0 + checksum: 3fbf3d6474e2346f7c3ea8344a41bf41e053789efbbd29df78365e9c9ca66e143da3e5407d8c9dd5ddc82a65680185cd6f0ec851c48635776d18a6a605a98e78 + languageName: node + linkType: hard + "@azure/core-rest-pipeline@npm:^1.1.0, @azure/core-rest-pipeline@npm:^1.5.0": version: 1.5.0 resolution: "@azure/core-rest-pipeline@npm:1.5.0" @@ -430,6 +479,24 @@ __metadata: languageName: node linkType: hard +"@azure/core-rest-pipeline@npm:^1.8.0, @azure/core-rest-pipeline@npm:^1.9.1": + version: 1.9.2 + resolution: "@azure/core-rest-pipeline@npm:1.9.2" + dependencies: + "@azure/abort-controller": ^1.0.0 + "@azure/core-auth": ^1.4.0 + "@azure/core-tracing": ^1.0.1 + "@azure/core-util": ^1.0.0 + "@azure/logger": ^1.0.0 + form-data: ^4.0.0 + http-proxy-agent: ^5.0.0 + https-proxy-agent: ^5.0.0 + tslib: ^2.2.0 + uuid: ^8.3.0 + checksum: cdfdaf8bdb0778f40ecfeb8b5da0b4f8643d641b75bb5869485afd38f7c3127ab088ac2fe2980df03d28fb6715928e04560343ee6416246aebe75443c4dd2906 + languageName: node + linkType: hard + "@azure/core-tracing@npm:1.0.0-preview.13": version: 1.0.0-preview.13 resolution: "@azure/core-tracing@npm:1.0.0-preview.13" @@ -440,7 +507,7 @@ __metadata: languageName: node linkType: hard -"@azure/core-tracing@npm:^1.0.0": +"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1": version: 1.0.1 resolution: "@azure/core-tracing@npm:1.0.1" dependencies: @@ -2839,6 +2906,24 @@ __metadata: languageName: unknown linkType: soft +"@backstage/app-defaults@npm:^1.0.5": + version: 1.0.5 + resolution: "@backstage/app-defaults@npm:1.0.5" + dependencies: + "@backstage/core-app-api": ^1.0.5 + "@backstage/core-components": ^0.11.0 + "@backstage/core-plugin-api": ^1.0.5 + "@backstage/plugin-permission-react": ^0.4.4 + "@backstage/theme": ^0.2.16 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + react-router-dom: 6.0.0-beta.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + checksum: c070eaa21dbbe2d0cb46d0c6fbcce623b305d2a715a0b126166b227d0f669c1fa0375573e21ce57c468367e5a3782764fb3b7738c71e77a9d5bab70ea7e3e280 + languageName: node + linkType: hard + "@backstage/backend-app-api@^0.2.1-next.0, @backstage/backend-app-api@^0.2.1-next.1, @backstage/backend-app-api@workspace:packages/backend-app-api": version: 0.0.0-use.local resolution: "@backstage/backend-app-api@workspace:packages/backend-app-api" @@ -2941,6 +3026,66 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-common@npm:^0.15.0": + version: 0.15.0 + resolution: "@backstage/backend-common@npm:0.15.0" + dependencies: + "@backstage/cli-common": ^0.1.9 + "@backstage/config": ^1.0.1 + "@backstage/config-loader": ^1.1.3 + "@backstage/errors": ^1.1.0 + "@backstage/integration": ^1.3.0 + "@backstage/types": ^1.0.0 + "@google-cloud/storage": ^6.0.0 + "@keyv/redis": ^2.2.3 + "@manypkg/get-packages": ^1.1.3 + "@octokit/rest": ^19.0.3 + "@types/cors": ^2.8.6 + "@types/dockerode": ^3.3.0 + "@types/express": ^4.17.6 + "@types/luxon": ^3.0.0 + "@types/webpack-env": ^1.15.2 + archiver: ^5.0.2 + aws-sdk: ^2.840.0 + base64-stream: ^1.0.0 + compression: ^1.7.4 + concat-stream: ^2.0.0 + cors: ^2.8.5 + dockerode: ^3.3.1 + express: ^4.17.1 + express-promise-router: ^4.1.0 + fs-extra: 10.1.0 + git-url-parse: ^12.0.0 + helmet: ^5.0.2 + isomorphic-git: ^1.8.0 + jose: ^4.6.0 + keyv: ^4.0.3 + keyv-memcache: ^1.2.5 + knex: ^2.0.0 + lodash: ^4.17.21 + logform: ^2.3.2 + luxon: ^3.0.0 + minimatch: ^5.0.0 + minimist: ^1.2.5 + morgan: ^1.10.0 + node-abort-controller: ^3.0.1 + node-fetch: ^2.6.7 + raw-body: ^2.4.1 + selfsigned: ^2.0.0 + stoppable: ^1.1.0 + tar: ^6.1.2 + winston: ^3.2.1 + yauzl: ^2.10.0 + yn: ^4.0.0 + peerDependencies: + pg-connection-string: ^2.3.0 + peerDependenciesMeta: + pg-connection-string: + optional: true + checksum: 331e774247f79d4a041069b604a289694a920b43e579e5b4437480460ddf027a6e7106e1acc0e09b9d6c6414a2324eaea5d5d40972055e138409cbe05c2f66bd + languageName: node + linkType: hard + "@backstage/backend-defaults@^0.1.1-next.0, @backstage/backend-defaults@workspace:packages/backend-defaults": version: 0.0.0-use.local resolution: "@backstage/backend-defaults@workspace:packages/backend-defaults" @@ -3200,6 +3345,114 @@ __metadata: languageName: unknown linkType: soft +"@backstage/cli@npm:^0.18.0": + version: 0.18.1 + resolution: "@backstage/cli@npm:0.18.1" + dependencies: + "@backstage/cli-common": ^0.1.9 + "@backstage/config": ^1.0.1 + "@backstage/config-loader": ^1.1.3 + "@backstage/errors": ^1.1.0 + "@backstage/release-manifests": ^0.0.5 + "@backstage/types": ^1.0.0 + "@hot-loader/react-dom-v16": "npm:@hot-loader/react-dom@^16.0.2" + "@hot-loader/react-dom-v17": "npm:@hot-loader/react-dom@^17.0.2" + "@manypkg/get-packages": ^1.1.3 + "@octokit/request": ^6.0.0 + "@rollup/plugin-commonjs": ^22.0.0 + "@rollup/plugin-json": ^4.1.0 + "@rollup/plugin-node-resolve": ^13.0.6 + "@rollup/plugin-yaml": ^3.1.0 + "@spotify/eslint-config-base": ^14.0.0 + "@spotify/eslint-config-react": ^14.0.0 + "@spotify/eslint-config-typescript": ^14.0.0 + "@sucrase/jest-plugin": ^2.1.1 + "@sucrase/webpack-loader": ^2.0.0 + "@svgr/plugin-jsx": 6.3.x + "@svgr/plugin-svgo": 6.3.x + "@svgr/rollup": 6.3.x + "@svgr/webpack": 6.3.x + "@types/webpack-env": ^1.15.2 + "@typescript-eslint/eslint-plugin": ^5.9.0 + "@typescript-eslint/parser": ^5.9.0 + "@yarnpkg/lockfile": ^1.1.0 + "@yarnpkg/parsers": ^3.0.0-rc.4 + bfj: ^7.0.2 + buffer: ^6.0.3 + chalk: ^4.0.0 + chokidar: ^3.3.1 + commander: ^9.1.0 + css-loader: ^6.5.1 + diff: ^5.0.0 + esbuild: ^0.14.10 + esbuild-loader: ^2.18.0 + eslint: ^8.6.0 + eslint-config-prettier: ^8.3.0 + eslint-formatter-friendly: ^7.0.0 + eslint-plugin-deprecation: ^1.3.2 + eslint-plugin-import: ^2.25.4 + eslint-plugin-jest: ^26.1.2 + eslint-plugin-jsx-a11y: ^6.5.1 + eslint-plugin-monorepo: ^0.3.2 + eslint-plugin-react: ^7.28.0 + eslint-plugin-react-hooks: ^4.3.0 + eslint-webpack-plugin: ^3.1.1 + express: ^4.17.1 + fork-ts-checker-webpack-plugin: ^7.0.0-alpha.8 + fs-extra: 10.1.0 + glob: ^7.1.7 + global-agent: ^3.0.0 + handlebars: ^4.7.3 + html-webpack-plugin: ^5.3.1 + inquirer: ^8.2.0 + jest: ^27.5.1 + jest-css-modules: ^2.1.0 + jest-runtime: ^27.5.1 + jest-transform-yaml: ^1.0.0 + json-schema: ^0.4.0 + lodash: ^4.17.21 + mini-css-extract-plugin: ^2.4.2 + minimatch: 5.1.0 + node-fetch: ^2.6.7 + node-libs-browser: ^2.2.1 + npm-packlist: ^5.0.0 + ora: ^5.3.0 + postcss: ^8.1.0 + process: ^0.11.10 + react-dev-utils: ^12.0.0-next.60 + react-hot-loader: ^4.13.0 + recursive-readdir: ^2.2.2 + replace-in-file: ^6.0.0 + rollup: ^2.60.2 + rollup-plugin-dts: ^4.0.1 + rollup-plugin-esbuild: ^4.7.2 + rollup-plugin-postcss: ^4.0.0 + rollup-pluginutils: ^2.8.2 + run-script-webpack-plugin: ^0.1.0 + semver: ^7.3.2 + style-loader: ^3.3.1 + sucrase: ^3.20.2 + tar: ^6.1.2 + terser-webpack-plugin: ^5.1.3 + util: ^0.12.3 + webpack: ^5.66.0 + webpack-dev-server: ^4.7.3 + webpack-node-externals: ^3.0.0 + yaml: ^2.0.0 + yml-loader: ^2.1.0 + yn: ^4.0.0 + zod: ^3.11.6 + peerDependencies: + "@microsoft/api-extractor": ^7.21.2 + peerDependenciesMeta: + "@microsoft/api-extractor": + optional: true + bin: + backstage-cli: bin/backstage-cli + checksum: e3e0a14a87be6add3adcc9d3a3f6d34064315e4a420a2ab97401f5074a0cce96c80908ffd9687db281581b5a459c8900b2683ae686ade38b24df22535296cc3f + languageName: node + linkType: hard + "@backstage/codemods@workspace:*, @backstage/codemods@workspace:packages/codemods": version: 0.0.0-use.local resolution: "@backstage/codemods@workspace:packages/codemods" @@ -3247,6 +3500,29 @@ __metadata: languageName: unknown linkType: soft +"@backstage/config-loader@npm:^1.1.3": + version: 1.1.3 + resolution: "@backstage/config-loader@npm:1.1.3" + dependencies: + "@backstage/cli-common": ^0.1.9 + "@backstage/config": ^1.0.1 + "@backstage/errors": ^1.1.0 + "@backstage/types": ^1.0.0 + "@types/json-schema": ^7.0.6 + ajv: ^8.10.0 + chokidar: ^3.5.2 + fs-extra: 10.1.0 + json-schema: ^0.4.0 + json-schema-merge-allof: ^0.8.1 + json-schema-traverse: ^1.0.0 + node-fetch: ^2.6.7 + typescript-json-schema: ^0.54.0 + yaml: ^2.0.0 + yup: ^0.32.9 + checksum: b7467887a94977509ea1a95227521142318ea8c13f45c37959ed792668abf33e30377a79931f46891628389513c1459824fe6cb53a2dc8d5e6cb7abf57162371 + languageName: node + linkType: hard + "@backstage/config@^1.0.0, @backstage/config@^1.0.1, @backstage/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@backstage/config@workspace:packages/config" @@ -3293,6 +3569,27 @@ __metadata: languageName: unknown linkType: soft +"@backstage/core-app-api@npm:^1.0.5": + version: 1.0.5 + resolution: "@backstage/core-app-api@npm:1.0.5" + dependencies: + "@backstage/config": ^1.0.1 + "@backstage/core-plugin-api": ^1.0.5 + "@backstage/types": ^1.0.0 + "@backstage/version-bridge": ^1.0.1 + "@types/prop-types": ^15.7.3 + prop-types: ^15.7.2 + react-router-dom: 6.0.0-beta.0 + react-use: ^17.2.4 + zen-observable: ^0.8.15 + zod: ^3.11.6 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + checksum: e5cb3ff2c7aefc19406f2895eeba5a9cf3f332b14ebfe4b5c304cc4e84d76f80c027d76ffa7fc2c4b6f856e8c534f689a1fe726f54fdf0d36017d909c1727aca + languageName: node + linkType: hard + "@backstage/core-components@^0.11.1-next.1, @backstage/core-components@^0.11.1-next.2, @backstage/core-components@workspace:packages/core-components": version: 0.0.0-use.local resolution: "@backstage/core-components@workspace:packages/core-components" @@ -3518,6 +3815,37 @@ __metadata: languageName: unknown linkType: soft +"@backstage/dev-utils@npm:^1.0.4": + version: 1.0.5 + resolution: "@backstage/dev-utils@npm:1.0.5" + dependencies: + "@backstage/app-defaults": ^1.0.5 + "@backstage/catalog-model": ^1.1.0 + "@backstage/core-app-api": ^1.0.5 + "@backstage/core-components": ^0.11.0 + "@backstage/core-plugin-api": ^1.0.5 + "@backstage/integration-react": ^1.1.3 + "@backstage/plugin-catalog-react": ^1.1.3 + "@backstage/test-utils": ^1.1.3 + "@backstage/theme": ^0.2.16 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + react-hot-loader: ^4.13.0 + react-router: 6.0.0-beta.0 + react-router-dom: 6.0.0-beta.0 + react-use: ^17.2.4 + zen-observable: ^0.8.15 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-dom: ^16.13.1 || ^17.0.0 + checksum: 3d8787c91d38d9573b6084ce069c5e7fea55207b259fd9edd629ea9e2970bdc3858f680a1a637877e326af657134dfc5340545eca1b6d67fdcc2d3f703f4f665 + languageName: node + linkType: hard + "@backstage/errors@1.1.0, @backstage/errors@^1.0.0, @backstage/errors@^1.1.0, @backstage/errors@^1.1.0-next.0, @backstage/errors@workspace:packages/errors": version: 0.0.0-use.local resolution: "@backstage/errors@workspace:packages/errors" @@ -3556,7 +3884,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/integration-react@npm:^1.0.0": +"@backstage/integration-react@npm:^1.0.0, @backstage/integration-react@npm:^1.1.3": version: 1.1.3 resolution: "@backstage/integration-react@npm:1.1.3" dependencies: @@ -4074,6 +4402,54 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-azure-functions-backend@workspace:plugins/azure-functions-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-azure-functions-backend@workspace:plugins/azure-functions-backend" + dependencies: + "@azure/arm-appservice": ^13.0.0 + "@azure/identity": ^2.1.0 + "@backstage/backend-common": ^0.15.0 + "@backstage/cli": ^0.18.0 + "@backstage/config": ^1.0.1 + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.17.1 + express-promise-router: ^4.1.0 + msw: ^0.44.0 + node-fetch: ^2.6.7 + supertest: ^4.0.2 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-azure-functions@workspace:plugins/azure-functions": + version: 0.0.0-use.local + resolution: "@backstage/plugin-azure-functions@workspace:plugins/azure-functions" + dependencies: + "@backstage/cli": ^0.18.0 + "@backstage/core-app-api": ^1.0.5 + "@backstage/core-components": ^0.11.0 + "@backstage/core-plugin-api": ^1.0.5 + "@backstage/dev-utils": ^1.0.4 + "@backstage/test-utils": ^1.1.3 + "@backstage/theme": ^0.2.16 + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/jest": "*" + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^0.44.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-badges-backend@^0.1.30-next.0, @backstage/plugin-badges-backend@workspace:plugins/badges-backend": version: 0.0.0-use.local resolution: "@backstage/plugin-badges-backend@workspace:plugins/badges-backend" @@ -7400,6 +7776,15 @@ __metadata: languageName: unknown linkType: soft +"@backstage/release-manifests@npm:^0.0.5": + version: 0.0.5 + resolution: "@backstage/release-manifests@npm:0.0.5" + dependencies: + cross-fetch: ^3.1.5 + checksum: 6e7a37d11a24fdbd99e45354c1703e12be691da37df27c752b3f1422c5d75ec89e45cc4808e9a2f98a37a525f4076428ec3cba2e9cdd579f4383b7f1ccdd564b + languageName: node + linkType: hard + "@backstage/test-utils@^1.2.0-next.1, @backstage/test-utils@^1.2.0-next.2, @backstage/test-utils@workspace:packages/test-utils": version: 0.0.0-use.local resolution: "@backstage/test-utils@workspace:packages/test-utils" @@ -7430,6 +7815,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/test-utils@npm:^1.1.3": + version: 1.1.3 + resolution: "@backstage/test-utils@npm:1.1.3" + dependencies: + "@backstage/config": ^1.0.1 + "@backstage/core-app-api": ^1.0.5 + "@backstage/core-plugin-api": ^1.0.5 + "@backstage/plugin-permission-common": ^0.6.3 + "@backstage/plugin-permission-react": ^0.4.4 + "@backstage/theme": ^0.2.16 + "@backstage/types": ^1.0.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.11.2 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + cross-fetch: ^3.1.5 + react-router: 6.0.0-beta.0 + react-router-dom: 6.0.0-beta.0 + zen-observable: ^0.8.15 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + checksum: c83feac65473848691abc7d8d40fd5e0f6e179387266810fc36c806d336c640255aada09f17f6568b6bb4a48bc030eb8810195edbaf6eff5c75207daf7e7adc5 + languageName: node + linkType: hard + "@backstage/theme@^0.2.15, @backstage/theme@^0.2.16, @backstage/theme@^0.2.6, @backstage/theme@^0.2.7, @backstage/theme@^0.2.9, @backstage/theme@workspace:packages/theme": version: 0.0.0-use.local resolution: "@backstage/theme@workspace:packages/theme" @@ -9021,6 +9433,33 @@ __metadata: languageName: node linkType: hard +"@hot-loader/react-dom-v16@npm:@hot-loader/react-dom@^16.0.2": + version: 16.14.0 + resolution: "@hot-loader/react-dom@npm:16.14.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + scheduler: ^0.19.1 + peerDependencies: + react: ^16.14.0 + checksum: 9362ef1dfcea97eb833983c3cfcd3bb813ef5cd7dc3f91ad4927fabbaf05db2b81751aaa98e5c233004081771ca5d5478bd648a9a969e08eb9cf1c6a1ee0de00 + languageName: node + linkType: hard + +"@hot-loader/react-dom-v17@npm:@hot-loader/react-dom@^17.0.2": + version: 17.0.2 + resolution: "@hot-loader/react-dom@npm:17.0.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + scheduler: ^0.20.2 + peerDependencies: + react: 17.0.2 + checksum: 180de3211aff1d60fd9d2a9130ba7977e2e356b1ecd2e7c50a98e6507bb7873f0523b44fa63142f0ffdc629d2933994f4add86e1f19288941f9d223bd28f5e48 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.10.4": version: 0.10.4 resolution: "@humanwhocodes/config-array@npm:0.10.4" @@ -19016,7 +19455,7 @@ __metadata: languageName: node linkType: hard -"component-emitter@npm:^1.2.1, component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": +"component-emitter@npm:^1.2.0, component-emitter@npm:^1.2.1, component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b @@ -19402,7 +19841,7 @@ __metadata: languageName: node linkType: hard -"cookiejar@npm:^2.1.3": +"cookiejar@npm:^2.1.0, cookiejar@npm:^2.1.3": version: 2.1.3 resolution: "cookiejar@npm:2.1.3" checksum: 88259983ebc52ceb23cdacfa48762b6a518a57872eff1c7ed01d214fff5cf492e2660d7d5c04700a28f1787a76811df39e8639f8e17670b3cf94ecd86e161f07 @@ -21997,6 +22436,23 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest@npm:^26.1.2": + version: 26.9.0 + resolution: "eslint-plugin-jest@npm:26.9.0" + dependencies: + "@typescript-eslint/utils": ^5.10.0 + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 6d5fd5c95368f1ca2640389aeb7ce703d6202493c3ec6bdedb4eaca37233710508b0c75829e727765a16fd27029a466d34202bc7f2811c752038ccbbce224400 + languageName: node + linkType: hard + "eslint-plugin-jest@npm:^27.0.0": version: 27.0.2 resolution: "eslint-plugin-jest@npm:27.0.2" @@ -23431,7 +23887,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^2.3.2, form-data@npm:^2.5.0": +"form-data@npm:^2.3.1, form-data@npm:^2.3.2, form-data@npm:^2.5.0": version: 2.5.1 resolution: "form-data@npm:2.5.1" dependencies: @@ -23502,6 +23958,13 @@ __metadata: languageName: node linkType: hard +"formidable@npm:^1.2.0": + version: 1.2.6 + resolution: "formidable@npm:1.2.6" + checksum: 2b68ed07ba88302b9c63f8eda94f19a460cef6017bfda48348f09f41d2a36660c9353137991618e0e4c3db115b41e4b8f6fa63bc973b7a7c91dec66acdd02a56 + languageName: node + linkType: hard + "formidable@npm:^2.0.1": version: 2.0.1 resolution: "formidable@npm:2.0.1" @@ -24196,7 +24659,7 @@ __metadata: languageName: node linkType: hard -"global@npm:~4.4.0": +"global@npm:^4.3.0, global@npm:~4.4.0": version: 4.4.0 resolution: "global@npm:4.4.0" dependencies: @@ -24886,6 +25349,13 @@ __metadata: languageName: node linkType: hard +"helmet@npm:^5.0.2": + version: 5.1.1 + resolution: "helmet@npm:5.1.1" + checksum: b72ba26cc431804ad3b8ecdc18db95409a492cbb7a7e825efc27fc502b9433fec39fc083f2aad4fe7ed1a89a4287560b59f4435f9689eebbae6a2b61a1ec1b7d + languageName: node + linkType: hard + "helmet@npm:^6.0.0": version: 6.0.0 resolution: "helmet@npm:6.0.0" @@ -29763,7 +30233,7 @@ __metadata: languageName: node linkType: hard -"methods@npm:^1.0.0, methods@npm:^1.1.2, methods@npm:~1.1.2": +"methods@npm:^1.0.0, methods@npm:^1.1.1, methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" checksum: 0917ff4041fa8e2f2fda5425a955fe16ca411591fbd123c0d722fcf02b73971ed6f764d85f0a6f547ce49ee0221ce2c19a5fa692157931cecb422984f1dcd13a @@ -30173,7 +30643,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0, mime@npm:^1.3.4": +"mime@npm:1.6.0, mime@npm:^1.3.4, mime@npm:^1.4.1": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -30619,6 +31089,41 @@ __metadata: languageName: node linkType: hard +"msw@npm:^0.44.0": + version: 0.44.2 + resolution: "msw@npm:0.44.2" + dependencies: + "@mswjs/cookies": ^0.2.2 + "@mswjs/interceptors": ^0.17.2 + "@open-draft/until": ^1.0.3 + "@types/cookie": ^0.4.1 + "@types/js-levenshtein": ^1.1.1 + chalk: 4.1.1 + chokidar: ^3.4.2 + cookie: ^0.4.2 + graphql: ^16.3.0 + headers-polyfill: ^3.0.4 + inquirer: ^8.2.0 + is-node-process: ^1.0.1 + js-levenshtein: ^1.1.6 + node-fetch: ^2.6.7 + outvariant: ^1.3.0 + path-to-regexp: ^6.2.0 + statuses: ^2.0.0 + strict-event-emitter: ^0.2.0 + type-fest: ^1.2.2 + yargs: ^17.3.1 + peerDependencies: + typescript: ">= 4.2.x <= 4.7.x" + peerDependenciesMeta: + typescript: + optional: true + bin: + msw: cli/index.js + checksum: 739d536ee09d1c0a2cbc9dbe917f167c42115a6548f99780850ce9a63a5e7c377cf9b5f1a3b497e85ce7304025ab22966e997fe73adbd8d2ab968b8685f15a24 + languageName: node + linkType: hard + "msw@npm:^0.47.0": version: 0.47.0 resolution: "msw@npm:0.47.0" @@ -33718,7 +34223,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -34025,6 +34530,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.5.1": + version: 6.11.0 + resolution: "qs@npm:6.11.0" + dependencies: + side-channel: ^1.0.4 + checksum: 6e1f29dd5385f7488ec74ac7b6c92f4d09a90408882d0c208414a34dd33badc1a621019d4c799a3df15ab9b1d0292f97c1dd71dc7c045e69f81a8064e5af7297 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.2 resolution: "qs@npm:6.5.2" @@ -34405,6 +34919,29 @@ __metadata: languageName: node linkType: hard +"react-hot-loader@npm:^4.13.0": + version: 4.13.0 + resolution: "react-hot-loader@npm:4.13.0" + dependencies: + fast-levenshtein: ^2.0.6 + global: ^4.3.0 + hoist-non-react-statics: ^3.3.0 + loader-utils: ^1.1.0 + prop-types: ^15.6.1 + react-lifecycles-compat: ^3.0.4 + shallowequal: ^1.1.0 + source-map: ^0.7.3 + peerDependencies: + "@types/react": "^15.0.0 || ^16.0.0 || ^17.0.0 " + react: "^15.0.0 || ^16.0.0 || ^17.0.0 " + react-dom: "^15.0.0 || ^16.0.0 || ^17.0.0 " + peerDependenciesMeta: + "@types/react": + optional: true + checksum: effdbf4644ce912ae20ad94be62083970c74b26a59fe24ed0024cf73190a5b3edf59650cb693bdd7b70791df8ab8530de273d73b895c4831a91da8a76683e3a3 + languageName: node + linkType: hard + "react-icons@npm:^4.4.0": version: 4.4.0 resolution: "react-icons@npm:4.4.0" @@ -37867,6 +38404,24 @@ __metadata: languageName: node linkType: hard +"superagent@npm:^3.8.3": + version: 3.8.3 + resolution: "superagent@npm:3.8.3" + dependencies: + component-emitter: ^1.2.0 + cookiejar: ^2.1.0 + debug: ^3.1.0 + extend: ^3.0.0 + form-data: ^2.3.1 + formidable: ^1.2.0 + methods: ^1.1.1 + mime: ^1.4.1 + qs: ^6.5.1 + readable-stream: ^2.3.5 + checksum: b13d0303259d76c9180bd40d97d9f0713760f5ced1aef089bdb2fcdf69cfaef89004cd6e986416d59bd9a2f0f9933d72521b5171fa26f89b781a2c3460c516fe + languageName: node + linkType: hard + "superagent@npm:^8.0.0": version: 8.0.0 resolution: "superagent@npm:8.0.0" @@ -37886,6 +38441,16 @@ __metadata: languageName: node linkType: hard +"supertest@npm:^4.0.2": + version: 4.0.2 + resolution: "supertest@npm:4.0.2" + dependencies: + methods: ^1.1.2 + superagent: ^3.8.3 + checksum: ec848088c5d0f6743a0829331c274f67ab4c2e5e6ba724f2fdf4965a73afa22d5091448323e28ae5b69137e42fe3ac67235dfc5b0a0acda3c088bdd193313319 + languageName: node + linkType: hard + "supertest@npm:^6.1.3, supertest@npm:^6.1.6, supertest@npm:^6.2.4": version: 6.2.4 resolution: "supertest@npm:6.2.4" @@ -40196,7 +40761,7 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5, webpack@npm:^5.70.0": +"webpack@npm:^5, webpack@npm:^5.66.0, webpack@npm:^5.70.0": version: 5.74.0 resolution: "webpack@npm:5.74.0" dependencies: