This is page 3 of 3. Use http://codebase.md/hatrigt/hana-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .env.example ├── .github │ └── pull_request_template.md ├── .gitignore ├── .npmignore ├── claude_template.json ├── docs │ ├── hana_mcp_architecture.svg │ └── hana_mcp_ui.gif ├── hana-mcp-server.js ├── hana-mcp-ui │ ├── .gitignore │ ├── bin │ │ └── cli.js │ ├── hana_mcp_ui.gif │ ├── index.html │ ├── logo.png │ ├── package.json │ ├── postcss.config.js │ ├── README.md │ ├── server │ │ └── index.js │ ├── src │ │ ├── App.jsx │ │ ├── components │ │ │ ├── ClaudeConfigTile.jsx │ │ │ ├── ClaudeDesktopView.jsx │ │ │ ├── ClaudeServerCard.jsx │ │ │ ├── ConfigurationModal.jsx │ │ │ ├── ConnectionDetailsModal.jsx │ │ │ ├── DashboardView.jsx │ │ │ ├── DatabaseListView.jsx │ │ │ ├── EnhancedServerCard.jsx │ │ │ ├── EnvironmentManager.jsx │ │ │ ├── EnvironmentSelector.jsx │ │ │ ├── layout │ │ │ │ ├── index.js │ │ │ │ └── VerticalSidebar.jsx │ │ │ ├── MainApp.jsx │ │ │ ├── PathConfigModal.jsx │ │ │ ├── PathSetupModal.jsx │ │ │ ├── SearchAndFilter.jsx │ │ │ └── ui │ │ │ ├── DatabaseTypeBadge.jsx │ │ │ ├── GlassCard.jsx │ │ │ ├── GlassWindow.jsx │ │ │ ├── GradientButton.jsx │ │ │ ├── IconComponent.jsx │ │ │ ├── index.js │ │ │ ├── LoadingSpinner.jsx │ │ │ ├── MetricCard.jsx │ │ │ ├── StatusBadge.jsx │ │ │ └── Tabs.jsx │ │ ├── index.css │ │ ├── main.jsx │ │ └── utils │ │ ├── cn.js │ │ ├── databaseTypes.js │ │ └── theme.js │ ├── start.js │ ├── tailwind.config.js │ └── vite.config.js ├── LICENSE ├── manifest.yml ├── package-lock.json ├── package.json ├── README.md ├── setup.sh ├── src │ ├── constants │ │ ├── mcp-constants.js │ │ └── tool-definitions.js │ ├── database │ │ ├── connection-manager.js │ │ ├── hana-client.js │ │ └── query-executor.js │ ├── server │ │ ├── index.js │ │ ├── lifecycle-manager.js │ │ └── mcp-handler.js │ ├── tools │ │ ├── config-tools.js │ │ ├── index-tools.js │ │ ├── index.js │ │ ├── query-tools.js │ │ ├── schema-tools.js │ │ └── table-tools.js │ └── utils │ ├── config.js │ ├── formatters.js │ ├── logger.js │ └── validators.js └── tests ├── automated │ └── test-mcp-inspector.js ├── manual │ └── manual-test.js ├── mcpInspector │ ├── mcp-inspector-config.json │ └── mcp-inspector-config.template.json ├── mcpTestingGuide.md └── README.md ``` # Files -------------------------------------------------------------------------------- /docs/hana_mcp_architecture.svg: -------------------------------------------------------------------------------- ``` 1 | <svg viewBox="0 0 1400 700" xmlns="http://www.w3.org/2000/svg"> 2 | <!-- Clean background --> 3 | <rect width="1400" height="700" fill="#ffffff"/> 4 | 5 | <!-- Title --> 6 | <text x="700" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#1a1a1a"> 7 | HANA MCP Server Architecture 8 | </text> 9 | <text x="700" y="65" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#666666"> 10 | Enterprise AI-Database Integration Platform 11 | </text> 12 | 13 | <!-- Client Applications Layer --> 14 | <rect x="80" y="100" width="220" height="130" rx="8" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/> 15 | <text x="190" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#212529"> 16 | MCP Clients 17 | </text> 18 | <rect x="100" y="145" width="180" height="65" rx="4" fill="#ffffff" stroke="#e9ecef" stroke-width="1"/> 19 | <text x="190" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> 20 | • Claude Desktop 21 | </text> 22 | <text x="190" y="185" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> 23 | • VSCode Extensions 24 | </text> 25 | <text x="190" y="205" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> 26 | • Custom AI Applications 27 | </text> 28 | 29 | <!-- MCP Protocol Bridge --> 30 | <rect x="380" y="140" width="140" height="50" rx="25" fill="#e9ecef" stroke="#ced4da" stroke-width="1"/> 31 | <text x="450" y="160" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#495057"> 32 | MCP Protocol 33 | </text> 34 | <text x="450" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d"> 35 | JSON-RPC 36 | </text> 37 | 38 | <!-- HANA MCP Server Core --> 39 | <rect x="600" y="80" width="260" height="170" rx="8" fill="#343a40" stroke="#495057" stroke-width="2"/> 40 | <text x="730" y="110" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#ffffff"> 41 | HANA MCP Server 42 | </text> 43 | <text x="730" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#f8f9fa"> 44 | Enterprise Database Gateway 45 | </text> 46 | 47 | <!-- Server components --> 48 | <rect x="620" y="145" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> 49 | <text x="670" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Connection</text> 50 | <text x="670" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Manager</text> 51 | 52 | <rect x="740" y="145" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> 53 | <text x="790" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Schema</text> 54 | <text x="790" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Inspector</text> 55 | 56 | <rect x="620" y="200" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> 57 | <text x="670" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Query</text> 58 | <text x="670" y="230" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Engine</text> 59 | 60 | <rect x="740" y="200" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> 61 | <text x="790" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Tool</text> 62 | <text x="790" y="230" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Handler</text> 63 | 64 | <!-- SAP HANA Database --> 65 | <rect x="940" y="80" width="220" height="170" rx="8" fill="#212529" stroke="#343a40" stroke-width="2"/> 66 | <text x="1050" y="110" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#ffffff"> 67 | SAP HANA 68 | </text> 69 | <text x="1050" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#f8f9fa"> 70 | Enterprise Database 71 | </text> 72 | 73 | <!-- Database features --> 74 | <rect x="960" y="145" width="180" height="85" rx="4" fill="#343a40" stroke="#495057" stroke-width="1"/> 75 | <text x="1050" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#ffffff">In-Memory Processing</text> 76 | <text x="1050" y="185" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Columnar Store Engine</text> 77 | <text x="1050" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Real-time Analytics</text> 78 | <text x="1050" y="215" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Enterprise Security</text> 79 | 80 | <!-- Available MCP Tools Section --> 81 | <text x="700" y="310" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="bold" fill="#212529"> 82 | Available MCP Tools 83 | </text> 84 | 85 | <!-- Connection Tools --> 86 | <rect x="80" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 87 | <rect x="90" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> 88 | <text x="180" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Connection Tools</text> 89 | <text x="90" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_test_connection</text> 90 | <text x="90" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_show_config</text> 91 | <text x="90" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_show_env_vars</text> 92 | <text x="90" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Database connectivity & setup</text> 93 | 94 | <!-- Schema Tools --> 95 | <rect x="300" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 96 | <rect x="310" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> 97 | <text x="400" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Schema Tools</text> 98 | <text x="310" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_schemas</text> 99 | <text x="310" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_tables</text> 100 | <text x="310" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_describe_table</text> 101 | <text x="310" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Metadata & structure discovery</text> 102 | 103 | <!-- Index Tools --> 104 | <rect x="520" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 105 | <rect x="530" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> 106 | <text x="620" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Index Tools</text> 107 | <text x="530" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_indexes</text> 108 | <text x="530" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_describe_index</text> 109 | <text x="530" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Performance optimization</text> 110 | <text x="530" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Index management & analysis</text> 111 | 112 | <!-- Query Tools --> 113 | <rect x="740" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 114 | <rect x="750" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> 115 | <text x="840" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Query Tools</text> 116 | <text x="750" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_execute_query</text> 117 | <text x="750" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Parameterized queries</text> 118 | <text x="750" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Custom SQL execution</text> 119 | <text x="750" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Data retrieval & analysis</text> 120 | 121 | <!-- Browser Control Tools --> 122 | <rect x="960" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 123 | <rect x="970" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> 124 | <text x="1060" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Browser Control</text> 125 | <text x="970" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• open_url</text> 126 | <text x="970" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• get_page_content</text> 127 | <text x="970" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• execute_javascript</text> 128 | <text x="970" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Web automation & integration</text> 129 | 130 | <!-- Configuration and Benefits Section --> 131 | <!-- Configuration --> 132 | <rect x="180" y="500" width="350" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 133 | <rect x="190" y="510" width="330" height="25" rx="3" fill="#f8f9fa"/> 134 | <text x="355" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> 135 | Configuration & Security 136 | </text> 137 | <text x="200" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Environment Variables (HANA_HOST, HANA_USER, etc.)</text> 138 | <text x="200" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• SSL/TLS Encryption & Certificate Validation</text> 139 | <text x="200" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Connection Pooling & Resource Management</text> 140 | 141 | <!-- Key Benefits --> 142 | <rect x="580" y="500" width="350" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 143 | <rect x="590" y="510" width="330" height="25" rx="3" fill="#f8f9fa"/> 144 | <text x="755" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> 145 | Enterprise Benefits 146 | </text> 147 | <text x="600" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Seamless AI-Database Integration</text> 148 | <text x="600" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Enterprise-Grade Security & Compliance</text> 149 | <text x="600" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Real-time Analytics & Decision Support</text> 150 | 151 | <!-- Performance Metrics --> 152 | <rect x="980" y="500" width="240" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> 153 | <rect x="990" y="510" width="220" height="25" rx="3" fill="#f8f9fa"/> 154 | <text x="1100" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> 155 | Performance 156 | </text> 157 | <text x="1000" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Sub-second Query Response</text> 158 | <text x="1000" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Concurrent User Support</text> 159 | <text x="1000" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Optimized Memory Usage</text> 160 | 161 | <!-- Simple Data Flow Arrows --> 162 | <polygon points="300,165 380,165 375,160 375,170" fill="#6c757d"/> 163 | <polygon points="520,165 600,165 595,160 595,170" fill="#6c757d"/> 164 | <polygon points="860,165 940,165 935,160 935,170" fill="#6c757d"/> 165 | 166 | <!-- Flow labels --> 167 | <text x="340" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">JSON-RPC</text> 168 | <text x="560" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Tool Calls</text> 169 | <text x="900" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">SQL Queries</text> 170 | 171 | <!-- Footer --> 172 | <text x="700" y="660" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529"> 173 | Enterprise AI-Database Integration Platform 174 | </text> 175 | <text x="700" y="680" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6c757d"> 176 | Enabling secure, scalable AI-powered database interactions through standardized protocols 177 | </text> 178 | </svg> ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/MainApp.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import toast from 'react-hot-toast'; 4 | import { motion, AnimatePresence } from 'framer-motion'; 5 | 6 | // Import components 7 | import VerticalSidebar from './layout/VerticalSidebar'; 8 | import DashboardView from './DashboardView'; 9 | import DatabaseListView from './DatabaseListView'; 10 | import ClaudeConfigTile from './ClaudeConfigTile' 11 | import ClaudeDesktopView from './ClaudeDesktopView' 12 | import ConnectionDetailsModal from './ConnectionDetailsModal'; 13 | 14 | // Import existing components 15 | import ConfigurationModal from './ConfigurationModal'; 16 | import EnvironmentSelector from './EnvironmentSelector'; 17 | import PathSetupModal from './PathSetupModal'; 18 | import { LoadingOverlay, GlassWindow } from './ui'; 19 | 20 | const API_BASE = 'http://localhost:3001/api'; 21 | 22 | const MainApp = () => { 23 | // State management 24 | const [activeView, setActiveView] = useState('dashboard'); 25 | const [hanaServers, setHanaServers] = useState({}); 26 | const [claudeServers, setClaudeServers] = useState([]); 27 | const [claudeConfigPath, setClaudeConfigPath] = useState(null); 28 | const [activeEnvironments, setActiveEnvironments] = useState({}); 29 | 30 | // UI State 31 | const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); 32 | const [isPathSetupOpen, setIsPathSetupOpen] = useState(false) 33 | const [isConnectionDetailsOpen, setIsConnectionDetailsOpen] = useState(false) 34 | const [selectedConnection, setSelectedConnection] = useState(null); 35 | const [isEnvironmentSelectorOpen, setIsEnvironmentSelectorOpen] = useState(false); 36 | const [selectedServer, setSelectedServer] = useState(null); 37 | const [deploymentTarget, setDeploymentTarget] = useState(null); 38 | const [activeTab, setActiveTab] = useState('Production'); 39 | const [isLoading, setIsLoading] = useState(false); 40 | 41 | // Form data for multi-environment configuration 42 | const [formData, setFormData] = useState({ 43 | name: '', 44 | description: '', 45 | environments: { 46 | Production: { 47 | HANA_HOST: '', 48 | HANA_PORT: '443', 49 | HANA_USER: '', 50 | HANA_PASSWORD: '', 51 | HANA_SCHEMA: '', 52 | HANA_SSL: 'true', 53 | HANA_ENCRYPT: 'true', 54 | HANA_VALIDATE_CERT: 'true', 55 | LOG_LEVEL: 'info', 56 | ENABLE_FILE_LOGGING: 'true', 57 | ENABLE_CONSOLE_LOGGING: 'false' 58 | }, 59 | Development: { 60 | HANA_HOST: '', 61 | HANA_PORT: '443', 62 | HANA_USER: '', 63 | HANA_PASSWORD: '', 64 | HANA_SCHEMA: '', 65 | HANA_SSL: 'true', 66 | HANA_ENCRYPT: 'true', 67 | HANA_VALIDATE_CERT: 'false', 68 | LOG_LEVEL: 'debug', 69 | ENABLE_FILE_LOGGING: 'true', 70 | ENABLE_CONSOLE_LOGGING: 'true' 71 | }, 72 | Staging: { 73 | HANA_HOST: '', 74 | HANA_PORT: '443', 75 | HANA_USER: '', 76 | HANA_PASSWORD: '', 77 | HANA_SCHEMA: '', 78 | HANA_SSL: 'true', 79 | HANA_ENCRYPT: 'true', 80 | HANA_VALIDATE_CERT: 'true', 81 | LOG_LEVEL: 'info', 82 | ENABLE_FILE_LOGGING: 'true', 83 | ENABLE_CONSOLE_LOGGING: 'false' 84 | } 85 | } 86 | }); 87 | 88 | const [pathInput, setPathInput] = useState(''); 89 | 90 | // Load data on component mount 91 | useEffect(() => { 92 | loadData(); 93 | }, []); 94 | 95 | // Auto-refresh Claude data when Claude tab is opened (only if not initial load) 96 | useEffect(() => { 97 | if (activeView === 'claude' && claudeServers.length > 0) { 98 | // Only refresh if we already have data (prevents race condition on first load) 99 | // Silent refresh (no toast notification) 100 | refreshClaudeData(false); 101 | } 102 | }, [activeView]); 103 | 104 | const loadData = async () => { 105 | try { 106 | setIsLoading(true); 107 | await Promise.all([ 108 | loadHanaServers(), 109 | loadClaudeServers(), 110 | loadClaudeConfigPath(), 111 | loadActiveEnvironments() 112 | ]); 113 | } catch (error) { 114 | console.error('Error loading data:', error); 115 | toast.error('Failed to load data'); 116 | } finally { 117 | setIsLoading(false); 118 | } 119 | }; 120 | 121 | const refreshClaudeData = async (showToast = true) => { 122 | try { 123 | await Promise.all([ 124 | loadClaudeServers(), 125 | loadActiveEnvironments() 126 | ]); 127 | if (showToast) { 128 | toast.success('Configuration refreshed'); 129 | } 130 | } catch (error) { 131 | console.error('Error refreshing Claude data:', error); 132 | if (showToast) { 133 | toast.error('Failed to refresh configuration'); 134 | } 135 | } 136 | }; 137 | 138 | const loadHanaServers = async () => { 139 | try { 140 | const response = await axios.get(`${API_BASE}/hana-servers`); 141 | setHanaServers(response.data); 142 | } catch (error) { 143 | console.error('Error loading HANA servers:', error); 144 | } 145 | }; 146 | 147 | const loadClaudeServers = async () => { 148 | try { 149 | const response = await axios.get(`${API_BASE}/claude`); 150 | setClaudeServers(response.data); 151 | } catch (error) { 152 | console.error('Error loading Claude servers:', error); 153 | } 154 | }; 155 | 156 | const loadClaudeConfigPath = async () => { 157 | try { 158 | const response = await axios.get(`${API_BASE}/claude/config-path`); 159 | setClaudeConfigPath(response.data.configPath); 160 | 161 | if (!response.data.configPath) { 162 | setPathInput(response.data.defaultPath || ''); 163 | setIsPathSetupOpen(true); 164 | } 165 | } catch (error) { 166 | console.error('Error loading Claude config path:', error); 167 | } 168 | }; 169 | 170 | const loadActiveEnvironments = async () => { 171 | try { 172 | const response = await axios.get(`${API_BASE}/claude/active-environments`); 173 | setActiveEnvironments(response.data); 174 | } catch (error) { 175 | console.error('Error loading active environments:', error); 176 | } 177 | }; 178 | 179 | // Form handlers 180 | const handleFormChange = (environment, field, value) => { 181 | setFormData(prev => ({ 182 | ...prev, 183 | environments: { 184 | ...prev.environments, 185 | [environment]: { 186 | ...(prev.environments[environment] || {}), 187 | [field]: value 188 | } 189 | } 190 | })); 191 | }; 192 | 193 | const handleServerInfoChange = (field, value) => { 194 | setFormData(prev => ({ 195 | ...prev, 196 | [field]: value 197 | })); 198 | }; 199 | 200 | // Handle environment-specific updates 201 | const handleEnvironmentUpdate = (environments) => { 202 | setFormData(prev => ({ 203 | ...prev, 204 | environments: environments 205 | })); 206 | }; 207 | 208 | // Navigation handlers 209 | const handleViewChange = (view) => { 210 | setActiveView(view); 211 | 212 | // Handle special actions 213 | if (view === 'add-database') { 214 | openConfigModal(); 215 | return; 216 | } 217 | }; 218 | 219 | const handleQuickAction = (actionId) => { 220 | switch (actionId) { 221 | case 'add-database': 222 | openConfigModal(); 223 | break; 224 | case 'manage-databases': 225 | setActiveView('databases'); 226 | break; 227 | case 'claude-integration': 228 | setActiveView('claude'); 229 | break; 230 | default: 231 | console.warn(`Unknown quick action: ${actionId}`); 232 | } 233 | }; 234 | 235 | const handleBulkAction = (action, selectedItems) => { 236 | switch (action) { 237 | case 'deploy': 238 | toast.success(`Adding ${selectedItems.length} database(s) to Claude`); 239 | break; 240 | case 'test': 241 | toast.success(`Testing connections for ${selectedItems.length} database(s)`); 242 | break; 243 | case 'export': 244 | toast.success(`Exporting ${selectedItems.length} database configuration(s)`); 245 | break; 246 | default: 247 | console.warn(`Unknown bulk action: ${action}`); 248 | } 249 | }; 250 | 251 | const handleConfigPathChange = async (newPath) => { 252 | try { 253 | // Update the config path via API 254 | await axios.post(`${API_BASE}/claude/config-path`, { 255 | configPath: newPath 256 | }); 257 | 258 | // Update local state 259 | setClaudeConfigPath(newPath); 260 | toast.success('Configuration path updated successfully'); 261 | 262 | // Refresh Claude data to reflect changes 263 | await refreshClaudeData(false); 264 | } catch (error) { 265 | console.error('Error updating config path:', error); 266 | toast.error('Failed to update configuration path'); 267 | } 268 | }; 269 | 270 | // Modal handlers 271 | const openConfigModal = (server = null) => { 272 | if (server) { 273 | setFormData(server); 274 | setSelectedServer(server); 275 | // Set active tab to first available environment when editing 276 | const envKeys = Object.keys(server.environments || {}); 277 | setActiveTab(envKeys.length > 0 ? envKeys[0] : null); 278 | } else { 279 | // Reset form for new server 280 | setFormData({ 281 | name: '', 282 | description: '', 283 | environments: {} 284 | }); 285 | setSelectedServer(null); 286 | setActiveTab(null); 287 | } 288 | setIsConfigModalOpen(true); 289 | }; 290 | 291 | 292 | 293 | const closeConfigModal = () => { 294 | setIsConfigModalOpen(false); 295 | setSelectedServer(null); 296 | setActiveTab(null); 297 | }; 298 | 299 | // Server operations 300 | const saveServer = async () => { 301 | try { 302 | setIsLoading(true); 303 | 304 | 305 | 306 | if (selectedServer) { 307 | await axios.put(`${API_BASE}/hana-servers/${selectedServer.name}`, formData); 308 | toast.success('Database updated successfully'); 309 | } else { 310 | await axios.post(`${API_BASE}/hana-servers`, formData); 311 | toast.success('Database created successfully'); 312 | } 313 | 314 | closeConfigModal(); 315 | await loadHanaServers(); 316 | } catch (error) { 317 | console.error('Error saving server:', error); 318 | toast.error(error.response?.data?.error || 'Failed to save database'); 319 | } finally { 320 | setIsLoading(false); 321 | } 322 | }; 323 | 324 | const deleteServer = async (serverName) => { 325 | try { 326 | setIsLoading(true); 327 | await axios.delete(`${API_BASE}/hana-servers/${serverName}`); 328 | toast.success('Database deleted successfully'); 329 | await loadHanaServers(); 330 | } catch (error) { 331 | console.error('Error deleting server:', error); 332 | toast.error('Failed to delete database'); 333 | } finally { 334 | setIsLoading(false); 335 | } 336 | }; 337 | 338 | // Claude operations 339 | const openEnvironmentSelector = (serverName) => { 340 | setDeploymentTarget(serverName); 341 | setIsEnvironmentSelectorOpen(true); 342 | }; 343 | 344 | const deployToClaude = async (environment) => { 345 | try { 346 | setIsLoading(true); 347 | await axios.post(`${API_BASE}/apply-to-claude`, { 348 | serverName: deploymentTarget, 349 | environment: environment 350 | }); 351 | toast.success(`Added ${deploymentTarget} (${environment}) to Claude Desktop configuration`); 352 | setIsEnvironmentSelectorOpen(false); 353 | setDeploymentTarget(null); 354 | await loadClaudeServers(); 355 | await loadActiveEnvironments(); 356 | } catch (error) { 357 | console.error('Error adding to Claude:', error); 358 | toast.error(error.response?.data?.error || 'Failed to add to Claude configuration'); 359 | } finally { 360 | setIsLoading(false); 361 | } 362 | }; 363 | 364 | const removeFromClaude = async (serverName) => { 365 | try { 366 | setIsLoading(true); 367 | await axios.delete(`${API_BASE}/claude/${encodeURIComponent(serverName)}`); 368 | toast.success(`Removed ${serverName} from Claude Desktop`); 369 | await loadClaudeServers(); 370 | await loadActiveEnvironments(); 371 | } catch (error) { 372 | console.error('Error removing from Claude:', error); 373 | toast.error('Failed to remove from Claude'); 374 | } finally { 375 | setIsLoading(false); 376 | } 377 | }; 378 | 379 | // Claude path operations 380 | const saveClaudePath = async () => { 381 | try { 382 | setIsLoading(true); 383 | await axios.post(`${API_BASE}/claude/config-path`, { 384 | configPath: pathInput 385 | }); 386 | setClaudeConfigPath(pathInput); 387 | setIsPathSetupOpen(false); 388 | toast.success('Claude config path saved successfully'); 389 | await loadClaudeServers(); 390 | } catch (error) { 391 | console.error('Error saving Claude path:', error); 392 | toast.error('Failed to save Claude config path'); 393 | } finally { 394 | setIsLoading(false); 395 | } 396 | }; 397 | 398 | // Render main content based on active view 399 | const renderMainContent = () => { 400 | switch (activeView) { 401 | case 'dashboard': 402 | return ( 403 | <DashboardView 404 | hanaServers={hanaServers} 405 | claudeServers={claudeServers} 406 | activeEnvironments={activeEnvironments} 407 | onQuickAction={handleQuickAction} 408 | /> 409 | ); 410 | case 'databases': 411 | return ( 412 | <DatabaseListView 413 | hanaServers={hanaServers} 414 | claudeServers={claudeServers} 415 | activeEnvironments={activeEnvironments} 416 | onEditServer={openConfigModal} 417 | onAddToClaudeServer={openEnvironmentSelector} 418 | onDeleteServer={deleteServer} 419 | onBulkAction={handleBulkAction} 420 | onAddDatabase={() => openConfigModal()} 421 | /> 422 | ); 423 | case 'claude': 424 | return ( 425 | <ClaudeDesktopView 426 | claudeConfigPath={claudeConfigPath} 427 | claudeServers={claudeServers} 428 | activeEnvironments={activeEnvironments} 429 | onSetupPath={() => setIsPathSetupOpen(true)} 430 | onRemoveConnection={removeFromClaude} 431 | onViewConnection={(connection) => { 432 | setSelectedConnection(connection); 433 | setIsConnectionDetailsOpen(true); 434 | }} 435 | onRefresh={refreshClaudeData} 436 | onConfigPathChange={handleConfigPathChange} 437 | /> 438 | ); 439 | 440 | 441 | default: 442 | return ( 443 | <DashboardView 444 | hanaServers={hanaServers} 445 | claudeServers={claudeServers} 446 | activeEnvironments={activeEnvironments} 447 | onQuickAction={handleQuickAction} 448 | /> 449 | ); 450 | } 451 | }; 452 | 453 | return ( 454 | <GlassWindow maxWidth="full" maxHeight="full"> 455 | <div className="flex h-full bg-transparent p-3 sm:p-4 overflow-hidden"> 456 | {/* Loading Overlay */} 457 | {isLoading && ( 458 | <LoadingOverlay message="Processing your request..." /> 459 | )} 460 | 461 | {/* Floating Sidebar */} 462 | <div className="flex-shrink-0 h-full"> 463 | <VerticalSidebar 464 | activeView={activeView} 465 | onViewChange={handleViewChange} 466 | databaseCount={Object.keys(hanaServers).length} 467 | activeConnections={claudeServers.length} 468 | claudeConfigured={!!claudeConfigPath} 469 | /> 470 | </div> 471 | 472 | {/* Main Content */} 473 | <div className="flex-1 flex flex-col overflow-hidden bg-white/50 backdrop-blur-sm rounded-r-2xl sm:rounded-r-3xl ml-3"> 474 | {/* Main Content Area */} 475 | <main className="flex-1 overflow-hidden p-4 sm:p-6 pb-8"> 476 | <AnimatePresence mode="wait"> 477 | <motion.div 478 | key={activeView} 479 | initial={{ opacity: 0, x: 20 }} 480 | animate={{ opacity: 1, x: 0 }} 481 | exit={{ opacity: 0, x: -20 }} 482 | transition={{ duration: 0.2 }} 483 | className="h-full" 484 | > 485 | {renderMainContent()} 486 | </motion.div> 487 | </AnimatePresence> 488 | </main> 489 | </div> 490 | </div> 491 | 492 | {/* Modals */} 493 | {isConfigModalOpen && ( 494 | <ConfigurationModal 495 | isOpen={isConfigModalOpen} 496 | onClose={closeConfigModal} 497 | server={selectedServer} 498 | formData={formData} 499 | activeTab={activeTab} 500 | setActiveTab={setActiveTab} 501 | onFormChange={handleFormChange} 502 | onServerInfoChange={handleServerInfoChange} 503 | onSave={saveServer} 504 | isLoading={isLoading} 505 | /> 506 | )} 507 | 508 | {isEnvironmentSelectorOpen && deploymentTarget && ( 509 | <EnvironmentSelector 510 | isOpen={isEnvironmentSelectorOpen} 511 | onClose={() => { 512 | setIsEnvironmentSelectorOpen(false); 513 | setDeploymentTarget(null); 514 | }} 515 | serverName={deploymentTarget} 516 | environments={hanaServers[deploymentTarget]?.environments || {}} 517 | activeEnvironment={activeEnvironments[deploymentTarget]} 518 | onDeploy={deployToClaude} 519 | isLoading={isLoading} 520 | /> 521 | )} 522 | 523 | {isPathSetupOpen && ( 524 | <PathSetupModal 525 | isOpen={isPathSetupOpen} 526 | onClose={() => setIsPathSetupOpen(false)} 527 | pathInput={pathInput} 528 | setPathInput={setPathInput} 529 | onSave={saveClaudePath} 530 | isLoading={isLoading} 531 | /> 532 | )} 533 | 534 | <ConnectionDetailsModal 535 | isOpen={isConnectionDetailsOpen} 536 | onClose={() => { 537 | setIsConnectionDetailsOpen(false); 538 | setSelectedConnection(null); 539 | }} 540 | connection={selectedConnection} 541 | /> 542 | </GlassWindow> 543 | ); 544 | }; 545 | 546 | export default MainApp; 547 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/server/index.js: -------------------------------------------------------------------------------- ```javascript 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import fs from 'fs-extra'; 4 | import { join, dirname } from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import { homedir } from 'os'; 7 | import { existsSync } from 'fs'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const app = express(); 13 | const PORT = process.env.PORT || 3001; 14 | 15 | // Middleware 16 | app.use(cors()); 17 | app.use(express.json()); 18 | 19 | // Storage paths - ensure they work regardless of where the server is started from 20 | const UI_ROOT = dirname(__dirname); // This is the hana-mcp-ui directory 21 | const DATA_DIR = join(UI_ROOT, 'data'); 22 | const SERVERS_FILE = join(DATA_DIR, 'hana-servers.json'); 23 | const CONFIG_FILE = join(DATA_DIR, 'config.json'); 24 | const BACKUPS_DIR = join(DATA_DIR, 'backups'); 25 | const BACKUP_HISTORY_FILE = join(DATA_DIR, 'backup-history.json'); 26 | 27 | // Ensure data directory exists 28 | fs.ensureDirSync(DATA_DIR); 29 | fs.ensureDirSync(BACKUPS_DIR); 30 | 31 | // Default Claude config paths by OS 32 | const getDefaultClaudeConfigPath = () => { 33 | const platform = process.platform; 34 | switch (platform) { 35 | case 'darwin': 36 | return join(homedir(), 'Library/Application Support/Claude/claude_desktop_config.json'); 37 | case 'win32': 38 | return join(homedir(), 'AppData/Roaming/Claude/claude_desktop_config.json'); 39 | default: 40 | return join(homedir(), '.config/claude/claude_desktop_config.json'); 41 | } 42 | }; 43 | 44 | // Helper functions 45 | const loadServers = async () => { 46 | try { 47 | if (await fs.pathExists(SERVERS_FILE)) { 48 | return await fs.readJson(SERVERS_FILE); 49 | } 50 | return {}; 51 | } catch (error) { 52 | console.error('Error loading servers:', error); 53 | return {}; 54 | } 55 | }; 56 | 57 | const saveServers = async (servers) => { 58 | try { 59 | await fs.writeJson(SERVERS_FILE, servers, { spaces: 2 }); 60 | } catch (error) { 61 | console.error('Error saving servers:', error); 62 | throw error; 63 | } 64 | }; 65 | 66 | const loadConfig = async () => { 67 | try { 68 | if (await fs.pathExists(CONFIG_FILE)) { 69 | return await fs.readJson(CONFIG_FILE); 70 | } 71 | return {}; 72 | } catch (error) { 73 | console.error('Error loading config:', error); 74 | return {}; 75 | } 76 | }; 77 | 78 | const saveConfig = async (config) => { 79 | try { 80 | await fs.writeJson(CONFIG_FILE, config, { spaces: 2 }); 81 | } catch (error) { 82 | console.error('Error saving config:', error); 83 | throw error; 84 | } 85 | }; 86 | 87 | const loadClaudeConfig = async (configPath) => { 88 | try { 89 | if (await fs.pathExists(configPath)) { 90 | return await fs.readJson(configPath); 91 | } 92 | return { mcpServers: {} }; 93 | } catch (error) { 94 | console.error('Error loading Claude config:', error); 95 | return { mcpServers: {} }; 96 | } 97 | }; 98 | 99 | const saveClaudeConfig = async (configPath, config, skipBackup = false) => { 100 | try { 101 | // Create backup before saving (unless explicitly skipped) 102 | if (!skipBackup && await fs.pathExists(configPath)) { 103 | await createBackup(configPath, 'Auto backup before save'); 104 | } 105 | 106 | await fs.ensureDir(dirname(configPath)); 107 | await fs.writeJson(configPath, config, { spaces: 2 }); 108 | } catch (error) { 109 | console.error('Error saving Claude config:', error); 110 | throw error; 111 | } 112 | }; 113 | 114 | // Helper function to identify HANA MCP servers 115 | const isHanaMcpServer = (server) => { 116 | // Must have the correct command 117 | if (server.command !== 'hana-mcp-server') { 118 | return false; 119 | } 120 | 121 | // Must have HANA-specific environment variables for a complete HANA server 122 | if (!server.env) { 123 | return false; 124 | } 125 | 126 | const hasHanaHost = server.env.HANA_HOST; 127 | const hasHanaUser = server.env.HANA_USER; 128 | const hasHanaSchema = server.env.HANA_SCHEMA; 129 | 130 | // Must have all core HANA environment variables 131 | return hasHanaHost && hasHanaUser && hasHanaSchema; 132 | }; 133 | 134 | // Helper function to create composite server name 135 | const createCompositeServerName = (serverName, environment) => { 136 | return `${serverName} - ${environment}`; 137 | }; 138 | 139 | // Helper function to parse composite server name 140 | const parseCompositeServerName = (compositeName) => { 141 | const parts = compositeName.split(' - '); 142 | if (parts.length >= 2) { 143 | const environment = parts.pop(); // Last part is environment 144 | const serverName = parts.join(' - '); // Everything else is server name 145 | return { serverName, environment }; 146 | } 147 | return { serverName: compositeName, environment: null }; 148 | }; 149 | 150 | // Helper function to filter only HANA MCP servers from Claude config 151 | const filterHanaMcpServers = (mcpServers) => { 152 | const hanaServers = {}; 153 | 154 | for (const [name, server] of Object.entries(mcpServers || {})) { 155 | if (isHanaMcpServer(server)) { 156 | hanaServers[name] = server; 157 | } 158 | } 159 | 160 | return hanaServers; 161 | }; 162 | 163 | // Helper function to merge HANA servers while preserving non-HANA servers 164 | const mergeWithPreservation = (originalConfig, hanaServers) => { 165 | const newConfig = { ...originalConfig }; 166 | 167 | // Start with original mcpServers 168 | newConfig.mcpServers = { ...originalConfig.mcpServers }; 169 | 170 | // add new HANA servers 171 | newConfig.mcpServers = { 172 | ...newConfig.mcpServers, 173 | ...hanaServers 174 | }; 175 | 176 | return newConfig; 177 | }; 178 | 179 | // Backup management functions 180 | const loadBackupHistory = async () => { 181 | try { 182 | if (await fs.pathExists(BACKUP_HISTORY_FILE)) { 183 | return await fs.readJson(BACKUP_HISTORY_FILE); 184 | } 185 | return []; 186 | } catch (error) { 187 | console.error('Error loading backup history:', error); 188 | return []; 189 | } 190 | }; 191 | 192 | const saveBackupHistory = async (history) => { 193 | try { 194 | await fs.writeJson(BACKUP_HISTORY_FILE, history, { spaces: 2 }); 195 | } catch (error) { 196 | console.error('Error saving backup history:', error); 197 | throw error; 198 | } 199 | }; 200 | 201 | const createBackup = async (configPath, reason = 'Manual backup') => { 202 | try { 203 | if (!await fs.pathExists(configPath)) { 204 | throw new Error('Config file does not exist'); 205 | } 206 | 207 | const timestamp = new Date().toISOString(); 208 | const backupId = `backup_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; 209 | const backupFileName = `${backupId}.json`; 210 | const backupFilePath = join(BACKUPS_DIR, backupFileName); 211 | 212 | // Read and backup the config 213 | const config = await fs.readJson(configPath); 214 | await fs.writeJson(backupFilePath, config, { spaces: 2 }); 215 | 216 | // Create backup metadata 217 | const backupEntry = { 218 | id: backupId, 219 | timestamp, 220 | fileName: backupFileName, 221 | reason, 222 | size: JSON.stringify(config).length, 223 | mcpServerCount: Object.keys(config.mcpServers || {}).length, 224 | hanaServerCount: Object.keys(filterHanaMcpServers(config.mcpServers || {})).length 225 | }; 226 | 227 | // Update history 228 | const history = await loadBackupHistory(); 229 | history.unshift(backupEntry); // Add to beginning (most recent first) 230 | 231 | // Keep only last 50 backups 232 | if (history.length > 50) { 233 | const oldBackups = history.splice(50); 234 | // Delete old backup files 235 | for (const oldBackup of oldBackups) { 236 | const oldBackupPath = join(BACKUPS_DIR, oldBackup.fileName); 237 | if (await fs.pathExists(oldBackupPath)) { 238 | await fs.remove(oldBackupPath); 239 | } 240 | } 241 | } 242 | 243 | await saveBackupHistory(history); 244 | return backupEntry; 245 | } catch (error) { 246 | console.error('Error creating backup:', error); 247 | throw error; 248 | } 249 | }; 250 | 251 | const restoreBackup = async (backupId, configPath) => { 252 | try { 253 | const history = await loadBackupHistory(); 254 | const backup = history.find(b => b.id === backupId); 255 | 256 | if (!backup) { 257 | throw new Error('Backup not found'); 258 | } 259 | 260 | const backupFilePath = join(BACKUPS_DIR, backup.fileName); 261 | if (!await fs.pathExists(backupFilePath)) { 262 | throw new Error('Backup file not found'); 263 | } 264 | 265 | // Create a backup of current state before restoring 266 | await createBackup(configPath, `Before restoring to ${backup.timestamp}`); 267 | 268 | // Restore the backup 269 | const backupConfig = await fs.readJson(backupFilePath); 270 | await saveClaudeConfig(configPath, backupConfig, true); // Skip backup when restoring 271 | 272 | return backup; 273 | } catch (error) { 274 | console.error('Error restoring backup:', error); 275 | throw error; 276 | } 277 | }; 278 | 279 | // API Routes 280 | 281 | // Get Claude Desktop config path 282 | app.get('/api/claude/config-path', async (req, res) => { 283 | try { 284 | const config = await loadConfig(); 285 | res.json({ 286 | configPath: config.claudeConfigPath || null, 287 | defaultPath: getDefaultClaudeConfigPath() 288 | }); 289 | } catch (error) { 290 | res.status(500).json({ error: error.message }); 291 | } 292 | }); 293 | 294 | // Set Claude Desktop config path 295 | app.post('/api/claude/config-path', async (req, res) => { 296 | try { 297 | const { configPath } = req.body; 298 | 299 | if (!configPath) { 300 | return res.status(400).json({ error: 'Config path is required' }); 301 | } 302 | 303 | // Validate path exists or can be created 304 | const dir = dirname(configPath); 305 | await fs.ensureDir(dir); 306 | 307 | const config = await loadConfig(); 308 | config.claudeConfigPath = configPath; 309 | await saveConfig(config); 310 | 311 | res.json({ success: true, configPath }); 312 | } catch (error) { 313 | res.status(500).json({ error: error.message }); 314 | } 315 | }); 316 | 317 | // Get all local HANA servers 318 | app.get('/api/hana-servers', async (req, res) => { 319 | try { 320 | const servers = await loadServers(); 321 | res.json(servers); 322 | } catch (error) { 323 | res.status(500).json({ error: error.message }); 324 | } 325 | }); 326 | 327 | // Create new HANA server 328 | app.post('/api/hana-servers', async (req, res) => { 329 | try { 330 | const serverConfig = req.body; 331 | 332 | if (!serverConfig.name) { 333 | return res.status(400).json({ error: 'Server name is required' }); 334 | } 335 | 336 | const servers = await loadServers(); 337 | 338 | if (servers[serverConfig.name]) { 339 | return res.status(409).json({ error: 'Server with this name already exists' }); 340 | } 341 | 342 | // Add metadata 343 | serverConfig.created = new Date().toISOString(); 344 | serverConfig.modified = new Date().toISOString(); 345 | serverConfig.version = '1.0.0'; 346 | 347 | servers[serverConfig.name] = serverConfig; 348 | await saveServers(servers); 349 | 350 | res.status(201).json(serverConfig); 351 | } catch (error) { 352 | res.status(500).json({ error: error.message }); 353 | } 354 | }); 355 | 356 | // Update HANA server 357 | app.put('/api/hana-servers/:name', async (req, res) => { 358 | try { 359 | const { name } = req.params; 360 | const updatedConfig = req.body; 361 | 362 | const servers = await loadServers(); 363 | 364 | if (!servers[name]) { 365 | return res.status(404).json({ error: 'Server not found' }); 366 | } 367 | 368 | // Preserve creation date, update modified date 369 | updatedConfig.created = servers[name].created; 370 | updatedConfig.modified = new Date().toISOString(); 371 | 372 | servers[name] = updatedConfig; 373 | await saveServers(servers); 374 | 375 | res.json(updatedConfig); 376 | } catch (error) { 377 | res.status(500).json({ error: error.message }); 378 | } 379 | }); 380 | 381 | // Delete HANA server 382 | app.delete('/api/hana-servers/:name', async (req, res) => { 383 | try { 384 | const { name } = req.params; 385 | const servers = await loadServers(); 386 | 387 | if (!servers[name]) { 388 | return res.status(404).json({ error: 'Server not found' }); 389 | } 390 | 391 | delete servers[name]; 392 | await saveServers(servers); 393 | 394 | res.json({ success: true }); 395 | } catch (error) { 396 | res.status(500).json({ error: error.message }); 397 | } 398 | }); 399 | 400 | 401 | 402 | // Apply server to Claude Desktop 403 | app.post('/api/apply-to-claude', async (req, res) => { 404 | try { 405 | const { serverName, environment } = req.body; 406 | 407 | if (!serverName || !environment) { 408 | return res.status(400).json({ error: 'Server name and environment are required' }); 409 | } 410 | 411 | const config = await loadConfig(); 412 | const claudeConfigPath = config.claudeConfigPath; 413 | 414 | if (!claudeConfigPath) { 415 | return res.status(400).json({ error: 'Claude config path not set' }); 416 | } 417 | 418 | const servers = await loadServers(); 419 | const server = servers[serverName]; 420 | 421 | if (!server) { 422 | return res.status(404).json({ error: 'Server not found' }); 423 | } 424 | 425 | // Find environment with case-insensitive matching 426 | let envConfig = server.environments?.[environment]; 427 | let actualEnvironmentName = environment; 428 | 429 | if (!envConfig) { 430 | // Try case-insensitive matching 431 | const envKeys = Object.keys(server.environments || {}); 432 | const matchingKey = envKeys.find(key => key.toLowerCase() === environment.toLowerCase()); 433 | 434 | if (matchingKey) { 435 | envConfig = server.environments[matchingKey]; 436 | actualEnvironmentName = matchingKey; 437 | } else { 438 | return res.status(404).json({ error: 'Environment not found' }); 439 | } 440 | } 441 | 442 | const claudeConfig = await loadClaudeConfig(claudeConfigPath); 443 | 444 | // create a new HANA server 445 | const newHanaServer = { 446 | [serverName]: { 447 | command: 'hana-mcp-server', 448 | env: envConfig 449 | } 450 | }; 451 | 452 | // Merge while preserving non-HANA servers 453 | const updatedConfig = mergeWithPreservation(claudeConfig, newHanaServer); 454 | 455 | await saveClaudeConfig(claudeConfigPath, updatedConfig); 456 | 457 | res.json({ success: true, serverName, environment: actualEnvironmentName }); 458 | } catch (error) { 459 | res.status(500).json({ error: error.message }); 460 | } 461 | }); 462 | 463 | // Remove server from Claude Desktop 464 | app.delete('/api/claude/:serverName', async (req, res) => { 465 | try { 466 | const { serverName } = req.params; 467 | 468 | const config = await loadConfig(); 469 | const claudeConfigPath = config.claudeConfigPath; 470 | 471 | if (!claudeConfigPath) { 472 | return res.status(400).json({ error: 'Claude config path not set' }); 473 | } 474 | 475 | const claudeConfig = await loadClaudeConfig(claudeConfigPath); 476 | 477 | const serverToDelete = claudeConfig.mcpServers[serverName]; 478 | if (!serverToDelete) { 479 | return res.status(404).json({ error: 'Server not found in Claude config' }); 480 | } 481 | 482 | // Only delete if it's a HANA MCP server 483 | if (!isHanaMcpServer(serverToDelete)) { 484 | return res.status(400).json({ error: 'Cannot delete non-HANA MCP server' }); 485 | } 486 | 487 | delete claudeConfig.mcpServers[serverName]; 488 | await saveClaudeConfig(claudeConfigPath, claudeConfig); 489 | 490 | res.json({ success: true }); 491 | } catch (error) { 492 | res.status(500).json({ error: error.message }); 493 | } 494 | }); 495 | 496 | // Get Claude Desktop servers 497 | app.get('/api/claude', async (req, res) => { 498 | try { 499 | const config = await loadConfig(); 500 | const claudeConfigPath = config.claudeConfigPath; 501 | 502 | if (!claudeConfigPath) { 503 | return res.json([]); 504 | } 505 | 506 | const claudeConfig = await loadClaudeConfig(claudeConfigPath); 507 | const claudeServers = []; 508 | 509 | // Filter only HANA MCP servers 510 | const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); 511 | 512 | for (const [serverName, server] of Object.entries(hanaServers)) { 513 | const serverData = { 514 | name: serverName, 515 | environment: server.env?.ENVIRONMENT || 'Development', 516 | env: server.env || {} 517 | }; 518 | claudeServers.push(serverData); 519 | } 520 | 521 | res.json(claudeServers); 522 | } catch (error) { 523 | console.error('Error loading Claude servers:', error); 524 | res.status(500).json({ error: error.message }); 525 | } 526 | }); 527 | 528 | // Get active environments 529 | app.get('/api/claude/active-environments', async (req, res) => { 530 | try { 531 | const config = await loadConfig(); 532 | const claudeConfigPath = config.claudeConfigPath; 533 | 534 | if (!claudeConfigPath) { 535 | return res.json({}); 536 | } 537 | 538 | const claudeConfig = await loadClaudeConfig(claudeConfigPath); 539 | const servers = await loadServers(); 540 | const activeEnvironments = {}; 541 | 542 | // Filter only HANA MCP servers 543 | const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); 544 | 545 | for (const [serverName, claudeServer] of Object.entries(hanaServers)) { 546 | if (servers[serverName]) { 547 | // Store the active environment for this server 548 | activeEnvironments[serverName] = claudeServer.env?.ENVIRONMENT || 'Development'; 549 | } 550 | } 551 | 552 | res.json(activeEnvironments); 553 | } catch (error) { 554 | res.status(500).json({ error: error.message }); 555 | } 556 | }); 557 | 558 | // Validate connection 559 | app.post('/api/validate-connection', async (req, res) => { 560 | try { 561 | const config = req.body; 562 | 563 | // Basic validation 564 | const required = ['HANA_HOST', 'HANA_USER', 'HANA_PASSWORD', 'HANA_SCHEMA']; 565 | for (const field of required) { 566 | if (!config[field]) { 567 | return res.status(400).json({ 568 | valid: false, 569 | error: `${field} is required` 570 | }); 571 | } 572 | } 573 | 574 | // For now, just validate required fields 575 | // In a real implementation, you could test the actual connection 576 | res.json({ valid: true }); 577 | } catch (error) { 578 | res.status(500).json({ valid: false, error: error.message }); 579 | } 580 | }); 581 | 582 | // Backup Management APIs 583 | 584 | // Get backup history 585 | app.get('/api/claude/backups', async (req, res) => { 586 | try { 587 | const history = await loadBackupHistory(); 588 | res.json(history); 589 | } catch (error) { 590 | res.status(500).json({ error: error.message }); 591 | } 592 | }); 593 | 594 | // Create manual backup 595 | app.post('/api/claude/backups', async (req, res) => { 596 | try { 597 | const { reason = 'Manual backup' } = req.body; 598 | 599 | const config = await loadConfig(); 600 | const claudeConfigPath = config.claudeConfigPath; 601 | 602 | if (!claudeConfigPath) { 603 | return res.status(400).json({ error: 'Claude config path not set' }); 604 | } 605 | 606 | const backup = await createBackup(claudeConfigPath, reason); 607 | res.json(backup); 608 | } catch (error) { 609 | res.status(500).json({ error: error.message }); 610 | } 611 | }); 612 | 613 | // Restore backup 614 | app.post('/api/claude/backups/:backupId/restore', async (req, res) => { 615 | try { 616 | const { backupId } = req.params; 617 | 618 | const config = await loadConfig(); 619 | const claudeConfigPath = config.claudeConfigPath; 620 | 621 | if (!claudeConfigPath) { 622 | return res.status(400).json({ error: 'Claude config path not set' }); 623 | } 624 | 625 | const backup = await restoreBackup(backupId, claudeConfigPath); 626 | res.json({ success: true, backup }); 627 | } catch (error) { 628 | res.status(500).json({ error: error.message }); 629 | } 630 | }); 631 | 632 | // Delete backup 633 | app.delete('/api/claude/backups/:backupId', async (req, res) => { 634 | try { 635 | const { backupId } = req.params; 636 | 637 | const history = await loadBackupHistory(); 638 | const backupIndex = history.findIndex(b => b.id === backupId); 639 | 640 | if (backupIndex === -1) { 641 | return res.status(404).json({ error: 'Backup not found' }); 642 | } 643 | 644 | const backup = history[backupIndex]; 645 | const backupFilePath = join(BACKUPS_DIR, backup.fileName); 646 | 647 | // Remove from history 648 | history.splice(backupIndex, 1); 649 | await saveBackupHistory(history); 650 | 651 | // Delete backup file 652 | if (await fs.pathExists(backupFilePath)) { 653 | await fs.remove(backupFilePath); 654 | } 655 | 656 | res.json({ success: true }); 657 | } catch (error) { 658 | res.status(500).json({ error: error.message }); 659 | } 660 | }); 661 | 662 | // Get backup details 663 | app.get('/api/claude/backups/:backupId', async (req, res) => { 664 | try { 665 | const { backupId } = req.params; 666 | 667 | const history = await loadBackupHistory(); 668 | const backup = history.find(b => b.id === backupId); 669 | 670 | if (!backup) { 671 | return res.status(404).json({ error: 'Backup not found' }); 672 | } 673 | 674 | const backupFilePath = join(BACKUPS_DIR, backup.fileName); 675 | if (!await fs.pathExists(backupFilePath)) { 676 | return res.status(404).json({ error: 'Backup file not found' }); 677 | } 678 | 679 | const backupConfig = await fs.readJson(backupFilePath); 680 | res.json({ 681 | ...backup, 682 | config: backupConfig 683 | }); 684 | } catch (error) { 685 | res.status(500).json({ error: error.message }); 686 | } 687 | }); 688 | 689 | // Health check 690 | app.get('/api/status', async (req, res) => { 691 | try { 692 | const config = await loadConfig(); 693 | const claudeConfigPath = config.claudeConfigPath; 694 | let claudeConfigExists = false; 695 | 696 | if (claudeConfigPath) { 697 | claudeConfigExists = await fs.pathExists(claudeConfigPath); 698 | } 699 | 700 | res.json({ 701 | status: 'healthy', 702 | version: '1.0.0', 703 | claudeConfigPath, 704 | claudeConfigExists, 705 | timestamp: new Date().toISOString() 706 | }); 707 | } catch (error) { 708 | res.status(500).json({ error: error.message }); 709 | } 710 | }); 711 | 712 | app.listen(PORT, () => { 713 | console.log(`🚀 HANA MCP UI Backend running on port ${PORT}`); 714 | }); ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ConfigurationModal.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { useState, useEffect } from 'react' 3 | import { XMarkIcon, ServerIcon, PlusIcon, PencilIcon, ExclamationTriangleIcon, TrashIcon } from '@heroicons/react/24/outline' 4 | import toast from 'react-hot-toast' 5 | import { 6 | detectDatabaseType, 7 | shouldShowMDCFields, 8 | validateForDatabaseType 9 | } from '../utils/databaseTypes' 10 | 11 | const ConfigurationModal = ({ 12 | isOpen, 13 | onClose, 14 | server, 15 | formData, 16 | activeTab, 17 | setActiveTab, 18 | onFormChange, 19 | onServerInfoChange, 20 | onSave, 21 | isLoading 22 | }) => { 23 | const [availableEnvironments, setAvailableEnvironments] = useState([ 24 | { id: 'development', name: 'Development', color: 'blue' }, 25 | { id: 'staging', name: 'Staging', color: 'amber' }, 26 | { id: 'production', name: 'Production', color: 'green' } 27 | ]) 28 | const [selectedEnvironments, setSelectedEnvironments] = useState(new Set()) 29 | const [showEnvironmentSelector, setShowEnvironmentSelector] = useState(false) 30 | const [validationErrors, setValidationErrors] = useState({}) 31 | const [showDeleteConfirm, setShowDeleteConfirm] = useState(null) 32 | 33 | useEffect(() => { 34 | // Load saved environments 35 | const savedEnvironments = localStorage.getItem('hana-environments') 36 | if (savedEnvironments) { 37 | setAvailableEnvironments(JSON.parse(savedEnvironments)) 38 | } 39 | 40 | // Set selected environments based on formData.environments 41 | if (formData && formData.environments) { 42 | const envKeys = Object.keys(formData.environments) 43 | setSelectedEnvironments(new Set(envKeys)) 44 | } else { 45 | // Clear selected environments for new server 46 | setSelectedEnvironments(new Set()) 47 | } 48 | }, [formData, isOpen]) 49 | 50 | useEffect(() => { 51 | if (!isOpen) return 52 | const onKeyDown = (e) => { 53 | if (e.key === 'Escape') { 54 | onClose() 55 | } 56 | } 57 | window.addEventListener('keydown', onKeyDown) 58 | return () => window.removeEventListener('keydown', onKeyDown) 59 | }, [isOpen, onClose]) 60 | 61 | // validation function with database type support 62 | const validateEnvironment = (envId, envData) => { 63 | const detectedType = detectDatabaseType(envData) 64 | const manualType = envData.HANA_CONNECTION_TYPE || 'auto' 65 | const dbType = manualType === 'auto' ? detectedType : manualType 66 | 67 | const validation = validateForDatabaseType(envData, dbType) 68 | return validation.errors 69 | } 70 | 71 | // Validate all environments 72 | const validateAllEnvironments = () => { 73 | const allErrors = {} 74 | 75 | if (formData.environments) { 76 | Object.keys(formData.environments).forEach(envId => { 77 | const envErrors = validateEnvironment(envId, formData.environments[envId]) 78 | if (Object.keys(envErrors).length > 0) { 79 | allErrors[envId] = envErrors 80 | } 81 | }) 82 | } 83 | 84 | setValidationErrors(allErrors) 85 | return Object.keys(allErrors).length === 0 86 | } 87 | 88 | // Check if current environment has validation errors 89 | const getCurrentEnvironmentErrors = () => { 90 | if (!activeTab || !validationErrors[activeTab]) return {} 91 | return validationErrors[activeTab] 92 | } 93 | 94 | // Handle form change with validation 95 | const handleFormChange = (environment, field, value) => { 96 | onFormChange(environment, field, value) 97 | 98 | // Clear validation error for this field if it exists 99 | if (validationErrors[environment] && validationErrors[environment][field]) { 100 | const newErrors = { ...validationErrors } 101 | delete newErrors[environment][field] 102 | if (Object.keys(newErrors[environment]).length === 0) { 103 | delete newErrors[environment] 104 | } 105 | setValidationErrors(newErrors) 106 | } 107 | } 108 | 109 | const addEnvironment = (envId) => { 110 | 111 | 112 | const newSelected = new Set(selectedEnvironments) 113 | newSelected.add(envId) 114 | setSelectedEnvironments(newSelected) 115 | 116 | // Initialize the environment in formData if it doesn't exist 117 | if (!formData.environments || !formData.environments[envId]) { 118 | onFormChange(envId, 'ENVIRONMENT', envId.toUpperCase()) 119 | } 120 | 121 | // Make the newly added environment active 122 | setActiveTab(envId) 123 | setShowEnvironmentSelector(false) 124 | } 125 | 126 | const removeEnvironment = (envId) => { 127 | 128 | const newSelected = new Set(selectedEnvironments) 129 | newSelected.delete(envId) 130 | 131 | // Remove the environment from formData as well 132 | if (formData.environments && formData.environments[envId]) { 133 | const newFormData = { ...formData } 134 | delete newFormData.environments[envId] 135 | // Update the parent's formData 136 | onServerInfoChange('environments', newFormData.environments) 137 | } 138 | 139 | // Clear validation errors for this environment 140 | if (validationErrors[envId]) { 141 | const newErrors = { ...validationErrors } 142 | delete newErrors[envId] 143 | setValidationErrors(newErrors) 144 | } 145 | 146 | // If we removed the active tab, switch to the first available 147 | if (activeTab === envId) { 148 | const remaining = Array.from(newSelected) 149 | setActiveTab(remaining.length > 0 ? remaining[0] : null) 150 | } 151 | 152 | setSelectedEnvironments(newSelected) 153 | setShowDeleteConfirm(null) 154 | 155 | 156 | } 157 | 158 | const getAvailableEnvironmentsToAdd = () => { 159 | const available = availableEnvironments.filter(env => !selectedEnvironments.has(env.id)) 160 | return available 161 | } 162 | 163 | // Handle save with validation 164 | const handleSave = () => { 165 | if (!formData.name.trim()) { 166 | toast.error('Server name is required') 167 | return 168 | } 169 | 170 | // Validate all environments 171 | if (!validateAllEnvironments()) { 172 | toast.error('Please fill in all required fields for selected environments') 173 | return 174 | } 175 | 176 | onSave() 177 | } 178 | 179 | if (!isOpen) return null 180 | 181 | return ( 182 | <motion.div 183 | className='fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4' 184 | initial={{ opacity: 0 }} 185 | animate={{ opacity: 1 }} 186 | exit={{ opacity: 0 }} 187 | onClick={onClose} 188 | > 189 | <motion.div 190 | className='bg-white rounded-2xl shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden border border-gray-200 flex flex-col' 191 | initial={{ scale: 0.9, opacity: 0, y: 20 }} 192 | animate={{ scale: 1, opacity: 1, y: 0 }} 193 | exit={{ scale: 0.9, opacity: 0, y: 20 }} 194 | transition={{ type: 'spring', stiffness: 300, damping: 25 }} 195 | onClick={(e) => e.stopPropagation()} 196 | > 197 | {/* Fixed Header */} 198 | <div className='sticky top-0 z-10 px-8 py-6 border-b border-gray-100 bg-white rounded-t-2xl'> 199 | <div className='flex items-center justify-between'> 200 | <div className='flex items-center gap-4'> 201 | <div className='p-3 bg-gray-100 rounded-xl'> 202 | {server ? <PencilIcon className='w-5 h-5 text-gray-600' /> : <PlusIcon className='w-5 h-5 text-gray-600' />} 203 | </div> 204 | <div> 205 | <h2 className='text-2xl font-bold text-gray-900 leading-tight'> 206 | {server ? 'Edit HANA Server' : 'Add HANA Server'} 207 | </h2> 208 | <p className='text-base text-gray-600 mt-2 font-medium'> 209 | {server ? 'Update database connection settings' : 'Configure a new database connection'} 210 | </p> 211 | </div> 212 | </div> 213 | <button 214 | onClick={onClose} 215 | className='p-3 rounded-xl text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors' 216 | > 217 | <XMarkIcon className='w-5 h-5' /> 218 | </button> 219 | </div> 220 | </div> 221 | 222 | {/* Scrollable Body */} 223 | <div className='flex-1 overflow-y-auto p-8'> 224 | {/* Server Info */} 225 | <div className='mb-8'> 226 | <h3 className='text-xl font-bold text-gray-900 mb-6 flex items-center gap-3'> 227 | <ServerIcon className='w-5 h-5 text-gray-600' /> 228 | Server Information 229 | </h3> 230 | <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> 231 | <div> 232 | <label className='block text-base font-semibold text-gray-800 mb-3'> 233 | Server Name <span className='text-red-500'>*</span> 234 | </label> 235 | <input 236 | type='text' 237 | value={formData.name} 238 | onChange={(e) => onServerInfoChange('name', e.target.value)} 239 | placeholder='e.g. Production HANA' 240 | disabled={!!server} 241 | className={`w-full px-4 py-3 border border-gray-300 rounded-xl text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors ${ 242 | server 243 | ? 'bg-gray-100 text-gray-600 cursor-not-allowed' 244 | : 'text-gray-400' 245 | }`} 246 | /> 247 | 248 | </div> 249 | <div> 250 | <label className='block text-base font-semibold text-gray-800 mb-3'> 251 | Description 252 | </label> 253 | <input 254 | type='text' 255 | value={formData.description} 256 | onChange={(e) => onServerInfoChange('description', e.target.value)} 257 | placeholder='Optional description' 258 | className='w-full px-4 py-3 border border-gray-300 rounded-xl text-gray-900 placeholder-gray-400 text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors' 259 | /> 260 | </div> 261 | </div> 262 | </div> 263 | 264 | {/* Environment Configuration */} 265 | <div className='mb-10'> 266 | <div className='flex items-center justify-between mb-6'> 267 | <h3 className='text-xl font-bold text-gray-900'>Environment Configuration</h3> 268 | <span className='text-sm px-4 py-2 bg-gray-100 text-gray-600 rounded-lg font-semibold'>Optional</span> 269 | </div> 270 | 271 | <p className='text-base text-gray-600 mb-6 font-medium'> 272 | Configure for specific environments: 273 | </p> 274 | {/* Configured Environments */} 275 | {selectedEnvironments.size > 0 && ( 276 | <div className='space-y-3 mb-6'> 277 | {Array.from(selectedEnvironments).map((envId) => { 278 | const env = availableEnvironments.find(e => e.id === envId) 279 | const hasErrors = validationErrors[envId] && Object.keys(validationErrors[envId]).length > 0 280 | const isDeleteConfirm = showDeleteConfirm === envId 281 | 282 | return ( 283 | <div 284 | key={envId} 285 | className={`flex items-center justify-between p-4 border rounded-xl group transition-all ${ 286 | hasErrors 287 | ? 'border-red-200 bg-red-50' 288 | : 'border-gray-200 bg-gray-50' 289 | }`} 290 | > 291 | <div className='flex items-center gap-4'> 292 | <div className={`w-4 h-4 rounded-full bg-${env?.color}-500`}></div> 293 | <div className='flex items-center gap-3'> 294 | <h4 className='text-base font-semibold text-gray-900'>{env?.name}</h4> 295 | {hasErrors && ( 296 | <div className='flex items-center gap-1 text-red-600'> 297 | <ExclamationTriangleIcon className='w-4 h-4' /> 298 | <span className='text-sm font-medium'> 299 | {Object.keys(validationErrors[envId]).length} required field(s) missing 300 | </span> 301 | </div> 302 | )} 303 | </div> 304 | </div> 305 | <div className='flex items-center gap-2'> 306 | {isDeleteConfirm ? ( 307 | <> 308 | <button 309 | type='button' 310 | onClick={() => setShowDeleteConfirm(null)} 311 | className='px-3 py-1 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors' 312 | > 313 | Cancel 314 | </button> 315 | <button 316 | type='button' 317 | onClick={() => removeEnvironment(envId)} 318 | className='px-3 py-1 text-sm font-medium text-white bg-red-600 border border-red-600 rounded-lg hover:bg-red-700 transition-colors' 319 | > 320 | Delete 321 | </button> 322 | </> 323 | ) : ( 324 | <button 325 | type='button' 326 | onClick={() => setShowDeleteConfirm(envId)} 327 | className='p-2 text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100' 328 | title='Delete environment configuration' 329 | > 330 | <TrashIcon className='w-4 h-4' /> 331 | </button> 332 | )} 333 | </div> 334 | </div> 335 | ) 336 | })} 337 | </div> 338 | )} 339 | 340 | {/* Add Environment Button */} 341 | {getAvailableEnvironmentsToAdd().length > 0 && ( 342 | <button 343 | type='button' 344 | onClick={() => setShowEnvironmentSelector(true)} 345 | className='w-full p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-[#86a0ff] hover:bg-[#86a0ff]/5 transition-colors flex items-center justify-center gap-4 text-gray-600 hover:text-[#86a0ff] group' 346 | > 347 | <PlusIcon className='w-5 h-5 group-hover:scale-110 transition-transform' /> 348 | <span className='text-lg font-bold'>Add Environment</span> 349 | </button> 350 | )} 351 | 352 | {/* Environment Tabs - Only show if environments are selected */} 353 | {selectedEnvironments.size > 0 && ( 354 | <> 355 | <div className='mt-8 mb-6'> 356 | <div className='flex border-b border-gray-200 overflow-x-auto'> 357 | {Array.from(selectedEnvironments).map((envId) => { 358 | const env = availableEnvironments.find(e => e.id === envId) 359 | const hasErrors = validationErrors[envId] && Object.keys(validationErrors[envId]).length > 0 360 | 361 | return ( 362 | <button 363 | key={envId} 364 | type='button' 365 | onClick={() => setActiveTab(envId)} 366 | className={`px-6 py-3 font-semibold text-sm transition-colors border-b-2 whitespace-nowrap flex items-center gap-2 ${ 367 | activeTab === envId 368 | ? 'text-blue-600 border-blue-600' 369 | : 'text-gray-500 border-transparent hover:text-gray-700' 370 | }`} 371 | > 372 | <div className='flex items-center gap-3'> 373 | <div className={`w-3 h-3 rounded-full bg-${env?.color}-500`}></div> 374 | {env?.name || envId} 375 | </div> 376 | {hasErrors && ( 377 | <div className='w-2 h-2 rounded-full bg-red-500'></div> 378 | )} 379 | </button> 380 | ) 381 | })} 382 | </div> 383 | </div> 384 | 385 | {/* Tab Content - Only show if an environment is selected */} 386 | {activeTab && selectedEnvironments.has(activeTab) && ( 387 | <motion.div 388 | key={activeTab} 389 | initial={{ opacity: 0, y: 10 }} 390 | animate={{ opacity: 1, y: 0 }} 391 | transition={{ duration: 0.2 }} 392 | > 393 | <EnvironmentForm 394 | environment={activeTab} 395 | data={formData.environments[activeTab] || {}} 396 | onChange={handleFormChange} 397 | errors={getCurrentEnvironmentErrors()} 398 | /> 399 | </motion.div> 400 | )} 401 | </> 402 | )} 403 | 404 | {/* No environments selected - encouraging message */} 405 | {selectedEnvironments.size === 0 && ( 406 | <div className='text-center py-12 bg-gray-50 border border-gray-200 rounded-xl'> 407 | <div className='w-12 h-12 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center'> 408 | <svg className='w-6 h-6 text-gray-600' fill='none' stroke='currentColor' viewBox='0 0 24 24'> 409 | <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' /> 410 | </svg> 411 | </div> 412 | <p className='text-lg font-bold text-gray-800'>No environments configured</p> 413 | <p className='text-base text-gray-600 mt-2'>Add environment-specific settings or skip for a basic connection</p> 414 | </div> 415 | )} 416 | </div> 417 | </div> 418 | 419 | {/* Fixed Footer */} 420 | <div className='sticky bottom-0 z-10 px-8 py-6 border-t border-gray-100 bg-gray-50 rounded-b-2xl flex justify-end gap-4'> 421 | <button 422 | onClick={onClose} 423 | disabled={isLoading} 424 | className='px-8 py-3 text-base font-semibold text-gray-700 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-gray-400 disabled:opacity-50 transition-colors shadow-sm hover:shadow-md' 425 | > 426 | Cancel 427 | </button> 428 | <button 429 | onClick={handleSave} 430 | disabled={isLoading} 431 | className='px-10 py-3 text-base font-semibold text-white bg-[#86a0ff] border border-transparent rounded-xl hover:bg-[#7990e6] focus:outline-none focus:ring-2 focus:ring-[#86a0ff] disabled:opacity-50 min-w-[160px] transition-colors shadow-md hover:shadow-lg' 432 | > 433 | {isLoading ? 'Saving...' : (server ? 'Update Server' : 'Add Server')} 434 | </button> 435 | </div> 436 | </motion.div> 437 | 438 | {/* Environment Selector Modal */} 439 | {showEnvironmentSelector && ( 440 | <div 441 | className='absolute inset-0 bg-black/20 flex items-center justify-center p-4 z-10' 442 | onClick={() => setShowEnvironmentSelector(false)} 443 | > 444 | <motion.div 445 | initial={{ opacity: 0, scale: 0.98 }} 446 | animate={{ opacity: 1, scale: 1 }} 447 | transition={{ duration: 0.15 }} 448 | className='bg-white rounded-xl shadow-xl max-w-md w-full border border-gray-200' 449 | onClick={(e) => e.stopPropagation()} 450 | > 451 | <div className='p-6 border-b border-gray-200'> 452 | <div className='flex items-center justify-between'> 453 | <h3 className='text-xl font-bold text-gray-900'>Add Environment</h3> 454 | <button 455 | onClick={() => setShowEnvironmentSelector(false)} 456 | className='p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-colors' 457 | > 458 | <XMarkIcon className='w-5 h-5' /> 459 | </button> 460 | </div> 461 | <p className='text-base text-gray-600 mt-2 font-medium'>Select an environment for which you want to configure the connection</p> 462 | </div> 463 | 464 | <div className='p-6 max-h-80 overflow-y-auto'> 465 | <div className='space-y-3'> 466 | {getAvailableEnvironmentsToAdd().map((env) => ( 467 | <button 468 | key={env.id} 469 | type='button' 470 | onClick={(e) => { 471 | e.preventDefault() 472 | e.stopPropagation() 473 | addEnvironment(env.id) 474 | }} 475 | className='w-full p-5 text-left border border-gray-200 rounded-xl hover:bg-gray-50 hover:border-[#86a0ff] hover:shadow-sm transition-all duration-200 group' 476 | > 477 | <div className='flex items-center gap-4'> 478 | <div className={`w-5 h-5 rounded-full bg-${env.color}-500 shadow-sm`}></div> 479 | <div className='flex-1'> 480 | <h4 className='text-lg font-bold text-gray-900 group-hover:text-[#86a0ff] transition-colors'>{env.name}</h4> 481 | </div> 482 | <svg className='w-5 h-5 text-gray-400 group-hover:text-[#86a0ff] transition-colors' fill='none' stroke='currentColor' viewBox='0 0 24 24'> 483 | <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 4v16m8-8H4' /> 484 | </svg> 485 | </div> 486 | </button> 487 | ))} 488 | 489 | {getAvailableEnvironmentsToAdd().length === 0 && ( 490 | <div className='text-center py-8 text-gray-500'> 491 | <svg className='w-12 h-12 mx-auto mb-4 text-gray-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'> 492 | <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' /> 493 | </svg> 494 | <p className='text-lg font-semibold text-gray-600'>All available environments are already configured</p> 495 | </div> 496 | )} 497 | </div> 498 | </div> 499 | </motion.div> 500 | </div> 501 | )} 502 | </motion.div> 503 | ) 504 | } 505 | 506 | // Toggle Switch Component 507 | const ToggleSwitch = ({ label, value, onChange, description }) => { 508 | const isEnabled = value === 'true' || value === true 509 | 510 | return ( 511 | <div className="flex items-center justify-between"> 512 | <div className="flex-1"> 513 | <label className="block text-base font-semibold text-gray-700">{label}</label> 514 | {description && <p className="text-sm text-gray-500 mt-1 font-medium">{description}</p>} 515 | </div> 516 | <button 517 | type="button" 518 | onClick={() => onChange(!isEnabled ? 'true' : 'false')} 519 | className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:ring-offset-2 ${ 520 | isEnabled ? 'bg-[#86a0ff]' : 'bg-gray-200' 521 | }`} 522 | > 523 | <span 524 | className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ 525 | isEnabled ? 'translate-x-6' : 'translate-x-1' 526 | }`} 527 | /> 528 | </button> 529 | </div> 530 | ) 531 | } 532 | 533 | // Environment Form Component 534 | const EnvironmentForm = ({ environment, data = {}, onChange, errors = {} }) => { 535 | // Get environment display name 536 | const getEnvironmentDisplayName = (envId) => { 537 | const envMap = { 538 | 'development': 'DEVELOPMENT', 539 | 'staging': 'STAGING', 540 | 'production': 'PRODUCTION', 541 | 'testing': 'TESTING', 542 | 'qa': 'QA' 543 | } 544 | return envMap[envId] || envId.toUpperCase() 545 | } 546 | 547 | // Ensure ENVIRONMENT parameter is automatically set 548 | const environmentValue = data.ENVIRONMENT || getEnvironmentDisplayName(environment) 549 | 550 | // Database type state - default to single_container if not specified 551 | const [manualType, setManualType] = useState(data.HANA_CONNECTION_TYPE || 'single_container') 552 | 553 | // Note: We no longer use auto-detect in the UI, users must explicitly select database type 554 | 555 | // Database type options for radio buttons 556 | const databaseTypeOptions = [ 557 | { 558 | label: 'Single-Container Database', 559 | value: 'single_container', 560 | description: 'Basic HANA database - HOST:PORT connection', 561 | required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'SCHEMA'] 562 | }, 563 | { 564 | label: 'MDC System Database', 565 | value: 'mdc_system', 566 | description: 'Multi-tenant system database - HOST:PORT;INSTANCE', 567 | required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'INSTANCE_NUMBER'] 568 | }, 569 | { 570 | label: 'MDC Tenant Database', 571 | value: 'mdc_tenant', 572 | description: 'Multi-tenant tenant database - HOST:PORT + DATABASE_NAME', 573 | required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'INSTANCE_NUMBER', 'DATABASE_NAME'] 574 | } 575 | ] 576 | 577 | // Auto-set default values when component renders 578 | useEffect(() => { 579 | const defaults = { 580 | ENVIRONMENT: environmentValue, 581 | HANA_PORT: '443', 582 | HANA_SSL: 'true', 583 | HANA_ENCRYPT: 'true', 584 | HANA_VALIDATE_CERT: 'true', 585 | HANA_CONNECTION_TYPE: 'auto', 586 | LOG_LEVEL: 'info', 587 | ENABLE_FILE_LOGGING: 'true', 588 | ENABLE_CONSOLE_LOGGING: 'false' 589 | } 590 | 591 | // Set any missing default values 592 | Object.entries(defaults).forEach(([key, defaultValue]) => { 593 | if (!data[key]) { 594 | onChange(environment, key, defaultValue) 595 | } 596 | }) 597 | }, [environment, data, environmentValue, onChange]) 598 | 599 | // Handle connection type change 600 | const handleConnectionTypeChange = (e) => { 601 | const newType = e.target.value 602 | setManualType(newType) 603 | onChange(environment, 'HANA_CONNECTION_TYPE', newType) 604 | } 605 | 606 | // Helper function to render input field with error handling 607 | const renderInputField = (field, label, type = 'text', placeholder = '', required = false) => { 608 | const hasError = errors[field] 609 | 610 | return ( 611 | <div> 612 | <label className={`block text-base font-semibold mb-3 ${ 613 | hasError ? 'text-red-700' : 'text-gray-800' 614 | }`}> 615 | {label} {required && <span className='text-red-500'>*</span>} 616 | </label> 617 | <input 618 | type={type} 619 | value={data[field] || ''} 620 | onChange={(e) => onChange(environment, field, e.target.value)} 621 | placeholder={placeholder} 622 | className={`w-full px-4 py-3 border rounded-xl text-gray-900 placeholder-gray-400 text-base focus:outline-none focus:ring-2 transition-colors ${ 623 | hasError 624 | ? 'border-red-300 focus:ring-red-500 focus:border-red-500 bg-red-50' 625 | : 'border-gray-300 focus:ring-[#86a0ff] focus:border-[#86a0ff]' 626 | }`} 627 | /> 628 | {hasError && ( 629 | <p className='text-sm text-red-600 mt-1 font-medium flex items-center gap-1'> 630 | <ExclamationTriangleIcon className='w-3 h-3' /> 631 | {hasError} 632 | </p> 633 | )} 634 | </div> 635 | ) 636 | } 637 | 638 | return ( 639 | <div className='space-y-8'> 640 | 641 | {/* Connection Settings */} 642 | <div> 643 | <h4 className='text-lg font-bold text-gray-900 mb-6'>Connection Settings</h4> 644 | <div className='grid grid-cols-1 lg:grid-cols-3 gap-6'> 645 | {renderInputField('HANA_HOST', 'Host', 'text', 'your-hana-host.com', true)} 646 | {renderInputField('HANA_PORT', 'Port', 'number', '443')} 647 | {renderInputField('HANA_SCHEMA', 'Schema', 'text', 'your-schema', true)} 648 | </div> 649 | 650 | {/* Database Type Selection */} 651 | <div className='mt-6'> 652 | <label className='block text-base font-semibold mb-3 text-gray-800'> 653 | Database Type 654 | </label> 655 | <div className='space-y-3'> 656 | {databaseTypeOptions.map((option) => ( 657 | <label 658 | key={option.value} 659 | className={` 660 | flex items-start gap-4 p-4 rounded-xl border-2 cursor-pointer transition-all duration-200 661 | ${manualType === option.value 662 | ? 'border-[#86a0ff] bg-blue-50 shadow-md' 663 | : 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' 664 | } 665 | `} 666 | > 667 | <input 668 | type="radio" 669 | name="databaseType" 670 | value={option.value} 671 | checked={manualType === option.value} 672 | onChange={handleConnectionTypeChange} 673 | className="mt-1 w-4 h-4 text-[#86a0ff] border-gray-300 focus:ring-[#86a0ff] focus:ring-2" 674 | /> 675 | <div className="flex-1"> 676 | <div className="mb-1"> 677 | <span className="font-semibold text-gray-900">{option.label}</span> 678 | </div> 679 | <p className="text-sm text-gray-600 mb-2">{option.description}</p> 680 | <div> 681 | <p className="text-xs text-gray-500 mb-1">Required fields:</p> 682 | <div className="flex flex-wrap gap-1"> 683 | {option.required.map((field) => ( 684 | <span 685 | key={field} 686 | className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700" 687 | > 688 | {field.replace('HANA_', '')} 689 | </span> 690 | ))} 691 | </div> 692 | </div> 693 | </div> 694 | </label> 695 | ))} 696 | </div> 697 | </div> 698 | 699 | {/* MDC-specific fields - show conditionally */} 700 | {shouldShowMDCFields(detectDatabaseType(data), manualType) && ( 701 | <div className='mt-6'> 702 | <h5 className='text-base font-semibold text-gray-800 mb-2'>MDC Configuration</h5> 703 | <p className='text-sm text-gray-600 mb-4'> 704 | {manualType === 'mdc_system' 705 | ? 'MDC System Database requires instance number for connection string format: HOST:PORT;INSTANCE' 706 | : 'MDC Tenant Database requires both instance number and database name for connection' 707 | } 708 | </p> 709 | <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> 710 | {renderInputField('HANA_INSTANCE_NUMBER', 'Instance Number', 'number', '10', true)} 711 | {manualType === 'mdc_tenant' && renderInputField('HANA_DATABASE_NAME', 'Database Name', 'text', 'HQQ', true)} 712 | </div> 713 | </div> 714 | )} 715 | 716 | <div className='grid grid-cols-1 md:grid-cols-2 gap-6 mt-6'> 717 | {renderInputField('HANA_USER', 'Username', 'text', 'your-username', true)} 718 | {renderInputField('HANA_PASSWORD', 'Password', 'password', '••••••••', true)} 719 | </div> 720 | </div> 721 | 722 | {/* Security & SSL Configuration */} 723 | <div> 724 | <h4 className='text-lg font-bold text-gray-900 mb-6'>Security & SSL</h4> 725 | <div className='bg-gray-50 rounded-xl p-6 space-y-6'> 726 | <ToggleSwitch 727 | label="Enable SSL" 728 | description="Use SSL/TLS for secure connection" 729 | value={data.HANA_SSL || 'true'} 730 | onChange={(value) => onChange(environment, 'HANA_SSL', value)} 731 | /> 732 | <ToggleSwitch 733 | label="Encrypt Connection" 734 | description="Encrypt data transmission" 735 | value={data.HANA_ENCRYPT || 'true'} 736 | onChange={(value) => onChange(environment, 'HANA_ENCRYPT', value)} 737 | /> 738 | <ToggleSwitch 739 | label="Validate Certificate" 740 | description="Verify SSL certificate authenticity" 741 | value={data.HANA_VALIDATE_CERT || 'false'} 742 | onChange={(value) => onChange(environment, 'HANA_VALIDATE_CERT', value)} 743 | /> 744 | </div> 745 | </div> 746 | 747 | {/* Logging Configuration */} 748 | <div> 749 | <h4 className='text-lg font-bold text-gray-900 mb-6'>Logging Configuration</h4> 750 | 751 | <div className='grid grid-cols-1 md:grid-cols-3 gap-6'> 752 | <div> 753 | <label className='block text-base font-semibold text-gray-800 mb-3'>Log Level</label> 754 | <select 755 | value={data.LOG_LEVEL || 'info'} 756 | onChange={(e) => onChange(environment, 'LOG_LEVEL', e.target.value)} 757 | className='w-full px-4 py-3 border border-gray-300 rounded-xl text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors' 758 | > 759 | <option value='error'>Error</option> 760 | <option value='warn'>Warning</option> 761 | <option value='info'>Info</option> 762 | <option value='debug'>Debug</option> 763 | </select> 764 | </div> 765 | <div className='flex items-end'> 766 | <div className='w-full'> 767 | <ToggleSwitch 768 | label="File Logging" 769 | description="Save logs to file" 770 | value={data.ENABLE_FILE_LOGGING || 'true'} 771 | onChange={(value) => onChange(environment, 'ENABLE_FILE_LOGGING', value)} 772 | /> 773 | </div> 774 | </div> 775 | <div className='flex items-end'> 776 | <div className='w-full'> 777 | <ToggleSwitch 778 | label="Console Logging" 779 | description="Display logs in console" 780 | value={data.ENABLE_CONSOLE_LOGGING || 'false'} 781 | onChange={(value) => onChange(environment, 'ENABLE_CONSOLE_LOGGING', value)} 782 | /> 783 | </div> 784 | </div> 785 | </div> 786 | </div> 787 | </div> 788 | ) 789 | } 790 | 791 | export default ConfigurationModal ```