Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions dev/react-plugin-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [Overview](#overview)
- [Files](#files)
- [Quick Start](#quick-start)
- [Optional AI Agent Rules](#optional-ai-agent-rules)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -57,3 +58,10 @@ 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
```

## Optional AI Agent Rules

The bootstrap tool asks whether to include AI agent coding rules. Answering yes adds an
`ai-agent-rules/` directory to the generated project with Airflow React plugin conventions and
validation commands. The files use tool-agnostic Markdown, so follow the generated directory's
README to adapt them to the format supported by your AI coding tool.
23 changes: 21 additions & 2 deletions dev/react-plugin-tools/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import sys
from pathlib import Path

AI_AGENT_RULES_DIR = "ai-agent-rules"


def get_template_dir() -> Path:
"""Get the template directory path."""
Expand All @@ -46,6 +48,12 @@ def get_template_dir() -> Path:
return template_dir


def should_include_ai_agent_rules() -> bool:
"""Ask whether AI agent coding rules should be included."""
answer = input("Include AI agent coding rules? [y/N]: ")
return answer.strip().casefold() in {"y", "yes"}


def replace_template_variables(content: str, project_name: str) -> str:
"""Replace template variables in file content."""
return content.replace("{{PROJECT_NAME}}", project_name)
Expand All @@ -66,11 +74,15 @@ def remove_apache_license_header(content: str, file_extension: str) -> str:
return content


def copy_template_files(template_dir: Path, project_path: Path, project_name: str) -> None:
def copy_template_files(
template_dir: Path, project_path: Path, project_name: str, *, include_ai_agent_rules: bool
) -> None:
for item in template_dir.rglob("*"):
if item.is_file():
# Calculate relative path from template root
rel_path = item.relative_to(template_dir)
if not include_ai_agent_rules and rel_path.parts[0] == AI_AGENT_RULES_DIR:
continue
target_path = project_path / rel_path

target_path.parent.mkdir(parents=True, exist_ok=True)
Expand Down Expand Up @@ -105,6 +117,8 @@ def bootstrap_react_plugin(args) -> None:
print("Error: Project name should only contain letters, numbers, hyphens, and underscores")
sys.exit(1)

include_ai_agent_rules = should_include_ai_agent_rules()

print(f"Creating React plugin project: {project_name}")
print(f"Target directory: {project_path}")
print(f"Template directory: {template_dir}")
Expand All @@ -114,7 +128,12 @@ def bootstrap_react_plugin(args) -> None:
try:
# Copy template files
print("Copying template files...")
copy_template_files(template_dir, project_path, project_name)
copy_template_files(
template_dir,
project_path,
project_name,
include_ai_agent_rules=include_ai_agent_rules,
)

print(f"\n✅ Successfully created {project_name}!")
print("\nNext steps:")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
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)*

- [AI agent rules](#ai-agent-rules)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# AI agent rules

These Markdown files describe the coding and validation conventions for this Airflow React plugin.
They are deliberately tool-agnostic and are not loaded automatically by AI coding tools.

Adapt the files to the format supported by your tool:

- Cursor project rules belong in `.cursor/rules/` and use the `.mdc` extension. Add any metadata
required for the desired rule scope. See the [Cursor rules documentation](https://docs.cursor.com/context/rules).
- GitHub Copilot can use a repository-wide `.github/copilot-instructions.md` file or path-specific
`.github/instructions/*.instructions.md` files. Path-specific files require `applyTo` frontmatter.
See the [GitHub Copilot custom instructions documentation](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions).
- For other tools, consult their documentation and copy or convert these files as needed.

Keep the adapted rules version-controlled and update them when the plugin's dependencies, scripts,
or build configuration change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!--
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)*

- [Airflow plugin integration](#airflow-plugin-integration)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Airflow plugin integration

- Preserve the library build and the default component export from `src/main.tsx`; Airflow loads the
generated bundle dynamically.
- Keep React, React DOM, React Router, and the JSX runtime external in `vite.config.ts`. They are
shared with the Airflow host application and bundling another copy can break hooks or routing.
- Keep the `AirflowPlugin` UMD global name unless the host integration changes with it.
- Use `globalThis.ChakraUISystem` in production and retain the local theme fallback for development.
- Build UI with Chakra components and semantic theme tokens. Avoid raw color values and styling that
bypasses the inherited Airflow theme.
- Integrate through Airflow's documented public plugin and REST API surfaces. Do not import private
modules from the Airflow Core UI into the plugin bundle.
- Treat the React plugin interface as experimental and verify compatibility when upgrading external
dependencies shared with Airflow.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!--
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 and TypeScript](#react-and-typescript)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# React and TypeScript

- Write function components and hooks. Do not introduce class components.
- Keep TypeScript strict. Define explicit component props and domain types; do not use `any` or
suppress type errors.
- Use the configured `src` path alias instead of deep relative imports.
- Calculate derived values while rendering instead of synchronizing them with `useState` and
`useEffect`.
- Use `useEffect` only to synchronize with an external system. Keep dependencies complete and make
cleanup symmetrical with setup.
- Handle loading, error, empty, and success states for asynchronous work.
- Preserve keyboard behavior, focus management, accessible names, and semantic elements when adding
or changing interactions.
- Keep reusable components focused and place shared application code under `src/` rather than in the
library entry point.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!--
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)*

- [Testing and validation](#testing-and-validation)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

# Testing and validation

- Add a colocated `.test.tsx` file for new components and changed behavior.
- Use Vitest and Testing Library to test behavior visible to users. Prefer accessible queries and
user interactions over implementation details.
- Cover relevant loading, error, empty, and success states as well as important interactions.
- Do not test React, Chakra UI, React Router, or other third-party internals.
- Keep tests deterministic and restore mocks between tests.
- Before finishing a change, run `pnpm lint`, `pnpm test`, and `pnpm build`. Run `pnpm format` when
source formatting changes.
83 changes: 83 additions & 0 deletions scripts/tests/test_react_plugin_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 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.
from __future__ import annotations

import importlib.util
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest import mock

import pytest

MODULE_PATH = Path(__file__).resolve().parents[2] / "dev" / "react-plugin-tools" / "bootstrap.py"


@pytest.fixture
def bootstrap_module():
module_name = "test_react_plugin_bootstrap_module"
sys.modules.pop(module_name, None)
spec = importlib.util.spec_from_file_location(module_name, MODULE_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
yield module
finally:
sys.modules.pop(module_name, None)


@pytest.mark.parametrize(
"answer,expected",
[
("y", True),
("YES", True),
(" yes ", True),
("", False),
("n", False),
("anything else", False),
],
)
@mock.patch("builtins.input")
def test_should_include_ai_agent_rules(mock_input, bootstrap_module, answer, expected):
mock_input.return_value = answer

assert bootstrap_module.should_include_ai_agent_rules() is expected


@pytest.mark.parametrize(("answer", "include_ai_agent_rules"), [("yes", True), ("", False)])
@mock.patch("builtins.input")
def test_bootstrap_conditionally_includes_ai_agent_rules(
mock_input, bootstrap_module, tmp_path, monkeypatch, answer, include_ai_agent_rules
):
mock_input.return_value = answer
template_dir = tmp_path / "template"
rules_dir = template_dir / bootstrap_module.AI_AGENT_RULES_DIR
rules_dir.mkdir(parents=True)
(template_dir / "README.md").write_text("project", encoding="utf-8")
(rules_dir / "react.md").write_text("rules", encoding="utf-8")
project_path = tmp_path / "project"
get_template_dir = mock.create_autospec(bootstrap_module.get_template_dir, return_value=template_dir)
monkeypatch.setattr(bootstrap_module, "get_template_dir", get_template_dir)

bootstrap_module.bootstrap_react_plugin(SimpleNamespace(name="project", dir=project_path))

assert (project_path / "README.md").is_file()
assert (project_path / bootstrap_module.AI_AGENT_RULES_DIR / "react.md").exists() is (
include_ai_agent_rules
)
Loading