Compare commits
10 Commits
e046623d68
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
dd2f7edf92
|
|||
|
c5576acb19
|
|||
|
6b3716b62d
|
|||
|
946399e474
|
|||
|
62a4100e96
|
|||
|
546984b5c3
|
|||
|
4e7bc0aef4
|
|||
|
92a2b09bf2
|
|||
|
831c6051ea
|
|||
|
8c4442fb00
|
8
.gitignore
vendored
8
.gitignore
vendored
@@ -300,3 +300,11 @@ dist
|
|||||||
.yarn/install-state.gz
|
.yarn/install-state.gz
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
|
||||||
|
# ruff
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# Mac
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
# hatch_build.py
|
# hatch_build.py
|
||||||
import subprocess
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||||
|
|
||||||
|
|
||||||
class MarkdownBuildHook(BuildHookInterface):
|
class MarkdownBuildHook(BuildHookInterface):
|
||||||
def initialize(self, version, build_data):
|
def initialize(self, version, build_data):
|
||||||
# 1. Compile the UI exactly once per build
|
# 1. Compile the UI exactly once per build
|
||||||
ui_dir = pathlib.Path(__file__).parent / "ui"
|
ui_dir = pathlib.Path(__file__).parent / "markdown_view_plugin" / "ui"
|
||||||
dist_dir = ui_dir / "dist"
|
dist_dir = ui_dir / "dist"
|
||||||
|
|
||||||
# Clean any existing dist directory to ensure fresh build
|
# Clean any existing dist directory to ensure fresh build
|
||||||
@@ -15,10 +17,8 @@ class MarkdownBuildHook(BuildHookInterface):
|
|||||||
shutil.rmtree(dist_dir)
|
shutil.rmtree(dist_dir)
|
||||||
|
|
||||||
# Install dependencies and build the UI
|
# Install dependencies and build the UI
|
||||||
subprocess.run([
|
subprocess.run(["pnpm", "install", "--frozen-lockfile"], cwd=ui_dir, check=True)
|
||||||
"pnpm", "install", "--frozen-lockfile"
|
|
||||||
], cwd=ui_dir, check=True)
|
|
||||||
subprocess.run(["pnpm", "run", "build"], cwd=ui_dir, check=True)
|
subprocess.run(["pnpm", "run", "build"], cwd=ui_dir, check=True)
|
||||||
|
|
||||||
# 2. Force-include the compiled assets in the wheel
|
# 2. Force-include the compiled assets in the wheel
|
||||||
build_data["force_include"][str(dist_dir)] = "ui/dist"
|
build_data["force_include"][str(dist_dir)] = "markdown_view_plugin/ui/dist"
|
||||||
|
|||||||
3
markdown_view_plugin/markdown_view_plugin/__init__.py
Normal file
3
markdown_view_plugin/markdown_view_plugin/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from markdown_view_plugin.markdown_view_plugin import MarkdownViewPlugin
|
||||||
|
|
||||||
|
__all__ = ["MarkdownViewPlugin"]
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
# Python backend for the Markdown View Plugin using FastAPI
|
# Python backend for the Markdown View Plugin using FastAPI
|
||||||
|
|
||||||
|
import importlib.resources as resources
|
||||||
|
|
||||||
import anyio # For asynchronous file operations
|
import anyio # For asynchronous file operations
|
||||||
|
from airflow.plugins_manager import AirflowPlugin
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
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
|
# Use importlib.resources to locate static files and view.md
|
||||||
static_files_dir_ref = resources.files("markdown_view_plugin") / "ui" / "dist"
|
static_files_dir_ref = resources.files("markdown_view_plugin") / "ui" / "dist"
|
||||||
@@ -27,6 +28,7 @@ with resources.as_file(static_files_dir_ref) as static_files_dir:
|
|||||||
name="markdown_view_plugin_static",
|
name="markdown_view_plugin_static",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# API endpoint to get markdown content
|
# API endpoint to get markdown content
|
||||||
@markdown_fastapi_app.get("/markdown_view/api/view")
|
@markdown_fastapi_app.get("/markdown_view/api/view")
|
||||||
async def get_markdown_content_api():
|
async def get_markdown_content_api():
|
||||||
@@ -41,6 +43,7 @@ async def get_markdown_content_api():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
# Endpoint to serve the main index.html of the React app
|
# Endpoint to serve the main index.html of the React app
|
||||||
@markdown_fastapi_app.get("/markdown_view")
|
@markdown_fastapi_app.get("/markdown_view")
|
||||||
async def serve_markdown_ui():
|
async def serve_markdown_ui():
|
||||||
@@ -53,6 +56,7 @@ async def serve_markdown_ui():
|
|||||||
)
|
)
|
||||||
return FileResponse(str(index_html_path))
|
return FileResponse(str(index_html_path))
|
||||||
|
|
||||||
|
|
||||||
# Define the Airflow plugin
|
# Define the Airflow plugin
|
||||||
class MarkdownViewPlugin(AirflowPlugin):
|
class MarkdownViewPlugin(AirflowPlugin):
|
||||||
"""
|
"""
|
||||||
@@ -67,18 +71,6 @@ class MarkdownViewPlugin(AirflowPlugin):
|
|||||||
{
|
{
|
||||||
"app": markdown_fastapi_app,
|
"app": markdown_fastapi_app,
|
||||||
"name": "markdown_view_app", # A unique name for this FastAPI app
|
"name": "markdown_view_app", # A unique name for this FastAPI app
|
||||||
"app_mount": "/markdown_view_plugin_mount",
|
"url_prefix": "/plugins", # Required for Airflow 3 FastAPI plugins
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
# 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.
|
|
||||||
BIN
markdown_view_plugin/markdown_view_plugin/ui/.DS_Store
vendored
Normal file
BIN
markdown_view_plugin/markdown_view_plugin/ui/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -11,7 +11,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@chakra-ui/icons": "^2.2.4",
|
"@chakra-ui/icons": "^2.2.4",
|
||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^3.17.0",
|
||||||
|
"@chakra-ui/system": "^2.6.2",
|
||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.5",
|
"@emotion/styled": "^11.11.5",
|
||||||
"framer-motion": "^11.0.6",
|
"framer-motion": "^11.0.6",
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "~5.8.3",
|
||||||
"vite": "^5.1.0"
|
"vite": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
88
markdown_view_plugin/markdown_view_plugin/ui/src/View.tsx
Normal file
88
markdown_view_plugin/markdown_view_plugin/ui/src/View.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import { fetchMarkdownContent } from './api';
|
||||||
|
// import { SunIcon, MoonIcon } from '@chakra-ui/icons';
|
||||||
|
import { useColorMode } from '@chakra-ui/system';
|
||||||
|
|
||||||
|
// Custom renderer to mimic Airflow core UI style
|
||||||
|
const markdownComponents = {
|
||||||
|
h1: (props: any) => <h1 style={{ fontSize: '2rem', fontWeight: 700, margin: '1.5rem 0 1rem 0', color: 'var(--chakra-colors-gray-800, #2D3748)' }} {...props} />,
|
||||||
|
h2: (props: any) => <h2 style={{ fontSize: '1.5rem', fontWeight: 600, margin: '1.25rem 0 0.75rem 0', color: 'var(--chakra-colors-gray-700, #4A5568)' }} {...props} />,
|
||||||
|
h3: (props: any) => <h3 style={{ fontSize: '1.2rem', fontWeight: 600, margin: '1rem 0 0.5rem 0', color: 'var(--chakra-colors-gray-600, #718096)' }} {...props} />,
|
||||||
|
p: (props: any) => <p style={{ margin: '0.5em 0', lineHeight: 1.7, color: 'var(--chakra-colors-gray-800, #2D3748)' }} {...props} />,
|
||||||
|
code: (props: any) => {
|
||||||
|
const { inline, className, children, ...rest } = props;
|
||||||
|
return !inline ? (
|
||||||
|
<pre style={{ padding: '1em', borderRadius: '6px', background: '#F7FAFC', boxShadow: '0 1px 2px #e2e8f0', overflowX: 'auto', margin: '0.5em 0' }} {...rest}>
|
||||||
|
<code className={className}>{children}</code>
|
||||||
|
</pre>
|
||||||
|
) : (
|
||||||
|
<code style={{ background: '#EDF2F7', borderRadius: '4px', padding: '0.2em 0.4em', fontSize: '0.95em' }} {...rest}>{children}</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
a: (props: any) => <a style={{ color: '#3182CE', textDecoration: 'underline' }} target="_blank" rel="noopener noreferrer" {...props} />,
|
||||||
|
ul: (props: any) => <ul style={{ margin: '1em 0', paddingLeft: '1.5em', color: 'var(--chakra-colors-gray-800, #2D3748)' }} {...props} />,
|
||||||
|
ol: (props: any) => <ol style={{ margin: '1em 0', paddingLeft: '1.5em', color: 'var(--chakra-colors-gray-800, #2D3748)' }} {...props} />,
|
||||||
|
li: (props: any) => <li style={{ marginBottom: '0.25em' }} {...props} />,
|
||||||
|
table: (props: any) => <table style={{ width: '100%', borderCollapse: 'collapse', margin: '1em 0', fontSize: '1em' }} {...props} />,
|
||||||
|
thead: (props: any) => <thead style={{ background: '#F7FAFC' }} {...props} />,
|
||||||
|
tbody: (props: any) => <tbody {...props} />,
|
||||||
|
tr: (props: any) => <tr style={{ borderBottom: '1px solid #E2E8F0' }} {...props} />,
|
||||||
|
th: (props: any) => <th style={{ padding: '0.5em', textAlign: 'left', fontWeight: 700, borderBottom: '2px solid #E2E8F0', background: '#F7FAFC' }} {...props} />,
|
||||||
|
td: (props: any) => <td style={{ padding: '0.5em', borderBottom: '1px solid #E2E8F0' }} {...props} />,
|
||||||
|
hr: (props: any) => <hr style={{ margin: '2em 0', border: 0, borderTop: '1px solid #E2E8F0' }} {...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 (
|
||||||
|
<div style={{ maxWidth: '1200px', padding: '2em', color: '#E53E3E' }}>Error loading content: {error}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!markdown && !error) {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '1200px', padding: '2em' }}>Loading content...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '1200px', padding: '1.5em', minHeight: '100vh', display: 'flex', flexDirection: 'column', background: colorMode === 'dark' ? '#1A202C' : '#F7FAFC', color: colorMode === 'dark' ? '#F7FAFC' : '#2D3748' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2em', flexShrink: 0 }}>
|
||||||
|
<h1 style={{ fontSize: '2em', margin: 0, fontWeight: 700 }}>Markdown View</h1>
|
||||||
|
<button
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
onClick={toggleColorMode}
|
||||||
|
style={{ background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.5em', color: colorMode === 'dark' ? '#F7FAFC' : '#2D3748' }}
|
||||||
|
>
|
||||||
|
{colorMode === 'light' ? '🌙' : '☀️'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: '2em', border: '1px solid #E2E8F0', borderRadius: '12px', boxShadow: '0 2px 8px #E2E8F0', overflowY: 'auto', flexGrow: 1, background: colorMode === 'dark' ? '#2D3748' : '#fff' }}>
|
||||||
|
<ReactMarkdown components={markdownComponents} remarkPlugins={[remarkGfm]}>
|
||||||
|
{markdown}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default View;
|
||||||
11
markdown_view_plugin/markdown_view_plugin/ui/src/main.tsx
Normal file
11
markdown_view_plugin/markdown_view_plugin/ui/src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App'; // Removed .tsx extension
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
29
markdown_view_plugin/markdown_view_plugin/ui/src/theme.ts
Normal file
29
markdown_view_plugin/markdown_view_plugin/ui/src/theme.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Chakra UI v3 does not export extendTheme. Use a plain object for theme config or remove custom theme for now.
|
||||||
|
|
||||||
|
// Basic color mode config
|
||||||
|
// No config needed for ChakraProvider in v3
|
||||||
|
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// No theme export needed for ChakraProvider in v3
|
||||||
@@ -4,7 +4,8 @@ import react from '@vitejs/plugin-react'
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
base: '/markdown_view_plugin_mount/static/markdown_view_plugin/', // Important for Airflow to find assets
|
// The base must match the FastAPI static mount: /<url_prefix>/static/markdown_view_plugin/
|
||||||
|
base: '/plugins/static/markdown_view_plugin/',
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "airflow-markdown-view-plugin"
|
name = "markdown-view-plugin"
|
||||||
version = "0.1.0"
|
version = "0.1.13"
|
||||||
description = "Airflow UI plugin to render Markdown content using FastAPI and React."
|
description = "Airflow UI plugin to render Markdown content using FastAPI and React."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
@@ -29,8 +29,6 @@ classifiers = [
|
|||||||
license = {text = "MIT"} # Updated to new license expression
|
license = {text = "MIT"} # Updated to new license expression
|
||||||
# license-files = ["LICEN[CS]E*"] # Optional: if you add a LICENSE file
|
# 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]
|
[project.urls]
|
||||||
GitHub = "https://github.com/abhishekbhakat/airflow-markdown-view-plugin" # Replace with actual URL if it exists
|
GitHub = "https://github.com/abhishekbhakat/airflow-markdown-view-plugin" # Replace with actual URL if it exists
|
||||||
@@ -42,6 +40,7 @@ markdown_view_plugin = "markdown_view_plugin:MarkdownViewPlugin"
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"build>=1.2.2",
|
"build>=1.2.2",
|
||||||
|
"hatch>=1.14.1",
|
||||||
"pre-commit>=4.0.1",
|
"pre-commit>=4.0.1",
|
||||||
"ruff>=0.9.2"
|
"ruff>=0.9.2"
|
||||||
]
|
]
|
||||||
@@ -55,13 +54,29 @@ build-backend = "hatchling.build"
|
|||||||
path = "hatch_build.py"
|
path = "hatch_build.py"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[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 = [
|
include = [
|
||||||
|
"markdown_view_plugin",
|
||||||
"view.md",
|
"view.md",
|
||||||
"README.md",
|
"README.md",
|
||||||
"ui/dist" # This tells Hatch to include the ui/dist directory and its contents
|
]
|
||||||
|
exclude = [
|
||||||
|
"markdown_view_plugin/markdown_view_plugin/ui/*",
|
||||||
|
"markdown_view_plugin/markdown_view_plugin/ui/**",
|
||||||
|
"!markdown_view_plugin/markdown_view_plugin/ui/dist",
|
||||||
|
"!markdown_view_plugin/markdown_view_plugin/ui/dist/**"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist]
|
||||||
|
include = [
|
||||||
|
"markdown_view_plugin",
|
||||||
|
"view.md",
|
||||||
|
"README.md",
|
||||||
|
]
|
||||||
|
exclude = [
|
||||||
|
"markdown_view_plugin/markdown_view_plugin/ui/*",
|
||||||
|
"markdown_view_plugin/markdown_view_plugin/ui/**",
|
||||||
|
"!markdown_view_plugin/markdown_view_plugin/ui/dist",
|
||||||
|
"!markdown_view_plugin/markdown_view_plugin/ui/dist/**"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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>,
|
|
||||||
);
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
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;
|
|
||||||
2896
markdown_view_plugin/uv.lock
generated
Normal file
2896
markdown_view_plugin/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user