) => (
+
+
+ {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..c02f554b49
--- /dev/null
+++ b/plugins/apache-airflow/src/components/HomePage/HomePage.tsx
@@ -0,0 +1,56 @@
+/*
+ * 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..33447426ad
--- /dev/null
+++ b/plugins/apache-airflow/src/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 { 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..60233236f0
--- /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.getOptionalString('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';