You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by pi...@apache.org on 2025/07/21 10:23:47 UTC
(airflow) branch main updated: AIP-68 CLI to bootstrap a React App plugin (#53365)
This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 0f0ea3b5c44 AIP-68 CLI to bootstrap a React App plugin (#53365)
0f0ea3b5c44 is described below
commit 0f0ea3b5c444465777a7c5eb5188b301faed1aee
Author: Pierre Jeambrun <pi...@gmail.com>
AuthorDate: Mon Jul 21 12:23:41 2025 +0200
AIP-68 CLI to bootstrap a React App plugin (#53365)
* First working iteration
* Remove apache licence when generating the project
* Incorporate the theme, and small adjustments
* Fix linting errors
* Integrate to the airflow CLI
* Update test structure
* Add unit tests
* Small adjustments
* Address PR comment, build as library
* Fix react issues
* Smaller dependencies
* Move back dump command to CLI plugins
* Move back command to dev folder
* Update airflow-core/src/airflow/cli/cli_config.py
Co-authored-by: Jens Scheffler <95...@users.noreply.github.com>
---------
Co-authored-by: Jens Scheffler <95...@users.noreply.github.com>
---
airflow-core/src/airflow/ui/src/main.tsx | 8 +
.../src/airflow/ui/src/pages/ReactPlugin.tsx | 31 +++-
airflow-core/src/airflow/ui/tsconfig.node.json | 1 +
dev/react-plugin-tools/README.md | 59 +++++++
dev/react-plugin-tools/bootstrap.py | 179 +++++++++++++++++++++
.../react_plugin_template/.gitignore | 28 ++++
.../react_plugin_template/.prettierignore | 5 +
.../react_plugin_template/.prettierrc | 13 ++
.../react_plugin_template/README.md | 138 ++++++++++++++++
.../react_plugin_template/eslint.config.js | 58 +++++++
.../react_plugin_template/index.html | 13 ++
.../react_plugin_template/package.json | 75 +++++++++
.../src/context/colorMode/ColorModeProvider.tsx | 25 +++
.../src/context/colorMode/index.ts | 21 +++
.../src/context/colorMode/useColorMode.tsx | 33 ++++
.../react_plugin_template/src/dev.tsx | 30 ++++
.../react_plugin_template/src/main.tsx | 39 +++--
.../react_plugin_template/src/pages/HomePage.tsx | 38 ++---
.../react_plugin_template/src/theme.ts | 176 ++++++++++++++++++++
.../react_plugin_template/src/vite-env.d.ts | 20 +++
.../react_plugin_template/testsSetup.ts | 20 +++
.../react_plugin_template/tsconfig.app.json | 10 +-
.../react_plugin_template/tsconfig.json | 8 +
.../react_plugin_template/tsconfig.lib.json | 15 ++
.../react_plugin_template}/tsconfig.node.json | 4 +-
.../react_plugin_template/vite.config.ts | 84 ++++++++++
26 files changed, 1077 insertions(+), 54 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/main.tsx b/airflow-core/src/airflow/ui/src/main.tsx
index 2945773d181..2768d84809a 100644
--- a/airflow-core/src/airflow/ui/src/main.tsx
+++ b/airflow-core/src/airflow/ui/src/main.tsx
@@ -20,9 +20,11 @@ import { ChakraProvider } from "@chakra-ui/react";
import { QueryClientProvider } from "@tanstack/react-query";
import axios, { type AxiosError } from "axios";
import { StrictMode } from "react";
+import React from "react";
import { createRoot } from "react-dom/client";
import { I18nextProvider } from "react-i18next";
import { RouterProvider } from "react-router-dom";
+import * as ReactJSXRuntime from "react/jsx-runtime";
import type { HTTPExceptionResponse } from "openapi/requests/types.gen";
import { ColorModeProvider } from "src/context/colorMode";
@@ -35,6 +37,12 @@ import { client } from "./queryClient";
import { system } from "./theme";
import { clearToken, tokenHandler } from "./utils/tokenHandler";
+// Set React and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins.
+// Only one instance of React should be used.
+// Reflect will avoid type checking.
+Reflect.set(globalThis, "React", React);
+Reflect.set(globalThis, "ReactJSXRuntime", ReactJSXRuntime);
+
// redirect to login page if the API responds with unauthorized or forbidden errors
axios.interceptors.response.use(
(response) => response,
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
index 0f80af777c8..8a193205634 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
+import { type FC, lazy, Suspense } from "react";
import { useParams } from "react-router-dom";
import type { ReactAppResponse } from "openapi/requests/types.gen";
@@ -29,13 +29,30 @@ export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse
const Plugin = lazy(() =>
// We are assuming the plugin manager is trusted and the bundle_url is safe
- import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
- console.error("Component Failed Loading:", error);
+ import(/* @vite-ignore */ reactApp.bundle_url)
+ .then(() => {
+ const component = (
+ globalThis as unknown as {
+ AirflowPlugin: FC<{
+ dagId?: string;
+ mapIndex?: string;
+ runId?: string;
+ taskId?: string;
+ }>;
+ }
+ ).AirflowPlugin;
- return {
- default: <ErrorPage />,
- };
- }),
+ return {
+ default: component,
+ };
+ })
+ .catch((error: unknown) => {
+ console.error("Component Failed Loading:", error);
+
+ return {
+ default: ErrorPage,
+ };
+ }),
);
return (
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/airflow-core/src/airflow/ui/tsconfig.node.json
index a731173fc6c..1072cb12857 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/airflow-core/src/airflow/ui/tsconfig.node.json
@@ -18,6 +18,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"],
diff --git a/dev/react-plugin-tools/README.md b/dev/react-plugin-tools/README.md
new file mode 100644
index 00000000000..9dc95e7dacb
--- /dev/null
+++ b/dev/react-plugin-tools/README.md
@@ -0,0 +1,59 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+ -->
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [React Plugin Development Tools](#react-plugin-development-tools)
+ - [Overview](#overview)
+ - [Files](#files)
+ - [Quick Start](#quick-start)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# React Plugin Development Tools
+
+This directory contains tools for developing React-based Airflow plugins that can be dynamically loaded into the Airflow UI.
+
+## Overview
+
+These tools help you create React plugin projects that:
+
+- Build as libraries compatible with dynamic imports
+- Share React instances with the host Airflow application
+- Follow Airflow's UI development patterns and standards
+- Include proper TypeScript configuration and build setup
+
+## Files
+
+- `bootstrap.py` - CLI tool to create new React plugin projects
+- `react_plugin_template/` - Template directory with all the necessary files
+
+## Quick Start
+
+### Create a New Plugin Project
+
+```bash
+# From the dev/react-plugin-tools directory
+python bootstrap.py my-awesome-plugin
+
+# Or specify a custom directory
+python bootstrap.py my-awesome-plugin --dir /path/to/my-projects/my-awesome-plugin
+```
diff --git a/dev/react-plugin-tools/bootstrap.py b/dev/react-plugin-tools/bootstrap.py
new file mode 100755
index 00000000000..3406b26fe92
--- /dev/null
+++ b/dev/react-plugin-tools/bootstrap.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+"""
+Bootstrap React Plugin CLI Tool.
+
+This script provides a command-line interface to create new React UI plugin
+directories based on the airflow-core/ui project structure. It sets up all the
+necessary configuration files, dependencies, and basic structure for development
+with the same tooling as used in Airflow's core UI.
+"""
+
+from __future__ import annotations
+
+import argparse
+import re
+import shutil
+import sys
+from pathlib import Path
+
+
+def get_template_dir() -> Path:
+ """Get the template directory path."""
+ script_dir = Path(__file__).parent
+ template_dir = script_dir / "react_plugin_template"
+
+ if not template_dir.exists():
+ print(f"Error: Template directory not found at {template_dir}")
+ sys.exit(1)
+
+ return template_dir
+
+
+def replace_template_variables(content: str, project_name: str) -> str:
+ """Replace template variables in file content."""
+ return content.replace("{{PROJECT_NAME}}", project_name)
+
+
+def remove_apache_license_header(content: str, file_extension: str) -> str:
+ """Remove Apache license header from file content based on file type."""
+ if file_extension in [".ts", ".tsx", ".js", ".jsx"]:
+ license_pattern = r"/\*!\s*\*\s*Licensed to the Apache Software Foundation.*?\*/\s*"
+ content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+ elif file_extension in [".md"]:
+ license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*"
+ content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+ elif file_extension in [".html"]:
+ license_pattern = r"<!--\s*Licensed to the Apache Software Foundation.*?-->\s*"
+ content = re.sub(license_pattern, "", content, flags=re.DOTALL)
+
+ return content
+
+
+def copy_template_files(template_dir: Path, project_path: Path, project_name: str) -> None:
+ for item in template_dir.rglob("*"):
+ if item.is_file():
+ # Calculate relative path from template root
+ rel_path = item.relative_to(template_dir)
+ target_path = project_path / rel_path
+
+ target_path.parent.mkdir(parents=True, exist_ok=True)
+
+ with open(item, encoding="utf-8") as f:
+ content = f.read()
+
+ content = replace_template_variables(content, project_name)
+
+ file_extension = item.suffix.lower()
+ content = remove_apache_license_header(content, file_extension)
+
+ with open(target_path, "w", encoding="utf-8") as f:
+ f.write(content)
+
+ print(f" Created: {rel_path}")
+
+
+def bootstrap_react_plugin(args) -> None:
+ """Bootstrap a new React plugin project."""
+ project_name = args.name
+ target_dir = args.dir if args.dir else project_name
+
+ project_path = Path(target_dir).resolve()
+ template_dir = get_template_dir()
+
+ if project_path.exists():
+ print(f"Error: Directory '{project_path}' already exists!")
+ sys.exit(1)
+
+ if not project_name.replace("-", "").replace("_", "").isalnum():
+ print("Error: Project name should only contain letters, numbers, hyphens, and underscores")
+ sys.exit(1)
+
+ print(f"Creating React plugin project: {project_name}")
+ print(f"Target directory: {project_path}")
+ print(f"Template directory: {template_dir}")
+
+ project_path.mkdir(parents=True, exist_ok=True)
+
+ try:
+ # Copy template files
+ print("Copying template files...")
+ copy_template_files(template_dir, project_path, project_name)
+
+ print(f"\nā
Successfully created {project_name}!")
+ print("\nNext steps:")
+ print(f" cd {target_dir}")
+ print(" pnpm install")
+ print(" pnpm dev")
+ print("\nHappy coding! š")
+
+ except Exception as e:
+ print(f"Error creating project: {e}")
+ if project_path.exists():
+ shutil.rmtree(project_path)
+ sys.exit(1)
+
+
+def main():
+ """Main CLI entry point."""
+ parser = argparse.ArgumentParser(
+ description="Bootstrap a new React UI plugin project",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog="""
+Examples:
+ python bootstrap.py my-plugin
+ python bootstrap.py my-plugin --dir /path/to/projects/my-plugin
+
+This will create a new React project with all the necessary configuration
+files, dependencies, and structure needed for Airflow plugin development.
+ """,
+ )
+
+ parser.add_argument(
+ "name",
+ help="Name of the React plugin project (letters, numbers, hyphens, and underscores only)",
+ )
+
+ parser.add_argument(
+ "--dir",
+ "-d",
+ help="Target directory for the project (defaults to project name)",
+ )
+
+ parser.add_argument(
+ "--verbose",
+ "-v",
+ action="store_true",
+ help="Enable verbose output",
+ )
+
+ args = parser.parse_args()
+
+ try:
+ bootstrap_react_plugin(args)
+ except KeyboardInterrupt:
+ print("\n\nOperation cancelled by user.")
+ sys.exit(1)
+ except Exception as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dev/react-plugin-tools/react_plugin_template/.gitignore b/dev/react-plugin-tools/react_plugin_template/.gitignore
new file mode 100644
index 00000000000..aa80e7d6f38
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.gitignore
@@ -0,0 +1,28 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.pnpm-store
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Coverage
+coverage
diff --git a/dev/react-plugin-tools/react_plugin_template/.prettierignore b/dev/react-plugin-tools/react_plugin_template/.prettierignore
new file mode 100644
index 00000000000..c745cd9169a
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.prettierignore
@@ -0,0 +1,5 @@
+dist/
+*.md
+*.yaml
+coverage/*
+node_modules/
diff --git a/dev/react-plugin-tools/react_plugin_template/.prettierrc b/dev/react-plugin-tools/react_plugin_template/.prettierrc
new file mode 100644
index 00000000000..604ac7a3004
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/.prettierrc
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/prettierrc",
+ "endOfLine": "lf",
+ "importOrder": ["<THIRD_PARTY_MODULES>", "^src/", "^[./]"],
+ "importOrderSeparation": true,
+ "jsxSingleQuote": false,
+ "plugins": ["@trivago/prettier-plugin-sort-imports"],
+ "printWidth": 110,
+ "singleQuote": false,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "useTabs": false
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/README.md b/dev/react-plugin-tools/react_plugin_template/README.md
new file mode 100644
index 00000000000..112ef528e5f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/README.md
@@ -0,0 +1,138 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+ -->
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [{{PROJECT_NAME}}](#project_name)
+ - [Development](#development)
+ - [Best Practices](#best-practices)
+ - [Troubleshooting](#troubleshooting)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+# {{PROJECT_NAME}}
+
+This is a React plugin template for Apache Airflow that builds as a library component.
+
+## Development
+
+This template is configured to build your React component as a library that can be consumed by other applications.
+
+### Available Scripts
+
+- `pnpm dev` - Start development server with hot reload
+- `pnpm build` - Build the library for production
+- `pnpm build:types` - Generate TypeScript declaration files only
+- `pnpm build:lib` - Build the JavaScript library only
+- `pnpm test` - Run tests
+- `pnpm lint` - Check code quality
+- `pnpm format` - Format code
+
+### Library Output
+
+When you run `pnpm build`, the template generates:
+
+- `dist/main.js` - ES module JavaScript library
+- `dist/main.d.ts` - TypeScript declaration file
+- Source maps for debugging
+
+### Usage as a Library
+
+After building, other applications can import your component:
+
+```typescript
+import { PluginComponent } from 'your-plugin-name';
+
+// Use in your React app
+<PluginComponent />
+```
+
+### Development Mode
+
+For development and testing, use `pnpm dev` which will:
+
+- Start a development server on port 5173
+- Load the component using `src/dev.tsx` entry point
+- Enable hot module replacement
+
+### Library Configuration
+
+The template is configured with:
+
+- **Vite** for fast building and development
+- **TypeScript** with declaration file generation
+- **CSS injection** - styles are automatically injected into the JavaScript bundle
+- **External dependencies** - React and other common libraries are marked as external to reduce bundle size
+
+### Customization
+
+1. **Component Props**: Update the `PluginComponentProps` interface in `src/main.tsx`
+2. **External Dependencies**: Modify the `external` array in `vite.config.ts`
+3. **Build Output**: Adjust library configuration in `vite.config.ts`
+
+### Package Configuration
+
+The `package.json` is configured with:
+
+- `main` and `module` pointing to the built library
+- `types` pointing to generated declaration files
+- `exports` for modern import/export support
+- `files` array specifying what gets published
+
+This ensures your plugin can be consumed as both a CommonJS and ES module with full TypeScript support.
+
+## Best Practices
+
+1. **Keep React External**: Always mark React ecosystem as external to avoid conflicts
+2. **Global Naming**: Use standardized global name (`AirflowPlugin`) for consistency
+3. **Error Handling**: Implement proper error boundaries and fallbacks
+4. **TypeScript**: Use proper typing for plugin props and exports
+5. **Bundle Size**: Monitor bundle size and externalize large dependencies if needed
+
+## Troubleshooting
+
+### Common Issues
+
+**"Failed to resolve module specifier 'react'"**
+
+- Ensure React is marked as external in `vite.config.ts`
+- Verify host application exposes React globally
+
+**"Cannot read properties of null (reading 'useState')"**
+
+- React instance mismatch - check external configuration
+- Verify global React is properly set in host application
+
+**"Objects are not valid as a React child"**
+
+- Ensure you're returning component functions, not JSX elements
+- Check that lazy loading returns proper component structure
+
+**MIME type issues**
+
+- Ensure `.js` and `.cjs` files are served with correct MIME type
+
+For more help, check the main project documentation.
+
+### Deployment to Airflow Plugins
+
+Once the development is complete, you can build the library using `pnpm build` and then host the content of the `dist` folder. You can do that on your own infrastructure or within airflow
+by adding static file serving to your api server via registering a plugin `fastapi_apps`. You can take a look at the [Airflow documentation](https://airflow.apache.org/docs/apache-airflow/stable/plugins.html) for more information on how to do that.
diff --git a/dev/react-plugin-tools/react_plugin_template/eslint.config.js b/dev/react-plugin-tools/react_plugin_template/eslint.config.js
new file mode 100644
index 00000000000..57b4176ed9f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/eslint.config.js
@@ -0,0 +1,58 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 js from "@eslint/js";
+import globals from "globals";
+import perfectionist from "eslint-plugin-perfectionist";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+import prettierConfig from "eslint-config-prettier";
+
+export default tseslint.config(
+ { ignores: ["dist"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: "latest",
+ globals: globals.browser,
+ },
+ plugins: {
+ perfectionist,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "perfectionist/sort-objects": [
+ "error",
+ {
+ type: "natural",
+ order: "asc",
+ },
+ ],
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ },
+ },
+ prettierConfig
+);
diff --git a/dev/react-plugin-tools/react_plugin_template/index.html b/dev/react-plugin-tools/react_plugin_template/index.html
new file mode 100644
index 00000000000..0c61743326d
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/index.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>{{PROJECT_NAME}} - Development</title>
+ </head>
+ <body>
+ <div id="root" style="width:100%;height:100%;position:fixed;"></div>
+ <script type="module" src="/src/dev.tsx"></script>
+ </body>
+</html>
diff --git a/dev/react-plugin-tools/react_plugin_template/package.json b/dev/react-plugin-tools/react_plugin_template/package.json
new file mode 100644
index 00000000000..5f5ed1ffe82
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/package.json
@@ -0,0 +1,75 @@
+{
+ "name": "{{PROJECT_NAME}}",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "main": "./dist/main.js",
+ "module": "./dist/main.js",
+ "types": "./dist/main.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/main.js",
+ "types": "./dist/main.d.ts"
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "dev": "vite --port 5173 --strictPort",
+ "build": "vite build",
+ "build:types": "tsc --p tsconfig.lib.json",
+ "build:lib": "vite build",
+ "lint": "eslint --quiet && tsc --p tsconfig.app.json",
+ "lint:fix": "eslint --fix && tsc --p tsconfig.app.json",
+ "format": "pnpm prettier --write .",
+ "preview": "vite preview",
+ "test": "vitest run",
+ "coverage": "vitest run --coverage"
+ },
+ "dependencies": {
+ "@chakra-ui/anatomy": "^2.3.4",
+ "@chakra-ui/react": "^3.20.0",
+ "@emotion/react": "^11.14.0",
+ "next-themes": "^0.3.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@eslint/compat": "^1.2.9",
+ "@eslint/js": "^9.25.1",
+ "@stylistic/eslint-plugin": "^2.13.0",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.3.0",
+ "@trivago/prettier-plugin-sort-imports": "^4.3.0",
+ "@types/node": "^22.15.3",
+ "@types/react": "^18.3.19",
+ "@types/react-dom": "^18.3.5",
+ "@vitejs/plugin-react-swc": "^3.9.0",
+ "@vitest/coverage-v8": "^2.1.9",
+ "eslint": "^9.25.1",
+ "eslint-config-prettier": "^10.1.2",
+ "eslint-plugin-jsx-a11y": "^6.10.2",
+ "eslint-plugin-prettier": "^5.2.6",
+ "eslint-plugin-react": "^7.37.5",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-perfectionist": "^4.12.3",
+ "eslint-plugin-react-refresh": "^0.4.20",
+ "eslint-plugin-unicorn": "^55.0.0",
+ "globals": "^15.15.0",
+ "happy-dom": "^17.4.6",
+ "prettier": "^3.5.3",
+ "typescript": "~5.8.3",
+ "typescript-eslint": "^8.31.1",
+ "vite": "^5.4.19",
+ "vite-plugin-css-injected-by-js": "^3.5.2",
+ "vite-plugin-dts": "^4.3.0",
+ "vitest": "^2.1.9"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "@swc/core",
+ "esbuild"
+ ]
+ }
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx
new file mode 100644
index 00000000000..2b0c93cd403
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/ColorModeProvider.tsx
@@ -0,0 +1,25 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { ThemeProvider } from "next-themes";
+import type { ThemeProviderProps } from "next-themes/dist/types";
+
+export const ColorModeProvider = (props: ThemeProviderProps) => (
+ <ThemeProvider attribute="class" disableTransitionOnChange {...props} />
+);
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts
new file mode 100644
index 00000000000..30edc0a3216
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/index.ts
@@ -0,0 +1,21 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 "./ColorModeProvider";
+export * from "./useColorMode";
diff --git a/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx
new file mode 100644
index 00000000000..3cbca6b5f2e
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/context/colorMode/useColorMode.tsx
@@ -0,0 +1,33 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { useTheme } from "next-themes";
+
+export const useColorMode = () => {
+ const { resolvedTheme, setTheme } = useTheme();
+ const toggleColorMode = () => {
+ setTheme(resolvedTheme === "light" ? "dark" : "light");
+ };
+
+ return {
+ colorMode: resolvedTheme as "dark" | "light" | undefined,
+ setColorMode: setTheme,
+ toggleColorMode,
+ };
+};
diff --git a/dev/react-plugin-tools/react_plugin_template/src/dev.tsx b/dev/react-plugin-tools/react_plugin_template/src/dev.tsx
new file mode 100644
index 00000000000..76b2b0016fd
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/dev.tsx
@@ -0,0 +1,30 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+
+import PluginComponent from "./main";
+
+// Development entry point for testing the component
+createRoot(document.querySelector("#root") as HTMLDivElement).render(
+ <StrictMode>
+ <PluginComponent />
+ </StrictMode>,
+);
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/dev/react-plugin-tools/react_plugin_template/src/main.tsx
similarity index 50%
copy from airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
copy to dev/react-plugin-tools/react_plugin_template/src/main.tsx
index 0f80af777c8..8ee670c0dac 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/dev/react-plugin-tools/react_plugin_template/src/main.tsx
@@ -16,31 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
-import { useParams } from "react-router-dom";
-import type { ReactAppResponse } from "openapi/requests/types.gen";
+import { ChakraProvider } from "@chakra-ui/react";
+import { FC } from "react";
-import { ErrorPage } from "./Error";
+import { ColorModeProvider } from "src/context/colorMode";
+import { HomePage } from "src/pages/HomePage";
-export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse }) => {
- const { dagId, mapIndex, runId, taskId } = useParams();
+import { system } from "./theme";
- const Plugin = lazy(() =>
- // We are assuming the plugin manager is trusted and the bundle_url is safe
- import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
- console.error("Component Failed Loading:", error);
-
- return {
- default: <ErrorPage />,
- };
- }),
- );
+export interface PluginComponentProps {
+ // Add any props your plugin component needs
+}
+/**
+ * Main plugin component
+ */
+const PluginComponent: FC<PluginComponentProps> = (props) => {
return (
- <Suspense fallback={<Spinner />}>
- <Plugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />
- </Suspense>
+ <ChakraProvider value={system}>
+ <ColorModeProvider>
+ <HomePage />
+ </ColorModeProvider>
+ </ChakraProvider>
);
};
+
+export default PluginComponent;
diff --git a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx b/dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
similarity index 50%
copy from airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
copy to dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
index 0f80af777c8..3b90631de3d 100644
--- a/airflow-core/src/airflow/ui/src/pages/ReactPlugin.tsx
+++ b/dev/react-plugin-tools/react_plugin_template/src/pages/HomePage.tsx
@@ -16,31 +16,27 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Spinner } from "@chakra-ui/react";
-import { lazy, Suspense } from "react";
-import { useParams } from "react-router-dom";
-import type { ReactAppResponse } from "openapi/requests/types.gen";
+import { Box, Button, Heading, Text, VStack } from "@chakra-ui/react";
-import { ErrorPage } from "./Error";
+import { useColorMode } from "src/context/colorMode";
-export const ReactPlugin = ({ reactApp }: { readonly reactApp: ReactAppResponse }) => {
- const { dagId, mapIndex, runId, taskId } = useParams();
-
- const Plugin = lazy(() =>
- // We are assuming the plugin manager is trusted and the bundle_url is safe
- import(/* @vite-ignore */ reactApp.bundle_url).catch((error: unknown) => {
- console.error("Component Failed Loading:", error);
-
- return {
- default: <ErrorPage />,
- };
- }),
- );
+export const HomePage = () => {
+ const { toggleColorMode } = useColorMode();
return (
- <Suspense fallback={<Spinner />}>
- <Plugin dagId={dagId} mapIndex={mapIndex} runId={runId} taskId={taskId} />
- </Suspense>
+ <Box p={8} bg="bg.subtle" flexGrow={1} height="100%">
+ <VStack gap={8} align="center" justify="center" flexGrow={1} height="100%">
+ <Heading size="2xl" textAlign="center" color="fg">
+ Welcome to Your New React App!
+ </Heading>
+ <Text fontSize="lg" color="fg.muted">
+ This project was bootstrapped with the Airflow React Plugin tool.
+ </Text>
+ <Button onClick={toggleColorMode} colorPalette="blue">
+ Toggle Theme
+ </Button>
+ </VStack>
+ </Box>
);
};
diff --git a/dev/react-plugin-tools/react_plugin_template/src/theme.ts b/dev/react-plugin-tools/react_plugin_template/src/theme.ts
new file mode 100644
index 00000000000..706ae0ed57e
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/theme.ts
@@ -0,0 +1,176 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/* eslint-disable perfectionist/sort-objects */
+import { createSystem, defaultConfig, defineConfig } from "@chakra-ui/react";
+
+const generateSemanticTokens = (color: string, darkContrast: string = "white") => ({
+ solid: { value: `{colors.${color}.600}` },
+ contrast: { value: { _light: "white", _dark: darkContrast } },
+ fg: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+ muted: { value: { _light: `{colors.${color}.200}`, _dark: `{colors.${color}.800}` } },
+ subtle: { value: { _light: `{colors.${color}.100}`, _dark: `{colors.${color}.900}` } },
+ emphasized: { value: { _light: `{colors.${color}.300}`, _dark: `{colors.${color}.700}` } },
+ focusRing: { value: { _light: `{colors.${color}.800}`, _dark: `{colors.${color}.200}` } },
+});
+
+const customConfig = defineConfig({
+ theme: {
+ tokens: {
+ colors: {
+ // Default green was too light
+ success: {
+ "50": { value: "#E0FFE0" },
+ "100": { value: "#C2FFC2" },
+ "200": { value: "#80FF80" },
+ "300": { value: "#42FF42" },
+ "400": { value: "#00FF00" },
+ "500": { value: "#00C200" },
+ "600": { value: "#008000" },
+ "700": { value: "#006100" },
+ "800": { value: "#004200" },
+ "900": { value: "#001F00" },
+ "950": { value: "#000F00" },
+ },
+ failed: defaultConfig.theme?.tokens?.colors?.red ?? {},
+ // Default gray was too dark
+ queued: {
+ "50": { value: "#F5F5F5" },
+ "100": { value: "#EBEBEB" },
+ "200": { value: "#D4D4D4" },
+ "300": { value: "#BFBFBF" },
+ "400": { value: "#ABABAB" },
+ "500": { value: "#969696" },
+ "600": { value: "#808080" },
+ "700": { value: "#616161" },
+ "800": { value: "#404040" },
+ "900": { value: "#212121" },
+ "950": { value: "#0F0F0F" },
+ },
+ skipped: defaultConfig.theme?.tokens?.colors?.pink ?? {},
+ up_for_reschedule: defaultConfig.theme?.tokens?.colors?.cyan ?? {},
+ up_for_retry: defaultConfig.theme?.tokens?.colors?.yellow ?? {},
+ upstream_failed: defaultConfig.theme?.tokens?.colors?.orange ?? {},
+ // lime
+ running: {
+ "50": { value: "#EFFBEF" },
+ "100": { value: "#DEF7DE" },
+ "200": { value: "#B9EEB9" },
+ "300": { value: "#98E698" },
+ "400": { value: "#78DE78" },
+ "500": { value: "#53D553" },
+ "600": { value: "#32CD32" },
+ "700": { value: "#269C26" },
+ "800": { value: "#196719" },
+ "900": { value: "#0D350D" },
+ "950": { value: "#061906" },
+ },
+ // violet
+ restarting: {
+ "50": { value: "#F6EBFF" },
+ "100": { value: "#EDD6FF" },
+ "200": { value: "#D9A8FF" },
+ "300": { value: "#C880FF" },
+ "400": { value: "#B657FF" },
+ "500": { value: "#A229FF" },
+ "600": { value: "#8F00FF" },
+ "700": { value: "#6E00C2" },
+ "800": { value: "#480080" },
+ "900": { value: "#260042" },
+ "950": { value: "#11001F" },
+ },
+ // mediumpurple
+ deferred: {
+ "50": { value: "#F6F3FC" },
+ "100": { value: "#EDE7F9" },
+ "200": { value: "#DACEF3" },
+ "300": { value: "#C8B6ED" },
+ "400": { value: "#B9A1E7" },
+ "500": { value: "#A689E1" },
+ "600": { value: "#9370DB" },
+ "700": { value: "#6432C8" },
+ "800": { value: "#412182" },
+ "900": { value: "#211041" },
+ "950": { value: "#100821" },
+ },
+ // tan
+ scheduled: {
+ "50": { value: "#FBF8F4" },
+ "100": { value: "#F8F3ED" },
+ "200": { value: "#F1E7DA" },
+ "300": { value: "#E8D9C4" },
+ "400": { value: "#E1CDB2" },
+ "500": { value: "#DAC1A0" },
+ "600": { value: "#D2B48C" },
+ "700": { value: "#B9894B" },
+ "800": { value: "#7D5C31" },
+ "900": { value: "#3E2E18" },
+ "950": { value: "#21180D" },
+ },
+ // lightblue
+ none: {
+ "50": { value: "#F7FBFD" },
+ "100": { value: "#F3F9FB" },
+ "200": { value: "#E4F2F7" },
+ "300": { value: "#D8ECF3" },
+ "400": { value: "#C8E5EE" },
+ "500": { value: "#BDDFEB" },
+ "600": { value: "#ADD8E6" },
+ "700": { value: "#5FB2CE" },
+ "800": { value: "#30819C" },
+ "900": { value: "#18414E" },
+ "950": { value: "#0C2027" },
+ },
+ // lightgrey
+ removed: {
+ "50": { value: "#FCFCFC" },
+ "100": { value: "#F7F7F7" },
+ "200": { value: "#F0F0F0" },
+ "300": { value: "#E8E8E8" },
+ "400": { value: "#E0E0E0" },
+ "500": { value: "#DBDBDB" },
+ "600": { value: "#D3D3D3" },
+ "700": { value: "#9E9E9E" },
+ "800": { value: "#696969" },
+ "900": { value: "#363636" },
+ "950": { value: "#1A1A1A" },
+ },
+ },
+ },
+ semanticTokens: {
+ colors: {
+ success: generateSemanticTokens("success"),
+ failed: defaultConfig.theme?.semanticTokens?.colors?.red ?? {},
+ queued: generateSemanticTokens("queued"),
+ skipped: defaultConfig.theme?.semanticTokens?.colors?.pink ?? {},
+ up_for_reschedule: defaultConfig.theme?.semanticTokens?.colors?.cyan ?? {},
+ up_for_retry: defaultConfig.theme?.semanticTokens?.colors?.yellow ?? {},
+ upstream_failed: defaultConfig.theme?.semanticTokens?.colors?.orange ?? {},
+ running: generateSemanticTokens("running"),
+ restarting: generateSemanticTokens("restarting"),
+ deferred: generateSemanticTokens("deferred"),
+ scheduled: generateSemanticTokens("scheduled"),
+ none: generateSemanticTokens("none", "black"),
+ removed: generateSemanticTokens("removed", "black"),
+ },
+ },
+ },
+});
+
+export const system = createSystem(defaultConfig, customConfig);
diff --git a/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts b/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts
new file mode 100644
index 00000000000..a1fdcdd1e6f
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/src/vite-env.d.ts
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/// <reference types="vite/client" />
diff --git a/dev/react-plugin-tools/react_plugin_template/testsSetup.ts b/dev/react-plugin-tools/react_plugin_template/testsSetup.ts
new file mode 100644
index 00000000000..c060cb550be
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/testsSetup.ts
@@ -0,0 +1,20 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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/vitest";
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
similarity index 72%
copy from airflow-core/src/airflow/ui/tsconfig.node.json
copy to dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
index a731173fc6c..d91d91e9622 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.app.json
@@ -2,7 +2,8 @@
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
- "lib": ["ES2023"],
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
@@ -12,17 +13,18 @@
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
+ "jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
- "src/*": ["./src/*"],
- "openapi/*": ["./openapi-gen/*"]
+ "src/*": ["./src/*"]
}
},
- "include": ["vite.config.ts"]
+ "include": ["src"]
}
diff --git a/dev/react-plugin-tools/react_plugin_template/tsconfig.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.json
new file mode 100644
index 00000000000..b6a01bf3b76
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" },
+ { "path": "./tsconfig.lib.json" }
+ ]
+}
diff --git a/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json
new file mode 100644
index 00000000000..d423df60f83
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.lib.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "./tsconfig.app.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "noEmit": false,
+ "allowImportingTsExtensions": false
+ },
+ "include": ["src/main.tsx"],
+ "exclude": ["**/*.test.*", "**/*.spec.*"]
+}
diff --git a/airflow-core/src/airflow/ui/tsconfig.node.json b/dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
similarity index 89%
copy from airflow-core/src/airflow/ui/tsconfig.node.json
copy to dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
index a731173fc6c..9cd36cf34db 100644
--- a/airflow-core/src/airflow/ui/tsconfig.node.json
+++ b/dev/react-plugin-tools/react_plugin_template/tsconfig.node.json
@@ -18,10 +18,10 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
+ "noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
- "src/*": ["./src/*"],
- "openapi/*": ["./openapi-gen/*"]
+ "src/*": ["./src/*"]
}
},
"include": ["vite.config.ts"]
diff --git a/dev/react-plugin-tools/react_plugin_template/vite.config.ts b/dev/react-plugin-tools/react_plugin_template/vite.config.ts
new file mode 100644
index 00000000000..dc9384ff2bc
--- /dev/null
+++ b/dev/react-plugin-tools/react_plugin_template/vite.config.ts
@@ -0,0 +1,84 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 "@vitejs/plugin-react-swc";
+import { resolve } from "node:path";
+import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
+import dts from "vite-plugin-dts";
+import { defineConfig } from "vitest/config";
+
+// https://vitejs.dev/config/
+export default defineConfig(({ command }) => {
+ const isLibraryBuild = command === 'build';
+
+ return {
+ base: "./",
+ build: isLibraryBuild ? {
+ chunkSizeWarningLimit: 1600,
+ lib: {
+ entry: resolve("src", "main.tsx"),
+ fileName: 'main',
+ formats: ['umd'],
+ name: 'AirflowPlugin',
+ },
+ rollupOptions: {
+ external: ["react", "react-dom"],
+ output: {
+ globals: {
+ react: "React",
+ "react/jsx-runtime": "ReactJSXRuntime",
+ },
+ },
+ },
+ } : {
+ // Development build configuration
+ chunkSizeWarningLimit: 1600
+ },
+ define: {
+ global: "globalThis",
+ "process.env": "{}",
+ // Define process.env for browser compatibility
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ },
+ plugins: [
+ react(),
+ cssInjectedByJsPlugin(),
+ ...(isLibraryBuild ? [dts({
+ include: ["src/main.tsx"],
+ insertTypesEntry: true,
+ outDir: "dist"
+ })] : [])
+ ],
+ resolve: { alias: { src: "/src" } },
+ server: {
+ cors: true, // Only used by the dev server.
+ },
+ test: {
+ coverage: {
+ include: ["src/**/*.ts", "src/**/*.tsx"],
+ },
+ css: true,
+ environment: "happy-dom",
+ globals: true,
+ mockReset: true,
+ restoreMocks: true,
+ setupFiles: "./testsSetup.ts",
+ },
+ };
+});