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:
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]',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user