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:
2025-05-10 06:50:25 +00:00
parent 3593103630
commit e6d03a2c2e
17 changed files with 4282 additions and 0 deletions

View 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>

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
import View from './View';
function App() {
return (
<View />
);
}
export default App;

View 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;

View 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;
}
}

View 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>,
);

View 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;

View 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" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View 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]',
},
},
},
})