diff --git a/.changeset/weak-berries-heal.md b/.changeset/weak-berries-heal.md new file mode 100644 index 0000000000..4160d078fc --- /dev/null +++ b/.changeset/weak-berries-heal.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-apache-airflow': minor +--- + +Introduces a new plugin for the Apache Airflow workflow management platform. +This implementation has been tested with the Apache Airflow v2 API, +authenticating with basic authentication through the Backstage proxy plugin. + +Supported functionality includes: + +- Information card of version information of the Airflow instance +- Information card of instance health for the meta-database and scheduler +- Table of DAGs with meta information and status, along with a link to view + details in the Airflow UI diff --git a/app-config.yaml b/app-config.yaml index 4adaea0da4..6cd05f44e8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -97,6 +97,11 @@ proxy: headers: Authorization: ${ILERT_AUTH_HEADER} + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} + organization: name: My Company @@ -437,3 +442,6 @@ azureDevOps: host: dev.azure.com token: my-token organization: my-company + +apacheAirflow: + baseUrl: https://your.airflow.instance.com diff --git a/packages/app/package.json b/packages/app/package.json index daa7ad16f4..99b90952e3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -13,6 +13,7 @@ "@backstage/integration-react": "^0.1.15", "@backstage/plugin-api-docs": "^0.6.18", "@backstage/plugin-azure-devops": "^0.1.6", + "@backstage/plugin-apache-airflow": "^0.0.0", "@backstage/plugin-badges": "^0.2.16", "@backstage/plugin-catalog": "^0.7.4", "@backstage/plugin-catalog-graph": "^0.2.3", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 7e5d3db926..59f107c70d 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -87,6 +87,7 @@ import { providers } from './identityProviders'; import * as plugins from './plugins'; import { techDocsPage } from './components/techdocs/TechDocsPage'; +import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; const app = createApp({ apis, @@ -218,6 +219,7 @@ const routes = ( /> } /> } /> + } /> ); diff --git a/plugins/apache-airflow/.eslintrc.js b/plugins/apache-airflow/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/apache-airflow/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/apache-airflow/.gitignore b/plugins/apache-airflow/.gitignore new file mode 100644 index 0000000000..352a14e3cc --- /dev/null +++ b/plugins/apache-airflow/.gitignore @@ -0,0 +1,2 @@ +airflow/ +node_modules/ diff --git a/plugins/apache-airflow/README.md b/plugins/apache-airflow/README.md new file mode 100644 index 0000000000..c57ab9da43 --- /dev/null +++ b/plugins/apache-airflow/README.md @@ -0,0 +1,124 @@ +# Apache Airflow Plugin + +Welcome to the apache-airflow plugin! + +## Feature Requests & Ideas + +- [ ] Add support for running multiple instances of Airflow for monitoring + various deployment stages or business domains. ([Suggested by @JGoldman110](https://github.com/backstage/backstage/issues/735#issuecomment-985063468)) +- [ ] Make owner chips in the DAG table clickable, resolving to a user or group + in the entity catalog. ([Suggested by @julioz](https://github.com/backstage/backstage/pull/8348#discussion_r764766295)) + +## Installation + +1. Install the plugin with `yarn` in the root of your Backstage directory + +```sh +yarn --cwd packages/app add @backstage/plugin-apache-airflow +``` + +2. Import and use the plugin extension in `spp/src/App.tsx` + +```diff +--- a/packages/app/src/App.tsx ++++ b/packages/app/src/App.tsx +@@ -86,6 +86,7 @@ import { providers } from './identityProviders'; + import * as plugins from './plugins'; + + import { techDocsPage } from './components/techdocs/TechDocsPage'; ++import { ApacheAirflowPage } from '@backstage/plugin-apache-airflow'; + + const app = createApp({ + apis, +@@ -203,6 +204,7 @@ const routes = ( + element={} + /> + } /> ++ } /> + + ); +``` + +## Configuration + +For links to the Airflow instance, the `baseUrl` must be defined in +`app-config.yaml`. + +```yaml +apacheAirflow: + baseUrl: https://your.airflow.instance.com +``` + +This plugin uses the Backstage proxy to securely communicate with the Apache +Airflow API. Add the following to your `app-config.yaml` to enable this +configuration: + +```yaml +proxy: + '/airflow': + target: https://your.airflow.instance.com/api/v1 + headers: + Authorization: ${AIRFLOW_BASIC_AUTH_HEADER} +``` + +In your production deployment of Backstage, you would also need to ensure that +you've set the `AIRFLOW_BASIC_AUTH_HEADER` environment variable before starting +the backend. + +While working locally, you may wish to hard-code your API key in your +`app-config.local.yaml` like this: + +```yaml +# app-config.local.yaml +proxy: + '/airflow': + target: http://localhost:8080/api/v1 + headers: + Authorization: Basic YWlyZmxvdzphaXJmbG93 +``` + +Where the basic authorization token is the base64 encoding of the username and +password of your instance. + +```sh +echo -n "airflow:airflow" | base64 -w0 +``` + +## Development + +For local development, you can setup a local Airflow instance for development +purposes by [running Airflow with Docker Compose][2]. + +To verify that Airflow is running, and the API is functioning as expected, you +can run the following `curl` command: + +```sh +curl -X GET \ + --user "airflow:airflow" \ + localhost:8080/api/v1/dags +``` + +To run the Backstage proxy, you will have to run start the `example-backend` +plugin. + +```sh +yarn workspace example-backend start +``` + +To verify that the proxy is configured correctly, you can curl the Backstage +proxy endpoint. If using basic authentication, you will have to base64 encode +the username and password: + +```sh +curl http://localhost:7007/api/proxy/airflow/dags +``` + +And finally, to run an instance of this plugin, you can run: + +```sh +yarn start +``` + +[1]: https://airflow.apache.org/docs/apache-airflow/stable/security/api.html +[2]: https://airflow.apache.org/docs/apache-airflow/stable/start/docker.html +[3]: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html diff --git a/plugins/apache-airflow/api-report.md b/plugins/apache-airflow/api-report.md new file mode 100644 index 0000000000..ca151f0734 --- /dev/null +++ b/plugins/apache-airflow/api-report.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-apache-airflow" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "ApacheAirflowPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ApacheAirflowPage: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "apacheAirflowPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const apacheAirflowPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; +``` diff --git a/plugins/apache-airflow/config.d.ts b/plugins/apache-airflow/config.d.ts new file mode 100644 index 0000000000..3282116491 --- /dev/null +++ b/plugins/apache-airflow/config.d.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 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 interface Config { + /** Configurations for the Apache Airflow plugin */ + apacheAirflow: { + /** + * The base url of the Apache Airflow installation. + * @visibility frontend + */ + baseUrl: string; + }; +} diff --git a/plugins/apache-airflow/dev/index.tsx b/plugins/apache-airflow/dev/index.tsx new file mode 100644 index 0000000000..bc8ce977a5 --- /dev/null +++ b/plugins/apache-airflow/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright 2021 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 { createDevApp } from '@backstage/dev-utils'; +import { apacheAirflowPlugin, ApacheAirflowPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(apacheAirflowPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/apache-airflow', + }) + .render(); diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json new file mode 100644 index 0000000000..a361961d12 --- /dev/null +++ b/plugins/apache-airflow/package.json @@ -0,0 +1,52 @@ +{ + "name": "@backstage/plugin-apache-airflow", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.8.0", + "@backstage/core-plugin-api": "^0.3.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "cross-fetch": "^3.0.6", + "qs": "^6.10.1", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.0", + "@backstage/dev-utils": "^0.2.14", + "@backstage/test-utils": "^0.1.24", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "msw": "^0.35.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts new file mode 100644 index 0000000000..b0a0229df4 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021 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, DiscoveryApi } from '@backstage/core-plugin-api'; +import { Dag, InstanceStatus, InstanceVersion } from './types'; + +export const apacheAirflowApiRef = createApiRef({ + id: 'plugin.apacheairflow.service', + description: 'Used by the Apache Airflow plugin to make requests', +}); + +export type ApacheAirflowApi = { + discoveryApi: DiscoveryApi; + baseUrl: string; + listDags(options?: { objectsPerRequest: number }): Promise; + updateDag(dagId: string, isPaused: boolean): Promise; + getInstanceStatus(): Promise; + getInstanceVersion(): Promise; +}; diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts new file mode 100644 index 0000000000..704b17eb34 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.test.ts @@ -0,0 +1,163 @@ +/* + * Copyright 2021 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 { UrlPatternDiscovery } from '@backstage/core-app-api'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ApacheAirflowClient } from './index'; +import { Dag } from './types'; + +const server = setupServer(); + +const dags: Dag[] = [ + { + dag_id: 'mock_dag_1', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_2', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_3', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_4', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, + { + dag_id: 'mock_dag_5', + fileloc: '', + file_token: '', + owners: ['admin'], + schedule_interval: { __type: 'CronExpression', value: '* * 0 0 0' }, + tags: [{ name: 'exmaple' }], + }, +]; + +describe('ApacheAirflowClient', () => { + setupRequestMockHandlers(server); + + const mockBaseUrl = 'http://backstage:9191/api/proxy'; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); + + const setupHandlers = () => { + server.use( + rest.get(`${mockBaseUrl}/airflow/dags`, (req, res, ctx) => { + expect(req.url.searchParams.get('limit')).toBe('2'); + + // emulate paging to check if everything is requested + if (req.url.searchParams.get('offset') === '0') { + return res( + ctx.json({ + dags: dags.slice(0, 2), + total_entries: dags.length, + }), + ); + } + + // page offset 2 + if (req.url.searchParams.get('offset') === '2') { + return res( + ctx.json({ + dags: dags.slice(2, 4), + total_entries: dags.length, + }), + ); + } + + // page offset 4 + expect(req.url.searchParams.get('offset')).toBe('4'); + return res( + ctx.json({ + dags: dags.slice(4), + total_entries: dags.length, + }), + ); + }), + + rest.patch(`${mockBaseUrl}/airflow/dags/:dag_id`, (req, res, ctx) => { + const { dag_id } = req.params; + const body = JSON.parse(req.body as string); + expect(body.is_paused).toBeDefined(); + return res( + ctx.json({ + dag_id: dag_id, + root_dag_id: 'string', + is_paused: body.is_paused, + is_active: true, + is_subdag: true, + fileloc: 'string', + file_token: 'string', + owners: ['string'], + description: 'string', + schedule_interval: { + __type: 'string', + days: 0, + seconds: 0, + microseconds: 0, + }, + tags: [{}], + }), + ); + }), + ); + }; + + it('list dags should return all dags with emulated pagination', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + + // call with limit of 2, to force two paginations in requesting all dags + // as our mocked response has 4 total entries + const responseDags = await client.listDags({ objectsPerRequest: 2 }); + expect(responseDags.length).toEqual(5); + expect(responseDags).toEqual(dags); + }); + + it('update dag should return dag information with updated paused attribute', async () => { + setupHandlers(); + const client = new ApacheAirflowClient({ + discoveryApi: discoveryApi, + baseUrl: 'localhost:8080/', + }); + const dagId = 'mock_dag_1'; + const response: Dag = await client.updateDag(dagId, true); + expect(response.dag_id).toEqual(dagId); + expect(response.is_paused).toEqual(true); + }); +}); diff --git a/plugins/apache-airflow/src/api/ApacheAirflowClient.ts b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts new file mode 100644 index 0000000000..c184e304b4 --- /dev/null +++ b/plugins/apache-airflow/src/api/ApacheAirflowClient.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2021 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 { DiscoveryApi } from '@backstage/core-plugin-api'; +import fetch from 'cross-fetch'; +import qs from 'qs'; +import { ApacheAirflowApi } from './ApacheAirflowApi'; +import { + Dag, + Dags, + InstanceStatus, + InstanceVersion, + ListDagsParams, +} from './types'; + +export class ApacheAirflowClient implements ApacheAirflowApi { + discoveryApi: DiscoveryApi; + baseUrl: string; + + constructor({ + discoveryApi, + baseUrl = 'http://localhost:8080', + }: { + discoveryApi: DiscoveryApi; + baseUrl: string; + }) { + this.discoveryApi = discoveryApi; + this.baseUrl = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + } + + /** + * List all DAGs in the Airflow instance + * + * @remarks + * All DAGs with a limit of 100 results per request are returned; this may be + * bogged-down for instances with many DAGs, in which case table pagination + * should be implemented + * + * @param {number} objectsPerRequest records returned per request in pagination + * @returns {Promise} + */ + async listDags(options = { objectsPerRequest: 100 }): Promise { + const dags: Dag[] = []; + const searchParams: ListDagsParams = { + limit: options.objectsPerRequest, + offset: 0, + }; + + for (;;) { + const response = await this.fetch( + `/dags?${qs.stringify(searchParams)}`, + ); + dags.push(...response.dags); + + if (dags.length >= response.total_entries) { + break; + } + if (typeof searchParams.offset !== 'undefined') { + searchParams.offset += options.objectsPerRequest; + } + } + return dags; + } + + async updateDag(dagId: string, isPaused: boolean): Promise { + const init = { + method: 'PATCH', + body: JSON.stringify({ is_paused: isPaused }), + }; + return await this.fetch(`/dags/${dagId}`, init); + } + + async getInstanceStatus(): Promise { + return await this.fetch('/health'); + } + + async getInstanceVersion(): Promise { + return await this.fetch('/version'); + } + + private async fetch(input: string, init?: RequestInit): Promise { + const proxyUri = `${await this.discoveryApi.getBaseUrl('proxy')}/airflow`; + const response = await fetch(`${proxyUri}${input}`, init); + if (!response.ok) throw new Error(response.statusText); + return await response.json(); + } +} diff --git a/plugins/apache-airflow/src/api/index.ts b/plugins/apache-airflow/src/api/index.ts new file mode 100644 index 0000000000..c6dfe2205e --- /dev/null +++ b/plugins/apache-airflow/src/api/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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. + */ + +/** + * An API definition and client for integrating with the Apache Airflow v2+ + * API + * + * @packageDocumentation + */ + +export * from './ApacheAirflowApi'; +export * from './ApacheAirflowClient'; diff --git a/plugins/apache-airflow/src/api/types/Dags.ts b/plugins/apache-airflow/src/api/types/Dags.ts new file mode 100644 index 0000000000..8f1d68fb2d --- /dev/null +++ b/plugins/apache-airflow/src/api/types/Dags.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2021 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. + */ + +/** + * Query parameters for listing DAGs + */ +export interface ListDagsParams { + limit?: number; + offset?: number; + order_by?: string; + tags?: Tag[]; + only_active?: boolean; +} + +export interface Dags { + dags: Dag[]; + total_entries: number; +} + +export interface Dag { + dag_id: string; + root_dag_id?: string; + is_paused?: boolean; + is_active?: boolean; + is_subdag?: boolean; + fileloc: string; + file_token: string; + owners: string[]; + description?: string; + schedule_interval: ScheduleInterval; + tags: Tag[]; +} + +export interface TimeDelta { + __type: 'TimeDelta'; + days: number; + seconds: number; + microseconds: number; +} + +export interface RelativeDelta { + __type: 'RelativeDelta'; + years: number; + months: number; + days: number; + leapdays: number; + hours: number; + minutes: number; + seconds: number; + microseconds: number; + year: number; + month: number; + day: number; + hour: number; + minute: number; + second: number; + microsecond: number; +} + +export interface CronExpression { + __type: 'CronExpression'; + value: string; +} + +// discrimant union of possible schedule interval types +export type ScheduleInterval = TimeDelta | RelativeDelta | CronExpression; + +export interface Tag { + name: string; +} diff --git a/plugins/apache-airflow/src/api/types/InstanceStatus.ts b/plugins/apache-airflow/src/api/types/InstanceStatus.ts new file mode 100644 index 0000000000..3b7a5ce416 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/InstanceStatus.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 interface InstanceStatus { + metadatabase: MetadatabaseStatus; + scheduler: SchedulerStatus; +} + +export interface MetadatabaseStatus { + status: 'healthy' | 'unhealthy'; +} + +export interface SchedulerStatus { + status: 'healthy' | 'unhealthy'; + latest_scheduler_heartbeat?: string; +} diff --git a/plugins/apache-airflow/src/api/types/InstanceVersion.ts b/plugins/apache-airflow/src/api/types/InstanceVersion.ts new file mode 100644 index 0000000000..b8d2727229 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/InstanceVersion.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 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 interface InstanceVersion { + version: string; + git_version?: string; +} diff --git a/plugins/apache-airflow/src/api/types/index.ts b/plugins/apache-airflow/src/api/types/index.ts new file mode 100644 index 0000000000..67c2d293a2 --- /dev/null +++ b/plugins/apache-airflow/src/api/types/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021 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 type { + ListDagsParams, + Dag, + Dags, + Tag, + TimeDelta, + RelativeDelta, + CronExpression, + ScheduleInterval, +} from './Dags'; +export type { InstanceStatus } from './InstanceStatus'; +export type { InstanceVersion } from './InstanceVersion'; diff --git a/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx new file mode 100644 index 0000000000..13122ca552 --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/DagTableComponent.tsx @@ -0,0 +1,151 @@ +/* + * Copyright 2021 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 { + Progress, + StatusError, + StatusOK, + Table, + TableColumn, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Box from '@material-ui/core/Box'; +import Chip from '@material-ui/core/Chip'; +import IconButton from '@material-ui/core/IconButton'; +import Switch from '@material-ui/core/Switch'; +import Tooltip from '@material-ui/core/Tooltip'; +import Typography from '@material-ui/core/Typography'; +import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { Dag } from '../../api/types'; +import { ScheduleIntervalLabel } from '../ScheduleIntervalLabel'; + +type DagTableRow = Dag & { + id: string; + dagUrl: string; +}; + +const columns: TableColumn[] = [ + { + title: 'Paused', + field: 'is_paused', + render: (row: Partial) => ( + + + + ), + width: '5%', + }, + { + title: 'DAG', + field: 'id', + render: (row: Partial) => ( +
+ + {row.id} + + + {row.tags?.map((tag, ix) => ( + + ))} + +
+ ), + width: '60%', + }, + { + title: 'Owner', + field: 'owners', + render: (row: Partial) => ( + + {row.owners?.map((owner, ix) => ( + + ))} + + ), + width: '10%', + }, + { + title: 'Active', + render: (row: Partial) => ( + + {row.is_active ? : } + + {row.is_active ? 'Active' : 'Inactive'} + + + ), + width: '10%', + }, + { + title: 'Schedule', + render: (row: Partial) => ( + + ), + width: '10%', + }, + { + title: 'Link', + field: 'dagUrl', + render: (row: Partial) => ( + + + + + + ), + width: '5%', + }, +]; + +type DenseTableProps = { + dags: Dag[]; +}; + +export const DenseTable = ({ dags }: DenseTableProps) => { + return ( + + ); +}; + +export const DagTableComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = useAsync(async (): Promise => { + return await apiClient.listDags(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + const data = value?.map(el => ({ + ...el, + id: el.dag_id, // table records require `id` attribute + dagUrl: `${apiClient.baseUrl}dag_details?dag_id=${el.dag_id}`, // construct path to DAG using `baseUrl` + })); + + return ; +}; diff --git a/plugins/apache-airflow/src/components/DagTableComponent/index.ts b/plugins/apache-airflow/src/components/DagTableComponent/index.ts new file mode 100644 index 0000000000..714cbce47d --- /dev/null +++ b/plugins/apache-airflow/src/components/DagTableComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { DagTableComponent } from './DagTableComponent'; diff --git a/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx b/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx new file mode 100644 index 0000000000..23afdd0a1e --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/HomePage.test.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2021 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 { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { ApacheAirflowApi, apacheAirflowApiRef } from '../../api'; +import { HomePage } from './HomePage'; + +describe('', () => { + const mockApi: jest.Mocked = { + getInstanceStatus: jest.fn().mockResolvedValue({ + metadatabase: { status: 'healthy' }, + scheduler: { status: 'healthy' }, + }), + getInstanceVersion: jest.fn().mockResolvedValue({ + version: 'v2.0.0', + }), + listDags: jest.fn().mockResolvedValue([ + { + dag_id: 'mock_dag_1', + }, + ]), + } as any; + + it('homepage should render', async () => { + const { getByText } = await renderInTestApp( + + + , + ); + expect(getByText('Apache Airflow')).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/HomePage/HomePage.tsx b/plugins/apache-airflow/src/components/HomePage/HomePage.tsx new file mode 100644 index 0000000000..442f1bf4a0 --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/HomePage.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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 { + Content, + ContentHeader, + Header, + HeaderLabel, + Page, + SupportButton, +} from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { DagTableComponent } from '../DagTableComponent'; +import { StatusComponent } from '../StatusComponent'; +import { VersionComponent } from '../VersionComponent'; + +export const HomePage = () => ( + +
+ +
+ + + + See an overview of your Apache Airflow instance, and manage workflows + + + + + + + + + + + + + + +
+); diff --git a/plugins/apache-airflow/src/components/HomePage/index.ts b/plugins/apache-airflow/src/components/HomePage/index.ts new file mode 100644 index 0000000000..41d7bc5d03 --- /dev/null +++ b/plugins/apache-airflow/src/components/HomePage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { HomePage } from './HomePage'; diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx new file mode 100644 index 0000000000..bfe4119bcd --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.test.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2021 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 { render } from '@testing-library/react'; +import React from 'react'; +import { CronExpression, RelativeDelta, TimeDelta } from '../../api/types'; +import { ScheduleIntervalLabel } from './ScheduleIntervalLabel'; + +describe('ScheduleIntervalLabel', () => { + it('should render with cronexpression interval', async () => { + const interval = { + __type: 'CronExpression', + value: '0 0 * * *', + } as CronExpression; + const { findByText } = render( + , + ); + expect(await findByText('0 0 * * *')).toBeInTheDocument(); + }); + + it('should render with time delta interval', async () => { + const interval = { + __type: 'TimeDelta', + days: 5, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('5 days 00:12:30')).toBeInTheDocument(); + }); + + it('should not render days with time delta interval with 0 days', async () => { + const interval = { + __type: 'TimeDelta', + days: 0, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('00:12:30')).toBeInTheDocument(); + }); + + it('should render singular day with time delta interval and 1 day', async () => { + const interval = { + __type: 'TimeDelta', + days: 1, + seconds: 750, + microseconds: 600, + } as TimeDelta; + const { findByText } = render( + , + ); + expect(await findByText('1 day 00:12:30')).toBeInTheDocument(); + }); + + it('should render with relative delta interval', async () => { + const interval = { + __type: 'RelativeDelta', + days: 15, + months: 6, + } as RelativeDelta; + const { findByText } = render( + , + ); + expect( + await findByText('relativedelta(days=+15,months=+6)'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx new file mode 100644 index 0000000000..3ae72098d0 --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/ScheduleIntervalLabel.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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 Chip from '@material-ui/core/Chip'; +import React from 'react'; +import { ScheduleInterval, TimeDelta, RelativeDelta } from '../../api/types'; + +interface Props { + interval: ScheduleInterval | undefined; +} + +const timeDeltaToLabel = (delta: TimeDelta): String => { + let label = ''; + const date = new Date(0); + date.setSeconds(delta.seconds); + const time = date.toISOString().substr(11, 8); + if (delta.days === 0) { + label = `${time}`; + } else if (delta.days === 1) { + label = `1 day ${time}`; + } else { + label = `${delta.days} days ${time}`; + } + return label; +}; + +const relativeDeltaToLabel = (delta: RelativeDelta) => { + const params = Object.entries(delta) + .filter(o => o[0] !== '__type' && o[1] !== null && o[1] !== 0) + .map(o => (o[1] > 0 ? `${o[0]}=+${o[1]}` : `${o[0]}=-${o[1]}`)); + return `relativedelta(${params})`; +}; + +export const ScheduleIntervalLabel = ({ interval }: Props) => { + let label: String = ''; + switch (interval?.__type) { + case 'TimeDelta': + label = timeDeltaToLabel(interval); + break; + case 'RelativeDelta': + label = relativeDeltaToLabel(interval); + break; + case 'CronExpression': + label = interval.value; + break; + default: + label = 'None'; + } + return ; +}; diff --git a/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts new file mode 100644 index 0000000000..cb6b2092f5 --- /dev/null +++ b/plugins/apache-airflow/src/components/ScheduleIntervalLabel/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { ScheduleIntervalLabel } from './ScheduleIntervalLabel'; diff --git a/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx b/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx new file mode 100644 index 0000000000..326be4b450 --- /dev/null +++ b/plugins/apache-airflow/src/components/StatusComponent/StatusComponent.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2021 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 { + InfoCard, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { InstanceStatus } from '../../api/types'; + +export const StatusComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = + useAsync(async (): Promise => { + return await apiClient.getInstanceStatus(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (value) { + const metadata = { + 'Metadatabase Status': value.metadatabase.status, + 'Scheduler Status': value.scheduler.status, + 'Latest Scheduler Heartbeat': value.scheduler.latest_scheduler_heartbeat, + }; + + return ( + + + + ); + } + return No status information found...; +}; diff --git a/plugins/apache-airflow/src/components/StatusComponent/index.ts b/plugins/apache-airflow/src/components/StatusComponent/index.ts new file mode 100644 index 0000000000..4898c15e26 --- /dev/null +++ b/plugins/apache-airflow/src/components/StatusComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { StatusComponent } from './StatusComponent'; diff --git a/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx b/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx new file mode 100644 index 0000000000..482d1ff0a7 --- /dev/null +++ b/plugins/apache-airflow/src/components/VersionComponent/VersionComponent.tsx @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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 { + InfoCard, + Progress, + StructuredMetadataTable, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import Alert from '@material-ui/lab/Alert'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { apacheAirflowApiRef } from '../../api'; +import { InstanceVersion } from '../../api/types'; + +export const VersionComponent = () => { + const apiClient = useApi(apacheAirflowApiRef); + const { value, loading, error } = + useAsync(async (): Promise => { + return await apiClient.getInstanceVersion(); + }, []); + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (value) { + const metadata = { + Version: value.version, + 'Git Version': value.git_version, + }; + + return ( + + + + ); + } + return No status information found...; +}; diff --git a/plugins/apache-airflow/src/components/VersionComponent/index.ts b/plugins/apache-airflow/src/components/VersionComponent/index.ts new file mode 100644 index 0000000000..4ace64d320 --- /dev/null +++ b/plugins/apache-airflow/src/components/VersionComponent/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { VersionComponent } from './VersionComponent'; diff --git a/plugins/apache-airflow/src/index.ts b/plugins/apache-airflow/src/index.ts new file mode 100644 index 0000000000..7186839c0a --- /dev/null +++ b/plugins/apache-airflow/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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. + */ + +/** + * A Backstage plugin that integrates towards Apache Airflow + * + * @packageDocumentation + */ + +export { apacheAirflowPlugin, ApacheAirflowPage } from './plugin'; diff --git a/plugins/apache-airflow/src/plugin.test.ts b/plugins/apache-airflow/src/plugin.test.ts new file mode 100644 index 0000000000..26997d74d8 --- /dev/null +++ b/plugins/apache-airflow/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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 { apacheAirflowPlugin } from './plugin'; + +describe('apache-airflow', () => { + it('should export plugin', () => { + expect(apacheAirflowPlugin).toBeDefined(); + }); +}); diff --git a/plugins/apache-airflow/src/plugin.ts b/plugins/apache-airflow/src/plugin.ts new file mode 100644 index 0000000000..04ef1974d4 --- /dev/null +++ b/plugins/apache-airflow/src/plugin.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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 { rootRouteRef } from './routes'; +import { apacheAirflowApiRef, ApacheAirflowClient } from './api'; +import { + createApiFactory, + createPlugin, + createRoutableExtension, + discoveryApiRef, + configApiRef, +} from '@backstage/core-plugin-api'; + +export const apacheAirflowPlugin = createPlugin({ + id: 'apache-airflow', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: apacheAirflowApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new ApacheAirflowClient({ + discoveryApi, + baseUrl: configApi.getString('apacheAirflow.baseUrl'), + }), + }), + ], +}); + +export const ApacheAirflowPage = apacheAirflowPlugin.provide( + createRoutableExtension({ + name: 'ApacheAirflowPage', + component: () => import('./components/HomePage').then(m => m.HomePage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/apache-airflow/src/routes.ts b/plugins/apache-airflow/src/routes.ts new file mode 100644 index 0000000000..2a0872eba4 --- /dev/null +++ b/plugins/apache-airflow/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'apache-airflow', +}); diff --git a/plugins/apache-airflow/src/setupTests.ts b/plugins/apache-airflow/src/setupTests.ts new file mode 100644 index 0000000000..427556fe26 --- /dev/null +++ b/plugins/apache-airflow/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill';