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 0000000000..d8603e8fa7 Binary files /dev/null and b/plugins/azure-functions/docs/functions-table.png differ diff --git a/plugins/azure-functions/package.json b/plugins/azure-functions/package.json new file mode 100644 index 0000000000..d917a31df2 --- /dev/null +++ b/plugins/azure-functions/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-azure-functions", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "frontend-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": { + "@backstage/core-components": "^0.11.0", + "@backstage/core-plugin-api": "^1.0.5", + "@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", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.18.0", + "@backstage/core-app-api": "^1.0.5", + "@backstage/dev-utils": "^1.0.4", + "@backstage/test-utils": "^1.1.3", + "@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" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/azure-functions/src/api/AzureFunctionsApi.ts b/plugins/azure-functions/src/api/AzureFunctionsApi.ts new file mode 100644 index 0000000000..3853f0dbe4 --- /dev/null +++ b/plugins/azure-functions/src/api/AzureFunctionsApi.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 { createApiRef } from '@backstage/core-plugin-api'; +import { FunctionsData } from './types'; + +export const azureFunctionsApiRef = createApiRef({ + 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: