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
+
+
+
+*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 (
+
+