feat: Add Markdown View Plugin with React and Chakra UI
- Implemented main application structure with App and View components. - Integrated React Markdown for rendering Markdown content. - Added API function to fetch Markdown content from the backend. - Created a custom Chakra UI theme to align with Airflow's design. - Configured Vite for building the application with appropriate asset paths. - Included a sample Markdown file for initial content display. - Set up TypeScript configuration for better development experience.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -299,4 +299,5 @@ dist
|
|||||||
.yarn/build-state.yml
|
.yarn/build-state.yml
|
||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
|||||||
76
markdown_view_plugin/README.md
Normal file
76
markdown_view_plugin/README.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
|
||||||
|
## Building the Plugin
|
||||||
|
|
||||||
|
You can build the plugin into a wheel file using any PEP-517 compatible build frontend. The build process will automatically:
|
||||||
|
|
||||||
|
1. Install UI dependencies using pnpm
|
||||||
|
2. Build the React frontend
|
||||||
|
3. Package everything into a wheel file
|
||||||
|
|
||||||
|
This is handled by a custom Hatch build hook (`hatch_build.py`).
|
||||||
|
|
||||||
|
### Example build commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using uv (recommended)
|
||||||
|
uv build
|
||||||
|
|
||||||
|
# Or using pip
|
||||||
|
pip wheel .
|
||||||
|
|
||||||
|
# Or using hatch directly
|
||||||
|
hatch build
|
||||||
|
```
|
||||||
|
|
||||||
|
The build process will fail if `pnpm` is not installed or if the UI build fails.
|
||||||
|
|
||||||
|
## Quick Test Checklist
|
||||||
|
|
||||||
|
After building, you can quickly verify that the UI static files are included in the wheel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# one-liner smoke test
|
||||||
|
rm -rf ui/dist && uv build
|
||||||
|
unzip -l dist/airflow_markdown_view_plugin-*.whl | grep 'ui/dist/index.*.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
If the grep shows your JS bundles, you’re golden. If not, the hook didn’t run—fix it before shipping.
|
||||||
|
|
||||||
|
# Markdown View Airflow Plugin
|
||||||
|
|
||||||
|
This plugin allows users to render Markdown content from a `view.md` file directly within the Airflow UI.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Displays content from a `markdown_view_plugin/view.md` file.
|
||||||
|
* Uses FastAPI for the backend and React/Vite for the frontend.
|
||||||
|
* Integrates with Airflow's UI theme, including light/dark mode support.
|
||||||
|
* Provides a theme toggle button within the plugin's view.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Place the Plugin:** Put the entire `markdown_view_plugin` directory into your Airflow `plugins` folder.
|
||||||
|
2. **Install UI Dependencies:** Navigate to the `markdown_view_plugin/ui` directory and run:
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
3. **Build the UI:** After installing dependencies, build the React frontend:
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
This will create a `dist` folder inside `markdown_view_plugin/ui`.
|
||||||
|
4. **Restart Airflow Webserver:** After adding the plugin and building the UI, restart your Airflow webserver and scheduler.
|
||||||
|
5. **Access the Plugin:** In the Airflow UI, navigate to the "Plugins" menu. You should find a link to the "Markdown View" (the exact name and category might depend on future menu link configuration in `markdown_view_plugin.py`). Clicking this link will take you to the plugin's page.
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
* To change the displayed content, edit the `markdown_view_plugin/view.md` file.
|
||||||
|
* The UI can be further customized by modifying the React components in `markdown_view_plugin/ui/src/`.
|
||||||
|
* The backend logic is in `markdown_view_plugin/markdown_view_plugin.py`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
* **Backend:** The FastAPI application is defined in `markdown_view_plugin.py`.
|
||||||
|
* **Frontend:** The React/Vite application is in the `markdown_view_plugin/ui/` directory.
|
||||||
|
* To run the frontend in development mode (with hot reloading), navigate to `markdown_view_plugin/ui` and run `pnpm run dev`.
|
||||||
|
* Ensure your `vite.config.ts` has the correct `base` path and any necessary proxy configurations if you are developing against a running Airflow instance.
|
||||||
24
markdown_view_plugin/hatch_build.py
Normal file
24
markdown_view_plugin/hatch_build.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# hatch_build.py
|
||||||
|
import subprocess
|
||||||
|
import pathlib
|
||||||
|
import shutil
|
||||||
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||||
|
|
||||||
|
class MarkdownBuildHook(BuildHookInterface):
|
||||||
|
def initialize(self, version, build_data):
|
||||||
|
# 1. Compile the UI exactly once per build
|
||||||
|
ui_dir = pathlib.Path(__file__).parent / "ui"
|
||||||
|
dist_dir = ui_dir / "dist"
|
||||||
|
|
||||||
|
# Clean any existing dist directory to ensure fresh build
|
||||||
|
if dist_dir.exists():
|
||||||
|
shutil.rmtree(dist_dir)
|
||||||
|
|
||||||
|
# Install dependencies and build the UI
|
||||||
|
subprocess.run([
|
||||||
|
"pnpm", "install", "--frozen-lockfile"
|
||||||
|
], cwd=ui_dir, check=True)
|
||||||
|
subprocess.run(["pnpm", "run", "build"], cwd=ui_dir, check=True)
|
||||||
|
|
||||||
|
# 2. Force-include the compiled assets in the wheel
|
||||||
|
build_data["force_include"][str(dist_dir)] = "ui/dist"
|
||||||
85
markdown_view_plugin/markdown_view_plugin.py
Normal file
85
markdown_view_plugin/markdown_view_plugin.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Python backend for the Markdown View Plugin using FastAPI
|
||||||
|
|
||||||
|
import anyio # For asynchronous file operations
|
||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from airflow.plugins_manager import AirflowPlugin
|
||||||
|
import importlib.resources as resources
|
||||||
|
|
||||||
|
# Use importlib.resources to locate static files and view.md
|
||||||
|
static_files_dir_ref = resources.files("markdown_view_plugin") / "ui" / "dist"
|
||||||
|
view_md_path_ref = resources.files("markdown_view_plugin") / "view.md"
|
||||||
|
|
||||||
|
# Create a FastAPI app
|
||||||
|
markdown_fastapi_app = FastAPI(
|
||||||
|
title="Markdown View Plugin",
|
||||||
|
description="A plugin to render Markdown content in Airflow UI.",
|
||||||
|
version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mount static files for the React UI
|
||||||
|
# The path "/static/markdown_view_plugin" must match the 'base' in ui/vite.config.ts
|
||||||
|
with resources.as_file(static_files_dir_ref) as static_files_dir:
|
||||||
|
markdown_fastapi_app.mount(
|
||||||
|
"/static/markdown_view_plugin",
|
||||||
|
StaticFiles(directory=str(static_files_dir)),
|
||||||
|
name="markdown_view_plugin_static",
|
||||||
|
)
|
||||||
|
|
||||||
|
# API endpoint to get markdown content
|
||||||
|
@markdown_fastapi_app.get("/markdown_view/api/view")
|
||||||
|
async def get_markdown_content_api():
|
||||||
|
"""Asynchronously read and return the content of view.md."""
|
||||||
|
with resources.as_file(view_md_path_ref) as view_md_path:
|
||||||
|
try:
|
||||||
|
async with await anyio.open_file(view_md_path) as f:
|
||||||
|
content = await f.read()
|
||||||
|
return {"markdown": content}
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise HTTPException(status_code=404, detail="view.md not found")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Endpoint to serve the main index.html of the React app
|
||||||
|
@markdown_fastapi_app.get("/markdown_view")
|
||||||
|
async def serve_markdown_ui():
|
||||||
|
"""Serve the main index.html for the plugin's UI."""
|
||||||
|
with resources.as_file(static_files_dir_ref / "index.html") as index_html_path:
|
||||||
|
if not index_html_path.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="index.html not found. Did you build the UI? (cd ui && pnpm run build)",
|
||||||
|
)
|
||||||
|
return FileResponse(str(index_html_path))
|
||||||
|
|
||||||
|
# Define the Airflow plugin
|
||||||
|
class MarkdownViewPlugin(AirflowPlugin):
|
||||||
|
"""
|
||||||
|
Airflow Plugin for Markdown View.
|
||||||
|
|
||||||
|
This plugin provides a FastAPI backend and a React frontend
|
||||||
|
to display Markdown content within the Airflow UI.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "markdown_view_plugin"
|
||||||
|
fastapi_apps = [
|
||||||
|
{
|
||||||
|
"app": markdown_fastapi_app,
|
||||||
|
"name": "markdown_view_app", # A unique name for this FastAPI app
|
||||||
|
"app_mount": "/markdown_view_plugin_mount",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
# Example menu link (optional, can also be configured via Airflow UI settings)
|
||||||
|
menu_links = [
|
||||||
|
{
|
||||||
|
"name": "Markdown Viewer",
|
||||||
|
"href": "/markdown_view_plugin_mount/markdown_view", # This should match the app_mount + @markdown_fastapi_app.get path for the UI
|
||||||
|
"category": "Plugins",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
# For Airflow to pick up the plugin, the class name must match the filename (snake_case to PascalCase)
|
||||||
|
# or be explicitly defined in an __init__.py in the plugin\'s root directory.
|
||||||
|
# Assuming filename is markdown_view_plugin.py, class MarkdownViewPlugin is correct.
|
||||||
|
|
||||||
105
markdown_view_plugin/pyproject.toml
Normal file
105
markdown_view_plugin/pyproject.toml
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
[project]
|
||||||
|
name = "airflow-markdown-view-plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Airflow UI plugin to render Markdown content using FastAPI and React."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
authors = [
|
||||||
|
{name = "Abhishek Bhakat", email = "abhishek.bhakat@hotmail.com"}
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"apache-airflow>=3.0.0", # Adjust as per your Airflow version compatibility
|
||||||
|
"fastapi>=0.100.0", # Specify a version range as needed
|
||||||
|
"anyio>=3.0.0", # Specify a version range as needed
|
||||||
|
"importlib-resources>=5.0.0" # For static file access in Python <3.9, stdlib in >=3.9
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Environment :: Plugins",
|
||||||
|
"Framework :: Apache Airflow",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: System :: Monitoring"
|
||||||
|
]
|
||||||
|
license = {text = "MIT"} # Updated to new license expression
|
||||||
|
# license-files = ["LICEN[CS]E*"] # Optional: if you add a LICENSE file
|
||||||
|
|
||||||
|
# Hatch-specific way to specify top-level Python modules
|
||||||
|
py-modules = ["markdown_view_plugin"]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
GitHub = "https://github.com/abhishekbhakat/airflow-markdown-view-plugin" # Replace with actual URL if it exists
|
||||||
|
Issues = "https://github.com/abhishekbhakat/airflow-markdown-view-plugin/issues" # Replace with actual URL
|
||||||
|
|
||||||
|
[project.entry-points."airflow.plugins"]
|
||||||
|
markdown_view_plugin = "markdown_view_plugin:MarkdownViewPlugin"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"build>=1.2.2",
|
||||||
|
"pre-commit>=4.0.1",
|
||||||
|
"ruff>=0.9.2"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling>=1.24"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.hooks.custom]
|
||||||
|
path = "hatch_build.py"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
# Ensure data files are included in the wheel at the root.
|
||||||
|
# markdown_view_plugin.py will be handled by `project.py-modules`.
|
||||||
|
# These paths are relative to the pyproject.toml file.
|
||||||
|
include = [
|
||||||
|
"view.md",
|
||||||
|
"README.md",
|
||||||
|
"ui/dist" # This tells Hatch to include the ui/dist directory and its contents
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 200
|
||||||
|
indent-width = 4
|
||||||
|
fix = true
|
||||||
|
preview = true
|
||||||
|
|
||||||
|
lint.select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"C90", # Complexity
|
||||||
|
"C", # flake8-comprehensions
|
||||||
|
"ISC", # flake8-implicit-str-concat
|
||||||
|
"T10", # flake8-debugger
|
||||||
|
"A", # flake8-builtins
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
|
||||||
|
lint.ignore = [
|
||||||
|
"C416", # Unnecessary list comprehension - rewrite as a generator expression
|
||||||
|
"C408", # Unnecessary `dict` call - rewrite as a literal
|
||||||
|
"ISC001", # Single line implicit string concatenation
|
||||||
|
"C901"
|
||||||
|
]
|
||||||
|
|
||||||
|
lint.fixable = ["ALL"]
|
||||||
|
lint.unfixable = []
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
skip-magic-trailing-comma = false
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
combine-as-imports = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.mccabe]
|
||||||
|
max-complexity = 12
|
||||||
13
markdown_view_plugin/ui/index.html
Normal file
13
markdown_view_plugin/ui/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" style="height: 100%;">
|
||||||
|
<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>Markdown View</title>
|
||||||
|
</head>
|
||||||
|
<body style="height: 100%; margin: 0;">
|
||||||
|
<div id="root" style="height: 100%;"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
markdown_view_plugin/ui/package.json
Normal file
35
markdown_view_plugin/ui/package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "markdown-view-plugin-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@chakra-ui/icons": "^2.2.4",
|
||||||
|
"@chakra-ui/react": "^2.8.2",
|
||||||
|
"@emotion/react": "^11.11.4",
|
||||||
|
"@emotion/styled": "^11.11.5",
|
||||||
|
"framer-motion": "^11.0.6",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-markdown": "^9.0.1",
|
||||||
|
"remark-gfm": "^4.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.55",
|
||||||
|
"@types/react-dom": "^18.2.19",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3639
markdown_view_plugin/ui/pnpm-lock.yaml
generated
Normal file
3639
markdown_view_plugin/ui/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
9
markdown_view_plugin/ui/src/App.tsx
Normal file
9
markdown_view_plugin/ui/src/App.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import View from './View';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<View />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
120
markdown_view_plugin/ui/src/View.tsx
Normal file
120
markdown_view_plugin/ui/src/View.tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import { fetchMarkdownContent } from './api';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Heading,
|
||||||
|
Text,
|
||||||
|
Code,
|
||||||
|
Link,
|
||||||
|
ListItem,
|
||||||
|
OrderedList,
|
||||||
|
UnorderedList,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Td,
|
||||||
|
Divider,
|
||||||
|
Container, // Added for layout
|
||||||
|
Flex, // Added for layout
|
||||||
|
IconButton,// Added for theme toggle
|
||||||
|
useColorMode // Hook for theme toggle
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { SunIcon, MoonIcon } from '@chakra-ui/icons'; // Icons for theme toggle
|
||||||
|
|
||||||
|
// Attempt to mimic Airflow's Chakra UI components styling
|
||||||
|
const chakraComponents = {
|
||||||
|
h1: (props: any) => <Heading as="h1" size="xl" my={4} {...props} />,
|
||||||
|
h2: (props: any) => <Heading as="h2" size="lg" my={3} {...props} />,
|
||||||
|
h3: (props: any) => <Heading as="h3" size="md" my={2} {...props} />,
|
||||||
|
p: (props: any) => <Text my={2} {...props} />,
|
||||||
|
code: (props: any) => {
|
||||||
|
const { inline, className, children, ...rest } = props;
|
||||||
|
return !inline ? (
|
||||||
|
<Box
|
||||||
|
as="pre"
|
||||||
|
p={4}
|
||||||
|
rounded="md"
|
||||||
|
bg="chakra-body-bg"
|
||||||
|
boxShadow="sm"
|
||||||
|
overflowX="auto"
|
||||||
|
my={2}
|
||||||
|
>
|
||||||
|
<Code className={className} bg="transparent" {...rest}>
|
||||||
|
{children}
|
||||||
|
</Code>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Code colorScheme="purple" fontSize="0.9em" {...rest}>{children}</Code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
a: (props: any) => <Link color="teal.500" isExternal {...props} />,
|
||||||
|
ul: (props: any) => <UnorderedList stylePosition="inside" my={2} {...props} />,
|
||||||
|
ol: (props: any) => <OrderedList stylePosition="inside" my={2} {...props} />,
|
||||||
|
li: (props: any) => <ListItem {...props} />,
|
||||||
|
table: (props: any) => <Table variant="simple" my={4} {...props} />,
|
||||||
|
thead: (props: any) => <Thead {...props} />,
|
||||||
|
tbody: (props: any) => <Tbody {...props} />,
|
||||||
|
tr: (props: any) => <Tr {...props} />,
|
||||||
|
th: (props: any) => <Th {...props} />,
|
||||||
|
td: (props: any) => <Td {...props} />,
|
||||||
|
hr: (props: any) => <Divider my={4} {...props} />,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const View: React.FC = () => {
|
||||||
|
const [markdown, setMarkdown] = useState<string>('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { colorMode, toggleColorMode } = useColorMode(); // Hook for theme toggle
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadContent = async () => {
|
||||||
|
try {
|
||||||
|
const content = await fetchMarkdownContent();
|
||||||
|
setMarkdown(content);
|
||||||
|
} catch (err) {
|
||||||
|
setError((err as Error).message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadContent();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Container maxW="container.xl" py={10}>
|
||||||
|
<Box color="red.500">Error loading content: {error}</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!markdown && !error) { // Show loading only if no error
|
||||||
|
return (
|
||||||
|
<Container maxW="container.xl" py={10}>
|
||||||
|
<Box>Loading content...</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxW="container.xl" py={6} height="100%" display="flex" flexDirection="column">
|
||||||
|
<Flex justifyContent="space-between" alignItems="center" mb={6} flexShrink={0}>
|
||||||
|
<Heading as="h1" size="lg">Markdown View</Heading>
|
||||||
|
<IconButton
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Box p={5} borderWidth="1px" borderRadius="lg" boxShadow="md" overflowY="auto" flexGrow={1}>
|
||||||
|
<ReactMarkdown components={chakraComponents} remarkPlugins={[remarkGfm]}>
|
||||||
|
{markdown}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default View;
|
||||||
19
markdown_view_plugin/ui/src/api.ts
Normal file
19
markdown_view_plugin/ui/src/api.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// API functions to interact with the Flask backend
|
||||||
|
|
||||||
|
// Adjusted API_BASE_URL to include the Airflow app_mount point
|
||||||
|
const API_BASE_URL = '/markdown_view_plugin_mount/markdown_view';
|
||||||
|
|
||||||
|
export async function fetchMarkdownContent(): Promise<string> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/view`);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
return data.markdown;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch markdown content:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
markdown_view_plugin/ui/src/main.tsx
Normal file
14
markdown_view_plugin/ui/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App'; // Removed .tsx extension
|
||||||
|
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
|
||||||
|
import theme from './theme'; // Removed .ts extension
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ChakraProvider theme={theme}> {/* Apply the custom theme */}
|
||||||
|
<ColorModeScript initialColorMode={theme.config.initialColorMode} />
|
||||||
|
<App />
|
||||||
|
</ChakraProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
71
markdown_view_plugin/ui/src/theme.ts
Normal file
71
markdown_view_plugin/ui/src/theme.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { extendTheme, type ThemeConfig } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
// Basic color mode config
|
||||||
|
const config: ThemeConfig = {
|
||||||
|
initialColorMode: 'system',
|
||||||
|
useSystemColorMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define some basic colors similar to Airflow's scheme
|
||||||
|
// This is a simplified version. For full consistency, you'd replicate more from Airflow's core theme.ts
|
||||||
|
const colors = {
|
||||||
|
airflow: {
|
||||||
|
50: '#EBF8FF', // Lightest blue
|
||||||
|
100: '#BEE3F8',
|
||||||
|
200: '#90CDF4',
|
||||||
|
300: '#63B3ED',
|
||||||
|
400: '#4299E1', // Primary blue
|
||||||
|
500: '#3182CE',
|
||||||
|
600: '#2B6CB0',
|
||||||
|
700: '#2C5282',
|
||||||
|
800: '#2A4365', // Darkest blue
|
||||||
|
900: '#1A365D',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
500: '#38A169', // Green
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
500: '#E53E3E', // Red
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const theme = extendTheme({
|
||||||
|
config,
|
||||||
|
colors,
|
||||||
|
styles: {
|
||||||
|
global: (props: any) => ({
|
||||||
|
body: {
|
||||||
|
bg: props.colorMode === 'dark' ? 'gray.800' : 'gray.50',
|
||||||
|
color: props.colorMode === 'dark' ? 'whiteAlpha.900' : 'gray.800',
|
||||||
|
height: '100%', // Ensure body takes full height
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
'#root': { // Ensure root div also takes full height
|
||||||
|
height: '100%',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Button: {
|
||||||
|
baseStyle: {
|
||||||
|
// Example: fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
solid: (props: any) => ({
|
||||||
|
bg: props.colorMode === 'dark' ? 'airflow.300' : 'airflow.500',
|
||||||
|
color: props.colorMode === 'dark' ? 'gray.800' : 'white',
|
||||||
|
_hover: {
|
||||||
|
bg: props.colorMode === 'dark' ? 'airflow.400' : 'airflow.600',
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// You can add more component-specific style overrides here if needed
|
||||||
|
// For example, for Markdown components if View.tsx styling needs to be theme-aware
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default theme;
|
||||||
25
markdown_view_plugin/ui/tsconfig.json
Normal file
25
markdown_view_plugin/ui/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
markdown_view_plugin/ui/tsconfig.node.json
Normal file
10
markdown_view_plugin/ui/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
18
markdown_view_plugin/ui/vite.config.ts
Normal file
18
markdown_view_plugin/ui/vite.config.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: '/markdown_view_plugin_mount/static/markdown_view_plugin/', // Important for Airflow to find assets
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'assets/[name].js',
|
||||||
|
chunkFileNames: 'assets/[name].js',
|
||||||
|
assetFileNames: 'assets/[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
18
markdown_view_plugin/view.md
Normal file
18
markdown_view_plugin/view.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Welcome to Markdown View Plugin!
|
||||||
|
|
||||||
|
This is a sample `view.md` file.
|
||||||
|
|
||||||
|
You can write **Markdown** here, and it will be rendered in the Airflow UI.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
* Supports standard Markdown syntax.
|
||||||
|
* Styled with Airflow's theme (hopefully!).
|
||||||
|
|
||||||
|
### Example List
|
||||||
|
1. First item
|
||||||
|
2. Second item
|
||||||
|
3. Third item
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Edit this file to change the content of your view.
|
||||||
Reference in New Issue
Block a user