This is page 2 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 -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/PathSetupModal.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { useEffect } from 'react' 3 | import { XMarkIcon, Cog6ToothIcon, ExclamationCircleIcon, InformationCircleIcon } from '@heroicons/react/24/outline' 4 | 5 | const PathSetupModal = ({ 6 | isOpen, 7 | onClose, 8 | pathInput, 9 | setPathInput, 10 | onSave, 11 | isLoading 12 | }) => { 13 | useEffect(() => { 14 | if (!isOpen) return 15 | const onKeyDown = (e) => { 16 | if (e.key === 'Escape') onClose() 17 | } 18 | window.addEventListener('keydown', onKeyDown) 19 | return () => window.removeEventListener('keydown', onKeyDown) 20 | }, [isOpen, onClose]) 21 | 22 | return ( 23 | <motion.div 24 | className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4" 25 | initial={{ opacity: 0 }} 26 | animate={{ opacity: 1 }} 27 | exit={{ opacity: 0 }} 28 | onClick={onClose} 29 | > 30 | <motion.div 31 | className="bg-white rounded-2xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden border border-gray-200 flex flex-col" 32 | initial={{ scale: 0.9, opacity: 0, y: 20 }} 33 | animate={{ scale: 1, opacity: 1, y: 0 }} 34 | exit={{ scale: 0.9, opacity: 0, y: 20 }} 35 | transition={{ type: "spring", stiffness: 300, damping: 25 }} 36 | onClick={(e) => e.stopPropagation()} 37 | > 38 | {/* Header */} 39 | <div className="px-8 py-6 border-b border-gray-100 bg-white rounded-t-2xl"> 40 | <div className="flex items-center justify-between"> 41 | <div className="flex items-center gap-4"> 42 | <div className="p-3 bg-gray-100 rounded-xl"> 43 | <Cog6ToothIcon className="w-5 h-5 text-gray-600" /> 44 | </div> 45 | <div> 46 | <h2 className="text-2xl font-bold text-gray-900 leading-tight"> 47 | Setup Claude Desktop Configuration 48 | </h2> 49 | <p className="text-base text-gray-600 mt-2 font-medium"> 50 | First-time setup: Configure Claude Desktop config file path 51 | </p> 52 | </div> 53 | </div> 54 | <button 55 | onClick={onClose} 56 | className="p-3 rounded-xl text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors" 57 | > 58 | <XMarkIcon className="w-5 h-5" /> 59 | </button> 60 | </div> 61 | </div> 62 | 63 | {/* Body */} 64 | <div className="flex-1 overflow-y-auto p-8"> 65 | {/* Info Alert */} 66 | <div className="bg-orange-50 border border-orange-200 rounded-xl p-4 mb-6"> 67 | <div className="flex items-start gap-3"> 68 | <ExclamationCircleIcon className="w-5 h-5 text-orange-500 mt-0.5 flex-shrink-0" /> 69 | <div> 70 | <h3 className="font-semibold text-orange-900 mb-1">Configuration Required</h3> 71 | <p className="text-orange-800 text-sm leading-relaxed"> 72 | To add servers to Claude Desktop, we need to know where your Claude configuration file is located. 73 | This is typically in your user directory. 74 | </p> 75 | </div> 76 | </div> 77 | </div> 78 | 79 | {/* Form */} 80 | <div className="mb-6"> 81 | <label className="block text-sm font-medium text-gray-700 mb-3"> 82 | Claude Desktop Config Path 83 | </label> 84 | <input 85 | type="text" 86 | value={pathInput} 87 | onChange={(e) => setPathInput(e.target.value)} 88 | placeholder="Enter path to claude_desktop_config.json" 89 | 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 font-mono" 90 | /> 91 | 92 | {/* Common Paths */} 93 | <div className="mt-4 bg-gray-50 border border-gray-200 rounded-xl p-4"> 94 | <div className="flex items-center gap-2 mb-3"> 95 | <InformationCircleIcon className="w-4 h-4 text-blue-500" /> 96 | <h4 className="text-sm font-medium text-gray-700">Common Paths:</h4> 97 | </div> 98 | <div className="space-y-2 text-sm text-gray-600 font-mono"> 99 | <div> 100 | <span className="text-gray-500">macOS:</span> 101 | <span className="ml-2">~/Library/Application Support/Claude/claude_desktop_config.json</span> 102 | </div> 103 | <div> 104 | <span className="text-gray-500">Windows:</span> 105 | <span className="ml-2">%APPDATA%/Claude/claude_desktop_config.json</span> 106 | </div> 107 | <div> 108 | <span className="text-gray-500">Linux:</span> 109 | <span className="ml-2">~/.config/claude/claude_desktop_config.json</span> 110 | </div> 111 | </div> 112 | </div> 113 | </div> 114 | </div> 115 | 116 | {/* Footer */} 117 | <div className="px-8 py-6 border-t border-gray-100 bg-gray-50 rounded-b-2xl flex justify-end gap-4"> 118 | <button 119 | onClick={onSave} 120 | disabled={isLoading || !pathInput.trim()} 121 | className="px-8 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-[150px] transition-colors shadow-md hover:shadow-lg" 122 | > 123 | {isLoading ? 'Saving...' : 'Save Configuration'} 124 | </button> 125 | </div> 126 | </motion.div> 127 | </motion.div> 128 | ) 129 | } 130 | 131 | export default PathSetupModal ``` -------------------------------------------------------------------------------- /tests/manual/manual-test.js: -------------------------------------------------------------------------------- ```javascript 1 | const { spawn } = require('child_process'); 2 | const readline = require('readline'); 3 | 4 | console.log('🔍 HANA MCP Server Manual Tester'); 5 | console.log('================================\n'); 6 | 7 | // Spawn the MCP server process 8 | const server = spawn('/opt/homebrew/bin/node', ['../../hana-mcp-server.js'], { 9 | stdio: ['pipe', 'pipe', 'pipe'], 10 | env: { 11 | HANA_HOST: "your-hana-host.com", 12 | HANA_PORT: "443", 13 | HANA_USER: "your-username", 14 | HANA_PASSWORD: "your-password", 15 | HANA_SCHEMA: "your-schema", 16 | HANA_SSL: "true", 17 | HANA_ENCRYPT: "true", 18 | HANA_VALIDATE_CERT: "true" 19 | } 20 | }); 21 | 22 | // Handle server output 23 | server.stdout.on('data', (data) => { 24 | try { 25 | const response = JSON.parse(data.toString().trim()); 26 | console.log('\n📤 Response:', JSON.stringify(response, null, 2)); 27 | } catch (error) { 28 | console.log('🔧 Server Log:', data.toString().trim()); 29 | } 30 | }); 31 | 32 | server.stderr.on('data', (data) => { 33 | console.log('🔧 Server Log:', data.toString().trim()); 34 | }); 35 | 36 | // Send request function 37 | function sendRequest(method, params = {}) { 38 | const request = { 39 | jsonrpc: '2.0', 40 | id: Date.now(), 41 | method, 42 | params 43 | }; 44 | 45 | console.log(`\n📤 Sending: ${method}`); 46 | server.stdin.write(JSON.stringify(request) + '\n'); 47 | } 48 | 49 | // Initialize server 50 | async function initializeServer() { 51 | console.log('🚀 Initializing server...'); 52 | sendRequest('initialize', { 53 | protocolVersion: '2024-11-05', 54 | capabilities: {}, 55 | clientInfo: { name: 'manual-test-client', version: '1.0.0' } 56 | }); 57 | await new Promise(resolve => setTimeout(resolve, 1000)); 58 | } 59 | 60 | // List available tools 61 | async function listTools() { 62 | console.log('\n📋 Listing tools...'); 63 | sendRequest('tools/list', {}); 64 | await new Promise(resolve => setTimeout(resolve, 1000)); 65 | } 66 | 67 | // Interactive menu 68 | function showMenu() { 69 | console.log('\n\n🎯 Available Tests:'); 70 | console.log('1. Show HANA Config'); 71 | console.log('2. Test Connection'); 72 | console.log('3. List Schemas'); 73 | console.log('4. List Tables'); 74 | console.log('5. Describe Table'); 75 | console.log('6. List Indexes'); 76 | console.log('7. Execute Query'); 77 | console.log('8. Show Environment Variables'); 78 | console.log('9. Exit'); 79 | console.log('\nEnter your choice (1-9):'); 80 | } 81 | 82 | // Test functions 83 | function testShowConfig() { 84 | sendRequest('tools/call', { 85 | name: "hana_show_config", 86 | arguments: {} 87 | }); 88 | } 89 | 90 | function testConnection() { 91 | sendRequest('tools/call', { 92 | name: "hana_test_connection", 93 | arguments: {} 94 | }); 95 | } 96 | 97 | function testListSchemas() { 98 | sendRequest('tools/call', { 99 | name: "hana_list_schemas", 100 | arguments: {} 101 | }); 102 | } 103 | 104 | function testListTables() { 105 | const rl = readline.createInterface({ 106 | input: process.stdin, 107 | output: process.stdout 108 | }); 109 | 110 | rl.question('Enter schema name (or press Enter for default): ', (schema) => { 111 | const args = schema.trim() ? { schema_name: schema.trim() } : {}; 112 | sendRequest('tools/call', { 113 | name: "hana_list_tables", 114 | arguments: args 115 | }); 116 | rl.close(); 117 | }); 118 | } 119 | 120 | function testDescribeTable() { 121 | const rl = readline.createInterface({ 122 | input: process.stdin, 123 | output: process.stdout 124 | }); 125 | 126 | rl.question('Enter schema name: ', (schema) => { 127 | rl.question('Enter table name: ', (table) => { 128 | sendRequest('tools/call', { 129 | name: "hana_describe_table", 130 | arguments: { 131 | schema_name: schema.trim(), 132 | table_name: table.trim() 133 | } 134 | }); 135 | rl.close(); 136 | }); 137 | }); 138 | } 139 | 140 | function testListIndexes() { 141 | const rl = readline.createInterface({ 142 | input: process.stdin, 143 | output: process.stdout 144 | }); 145 | 146 | rl.question('Enter schema name: ', (schema) => { 147 | rl.question('Enter table name: ', (table) => { 148 | sendRequest('tools/call', { 149 | name: "hana_list_indexes", 150 | arguments: { 151 | schema_name: schema.trim(), 152 | table_name: table.trim() 153 | } 154 | }); 155 | rl.close(); 156 | }); 157 | }); 158 | } 159 | 160 | function testExecuteQuery() { 161 | const rl = readline.createInterface({ 162 | input: process.stdin, 163 | output: process.stdout 164 | }); 165 | 166 | rl.question('Enter SQL query: ', (query) => { 167 | sendRequest('tools/call', { 168 | name: "hana_execute_query", 169 | arguments: { 170 | query: query.trim() 171 | } 172 | }); 173 | rl.close(); 174 | }); 175 | } 176 | 177 | function testShowEnvVars() { 178 | sendRequest('tools/call', { 179 | name: "hana_show_env_vars", 180 | arguments: {} 181 | }); 182 | } 183 | 184 | // Main interactive loop 185 | async function startInteractive() { 186 | await initializeServer(); 187 | await listTools(); 188 | 189 | const rl = readline.createInterface({ 190 | input: process.stdin, 191 | output: process.stdout 192 | }); 193 | 194 | const askQuestion = () => { 195 | showMenu(); 196 | rl.question('', (answer) => { 197 | switch (answer.trim()) { 198 | case '1': 199 | testShowConfig(); 200 | break; 201 | case '2': 202 | testConnection(); 203 | break; 204 | case '3': 205 | testListSchemas(); 206 | break; 207 | case '4': 208 | testListTables(); 209 | break; 210 | case '5': 211 | testDescribeTable(); 212 | break; 213 | case '6': 214 | testListIndexes(); 215 | break; 216 | case '7': 217 | testExecuteQuery(); 218 | break; 219 | case '8': 220 | testShowEnvVars(); 221 | break; 222 | case '9': 223 | console.log('👋 Goodbye!'); 224 | rl.close(); 225 | server.kill(); 226 | return; 227 | default: 228 | console.log('❌ Invalid option. Please select 1-9.'); 229 | } 230 | 231 | setTimeout(askQuestion, 2000); 232 | }); 233 | }; 234 | 235 | askQuestion(); 236 | } 237 | 238 | // Handle server exit 239 | server.on('close', (code) => { 240 | console.log(`\n🔚 Server closed with code ${code}`); 241 | process.exit(0); 242 | }); 243 | 244 | server.on('error', (error) => { 245 | console.error('❌ Server error:', error); 246 | process.exit(1); 247 | }); 248 | 249 | // Start interactive testing 250 | startInteractive().catch(console.error); ``` -------------------------------------------------------------------------------- /src/utils/config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Configuration management utility for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('./logger'); 6 | 7 | class Config { 8 | constructor() { 9 | this.config = this.loadConfig(); 10 | } 11 | 12 | loadConfig() { 13 | return { 14 | hana: { 15 | host: process.env.HANA_HOST, 16 | port: parseInt(process.env.HANA_PORT) || 443, 17 | user: process.env.HANA_USER, 18 | password: process.env.HANA_PASSWORD, 19 | schema: process.env.HANA_SCHEMA, 20 | instanceNumber: process.env.HANA_INSTANCE_NUMBER, 21 | databaseName: process.env.HANA_DATABASE_NAME, 22 | connectionType: process.env.HANA_CONNECTION_TYPE || 'auto', 23 | ssl: process.env.HANA_SSL !== 'false', 24 | encrypt: process.env.HANA_ENCRYPT !== 'false', 25 | validateCert: process.env.HANA_VALIDATE_CERT !== 'false' 26 | }, 27 | server: { 28 | logLevel: process.env.LOG_LEVEL || 'INFO', 29 | enableFileLogging: process.env.ENABLE_FILE_LOGGING === 'true', 30 | enableConsoleLogging: process.env.ENABLE_CONSOLE_LOGGING !== 'false' 31 | } 32 | }; 33 | } 34 | 35 | getHanaConfig() { 36 | return this.config.hana; 37 | } 38 | 39 | getServerConfig() { 40 | return this.config.server; 41 | } 42 | 43 | /** 44 | * Determine HANA database type based on configuration 45 | */ 46 | getHanaDatabaseType() { 47 | const hana = this.config.hana; 48 | 49 | // Use explicit type if set and not 'auto' 50 | if (hana.connectionType && hana.connectionType !== 'auto') { 51 | return hana.connectionType; 52 | } 53 | 54 | // Auto-detect based on available parameters 55 | if (hana.instanceNumber && hana.databaseName) { 56 | return 'mdc_tenant'; 57 | } else if (hana.instanceNumber && !hana.databaseName) { 58 | return 'mdc_system'; 59 | } else { 60 | return 'single_container'; 61 | } 62 | } 63 | 64 | /** 65 | * Build connection parameters based on database type 66 | */ 67 | getConnectionParams() { 68 | const hana = this.config.hana; 69 | const dbType = this.getHanaDatabaseType(); 70 | 71 | const baseParams = { 72 | uid: hana.user, 73 | pwd: hana.password, 74 | encrypt: hana.encrypt, 75 | sslValidateCertificate: hana.validateCert 76 | }; 77 | 78 | // Build connection string based on database type 79 | switch (dbType) { 80 | case 'mdc_tenant': 81 | baseParams.serverNode = `${hana.host}:${hana.port}`; 82 | baseParams.databaseName = hana.databaseName; 83 | break; 84 | case 'mdc_system': 85 | baseParams.serverNode = `${hana.host}:${hana.port}`; 86 | break; 87 | case 'single_container': 88 | default: 89 | baseParams.serverNode = `${hana.host}:${hana.port}`; 90 | break; 91 | } 92 | 93 | return baseParams; 94 | } 95 | 96 | isHanaConfigured() { 97 | const hana = this.config.hana; 98 | return !!(hana.host && hana.user && hana.password); 99 | } 100 | 101 | getHanaConnectionString() { 102 | const hana = this.config.hana; 103 | return `${hana.host}:${hana.port}`; 104 | } 105 | 106 | // Get configuration info for display (hiding sensitive data) 107 | getDisplayConfig() { 108 | const hana = this.config.hana; 109 | const dbType = this.getHanaDatabaseType(); 110 | 111 | return { 112 | databaseType: dbType, 113 | connectionType: hana.connectionType, 114 | host: hana.host || 'NOT SET', 115 | port: hana.port, 116 | user: hana.user || 'NOT SET', 117 | password: hana.password ? 'SET (hidden)' : 'NOT SET', 118 | schema: hana.schema || 'NOT SET', 119 | instanceNumber: hana.instanceNumber || 'NOT SET', 120 | databaseName: hana.databaseName || 'NOT SET', 121 | ssl: hana.ssl, 122 | encrypt: hana.encrypt, 123 | validateCert: hana.validateCert 124 | }; 125 | } 126 | 127 | // Get environment variables for display 128 | getEnvironmentVars() { 129 | return { 130 | HANA_HOST: process.env.HANA_HOST || 'NOT SET', 131 | HANA_PORT: process.env.HANA_PORT || 'NOT SET', 132 | HANA_USER: process.env.HANA_USER || 'NOT SET', 133 | HANA_PASSWORD: process.env.HANA_PASSWORD ? 'SET (hidden)' : 'NOT SET', 134 | HANA_SCHEMA: process.env.HANA_SCHEMA || 'NOT SET', 135 | HANA_INSTANCE_NUMBER: process.env.HANA_INSTANCE_NUMBER || 'NOT SET', 136 | HANA_DATABASE_NAME: process.env.HANA_DATABASE_NAME || 'NOT SET', 137 | HANA_CONNECTION_TYPE: process.env.HANA_CONNECTION_TYPE || 'NOT SET', 138 | HANA_SSL: process.env.HANA_SSL || 'NOT SET', 139 | HANA_ENCRYPT: process.env.HANA_ENCRYPT || 'NOT SET', 140 | HANA_VALIDATE_CERT: process.env.HANA_VALIDATE_CERT || 'NOT SET' 141 | }; 142 | } 143 | 144 | // Validate configuration 145 | validate() { 146 | const hana = this.config.hana; 147 | const errors = []; 148 | const dbType = this.getHanaDatabaseType(); 149 | 150 | // Common required fields 151 | if (!hana.host) errors.push('HANA_HOST is required'); 152 | if (!hana.user) errors.push('HANA_USER is required'); 153 | if (!hana.password) errors.push('HANA_PASSWORD is required'); 154 | 155 | // Type-specific validation 156 | switch (dbType) { 157 | case 'mdc_tenant': 158 | if (!hana.instanceNumber) errors.push('HANA_INSTANCE_NUMBER is required for MDC Tenant Database'); 159 | if (!hana.databaseName) errors.push('HANA_DATABASE_NAME is required for MDC Tenant Database'); 160 | break; 161 | case 'mdc_system': 162 | if (!hana.instanceNumber) errors.push('HANA_INSTANCE_NUMBER is required for MDC System Database'); 163 | break; 164 | case 'single_container': 165 | if (!hana.schema) errors.push('HANA_SCHEMA is recommended for Single-Container Database'); 166 | break; 167 | } 168 | 169 | if (errors.length > 0) { 170 | logger.warn('Configuration validation failed:', errors); 171 | return false; 172 | } 173 | 174 | logger.info(`Configuration validation passed for ${dbType} database type`); 175 | return true; 176 | } 177 | 178 | /** 179 | * Get default schema from environment variables 180 | */ 181 | getDefaultSchema() { 182 | return this.config.hana.schema; 183 | } 184 | 185 | /** 186 | * Check if default schema is configured 187 | */ 188 | hasDefaultSchema() { 189 | return !!this.config.hana.schema; 190 | } 191 | } 192 | 193 | // Create default config instance 194 | const config = new Config(); 195 | 196 | module.exports = { Config, config }; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ui/GradientButton.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { motion } from 'framer-motion' 2 | import { cn } from '../../utils/cn' 3 | import { colors, transitions } from '../../utils/theme' 4 | import { IconComponent } from './index' 5 | 6 | /** 7 | * Button - A versatile button component with multiple variants and styles 8 | * 9 | * @param {Object} props - Component props 10 | * @param {React.ReactNode} props.children - Button content 11 | * @param {string} props.variant - Visual variant (primary, secondary, success, warning, danger) 12 | * @param {string} props.style - Button style (solid, outline, ghost, link) 13 | * @param {string} props.size - Button size (xs, sm, md, lg, xl) 14 | * @param {boolean} props.loading - Whether the button is in loading state 15 | * @param {React.ElementType} props.icon - Icon component to render 16 | * @param {string} props.iconPosition - Position of the icon (left, right) 17 | * @param {boolean} props.fullWidth - Whether the button should take full width 18 | * @param {string} props.className - Additional CSS classes 19 | * @returns {JSX.Element} - Rendered button component 20 | */ 21 | const Button = ({ 22 | children, 23 | variant = 'primary', 24 | style = 'solid', 25 | size = 'md', 26 | loading = false, 27 | icon, 28 | iconPosition = 'left', 29 | fullWidth = false, 30 | className, 31 | ...props 32 | }) => { 33 | // Style variants (solid, outline, ghost, link) - blue theme to match design 34 | const styleVariants = { 35 | solid: { 36 | primary: 'bg-[#86a0ff] text-white hover:bg-[#7990e6] focus:ring-[#86a0ff]', 37 | secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200 focus:ring-blue-500', 38 | success: 'bg-[#86a0ff] text-white hover:bg-[#7990e6] focus:ring-[#86a0ff]', 39 | warning: 'bg-yellow-500 text-white hover:bg-yellow-600 focus:ring-yellow-500', 40 | danger: 'bg-red-50 text-red-600 hover:bg-red-100 hover:text-red-700 focus:ring-red-500 border border-red-200', 41 | }, 42 | outline: { 43 | primary: 'bg-transparent border border-[#86a0ff] text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', 44 | secondary: 'bg-transparent border border-gray-600 text-gray-600 hover:bg-gray-50 focus:ring-blue-500', 45 | success: 'bg-transparent border border-[#86a0ff] text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', 46 | warning: 'bg-transparent border border-yellow-500 text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500', 47 | danger: 'bg-transparent border border-red-300 text-red-600 hover:bg-red-50 focus:ring-red-500', 48 | }, 49 | ghost: { 50 | primary: 'bg-transparent text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', 51 | secondary: 'bg-transparent text-gray-600 hover:bg-gray-50 focus:ring-blue-500', 52 | success: 'bg-transparent text-[#86a0ff] hover:bg-[#86a0ff]/10 focus:ring-[#86a0ff]', 53 | warning: 'bg-transparent text-yellow-600 hover:bg-yellow-50 focus:ring-yellow-500', 54 | danger: 'bg-transparent text-red-600 hover:bg-red-50 focus:ring-red-500', 55 | }, 56 | link: { 57 | primary: 'bg-transparent text-[#86a0ff] hover:underline focus:ring-[#86a0ff] p-0 shadow-none', 58 | secondary: 'bg-transparent text-gray-600 hover:underline focus:ring-blue-500 p-0 shadow-none', 59 | success: 'bg-transparent text-[#86a0ff] hover:underline focus:ring-[#86a0ff] p-0 shadow-none', 60 | warning: 'bg-transparent text-yellow-600 hover:underline focus:ring-yellow-500 p-0 shadow-none', 61 | danger: 'bg-transparent text-red-600 hover:underline focus:ring-red-500 p-0 shadow-none', 62 | } 63 | }; 64 | 65 | // Size variants 66 | const sizes = { 67 | xs: 'px-2 py-1 text-xs', 68 | sm: 'px-3 py-1.5 text-sm', 69 | md: 'px-4 py-2 text-base', 70 | lg: 'px-5 py-2.5 text-lg', 71 | xl: 'px-6 py-3 text-xl' 72 | }; 73 | 74 | // Icon sizes based on button size 75 | const iconSizes = { 76 | xs: 'xs', 77 | sm: 'sm', 78 | md: 'md', 79 | lg: 'lg', 80 | xl: 'lg' 81 | }; 82 | 83 | // Get the appropriate variant classes 84 | const variantClasses = styleVariants[style][variant]; 85 | 86 | // Animation settings 87 | const animations = { 88 | solid: { 89 | hover: { scale: 1.02, y: -1 }, 90 | tap: { scale: 0.98 } 91 | }, 92 | outline: { 93 | hover: { scale: 1.02 }, 94 | tap: { scale: 0.98 } 95 | }, 96 | ghost: { 97 | hover: { scale: 1.02 }, 98 | tap: { scale: 0.98 } 99 | }, 100 | link: { 101 | hover: {}, 102 | tap: { scale: 0.98 } 103 | } 104 | }; 105 | 106 | return ( 107 | <motion.button 108 | className={cn( 109 | // Base styles 110 | 'rounded-lg font-medium inline-flex items-center justify-center gap-2 transition-colors', 111 | 'focus:outline-none focus:ring-2 focus:ring-offset-1', 112 | // Style and size variants 113 | variantClasses, 114 | sizes[size], 115 | // Full width option 116 | fullWidth && 'w-full', 117 | // Disabled state 118 | (loading || props.disabled) && 'opacity-60 cursor-not-allowed', 119 | // Custom classes 120 | className 121 | )} 122 | whileHover={!loading && !props.disabled ? animations[style].hover : {}} 123 | whileTap={!loading && !props.disabled ? animations[style].tap : {}} 124 | transition={{ type: "spring", stiffness: 400, damping: 25 }} 125 | disabled={loading || props.disabled} 126 | {...props} 127 | > 128 | {/* Loading spinner */} 129 | {loading && ( 130 | <svg className="animate-spin h-4 w-4 text-current" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> 131 | <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> 132 | <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> 133 | </svg> 134 | )} 135 | 136 | {/* Left icon */} 137 | {icon && iconPosition === 'left' && !loading && ( 138 | <IconComponent 139 | icon={icon} 140 | size={iconSizes[size]} 141 | variant={style === 'solid' ? 'white' : variant} 142 | /> 143 | )} 144 | 145 | {/* Button text */} 146 | {children && <span>{children}</span>} 147 | 148 | {/* Right icon */} 149 | {icon && iconPosition === 'right' && !loading && ( 150 | <IconComponent 151 | icon={icon} 152 | size={iconSizes[size]} 153 | variant={style === 'solid' ? 'white' : variant} 154 | /> 155 | )} 156 | </motion.button> 157 | ); 158 | }; 159 | 160 | // For backward compatibility, export as GradientButton 161 | const GradientButton = Button; 162 | 163 | export default GradientButton; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/tailwind.config.js: -------------------------------------------------------------------------------- ```javascript 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', 8 | theme: { 9 | extend: { 10 | fontFamily: { 11 | sans: ['Inter', 'system-ui', 'sans-serif'], 12 | display: ['Inter', 'system-ui', 'sans-serif'], 13 | body: ['Inter', 'system-ui', 'sans-serif'], 14 | }, 15 | fontSize: { 16 | 'xs': ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.025em' }], 17 | 'sm': ['0.875rem', { lineHeight: '1.25rem', letterSpacing: '0.025em' }], 18 | 'base': ['1rem', { lineHeight: '1.5rem', letterSpacing: '0.025em' }], 19 | 'lg': ['1.125rem', { lineHeight: '1.75rem', letterSpacing: '0.025em' }], 20 | 'xl': ['1.25rem', { lineHeight: '1.75rem', letterSpacing: '0.025em' }], 21 | '2xl': ['1.5rem', { lineHeight: '2rem', letterSpacing: '0.025em' }], 22 | '3xl': ['1.875rem', { lineHeight: '2.25rem', letterSpacing: '0.025em' }], 23 | '4xl': ['2.25rem', { lineHeight: '2.5rem', letterSpacing: '0.025em' }], 24 | '5xl': ['3rem', { lineHeight: '1', letterSpacing: '0.025em' }], 25 | '6xl': ['3.75rem', { lineHeight: '1', letterSpacing: '0.025em' }], 26 | '7xl': ['4.5rem', { lineHeight: '1', letterSpacing: '0.025em' }], 27 | '8xl': ['6rem', { lineHeight: '1', letterSpacing: '0.025em' }], 28 | '9xl': ['8rem', { lineHeight: '1', letterSpacing: '0.025em' }], 29 | }, 30 | fontWeight: { 31 | thin: '100', 32 | extralight: '200', 33 | light: '300', 34 | normal: '400', 35 | medium: '500', 36 | semibold: '600', 37 | bold: '700', 38 | extrabold: '800', 39 | black: '900', 40 | }, 41 | lineHeight: { 42 | 'tight': '1.25', 43 | 'snug': '1.375', 44 | 'normal': '1.5', 45 | 'relaxed': '1.625', 46 | 'loose': '2', 47 | }, 48 | letterSpacing: { 49 | 'tighter': '-0.05em', 50 | 'tight': '-0.025em', 51 | 'normal': '0em', 52 | 'wide': '0.025em', 53 | 'wider': '0.05em', 54 | 'widest': '0.1em', 55 | }, 56 | colors: { 57 | // Professional Light Color System - Matching Image Theme 58 | primary: { 59 | 50: '#eff6ff', 60 | 100: '#dbeafe', 61 | 200: '#bfdbfe', 62 | 300: '#93c5fd', 63 | 400: '#60a5fa', 64 | 500: '#3b82f6', 65 | 600: '#2563eb', 66 | 700: '#1d4ed8', 67 | 800: '#1e40af', 68 | 900: '#1e3a8a', 69 | 950: '#172554', 70 | }, 71 | accent: { 72 | 50: '#faf5ff', 73 | 100: '#f3e8ff', 74 | 200: '#e9d5ff', 75 | 300: '#d8b4fe', 76 | 400: '#c084fc', 77 | 500: '#a855f7', 78 | 600: '#9333ea', 79 | 700: '#7c3aed', 80 | 800: '#6b21a8', 81 | 900: '#581c87', 82 | 950: '#3b0764', 83 | }, 84 | // Professional grays 85 | gray: { 86 | 50: '#f9fafb', 87 | 100: '#f3f4f6', 88 | 200: '#e5e7eb', 89 | 300: '#d1d5db', 90 | 400: '#9ca3af', 91 | 500: '#6b7280', 92 | 600: '#4b5563', 93 | 700: '#374151', 94 | 800: '#1f2937', 95 | 900: '#111827', 96 | 950: '#030712', 97 | }, 98 | // Status colors with professional styling - Matching Image 99 | success: { 100 | 50: '#ecfdf5', 101 | 100: '#d1fae5', 102 | 200: '#a7f3d0', 103 | 300: '#6ee7b7', 104 | 400: '#34d399', 105 | 500: '#10b981', 106 | 600: '#059669', 107 | 700: '#047857', 108 | 800: '#065f46', 109 | 900: '#064e3b', 110 | }, 111 | warning: { 112 | 50: '#fffbeb', 113 | 100: '#fef3c7', 114 | 200: '#fde68a', 115 | 300: '#fcd34d', 116 | 400: '#fbbf24', 117 | 500: '#f59e0b', 118 | 600: '#d97706', 119 | 700: '#b45309', 120 | 800: '#92400e', 121 | 900: '#78350f', 122 | }, 123 | danger: { 124 | 50: '#fef2f2', 125 | 100: '#fee2e2', 126 | 200: '#fecaca', 127 | 300: '#fca5a5', 128 | 400: '#f87171', 129 | 500: '#ef4444', 130 | 600: '#dc2626', 131 | 700: '#b91c1c', 132 | 800: '#991b1b', 133 | 900: '#7f1d1d', 134 | }, 135 | info: { 136 | 50: '#f0f9ff', 137 | 100: '#e0f2fe', 138 | 200: '#bae6fd', 139 | 300: '#7dd3fc', 140 | 400: '#38bdf8', 141 | 500: '#0ea5e9', 142 | 600: '#0284c7', 143 | 700: '#0369a1', 144 | 800: '#075985', 145 | 900: '#0c4a6e', 146 | }, 147 | // Button colors matching image theme 148 | button: { 149 | primary: '#86a0ff', // New custom blue for primary actions 150 | secondary: '#f3f4f6', // Light gray for secondary 151 | success: '#10b981', // Green for success 152 | danger: '#ef4444', // Red for danger 153 | warning: '#f59e0b', // Orange for warning 154 | light: '#f3f4f6', // Light gray for subtle actions 155 | } 156 | }, 157 | backgroundColor: { 158 | 'glass': 'rgba(255, 255, 255, 0.9)', 159 | 'glass-dark': 'rgba(248, 250, 252, 0.8)', 160 | }, 161 | backdropBlur: { 162 | 'xs': '2px', 163 | 'glass': '20px', 164 | }, 165 | boxShadow: { 166 | 'glass': '0 4px 24px rgba(0, 0, 0, 0.06), 0 1px 6px rgba(0, 0, 0, 0.04)', 167 | 'glass-hover': '0 12px 32px rgba(0, 0, 0, 0.08), 0 4px 16px rgba(0, 0, 0, 0.06)', 168 | 'glow-blue': '0 0 24px rgba(59, 130, 246, 0.15)', 169 | 'glow-purple': '0 0 24px rgba(168, 85, 247, 0.15)', 170 | 'glow-green': '0 0 24px rgba(16, 185, 129, 0.15)', 171 | 'glow-red': '0 0 24px rgba(239, 68, 68, 0.15)', 172 | }, 173 | animation: { 174 | 'fade-in': 'fadeIn 0.5s ease-in-out', 175 | 'slide-up': 'slideUp 0.3s ease-out', 176 | 'pulse-glow': 'pulseGlow 2s ease-in-out infinite alternate', 177 | 'float': 'float 3s ease-in-out infinite', 178 | }, 179 | keyframes: { 180 | fadeIn: { 181 | '0%': { opacity: '0' }, 182 | '100%': { opacity: '1' }, 183 | }, 184 | slideUp: { 185 | '0%': { transform: 'translateY(10px)', opacity: '0' }, 186 | '100%': { transform: 'translateY(0)', opacity: '1' }, 187 | }, 188 | pulseGlow: { 189 | '0%': { boxShadow: '0 0 5px rgba(59, 130, 246, 0.2)' }, 190 | '100%': { boxShadow: '0 0 20px rgba(59, 130, 246, 0.4)' }, 191 | }, 192 | float: { 193 | '0%, 100%': { transform: 'translateY(0px)' }, 194 | '50%': { transform: 'translateY(-4px)' }, 195 | }, 196 | }, 197 | borderRadius: { 198 | 'xl': '12px', 199 | '2xl': '16px', 200 | '3xl': '24px', 201 | }, 202 | }, 203 | }, 204 | plugins: [ 205 | require('@tailwindcss/forms'), 206 | ], 207 | } ``` -------------------------------------------------------------------------------- /tests/mcpTestingGuide.md: -------------------------------------------------------------------------------- ```markdown 1 | # HANA MCP Server Testing Guide 2 | 3 | This guide shows you how to test your HANA MCP server using different methods, including MCP Inspector. 4 | 5 | ## 🎯 Testing Methods 6 | 7 | ### 1. **MCP Inspector (Recommended)** 8 | 9 | MCP Inspector is the official tool for testing MCP servers. Here's how to use it: 10 | 11 | #### Installation 12 | ```bash 13 | # Install MCP Inspector globally 14 | npm install -g @modelcontextprotocol/inspector 15 | 16 | # Or install locally 17 | npm install @modelcontextprotocol/inspector 18 | ``` 19 | 20 | #### Usage 21 | ```bash 22 | # Start MCP Inspector with your server 23 | mcp-inspector --config mcp-inspector-config.json 24 | 25 | # Or run directly with command 26 | mcp-inspector --command "/opt/homebrew/bin/node" --args "hana-mcp-server.js" --env-file .env 27 | ``` 28 | 29 | ### 2. **Manual Testing Scripts** 30 | 31 | We've created custom testing scripts for your convenience: 32 | 33 | #### Automated Test Suite 34 | ```bash 35 | # Run all tests automatically 36 | /opt/homebrew/bin/node test-mcp-inspector.js 37 | ``` 38 | 39 | #### Interactive Manual Tester 40 | ```bash 41 | # Interactive menu for testing individual tools 42 | /opt/homebrew/bin/node manual-test.js 43 | ``` 44 | 45 | ### 3. **Command Line Testing** 46 | 47 | Test individual commands manually: 48 | 49 | ```bash 50 | # Test initialization 51 | echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | /opt/homebrew/bin/node hana-mcp-server.js 52 | 53 | # Test tools listing 54 | echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | /opt/homebrew/bin/node hana-mcp-server.js 55 | 56 | # Test a specific tool 57 | echo '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hana_show_config","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js 58 | ``` 59 | 60 | ## 🔧 Configuration Files 61 | 62 | ### MCP Inspector Config (`mcp-inspector-config.json`) 63 | ```json 64 | { 65 | "mcpServers": { 66 | "HANA Database": { 67 | "command": "/opt/homebrew/bin/node", 68 | "args": [ 69 | "/Users/Common/ProjectsRepo/tools/hana-mcp-server/hana-mcp-server.js" 70 | ], 71 | "env": { 72 | "HANA_HOST": "your-hana-host.com", 73 | "HANA_PORT": "443", 74 | "HANA_USER": "your-username", 75 | "HANA_PASSWORD": "your-password", 76 | "HANA_SCHEMA": "your-schema", 77 | "HANA_SSL": "true", 78 | "HANA_ENCRYPT": "true", 79 | "HANA_VALIDATE_CERT": "true" 80 | } 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | ### Environment File (`.env`) 87 | ```bash 88 | HANA_HOST=your-hana-host.com 89 | HANA_PORT=443 90 | HANA_USER=your-username 91 | HANA_PASSWORD=your-password 92 | HANA_SCHEMA=your-schema 93 | HANA_SSL=true 94 | HANA_ENCRYPT=true 95 | HANA_VALIDATE_CERT=true 96 | ``` 97 | 98 | ## 🧪 Test Scenarios 99 | 100 | ### Basic Functionality Tests 101 | 1. **Server Initialization** - Verify server starts correctly 102 | 2. **Tools Discovery** - Check all 9 tools are available 103 | 3. **Configuration Display** - Show HANA connection details 104 | 4. **Connection Test** - Verify HANA database connectivity 105 | 106 | ### Database Operation Tests 107 | 1. **Schema Listing** - List all available schemas 108 | 2. **Table Discovery** - List tables in a specific schema 109 | 3. **Table Structure** - Describe table columns and types 110 | 4. **Index Information** - List and describe indexes 111 | 5. **Query Execution** - Run custom SQL queries 112 | 113 | ### Error Handling Tests 114 | 1. **Missing Parameters** - Test required parameter validation 115 | 2. **Invalid Credentials** - Test connection failure handling 116 | 3. **Invalid Queries** - Test SQL error handling 117 | 4. **Missing Tables/Schemas** - Test not found scenarios 118 | 119 | ## 📋 Expected Test Results 120 | 121 | ### Successful Initialization 122 | ```json 123 | { 124 | "jsonrpc": "2.0", 125 | "id": 1, 126 | "result": { 127 | "protocolVersion": "2024-11-05", 128 | "capabilities": { 129 | "tools": {} 130 | }, 131 | "serverInfo": { 132 | "name": "HANA MCP Server", 133 | "version": "1.0.0" 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | ### Tools List Response 140 | ```json 141 | { 142 | "jsonrpc": "2.0", 143 | "id": 2, 144 | "result": { 145 | "tools": [ 146 | { 147 | "name": "hana_show_config", 148 | "description": "Show the HANA database configuration", 149 | "inputSchema": { 150 | "type": "object", 151 | "properties": {}, 152 | "required": [] 153 | } 154 | }, 155 | // ... 8 more tools 156 | ] 157 | } 158 | } 159 | ``` 160 | 161 | ### Tool Execution Response 162 | ```json 163 | { 164 | "jsonrpc": "2.0", 165 | "id": 3, 166 | "result": { 167 | "content": [ 168 | { 169 | "type": "text", 170 | "text": "📋 Available schemas in HANA database:\n\n- SCHEMA1\n- SCHEMA2\n..." 171 | } 172 | ] 173 | } 174 | } 175 | ``` 176 | 177 | ## 🚨 Troubleshooting 178 | 179 | ### Common Issues 180 | 181 | 1. **"HANA client not connected"** 182 | - Check environment variables are set correctly 183 | - Verify HANA credentials are valid 184 | - Ensure network connectivity to HANA host 185 | 186 | 2. **"Tool not found"** 187 | - Verify tool name spelling 188 | - Check tools/list response includes the tool 189 | - Ensure server is properly initialized 190 | 191 | 3. **"Missing required parameters"** 192 | - Check tool documentation for required parameters 193 | - Verify parameter names match exactly 194 | - Ensure parameters are in correct format 195 | 196 | 4. **"Parse error"** 197 | - Verify JSON-RPC format is correct 198 | - Check for extra/missing commas in JSON 199 | - Ensure proper escaping of special characters 200 | 201 | ### Debug Mode 202 | Enable debug logging by setting environment variables: 203 | ```bash 204 | export LOG_LEVEL=debug 205 | export ENABLE_FILE_LOGGING=true 206 | ``` 207 | 208 | ## 📊 Performance Testing 209 | 210 | ### Load Testing 211 | ```bash 212 | # Test multiple concurrent requests 213 | for i in {1..10}; do 214 | echo '{"jsonrpc":"2.0","id":'$i',"method":"tools/call","params":{"name":"hana_list_schemas","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js & 215 | done 216 | wait 217 | ``` 218 | 219 | ### Response Time Testing 220 | ```bash 221 | # Measure response time 222 | time echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hana_list_schemas","arguments":{}}}' | /opt/homebrew/bin/node hana-mcp-server.js 223 | ``` 224 | 225 | ## 🔄 Continuous Testing 226 | 227 | ### Automated Test Script 228 | Create a CI/CD pipeline script: 229 | 230 | ```bash 231 | #!/bin/bash 232 | set -e 233 | 234 | echo "🧪 Running HANA MCP Server Tests..." 235 | 236 | # Start server 237 | node hana-mcp-server.js & 238 | SERVER_PID=$! 239 | 240 | # Wait for server to start 241 | sleep 2 242 | 243 | # Run tests 244 | node test-mcp-inspector.js 245 | 246 | # Cleanup 247 | kill $SERVER_PID 248 | 249 | echo "✅ All tests passed!" 250 | ``` 251 | 252 | ## 📝 Test Checklist 253 | 254 | - [ ] Server initializes correctly 255 | - [ ] All 9 tools are discoverable 256 | - [ ] Configuration tool shows correct settings 257 | - [ ] Connection test passes 258 | - [ ] Schema listing works 259 | - [ ] Table listing works 260 | - [ ] Table description works 261 | - [ ] Index listing works 262 | - [ ] Query execution works 263 | - [ ] Error handling works correctly 264 | - [ ] Performance is acceptable 265 | - [ ] No memory leaks detected 266 | 267 | ## 🎉 Success Criteria 268 | 269 | Your HANA MCP server is ready for production when: 270 | - All tests pass consistently 271 | - Response times are under 5 seconds 272 | - Error handling is robust 273 | - Documentation is complete 274 | - Integration with Claude Desktop works seamlessly ``` -------------------------------------------------------------------------------- /src/utils/formatters.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Response formatting utilities for HANA MCP Server 3 | */ 4 | 5 | const { logger } = require('./logger'); 6 | 7 | class Formatters { 8 | /** 9 | * Create a standard MCP tool response 10 | */ 11 | static createResponse(text, type = 'text') { 12 | return { 13 | content: [ 14 | { 15 | type, 16 | text 17 | } 18 | ] 19 | }; 20 | } 21 | 22 | /** 23 | * Create an error response 24 | */ 25 | static createErrorResponse(message, details = '') { 26 | const text = details ? `${message}\n\n${details}` : message; 27 | return this.createResponse(`❌ ${text}`); 28 | } 29 | 30 | /** 31 | * Create a success response 32 | */ 33 | static createSuccessResponse(message, details = '') { 34 | const text = details ? `${message}\n\n${details}` : message; 35 | return this.createResponse(`✅ ${text}`); 36 | } 37 | 38 | /** 39 | * Format configuration display 40 | */ 41 | static formatConfig(config) { 42 | const lines = [ 43 | 'HANA Configuration:', 44 | '', 45 | `Host: ${config.host}`, 46 | `Port: ${config.port}`, 47 | `User: ${config.user}`, 48 | `Password: ${config.password}`, 49 | `Schema: ${config.schema}`, 50 | `SSL: ${config.ssl}`, 51 | '' 52 | ]; 53 | 54 | const status = (config.host !== 'NOT SET' && config.user !== 'NOT SET' && config.password !== 'NOT SET') 55 | ? 'PROPERLY CONFIGURED' 56 | : 'MISSING REQUIRED VALUES'; 57 | 58 | lines.push(`Status: ${status}`); 59 | 60 | return lines.join('\n'); 61 | } 62 | 63 | /** 64 | * Format environment variables display 65 | */ 66 | static formatEnvironmentVars(envVars) { 67 | const lines = [ 68 | '🔧 Environment Variables:', 69 | '' 70 | ]; 71 | 72 | for (const [key, value] of Object.entries(envVars)) { 73 | lines.push(`${key}: ${value}`); 74 | } 75 | 76 | lines.push(''); 77 | lines.push('Mode: Real HANA Connection'); 78 | 79 | return lines.join('\n'); 80 | } 81 | 82 | /** 83 | * Format schema list 84 | */ 85 | static formatSchemaList(schemas) { 86 | const lines = [ 87 | '📋 Available schemas in HANA database:', 88 | '' 89 | ]; 90 | 91 | schemas.forEach(schema => { 92 | lines.push(`- ${schema}`); 93 | }); 94 | 95 | lines.push(''); 96 | lines.push(`Total schemas: ${schemas.length}`); 97 | 98 | return lines.join('\n'); 99 | } 100 | 101 | /** 102 | * Format table list 103 | */ 104 | static formatTableList(tables, schemaName) { 105 | const lines = [ 106 | `📋 Tables in schema '${schemaName}':`, 107 | '' 108 | ]; 109 | 110 | tables.forEach(table => { 111 | lines.push(`- ${table}`); 112 | }); 113 | 114 | lines.push(''); 115 | lines.push(`Total tables: ${tables.length}`); 116 | 117 | return lines.join('\n'); 118 | } 119 | 120 | /** 121 | * Format table structure 122 | */ 123 | static formatTableStructure(columns, schemaName, tableName) { 124 | const lines = [ 125 | `📋 Table structure for '${schemaName}.${tableName}':`, 126 | '' 127 | ]; 128 | 129 | if (columns.length === 0) { 130 | lines.push('No columns found.'); 131 | return lines.join('\n'); 132 | } 133 | 134 | // Create header 135 | const header = 'Column Name | Data Type | Length | Nullable | Default | Description'; 136 | const separator = '---------------------|--------------|--------|----------|---------|-------------'; 137 | 138 | lines.push(header); 139 | lines.push(separator); 140 | 141 | // Add columns 142 | columns.forEach(col => { 143 | const nullable = col.IS_NULLABLE === 'TRUE' ? 'YES' : 'NO'; 144 | const defaultValue = col.DEFAULT_VALUE || '-'; 145 | const description = col.COMMENTS || '-'; 146 | const dataType = col.DATA_TYPE_NAME + 147 | (col.LENGTH ? `(${col.LENGTH})` : '') + 148 | (col.SCALE ? `,${col.SCALE}` : ''); 149 | 150 | const line = `${col.COLUMN_NAME.padEnd(20)} | ${dataType.padEnd(12)} | ${(col.LENGTH || '-').toString().padEnd(6)} | ${nullable.padEnd(8)} | ${defaultValue.padEnd(8)} | ${description}`; 151 | lines.push(line); 152 | }); 153 | 154 | lines.push(''); 155 | lines.push(`Total columns: ${columns.length}`); 156 | 157 | return lines.join('\n'); 158 | } 159 | 160 | /** 161 | * Format index list 162 | */ 163 | static formatIndexList(indexMap, schemaName, tableName) { 164 | const lines = [ 165 | `📋 Indexes for table '${schemaName}.${tableName}':`, 166 | '' 167 | ]; 168 | 169 | if (Object.keys(indexMap).length === 0) { 170 | lines.push('No indexes found.'); 171 | return lines.join('\n'); 172 | } 173 | 174 | Object.entries(indexMap).forEach(([indexName, index]) => { 175 | const type = index.isUnique ? 'Unique' : index.type; 176 | const columns = index.columns.join(', '); 177 | lines.push(`- ${indexName} (${type}) - Columns: ${columns}`); 178 | }); 179 | 180 | lines.push(''); 181 | lines.push(`Total indexes: ${Object.keys(indexMap).length}`); 182 | 183 | return lines.join('\n'); 184 | } 185 | 186 | /** 187 | * Format index details 188 | */ 189 | static formatIndexDetails(results, schemaName, tableName, indexName) { 190 | if (results.length === 0) { 191 | return `❌ Index '${schemaName}.${tableName}.${indexName}' not found.`; 192 | } 193 | 194 | const indexInfo = results[0]; 195 | const columns = results.map(row => `${row.COLUMN_NAME} (${row.ORDER || 'ASC'})`).join(', '); 196 | 197 | const lines = [ 198 | `📋 Index details for '${schemaName}.${tableName}.${indexName}':`, 199 | '', 200 | `Index Name: ${indexInfo.INDEX_NAME}`, 201 | `Table: ${schemaName}.${tableName}`, 202 | `Type: ${indexInfo.INDEX_TYPE}`, 203 | `Unique: ${indexInfo.IS_UNIQUE === 'TRUE' ? 'Yes' : 'No'}`, 204 | `Columns: ${columns}`, 205 | `Total columns: ${results.length}` 206 | ]; 207 | 208 | return lines.join('\n'); 209 | } 210 | 211 | /** 212 | * Format query results as table 213 | */ 214 | static formatQueryResults(results, query) { 215 | const lines = [ 216 | '🔍 Query executed successfully:', 217 | '', 218 | `Query: ${query}`, 219 | '' 220 | ]; 221 | 222 | if (results.length === 0) { 223 | lines.push('Query executed successfully but returned no results.'); 224 | return lines.join('\n'); 225 | } 226 | 227 | // Format as markdown table 228 | const columns = Object.keys(results[0]); 229 | const header = `| ${columns.join(' | ')} |`; 230 | const separator = `| ${columns.map(() => '---').join(' | ')} |`; 231 | const rows = results.map(row => 232 | `| ${columns.map(col => String(row[col] || '')).join(' | ')} |` 233 | ).join('\n'); 234 | 235 | lines.push(`Results (${results.length} rows):`); 236 | lines.push(header); 237 | lines.push(separator); 238 | lines.push(rows); 239 | 240 | return lines.join('\n'); 241 | } 242 | 243 | /** 244 | * Format connection test result 245 | */ 246 | static formatConnectionTest(config, success, error = null, testResult = null) { 247 | if (!success) { 248 | const lines = [ 249 | '❌ Connection test failed!', 250 | '' 251 | ]; 252 | 253 | if (error) { 254 | lines.push(`Error: ${error}`); 255 | lines.push(''); 256 | } 257 | 258 | lines.push('Please check your HANA database configuration and ensure the database is accessible.'); 259 | lines.push(''); 260 | lines.push('Configuration:'); 261 | lines.push(`- Host: ${config.host}`); 262 | lines.push(`- Port: ${config.port}`); 263 | lines.push(`- User: ${config.user}`); 264 | lines.push(`- Schema: ${config.schema || 'default'}`); 265 | lines.push(`- SSL: ${config.ssl ? 'enabled' : 'disabled'}`); 266 | 267 | return lines.join('\n'); 268 | } 269 | 270 | const lines = [ 271 | '✅ Connection test successful!', 272 | '', 273 | 'Configuration looks good:', 274 | `- Host: ${config.host}`, 275 | `- Port: ${config.port}`, 276 | `- User: ${config.user}`, 277 | `- Schema: ${config.schema || 'default'}`, 278 | `- SSL: ${config.ssl ? 'enabled' : 'disabled'}` 279 | ]; 280 | 281 | if (testResult) { 282 | lines.push(''); 283 | lines.push(`Test query result: ${testResult}`); 284 | } 285 | 286 | return lines.join('\n'); 287 | } 288 | } 289 | 290 | module.exports = Formatters; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/EnvironmentManager.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState, useEffect } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import { GradientButton } from './ui'; 4 | import { cn } from '../utils/cn'; 5 | 6 | const EnvironmentManager = ({ isOpen, onClose, onSave }) => { 7 | const [environments, setEnvironments] = useState([ 8 | { id: 'development', name: 'Development', color: 'blue', required: false }, 9 | { id: 'staging', name: 'Staging', color: 'amber', required: false }, 10 | { id: 'production', name: 'Production', color: 'green', required: false } 11 | ]); 12 | const [newEnvironmentName, setNewEnvironmentName] = useState(''); 13 | const [selectedColor, setSelectedColor] = useState('purple'); 14 | 15 | useEffect(() => { 16 | if (!isOpen) return; 17 | const onKeyDown = (e) => { 18 | if (e.key === 'Escape') onClose(); 19 | }; 20 | window.addEventListener('keydown', onKeyDown); 21 | return () => window.removeEventListener('keydown', onKeyDown); 22 | }, [isOpen, onClose]); 23 | 24 | const colorOptions = [ 25 | { id: 'blue', name: 'Blue', class: 'bg-blue-500' }, 26 | { id: 'green', name: 'Green', class: 'bg-green-500' }, 27 | { id: 'amber', name: 'Amber', class: 'bg-amber-500' }, 28 | { id: 'purple', name: 'Purple', class: 'bg-purple-500' }, 29 | { id: 'indigo', name: 'Indigo', class: 'bg-indigo-500' }, 30 | { id: 'red', name: 'Red', class: 'bg-red-500' }, 31 | { id: 'pink', name: 'Pink', class: 'bg-pink-500' }, 32 | { id: 'teal', name: 'Teal', class: 'bg-teal-500' } 33 | ]; 34 | 35 | useEffect(() => { 36 | // Load existing environments from localStorage or API 37 | const savedEnvironments = localStorage.getItem('hana-environments'); 38 | if (savedEnvironments) { 39 | setEnvironments(JSON.parse(savedEnvironments)); 40 | } 41 | }, []); 42 | 43 | const addEnvironment = () => { 44 | if (!newEnvironmentName.trim()) return; 45 | 46 | const newEnv = { 47 | id: newEnvironmentName.toLowerCase().replace(/\s+/g, '-'), 48 | name: newEnvironmentName, 49 | color: selectedColor, 50 | required: false 51 | }; 52 | 53 | const updatedEnvironments = [...environments, newEnv]; 54 | setEnvironments(updatedEnvironments); 55 | setNewEnvironmentName(''); 56 | setSelectedColor('purple'); 57 | }; 58 | 59 | const removeEnvironment = (envId) => { 60 | setEnvironments(environments.filter(env => env.id !== envId)); 61 | }; 62 | 63 | const handleSave = () => { 64 | // Save to localStorage (in real app, this would be an API call) 65 | localStorage.setItem('hana-environments', JSON.stringify(environments)); 66 | onSave(environments); 67 | onClose(); 68 | }; 69 | 70 | if (!isOpen) return null; 71 | 72 | return ( 73 | <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> 74 | <motion.div 75 | initial={{ opacity: 0, scale: 0.95 }} 76 | animate={{ opacity: 1, scale: 1 }} 77 | exit={{ opacity: 0, scale: 0.95 }} 78 | className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto" 79 | > 80 | <div className="flex items-center justify-between mb-6"> 81 | <h2 className="text-xl font-semibold text-gray-900">Manage Environments</h2> 82 | <button 83 | onClick={onClose} 84 | className="text-gray-400 hover:text-gray-600" 85 | > 86 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 87 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> 88 | </svg> 89 | </button> 90 | </div> 91 | 92 | {/* Current Environments */} 93 | <div className="mb-6"> 94 | <h3 className="text-lg font-medium text-gray-900 mb-3">Current Environments</h3> 95 | <div className="space-y-2"> 96 | {environments.map((env) => ( 97 | <div 98 | key={env.id} 99 | className="flex items-center justify-between p-3 bg-gray-50 rounded-lg" 100 | > 101 | <div className="flex items-center space-x-3"> 102 | <div className={cn('w-4 h-4 rounded-full', `bg-${env.color}-500`)} /> 103 | <span className="font-medium text-gray-900">{env.name}</span> 104 | <span className="text-sm text-gray-500">({env.id})</span> 105 | </div> 106 | <button 107 | onClick={() => removeEnvironment(env.id)} 108 | className="text-red-600 hover:text-red-800 text-sm" 109 | > 110 | Remove 111 | </button> 112 | </div> 113 | ))} 114 | </div> 115 | </div> 116 | 117 | {/* Add New Environment */} 118 | <div className="mb-6"> 119 | <h3 className="text-lg font-medium text-gray-900 mb-3">Add New Environment</h3> 120 | <div className="space-y-4"> 121 | <div> 122 | <label className="block text-sm font-medium text-gray-700 mb-1"> 123 | Environment Name 124 | </label> 125 | <input 126 | type="text" 127 | value={newEnvironmentName} 128 | onChange={(e) => setNewEnvironmentName(e.target.value)} 129 | placeholder="e.g., Pre-Production, QA, Testing" 130 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" 131 | /> 132 | </div> 133 | 134 | <div> 135 | <label className="block text-sm font-medium text-gray-700 mb-2"> 136 | Color 137 | </label> 138 | <div className="flex flex-wrap gap-2"> 139 | {colorOptions.map((color) => ( 140 | <button 141 | key={color.id} 142 | onClick={() => setSelectedColor(color.id)} 143 | className={cn( 144 | 'w-8 h-8 rounded-full border-2 transition-all', 145 | color.class, 146 | selectedColor === color.id 147 | ? 'border-gray-800 scale-110' 148 | : 'border-gray-300 hover:scale-105' 149 | )} 150 | title={color.name} 151 | /> 152 | ))} 153 | </div> 154 | </div> 155 | 156 | <GradientButton 157 | onClick={addEnvironment} 158 | disabled={!newEnvironmentName.trim()} 159 | className="w-full" 160 | > 161 | Add Environment 162 | </GradientButton> 163 | </div> 164 | </div> 165 | 166 | {/* Note */} 167 | <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> 168 | <div className="flex"> 169 | <svg className="w-5 h-5 text-blue-600 mr-2 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 170 | <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" /> 171 | </svg> 172 | <div> 173 | <p className="text-sm text-blue-800"> 174 | <strong>Note:</strong> Environments are optional for databases. You can configure any combination of environments for each database based on your needs. 175 | </p> 176 | </div> 177 | </div> 178 | </div> 179 | 180 | {/* Actions */} 181 | <div className="flex justify-end space-x-3"> 182 | <GradientButton variant="secondary" onClick={onClose}> 183 | Cancel 184 | </GradientButton> 185 | <GradientButton onClick={handleSave}> 186 | Save Changes 187 | </GradientButton> 188 | </div> 189 | </motion.div> 190 | </div> 191 | ); 192 | }; 193 | 194 | export default EnvironmentManager; 195 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeDesktopView.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import ClaudeConfigTile from './ClaudeConfigTile'; 4 | import BackupHistoryModal from './BackupHistoryModal'; 5 | import { cn } from '../utils/cn'; 6 | import { ArchiveBoxIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; 7 | 8 | const ClaudeDesktopView = ({ 9 | claudeConfigPath, 10 | claudeServers, 11 | activeEnvironments, 12 | onSetupPath, 13 | onRemoveConnection, 14 | onViewConnection, 15 | onRefresh, 16 | onConfigPathChange 17 | }) => { 18 | const activeConnections = claudeServers.length; 19 | const [isRefreshing, setIsRefreshing] = useState(false); 20 | const [showBackupHistory, setShowBackupHistory] = useState(false); 21 | 22 | const handleRefresh = async () => { 23 | setIsRefreshing(true); 24 | try { 25 | await onRefresh(); 26 | } finally { 27 | setIsRefreshing(false); 28 | } 29 | }; 30 | 31 | return ( 32 | <div className="p-6 space-y-6 bg-gray-100 rounded-2xl sm:rounded-3xl"> 33 | <div className="flex items-center justify-between"> 34 | <div> 35 | <h1 className="text-2xl font-bold text-gray-900 mb-2">Claude Desktop Integration</h1> 36 | <p className="text-gray-600"> 37 | Manage your HANA database connections available in Claude Desktop 38 | </p> 39 | </div> 40 | <div className="flex items-center gap-3"> 41 | <button 42 | onClick={() => setShowBackupHistory(true)} 43 | className="flex items-center px-3 py-2 text-sm font-medium bg-gray-100 border border-gray-200 text-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors hover:bg-gray-200 hover:border-gray-300 shadow-sm hover:shadow-md" 44 | title="Manage configuration backups" 45 | > 46 | <ArchiveBoxIcon className="w-4 h-4 mr-2" /> 47 | Backups 48 | </button> 49 | <button 50 | onClick={handleRefresh} 51 | disabled={isRefreshing} 52 | className={cn( 53 | "flex items-center px-3 py-2 text-sm font-medium bg-gray-100 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors shadow-sm hover:shadow-md", 54 | isRefreshing 55 | ? "text-gray-400 cursor-not-allowed" 56 | : "text-gray-700 hover:bg-gray-200 hover:border-gray-300" 57 | )} 58 | title="Refresh configuration from Claude Desktop" 59 | > 60 | <ArrowPathIcon className={cn( 61 | "w-4 h-4 mr-2", 62 | isRefreshing && "animate-spin" 63 | )} /> 64 | {isRefreshing ? 'Refreshing...' : 'Refresh'} 65 | </button> 66 | </div> 67 | </div> 68 | 69 | {/* Configuration Status */} 70 | <ClaudeConfigTile 71 | claudeConfigPath={claudeConfigPath} 72 | claudeServers={claudeServers} 73 | onSetupPath={onSetupPath} 74 | onConfigPathChange={onConfigPathChange} 75 | /> 76 | 77 | {/* Active Database Connections */} 78 | <div className="bg-white rounded-xl border border-gray-200 p-6"> 79 | <div className="flex items-center justify-between mb-6"> 80 | <h2 className="text-lg font-semibold text-gray-900">Active Database Connections</h2> 81 | <div className="flex items-center"> 82 | <div className={cn( 83 | 'w-2 h-2 rounded-full mr-2', 84 | activeConnections > 0 ? 'bg-green-500' : 'bg-gray-300' 85 | )} /> 86 | <span className="text-sm text-gray-600"> 87 | {activeConnections} {activeConnections === 1 ? 'connection' : 'connections'} active 88 | </span> 89 | </div> 90 | </div> 91 | 92 | {activeConnections > 0 ? ( 93 | <div className="overflow-x-auto max-h-96 overflow-y-auto claude-table-scrollbar"> 94 | <table className="min-w-full divide-y divide-gray-200"> 95 | <thead className="bg-gray-100"> 96 | <tr> 97 | <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 98 | Database 99 | </th> 100 | <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 101 | Environment 102 | </th> 103 | <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 104 | Actions 105 | </th> 106 | </tr> 107 | </thead> 108 | <tbody className="bg-white divide-y divide-gray-200"> 109 | {claudeServers.map((server) => ( 110 | <tr 111 | key={server.name} 112 | className="hover:bg-gray-50 cursor-pointer transition-colors" 113 | onClick={() => onViewConnection(server)} 114 | > 115 | <td className="px-6 py-4 whitespace-nowrap"> 116 | <div className="flex items-center"> 117 | <div className="h-8 w-8 bg-blue-50 rounded-full flex items-center justify-center"> 118 | <svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 119 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 120 | </svg> 121 | </div> 122 | <div className="ml-4"> 123 | <div className="text-sm font-medium text-gray-900">{server.name}</div> 124 | <div className="text-sm text-gray-500">{server.env.HANA_HOST}</div> 125 | </div> 126 | </div> 127 | </td> 128 | <td className="px-6 py-4 whitespace-nowrap"> 129 | <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> 130 | <span className="w-1.5 h-1.5 bg-green-600 rounded-full mr-1.5"></span> 131 | {server.env?.ENVIRONMENT || 'Development'} 132 | </span> 133 | </td> 134 | <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> 135 | <button 136 | onClick={(e) => { 137 | e.stopPropagation(); 138 | onViewConnection(server); 139 | }} 140 | className="text-blue-600 hover:text-blue-900 mr-3" 141 | > 142 | View 143 | </button> 144 | <button 145 | onClick={(e) => { 146 | e.stopPropagation(); 147 | onRemoveConnection(server.name); 148 | }} 149 | className="text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-2 py-1 rounded transition-colors" 150 | > 151 | Remove 152 | </button> 153 | </td> 154 | </tr> 155 | ))} 156 | </tbody> 157 | </table> 158 | </div> 159 | ) : ( 160 | <div className="text-center py-12"> 161 | <div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4"> 162 | <svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 163 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> 164 | </svg> 165 | </div> 166 | <h3 className="text-lg font-medium text-gray-900 mb-2">No active connections</h3> 167 | <p className="text-gray-600 mb-4"> 168 | You haven't added any HANA databases to Claude Desktop yet 169 | </p> 170 | <button 171 | onClick={onSetupPath} 172 | className="inline-flex items-center px-4 py-2 bg-[#86a0ff] text-white text-sm font-medium rounded-lg hover:bg-[#7990e6] transition-colors shadow-sm hover:shadow-md" 173 | > 174 | <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 175 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> 176 | </svg> 177 | Setup Claude Desktop 178 | </button> 179 | </div> 180 | )} 181 | </div> 182 | 183 | {/* Backup History Modal */} 184 | <BackupHistoryModal 185 | isOpen={showBackupHistory} 186 | onClose={() => setShowBackupHistory(false)} 187 | /> 188 | </div> 189 | ); 190 | }; 191 | 192 | export default ClaudeDesktopView; 193 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/SearchAndFilter.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import { cn } from '../utils/cn'; 4 | 5 | const SearchAndFilter = ({ 6 | searchQuery, 7 | onSearchChange, 8 | filters, 9 | onFilterChange, 10 | onClearFilters, 11 | placeholder = "Search databases..." 12 | }) => { 13 | const [isFilterOpen, setIsFilterOpen] = useState(false); 14 | const [sortBy, setSortBy] = useState('name'); 15 | const [sortOrder, setSortOrder] = useState('asc'); 16 | 17 | const filterOptions = [ 18 | { id: 'all', label: 'All Databases', count: filters.total || 0 }, 19 | { id: 'active', label: 'Active in Claude', count: filters.activeInClaude || 0, highlight: true }, 20 | { id: 'production', label: 'Production', count: filters.production || 0 }, 21 | { id: 'development', label: 'Development', count: filters.development || 0 }, 22 | { id: 'staging', label: 'Staging', count: filters.staging || 0 } 23 | ]; 24 | 25 | const sortOptions = [ 26 | { id: 'name', label: 'Name' }, 27 | { id: 'created', label: 'Date Created' }, 28 | { id: 'modified', label: 'Last Modified' }, 29 | { id: 'environments', label: 'Environment Count' } 30 | ]; 31 | 32 | const handleSortChange = (newSortBy) => { 33 | if (sortBy === newSortBy) { 34 | setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); 35 | } else { 36 | setSortBy(newSortBy); 37 | setSortOrder('asc'); 38 | } 39 | // Call parent callback if provided 40 | if (onFilterChange) { 41 | onFilterChange({ sortBy: newSortBy, sortOrder: sortOrder === 'asc' ? 'desc' : 'asc' }); 42 | } 43 | }; 44 | 45 | return ( 46 | <div className="p-4"> 47 | <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4"> 48 | {/* Search Input */} 49 | <div className="flex-1 relative"> 50 | <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> 51 | <svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 52 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> 53 | </svg> 54 | </div> 55 | <input 56 | type="text" 57 | value={searchQuery} 58 | onChange={(e) => onSearchChange(e.target.value)} 59 | placeholder={placeholder} 60 | className="block w-full pl-10 pr-3 py-2 border border-gray-100 rounded-lg leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500" 61 | /> 62 | {searchQuery && ( 63 | <button 64 | onClick={() => onSearchChange('')} 65 | className="absolute inset-y-0 right-0 pr-4 flex items-center group" 66 | > 67 | <svg className="h-5 w-5 text-gray-400 group-hover:text-gray-600 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 68 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> 69 | </svg> 70 | </button> 71 | )} 72 | </div> 73 | 74 | <div className="flex items-center gap-4"> 75 | {/* Claude Integration Status */} 76 | {filters.activeInClaude > 0 && ( 77 | <div className="flex items-center space-x-2 px-3 py-2 bg-green-50 border border-green-200 rounded-lg"> 78 | <svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 79 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> 80 | </svg> 81 | <span className="text-sm font-medium text-green-700"> 82 | {filters.activeInClaude} Claude 83 | </span> 84 | </div> 85 | )} 86 | 87 | {/* Filter Toggle */} 88 | <button 89 | onClick={() => setIsFilterOpen(!isFilterOpen)} 90 | className={cn( 91 | 'flex items-center px-4 py-2 border rounded-lg transition-colors', 92 | isFilterOpen 93 | ? 'border-blue-300 bg-blue-50 text-blue-700' 94 | : 'border-gray-100 bg-white text-gray-700 hover:bg-gray-50' 95 | )} 96 | > 97 | <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 98 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z" /> 99 | </svg> 100 | Filters 101 | <svg className={cn("w-4 h-4 ml-2 transition-transform duration-200", isFilterOpen && "rotate-180")} fill="none" stroke="currentColor" viewBox="0 0 24 24"> 102 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> 103 | </svg> 104 | </button> 105 | 106 | {/* Sort Dropdown */} 107 | <div className="relative"> 108 | <select 109 | value={`${sortBy}-${sortOrder}`} 110 | onChange={(e) => { 111 | const [newSortBy, newSortOrder] = e.target.value.split('-'); 112 | setSortBy(newSortBy); 113 | setSortOrder(newSortOrder); 114 | if (onFilterChange) { 115 | onFilterChange({ sortBy: newSortBy, sortOrder: newSortOrder }); 116 | } 117 | }} 118 | className="appearance-none bg-white border border-gray-100 rounded-lg px-4 py-2 pr-8 text-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500" 119 | > 120 | {sortOptions.map(option => ( 121 | <option key={`${option.id}-asc`} value={`${option.id}-asc`}>{option.label} (A-Z)</option> 122 | ))} 123 | {sortOptions.map(option => ( 124 | <option key={`${option.id}-desc`} value={`${option.id}-desc`}>{option.label} (Z-A)</option> 125 | ))} 126 | </select> 127 | <div className="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none"> 128 | <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 129 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> 130 | </svg> 131 | </div> 132 | </div> 133 | </div> 134 | </div> 135 | 136 | {/* Filter Panel */} 137 | <AnimatePresence> 138 | {isFilterOpen && ( 139 | <motion.div 140 | initial={{ height: 0, opacity: 0 }} 141 | animate={{ height: 'auto', opacity: 1 }} 142 | exit={{ height: 0, opacity: 0 }} 143 | transition={{ duration: 0.3, ease: "easeInOut" }} 144 | className="overflow-hidden" 145 | > 146 | <div className="pt-4 mt-4 border-t border-gray-200"> 147 | <div className="flex items-center justify-between mb-3"> 148 | <h3 className="text-sm font-medium text-gray-900">Filter by Status</h3> 149 | <button 150 | onClick={onClearFilters} 151 | className="text-sm text-blue-600 hover:text-blue-800" 152 | > 153 | Clear all 154 | </button> 155 | </div> 156 | 157 | <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3"> 158 | {filterOptions.map(option => ( 159 | <button 160 | key={option.id} 161 | onClick={() => onFilterChange && onFilterChange({ status: option.id })} 162 | className={cn( 163 | 'flex items-center justify-between p-3 rounded-lg border transition-colors text-left', 164 | filters.activeFilter === option.id 165 | ? option.highlight 166 | ? 'border-green-300 bg-green-50 text-green-700' 167 | : 'border-blue-300 bg-blue-50 text-blue-700' 168 | : option.highlight 169 | ? 'border-green-200 bg-white text-green-700 hover:bg-green-50' 170 | : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' 171 | )} 172 | > 173 | <span className="text-sm font-medium flex items-center space-x-1"> 174 | {option.highlight && ( 175 | <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 176 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> 177 | </svg> 178 | )} 179 | <span>{option.label}</span> 180 | </span> 181 | <span className={cn( 182 | "text-xs px-2 py-0.5 rounded-full", 183 | option.highlight 184 | ? filters.activeFilter === option.id 185 | ? 'bg-green-200 text-green-700' 186 | : 'bg-green-100 text-green-600' 187 | : 'bg-gray-100 text-gray-600' 188 | )}> 189 | {option.count} 190 | </span> 191 | </button> 192 | ))} 193 | </div> 194 | </div> 195 | </motion.div> 196 | )} 197 | </AnimatePresence> 198 | </div> 199 | ); 200 | }; 201 | 202 | export default SearchAndFilter; 203 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/layout/VerticalSidebar.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { cn } from '../../utils/cn'; 4 | 5 | const VerticalSidebar = ({ 6 | activeView, 7 | onViewChange, 8 | databaseCount, 9 | activeConnections, 10 | claudeConfigured 11 | }) => { 12 | const [collapsed, setCollapsed] = useState(false); 13 | 14 | const navigationItems = [ 15 | { 16 | id: 'dashboard', 17 | label: 'Dashboard', 18 | icon: ( 19 | <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 20 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2v0a2 2 0 002-2h10a2 2 0 012 2v0a2 2 0 012 2z" /> 21 | </svg> 22 | ), 23 | description: 'Overview & insights' 24 | }, 25 | { 26 | id: 'databases', 27 | label: 'My Local Databases', 28 | icon: ( 29 | <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 30 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 31 | </svg> 32 | ), 33 | description: 'Manage configurations', 34 | count: databaseCount, 35 | hasSubmenu: true 36 | }, 37 | { 38 | id: 'claude', 39 | label: 'Claude Desktop', 40 | icon: ( 41 | <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 42 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> 43 | </svg> 44 | ), 45 | description: 'Integration status', 46 | count: activeConnections, 47 | status: claudeConfigured ? 'online' : 'offline' 48 | } 49 | ]; 50 | 51 | return ( 52 | <motion.div 53 | className={cn( 54 | 'bg-white border border-gray-200 flex flex-col h-full rounded-xl shadow-lg overflow-hidden', 55 | collapsed ? 'w-12 sm:w-16' : 'w-56 sm:w-64' 56 | )} 57 | initial={false} 58 | animate={{ 59 | width: collapsed ? 64 : 256, 60 | transition: { 61 | type: "spring", 62 | stiffness: 300, 63 | damping: 30, 64 | mass: 0.8 65 | } 66 | }} 67 | transition={{ 68 | type: "spring", 69 | stiffness: 300, 70 | damping: 30, 71 | mass: 0.8 72 | }} 73 | > 74 | {/* Header */} 75 | <div className="p-3 sm:p-4 border-b border-gray-200 rounded-t-xl"> 76 | <div className="flex items-center justify-between"> 77 | <motion.div 78 | className="flex items-center space-x-3" 79 | initial={false} 80 | animate={{ 81 | opacity: collapsed ? 0 : 1, 82 | x: collapsed ? -20 : 0, 83 | transition: { 84 | duration: 0.2, 85 | delay: collapsed ? 0 : 0.1 86 | } 87 | }} 88 | style={{ display: collapsed ? 'none' : 'flex' }} 89 | > 90 | <img 91 | src="/logo.png" 92 | alt="HANA MCP Logo" 93 | className="w-8 h-8 flex-shrink-0" 94 | /> 95 | <div> 96 | <h2 className="text-lg font-semibold text-gray-900">HANA MCP</h2> 97 | <p className="text-xs text-gray-500">Database Manager</p> 98 | </div> 99 | </motion.div> 100 | <motion.div 101 | className="flex flex-col items-center w-full space-y-2" 102 | initial={false} 103 | animate={{ 104 | opacity: collapsed ? 1 : 0, 105 | scale: collapsed ? 1 : 0.8, 106 | transition: { 107 | duration: 0.2, 108 | delay: collapsed ? 0.1 : 0 109 | } 110 | }} 111 | style={{ display: collapsed ? 'flex' : 'none' }} 112 | > 113 | <img 114 | src="/logo.png" 115 | alt="HANA MCP Logo" 116 | className="w-6 h-6" 117 | /> 118 | <motion.button 119 | onClick={() => setCollapsed(!collapsed)} 120 | className="p-1 rounded-lg hover:bg-gray-100 transition-colors" 121 | whileHover={{ scale: 1.05 }} 122 | whileTap={{ scale: 0.95 }} 123 | title="Expand sidebar" 124 | > 125 | <motion.svg 126 | className="w-4 h-4 text-gray-500" 127 | fill="none" 128 | stroke="currentColor" 129 | viewBox="0 0 24 24" 130 | animate={{ rotate: collapsed ? 180 : 0 }} 131 | transition={{ duration: 0.3, ease: "easeInOut" }} 132 | > 133 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" /> 134 | </motion.svg> 135 | </motion.button> 136 | </motion.div> 137 | <motion.button 138 | onClick={() => setCollapsed(!collapsed)} 139 | className={cn( 140 | "p-1.5 rounded-lg hover:bg-gray-100 transition-colors", 141 | collapsed && "hidden" 142 | )} 143 | whileHover={{ scale: 1.05 }} 144 | whileTap={{ scale: 0.95 }} 145 | > 146 | <motion.svg 147 | className="w-4 h-4 text-gray-500" 148 | fill="none" 149 | stroke="currentColor" 150 | viewBox="0 0 24 24" 151 | animate={{ rotate: collapsed ? 180 : 0 }} 152 | transition={{ duration: 0.3, ease: "easeInOut" }} 153 | > 154 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /> 155 | </motion.svg> 156 | </motion.button> 157 | </div> 158 | </div> 159 | 160 | {/* Navigation */} 161 | <nav className="flex-1 p-1 sm:p-2"> 162 | <ul className="space-y-1"> 163 | {navigationItems.map((item) => ( 164 | <li key={item.id}> 165 | <button 166 | onClick={() => onViewChange(item.id)} 167 | className={cn( 168 | 'w-full flex items-center p-3 rounded-lg text-left transition-all duration-200', 169 | 'hover:bg-gray-50 group', 170 | activeView === item.id 171 | ? 'bg-blue-50 text-blue-700 border border-blue-200' 172 | : 'text-gray-700 hover:text-gray-900' 173 | )} 174 | > 175 | <div className={cn( 176 | 'flex-shrink-0', 177 | activeView === item.id ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-600' 178 | )}> 179 | {item.icon} 180 | </div> 181 | 182 | <motion.div 183 | className="ml-3 flex-1 min-w-0" 184 | initial={false} 185 | animate={{ 186 | opacity: collapsed ? 0 : 1, 187 | x: collapsed ? -10 : 0, 188 | transition: { 189 | duration: 0.2, 190 | delay: collapsed ? 0 : 0.1 191 | } 192 | }} 193 | style={{ display: collapsed ? 'none' : 'block' }} 194 | > 195 | <div className="flex items-center justify-between"> 196 | <span className="text-sm font-medium truncate"> 197 | {item.label} 198 | </span> 199 | {item.count !== undefined && item.count > 0 && ( 200 | <motion.span 201 | className={cn( 202 | 'ml-2 px-2 py-0.5 text-xs font-medium rounded-full', 203 | activeView === item.id 204 | ? 'bg-blue-100 text-blue-700' 205 | : 'bg-gray-100 text-gray-600' 206 | )} 207 | initial={{ scale: 0 }} 208 | animate={{ scale: 1 }} 209 | transition={{ delay: 0.2 }} 210 | > 211 | {item.count} 212 | </motion.span> 213 | )} 214 | {item.status && ( 215 | <motion.div 216 | className={cn( 217 | 'ml-2 w-2 h-2 rounded-full', 218 | item.status === 'online' ? 'bg-green-500' : 'bg-gray-300' 219 | )} 220 | initial={{ scale: 0 }} 221 | animate={{ scale: 1 }} 222 | transition={{ delay: 0.2 }} 223 | /> 224 | )} 225 | </div> 226 | <p className="text-xs text-gray-500 truncate"> 227 | {item.description} 228 | </p> 229 | </motion.div> 230 | </button> 231 | </li> 232 | ))} 233 | </ul> 234 | </nav> 235 | 236 | {/* Quick Actions */} 237 | <motion.div 238 | className="p-3 sm:p-4 border-t border-gray-200 rounded-b-xl" 239 | initial={false} 240 | animate={{ 241 | opacity: collapsed ? 0 : 1, 242 | y: collapsed ? 20 : 0, 243 | transition: { 244 | duration: 0.2, 245 | delay: collapsed ? 0 : 0.15 246 | } 247 | }} 248 | style={{ display: collapsed ? 'none' : 'block' }} 249 | > 250 | <motion.button 251 | onClick={() => onViewChange('add-database')} 252 | className="w-full flex items-center justify-center px-4 py-2 bg-[#86a0ff] text-white text-sm font-medium rounded-lg hover:bg-[#7990e6] transition-colors" 253 | whileHover={{ scale: 1.02 }} 254 | whileTap={{ scale: 0.98 }} 255 | > 256 | <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 257 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> 258 | </svg> 259 | Add Database 260 | </motion.button> 261 | </motion.div> 262 | 263 | </motion.div> 264 | ); 265 | }; 266 | 267 | export default VerticalSidebar; 268 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/DashboardView.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState, useEffect } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { MetricCard } from './ui'; 4 | import { cn } from '../utils/cn'; 5 | import EnvironmentManager from './EnvironmentManager'; 6 | 7 | const DashboardView = ({ 8 | hanaServers, 9 | claudeServers, 10 | activeEnvironments, 11 | onQuickAction 12 | }) => { 13 | const [showEnvironmentManager, setShowEnvironmentManager] = useState(false); 14 | // Calculate insights 15 | const totalDatabases = Object.keys(hanaServers).length; 16 | const activeConnections = claudeServers.length; 17 | const totalEnvironments = Object.values(hanaServers).reduce((total, server) => { 18 | return total + Object.keys(server.environments || {}).length; 19 | }, 0); 20 | 21 | const environmentBreakdown = Object.values(hanaServers).reduce((breakdown, server) => { 22 | Object.keys(server.environments || {}).forEach(env => { 23 | breakdown[env] = (breakdown[env] || 0) + 1; 24 | }); 25 | return breakdown; 26 | }, {}); 27 | 28 | // Calculate real connection status 29 | const connectionStatus = activeConnections > 0 ? 'Connected' : 'Not Connected'; 30 | const configuredDatabases = Object.keys(hanaServers).filter(key => 31 | Object.keys(hanaServers[key].environments || {}).length > 0 32 | ).length; 33 | 34 | const quickActions = [ 35 | { 36 | id: 'add-database', 37 | title: 'Add New Database', 38 | description: 'Configure a new HANA database connection', 39 | icon: ( 40 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 41 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> 42 | </svg> 43 | ), 44 | color: 'blue', 45 | enabled: true 46 | }, 47 | { 48 | id: 'manage-databases', 49 | title: 'Manage Databases', 50 | description: 'View and configure your database connections', 51 | icon: ( 52 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 53 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 54 | </svg> 55 | ), 56 | color: 'green', 57 | enabled: totalDatabases > 0 58 | }, 59 | { 60 | id: 'claude-integration', 61 | title: 'Claude Integration', 62 | description: 'Manage Claude Desktop integration', 63 | icon: ( 64 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 65 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /> 66 | </svg> 67 | ), 68 | color: 'purple', 69 | enabled: true 70 | } 71 | ].filter(action => action.enabled); 72 | 73 | const getStatusIcon = (status) => { 74 | switch (status) { 75 | case 'success': 76 | return <div className="w-2 h-2 bg-green-500 rounded-full" />; 77 | case 'warning': 78 | return <div className="w-2 h-2 bg-yellow-500 rounded-full" />; 79 | case 'error': 80 | return <div className="w-2 h-2 bg-red-500 rounded-full" />; 81 | default: 82 | return <div className="w-2 h-2 bg-blue-500 rounded-full" />; 83 | } 84 | }; 85 | 86 | return ( 87 | <div className="p-4 space-y-4 bg-gray-100 rounded-2xl sm:rounded-3xl"> 88 | {/* Welcome Header */} 89 | <div className="mb-4"> 90 | <h1 className="text-xl font-bold text-gray-900 mb-1">Dashboard</h1> 91 | </div> 92 | 93 | {/* Key Metrics */} 94 | <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> 95 | <MetricCard 96 | title="Total Databases" 97 | value={totalDatabases} 98 | /> 99 | <MetricCard 100 | title="Active Connections" 101 | value={activeConnections} 102 | /> 103 | <MetricCard 104 | title="Total Environments" 105 | value={totalEnvironments} 106 | /> 107 | <MetricCard 108 | title="Configured Databases" 109 | value={configuredDatabases} 110 | /> 111 | </div> 112 | 113 | {/* Quick Actions */} 114 | <div className="bg-white rounded-xl border border-gray-200 p-4"> 115 | <h2 className="text-lg font-semibold text-gray-900 mb-3">Quick Actions</h2> 116 | <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> 117 | {quickActions.map((action) => ( 118 | <button 119 | key={action.id} 120 | onClick={() => onQuickAction(action.id)} 121 | className="flex flex-col items-center p-4 bg-white border border-gray-200 rounded-xl hover:border-[#86a0ff] hover:shadow-sm transition-all duration-200 group" 122 | > 123 | <div className="w-10 h-10 rounded-lg flex items-center justify-center mb-2 transition-colors text-gray-900 group-hover:text-[#86a0ff]"> 124 | {action.icon} 125 | </div> 126 | <h3 className="font-semibold text-gray-900 mb-1 text-sm">{action.title}</h3> 127 | <p className="text-xs text-gray-900 text-center">{action.description}</p> 128 | </button> 129 | ))} 130 | </div> 131 | </div> 132 | 133 | {/* System Status */} 134 | <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> 135 | <div className="bg-white rounded-xl border border-gray-200 p-4"> 136 | <h2 className="text-lg font-semibold text-gray-900 mb-3">System Status</h2> 137 | <div className="space-y-2"> 138 | <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> 139 | <div className="flex items-center"> 140 | <div className={cn( 141 | 'w-3 h-3 rounded-full mr-3', 142 | totalDatabases > 0 ? 'bg-green-500' : 'bg-gray-400' 143 | )} /> 144 | <span className="text-sm font-medium text-gray-900">Database Connections</span> 145 | </div> 146 | <span className="text-sm text-gray-600"> 147 | {totalDatabases > 0 ? `${totalDatabases} configured` : 'No databases'} 148 | </span> 149 | </div> 150 | 151 | <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> 152 | <div className="flex items-center"> 153 | <div className={cn( 154 | 'w-3 h-3 rounded-full mr-3', 155 | activeConnections > 0 ? 'bg-green-500' : 'bg-gray-400' 156 | )} /> 157 | <span className="text-sm font-medium text-gray-900">Claude Integration</span> 158 | </div> 159 | <span className="text-sm text-gray-600"> 160 | {activeConnections > 0 ? `${activeConnections} active` : 'Not connected'} 161 | </span> 162 | </div> 163 | 164 | <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> 165 | <div className="flex items-center"> 166 | <div className={cn( 167 | 'w-3 h-3 rounded-full mr-3', 168 | totalEnvironments > 0 ? 'bg-green-500' : 'bg-gray-400' 169 | )} /> 170 | <span className="text-sm font-medium text-gray-900">Environment Setup</span> 171 | </div> 172 | <span className="text-sm text-gray-600"> 173 | {totalEnvironments > 0 ? `${totalEnvironments} environments` : 'No environments'} 174 | </span> 175 | </div> 176 | </div> 177 | </div> 178 | 179 | {/* Environment Breakdown */} 180 | <div className="bg-white rounded-xl border border-gray-200 p-4"> 181 | <div className="flex items-center justify-between mb-3"> 182 | <h2 className="text-lg font-semibold text-gray-900">Environment Distribution</h2> 183 | <button 184 | onClick={() => setShowEnvironmentManager(true)} 185 | className="text-sm text-blue-600 hover:text-blue-800 flex items-center" 186 | > 187 | <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 188 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> 189 | </svg> 190 | Manage Environments 191 | </button> 192 | </div> 193 | <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3"> 194 | {Object.entries(environmentBreakdown).map(([env, count]) => ( 195 | <div key={env} className="text-center p-3 bg-gray-100 rounded-lg"> 196 | <div className="text-xl font-bold text-gray-900">{count}</div> 197 | <div className="text-xs text-gray-600">{env}</div> 198 | </div> 199 | ))} 200 | {Object.keys(environmentBreakdown).length === 0 && ( 201 | <div className="col-span-full text-center text-gray-500 py-6"> 202 | <svg className="w-10 h-10 mx-auto text-gray-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 203 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> 204 | </svg> 205 | <p className="text-sm">No environments configured yet</p> 206 | <button 207 | onClick={() => setShowEnvironmentManager(true)} 208 | className="mt-2 text-blue-600 hover:text-blue-800 text-xs" 209 | > 210 | Click to add environments 211 | </button> 212 | </div> 213 | )} 214 | </div> 215 | </div> 216 | </div> 217 | 218 | {/* Claude Integration Status */} 219 | <div className="bg-white rounded-xl border border-gray-200 p-4"> 220 | <div className="flex items-center justify-between mb-3"> 221 | <h2 className="text-lg font-semibold text-gray-900">Claude Desktop Integration</h2> 222 | <div className="flex items-center"> 223 | <div className={cn( 224 | 'w-2 h-2 rounded-full mr-2', 225 | activeConnections > 0 ? 'bg-green-500' : 'bg-gray-300' 226 | )} /> 227 | <span className="text-sm text-gray-600"> 228 | {activeConnections > 0 ? 'Connected' : 'Disconnected'} 229 | </span> 230 | </div> 231 | </div> 232 | 233 | <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> 234 | <div className="p-3 bg-green-50 rounded-lg"> 235 | <div className="text-base font-semibold text-green-900">{activeConnections}</div> 236 | <div className="text-xs text-green-700">Active Connections</div> 237 | </div> 238 | <div className="p-3 bg-blue-50 rounded-lg"> 239 | <div className="text-base font-semibold text-blue-900">{Math.max(0, totalDatabases - activeConnections)}</div> 240 | <div className="text-xs text-blue-700">Available to Add</div> 241 | </div> 242 | </div> 243 | </div> 244 | 245 | {/* Environment Manager Modal */} 246 | <EnvironmentManager 247 | isOpen={showEnvironmentManager} 248 | onClose={() => setShowEnvironmentManager(false)} 249 | onSave={(environments) => { 250 | // This would update the environments in the app state 251 | 252 | // In a real app, you'd update the global state here 253 | }} 254 | /> 255 | </div> 256 | ); 257 | }; 258 | 259 | export default DashboardView; 260 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/index.css: -------------------------------------------------------------------------------- ```css 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | 7 | /* Professional Light Theme Base Styles */ 8 | @layer base { 9 | * { 10 | @apply border-gray-200; 11 | } 12 | 13 | html { 14 | @apply scroll-smooth overflow-hidden; 15 | } 16 | 17 | body { 18 | @apply bg-gray-100 text-gray-900 font-sans; 19 | @apply h-screen overflow-hidden; 20 | font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; 21 | -webkit-font-smoothing: antialiased; 22 | -moz-osx-font-smoothing: grayscale; 23 | } 24 | 25 | /* Enhanced Typography System */ 26 | h1, .h1 { 27 | @apply text-4xl font-bold text-gray-900 leading-tight tracking-tight; 28 | } 29 | 30 | h2, .h2 { 31 | @apply text-3xl font-semibold text-gray-800 leading-tight tracking-tight; 32 | } 33 | 34 | h3, .h3 { 35 | @apply text-2xl font-semibold text-gray-800 leading-snug tracking-tight; 36 | } 37 | 38 | h4, .h4 { 39 | @apply text-xl font-medium text-gray-700 leading-snug; 40 | } 41 | 42 | h5, .h5 { 43 | @apply text-lg font-medium text-gray-700 leading-normal; 44 | } 45 | 46 | h6, .h6 { 47 | @apply text-base font-medium text-gray-700 leading-normal; 48 | } 49 | 50 | /* Body text styles */ 51 | p, .body-text { 52 | @apply text-base text-gray-600 leading-relaxed; 53 | } 54 | 55 | .body-text-sm { 56 | @apply text-sm text-gray-600 leading-relaxed; 57 | } 58 | 59 | .body-text-lg { 60 | @apply text-lg text-gray-600 leading-relaxed; 61 | } 62 | 63 | /* Special text styles */ 64 | .display-text { 65 | @apply text-5xl font-black text-gray-900 leading-none tracking-tight; 66 | } 67 | 68 | .hero-text { 69 | @apply text-6xl font-black text-gray-900 leading-none tracking-tight; 70 | } 71 | 72 | .caption-text { 73 | @apply text-sm font-medium text-gray-500 leading-tight; 74 | } 75 | 76 | .label-text { 77 | @apply text-sm font-semibold text-gray-700 uppercase tracking-wide; 78 | } 79 | 80 | /* Link styles */ 81 | a { 82 | @apply text-blue-600 hover:text-blue-700 transition-colors duration-200; 83 | } 84 | 85 | /* Custom scrollbar */ 86 | ::-webkit-scrollbar { 87 | @apply w-2; 88 | } 89 | 90 | ::-webkit-scrollbar-track { 91 | @apply bg-gray-100; 92 | } 93 | 94 | ::-webkit-scrollbar-thumb { 95 | @apply bg-gray-300 rounded-full; 96 | } 97 | 98 | ::-webkit-scrollbar-thumb:hover { 99 | @apply bg-gray-400; 100 | } 101 | 102 | /* Enhanced scrollbar for modal content */ 103 | .modal-scrollbar::-webkit-scrollbar { 104 | @apply w-3; 105 | } 106 | 107 | .modal-scrollbar::-webkit-scrollbar-track { 108 | @apply bg-gray-100 rounded-lg; 109 | } 110 | 111 | .modal-scrollbar::-webkit-scrollbar-thumb { 112 | @apply bg-gray-300 rounded-lg; 113 | } 114 | 115 | .modal-scrollbar::-webkit-scrollbar-thumb:hover { 116 | @apply bg-gray-500; 117 | } 118 | 119 | .modal-scrollbar::-webkit-scrollbar-corner { 120 | @apply bg-transparent; 121 | } 122 | } 123 | 124 | @layer components { 125 | /* Professional Light Card Base */ 126 | .glass-card { 127 | @apply bg-white rounded-2xl border border-gray-200; 128 | @apply shadow-sm shadow-gray-100/50 transition-all duration-300; 129 | @apply hover:shadow-md hover:shadow-gray-200/60 hover:-translate-y-0.5; 130 | } 131 | 132 | .glass-card-primary { 133 | @apply bg-blue-50 border-blue-200/60; 134 | } 135 | 136 | .glass-card-success { 137 | @apply bg-emerald-50 border-emerald-200/60; 138 | } 139 | 140 | .glass-card-warning { 141 | @apply bg-amber-50 border-amber-200/60; 142 | } 143 | 144 | .glass-card-danger { 145 | @apply bg-red-50 border-red-200/60; 146 | } 147 | 148 | /* Professional Buttons - Matching Image Theme */ 149 | .btn-gradient { 150 | @apply relative inline-flex items-center justify-center gap-2; 151 | @apply rounded-lg font-semibold px-6 py-2.5; 152 | @apply transition-all duration-200 overflow-hidden; 153 | @apply hover:-translate-y-0.5 active:translate-y-0 active:scale-95; 154 | @apply disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none; 155 | } 156 | 157 | .btn-primary { 158 | @apply bg-[#86a0ff] text-white; 159 | @apply shadow-sm hover:shadow-md hover:bg-[#7990e6]; 160 | @apply hover:shadow-[#86a0ff]/20; 161 | } 162 | 163 | .btn-secondary { 164 | @apply bg-gray-100 border border-gray-200 text-gray-700; 165 | @apply shadow-sm hover:shadow-md hover:bg-gray-200; 166 | @apply hover:border-gray-300; 167 | } 168 | 169 | .btn-success { 170 | @apply bg-emerald-600 text-white; 171 | @apply shadow-sm hover:shadow-md hover:bg-emerald-700; 172 | @apply hover:shadow-emerald-200/60; 173 | } 174 | 175 | .btn-danger { 176 | @apply bg-red-600 text-white; 177 | @apply shadow-sm hover:shadow-md hover:bg-red-700; 178 | @apply hover:shadow-red-200/60; 179 | } 180 | 181 | .btn-warning { 182 | @apply bg-amber-500 text-white; 183 | @apply shadow-sm hover:shadow-md hover:bg-amber-600; 184 | @apply hover:shadow-amber-200/60; 185 | } 186 | 187 | .btn-light { 188 | @apply bg-gray-100 border border-gray-200 text-gray-700; 189 | @apply shadow-sm hover:shadow-md hover:bg-gray-200; 190 | @apply hover:border-gray-300; 191 | } 192 | 193 | /* Form Elements */ 194 | .form-input { 195 | @apply w-full px-4 py-3 bg-white border border-gray-300; 196 | @apply rounded-lg text-gray-900 placeholder-gray-500; 197 | @apply transition-all duration-200 font-medium; 198 | @apply focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20; 199 | @apply focus:outline-none; 200 | } 201 | 202 | .form-select { 203 | @apply form-input; 204 | @apply appearance-none bg-no-repeat bg-right bg-[length:16px]; 205 | @apply bg-[url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")]; 206 | } 207 | 208 | .form-checkbox { 209 | @apply w-4 h-4 text-blue-600 bg-white border-gray-300; 210 | @apply rounded focus:ring-blue-500/20 focus:ring-2; 211 | } 212 | 213 | /* Status Badges - Matching Image Colors */ 214 | .status-badge { 215 | @apply inline-flex items-center gap-2 px-3 py-1 rounded-full; 216 | @apply text-sm font-semibold border; 217 | } 218 | 219 | .status-online { 220 | @apply bg-emerald-100 text-emerald-800 border-emerald-200; 221 | } 222 | 223 | .status-offline { 224 | @apply bg-gray-100 text-gray-700 border-gray-200; 225 | } 226 | 227 | .status-warning { 228 | @apply bg-amber-100 text-amber-800 border-amber-200; 229 | } 230 | 231 | .status-error { 232 | @apply bg-red-100 text-red-800 border-red-200; 233 | } 234 | 235 | .status-pending { 236 | @apply bg-amber-100 text-amber-800 border-amber-200; 237 | } 238 | 239 | .status-done { 240 | @apply bg-blue-100 text-blue-800 border-blue-200; 241 | } 242 | 243 | /* Environment Badges */ 244 | .env-production { 245 | @apply bg-red-100 border-red-300 text-red-800; 246 | } 247 | 248 | .env-development { 249 | @apply bg-blue-100 border-blue-300 text-blue-800; 250 | } 251 | 252 | .env-staging { 253 | @apply bg-amber-100 border-amber-300 text-amber-800; 254 | } 255 | 256 | /* Dashboard-specific typography classes */ 257 | .metric-value { 258 | @apply text-3xl font-bold text-gray-900 leading-none tracking-tight; 259 | } 260 | 261 | .metric-label { 262 | @apply text-sm font-semibold text-gray-600 uppercase tracking-wide; 263 | } 264 | 265 | .metric-description { 266 | @apply text-sm font-medium text-gray-500 leading-relaxed; 267 | } 268 | 269 | .card-title { 270 | @apply text-lg font-semibold text-gray-800 leading-tight; 271 | } 272 | 273 | .card-subtitle { 274 | @apply text-sm font-medium text-gray-600 leading-relaxed; 275 | } 276 | 277 | .table-header { 278 | @apply text-sm font-semibold text-gray-700 uppercase tracking-wide; 279 | } 280 | 281 | .table-cell { 282 | @apply text-sm font-medium text-gray-600 leading-relaxed; 283 | } 284 | } 285 | 286 | @layer utilities { 287 | .text-gradient { 288 | @apply bg-gradient-to-r from-[#86a0ff] to-[#7990e6] bg-clip-text text-transparent; 289 | } 290 | 291 | .border-gradient { 292 | @apply bg-gradient-to-r from-blue-200/50 to-blue-300/50; 293 | @apply border border-transparent bg-clip-padding; 294 | } 295 | 296 | /* Animation utilities */ 297 | .animate-in { 298 | @apply animate-fade-in; 299 | } 300 | 301 | .animate-up { 302 | @apply animate-slide-up; 303 | } 304 | 305 | .animate-glow { 306 | @apply animate-pulse-glow; 307 | } 308 | 309 | .animate-float { 310 | @apply animate-float; 311 | } 312 | 313 | /* Typography utilities */ 314 | .font-thin { 315 | font-weight: 100; 316 | } 317 | 318 | .font-extralight { 319 | font-weight: 200; 320 | } 321 | 322 | .font-light { 323 | font-weight: 300; 324 | } 325 | 326 | .font-normal { 327 | font-weight: 400; 328 | } 329 | 330 | .font-medium { 331 | font-weight: 500; 332 | } 333 | 334 | .font-semibold { 335 | font-weight: 600; 336 | } 337 | 338 | .font-bold { 339 | font-weight: 700; 340 | } 341 | 342 | .font-extrabold { 343 | font-weight: 800; 344 | } 345 | 346 | .font-black { 347 | font-weight: 900; 348 | } 349 | 350 | /* Text truncation utilities */ 351 | .line-clamp-1 { 352 | overflow: hidden; 353 | display: -webkit-box; 354 | -webkit-box-orient: vertical; 355 | -webkit-line-clamp: 1; 356 | } 357 | 358 | .line-clamp-2 { 359 | overflow: hidden; 360 | display: -webkit-box; 361 | -webkit-box-orient: vertical; 362 | -webkit-line-clamp: 2; 363 | } 364 | 365 | .line-clamp-3 { 366 | overflow: hidden; 367 | display: -webkit-box; 368 | -webkit-box-orient: vertical; 369 | -webkit-line-clamp: 3; 370 | } 371 | } 372 | 373 | /* Custom background patterns */ 374 | .bg-dots { 375 | background-image: radial-gradient(circle, rgba(99, 102, 241, 0.1) 1px, transparent 1px); 376 | background-size: 20px 20px; 377 | } 378 | 379 | /* Glass window specific styles */ 380 | .glass-window { 381 | backdrop-filter: blur(20px); 382 | -webkit-backdrop-filter: blur(20px); 383 | background: rgba(255, 255, 255, 0.8); 384 | border: 1px solid rgba(255, 255, 255, 0.2); 385 | } 386 | 387 | /* Perfect centering for glass window */ 388 | .glass-window-container { 389 | display: flex; 390 | align-items: center; 391 | justify-content: center; 392 | min-height: 100vh; 393 | padding: 1rem; 394 | } 395 | 396 | .glass-window-content { 397 | width: 100%; 398 | max-width: 110rem; /* 8xl - maximum width */ 399 | height: calc(100vh - 2rem); 400 | margin: 0 auto; 401 | box-sizing: border-box; 402 | } 403 | 404 | /* Ensure content is contained within the glass window */ 405 | .glass-window-content * { 406 | box-sizing: border-box; 407 | } 408 | 409 | /* Prevent content overflow */ 410 | .glass-window-content > div { 411 | max-width: 100%; 412 | overflow: visible; 413 | } 414 | 415 | /* Custom scrollbar for database list */ 416 | .database-list-scrollbar { 417 | scrollbar-width: thin; 418 | scrollbar-color: rgba(156, 163, 175, 0.5) transparent; 419 | } 420 | 421 | .database-list-scrollbar::-webkit-scrollbar { 422 | width: 6px; 423 | } 424 | 425 | .database-list-scrollbar::-webkit-scrollbar-track { 426 | background: transparent; 427 | } 428 | 429 | .database-list-scrollbar::-webkit-scrollbar-thumb { 430 | background: rgba(156, 163, 175, 0.5); 431 | border-radius: 3px; 432 | } 433 | 434 | .database-list-scrollbar::-webkit-scrollbar-thumb:hover { 435 | background: rgba(156, 163, 175, 0.7); 436 | } 437 | 438 | /* Custom scrollbar for Claude table */ 439 | .claude-table-scrollbar { 440 | scrollbar-width: thin; 441 | scrollbar-color: rgba(156, 163, 175, 0.5) transparent; 442 | } 443 | 444 | .claude-table-scrollbar::-webkit-scrollbar { 445 | width: 6px; 446 | } 447 | 448 | .claude-table-scrollbar::-webkit-scrollbar-track { 449 | background: transparent; 450 | } 451 | 452 | .claude-table-scrollbar::-webkit-scrollbar-thumb { 453 | background: rgba(156, 163, 175, 0.5); 454 | border-radius: 3px; 455 | } 456 | 457 | .claude-table-scrollbar::-webkit-scrollbar-thumb:hover { 458 | background: rgba(156, 163, 175, 0.7); 459 | } 460 | 461 | 462 | 463 | .bg-grid { 464 | background-image: 465 | linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px), 466 | linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px); 467 | background-size: 20px 20px; 468 | } 469 | 470 | /* Loading spinner */ 471 | @keyframes spin { 472 | to { 473 | transform: rotate(360deg); 474 | } 475 | } 476 | 477 | .loading-spinner { 478 | @apply inline-block w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full; 479 | animation: spin 1s linear infinite; 480 | } 481 | /* Shine effect for buttons */ 482 | .btn-shine::before { 483 | content: ''; 484 | @apply absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent; 485 | @apply opacity-0 transition-all duration-500 -skew-x-12; 486 | transform: translateX(-100%); 487 | } 488 | 489 | .btn-shine:hover::before { 490 | @apply opacity-100; 491 | transform: translateX(100%); 492 | } 493 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ConnectionDetailsModal.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useEffect } from 'react'; 2 | import { motion } from 'framer-motion'; 3 | import { EnvironmentBadge } from './ui/StatusBadge'; 4 | import { DatabaseTypeBadge } from './ui'; 5 | import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes'; 6 | 7 | const ConnectionDetailsModal = ({ isOpen, onClose, connection }) => { 8 | useEffect(() => { 9 | if (!isOpen) return; 10 | const onKeyDown = (e) => { 11 | if (e.key === 'Escape') onClose(); 12 | }; 13 | window.addEventListener('keydown', onKeyDown); 14 | return () => window.removeEventListener('keydown', onKeyDown); 15 | }, [isOpen, onClose]); 16 | 17 | if (!isOpen || !connection) return null; 18 | 19 | // Detect database type from connection data 20 | const databaseType = detectDatabaseType(connection.env || {}); 21 | 22 | return ( 23 | <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> 24 | <motion.div 25 | initial={{ opacity: 0, scale: 0.95 }} 26 | animate={{ opacity: 1, scale: 1 }} 27 | exit={{ opacity: 0, scale: 0.95 }} 28 | className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-4xl max-h-[90vh] overflow-hidden" 29 | > 30 | <div className="flex items-center justify-between mb-4"> 31 | <div className="flex items-center space-x-3"> 32 | <div className="h-10 w-10 bg-blue-50 rounded-full flex items-center justify-center"> 33 | <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> 34 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /> 35 | </svg> 36 | </div> 37 | <div> 38 | <h2 className="text-xl font-semibold text-gray-900">{connection.name}</h2> 39 | <p className="text-sm text-gray-500">Database Connection Details</p> 40 | </div> 41 | </div> 42 | <button 43 | onClick={onClose} 44 | className="text-gray-400 hover:text-gray-600 transition-colors" 45 | > 46 | <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 47 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> 48 | </svg> 49 | </button> 50 | </div> 51 | 52 | {/* Connection Status */} 53 | <div className="mb-4"> 54 | <div className="flex items-center justify-between p-4 bg-green-50 rounded-lg border border-green-200"> 55 | <div className="flex items-center space-x-3"> 56 | <div className="w-3 h-3 bg-green-500 rounded-full"></div> 57 | <div> 58 | <p className="text-sm font-medium text-green-800">Connected to Claude Desktop</p> 59 | <p className="text-xs text-green-600">Active and available for use</p> 60 | </div> 61 | </div> 62 | <EnvironmentBadge 63 | environment={connection.env?.ENVIRONMENT || connection.environment || 'Development'} 64 | active={true} 65 | size="sm" 66 | /> 67 | </div> 68 | </div> 69 | 70 | {/* Database Type Information */} 71 | <div className="mb-4"> 72 | <div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg border border-blue-200"> 73 | <div className="flex items-center space-x-3"> 74 | <DatabaseTypeBadge type={databaseType} size="md" /> 75 | <div> 76 | <p className="text-sm font-medium text-blue-800">Database Type</p> 77 | <p className="text-xs text-blue-600">{getDatabaseTypeDisplayName(databaseType)}</p> 78 | </div> 79 | </div> 80 | </div> 81 | </div> 82 | 83 | {/* Connection Configuration */} 84 | <div className="space-y-4"> 85 | {/* Basic Connection Settings */} 86 | <div> 87 | <h3 className="text-lg font-medium text-gray-900 mb-3">Connection Configuration</h3> 88 | <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> 89 | <div className="p-3 bg-gray-50 rounded-lg"> 90 | <label className="block text-sm font-medium text-gray-700 mb-1">Host</label> 91 | <p className="text-sm text-gray-900 font-mono break-all"> 92 | {connection.env?.HANA_HOST || 'Not configured'} 93 | </p> 94 | </div> 95 | <div className="p-3 bg-gray-50 rounded-lg"> 96 | <label className="block text-sm font-medium text-gray-700 mb-1">Port</label> 97 | <p className="text-sm text-gray-900 font-mono"> 98 | {connection.env?.HANA_PORT || '443'} 99 | </p> 100 | </div> 101 | <div className="p-3 bg-gray-50 rounded-lg"> 102 | <label className="block text-sm font-medium text-gray-700 mb-1">User</label> 103 | <p className="text-sm text-gray-900 font-mono break-all"> 104 | {connection.env?.HANA_USER || 'Not set'} 105 | </p> 106 | </div> 107 | <div className="p-3 bg-gray-50 rounded-lg"> 108 | <label className="block text-sm font-medium text-gray-700 mb-1">Schema</label> 109 | <p className="text-sm text-gray-900 font-mono break-all"> 110 | {connection.env?.HANA_SCHEMA || 'Not set'} 111 | </p> 112 | </div> 113 | </div> 114 | 115 | {/* MDC-specific fields - show conditionally */} 116 | {(databaseType === 'mdc_tenant' || databaseType === 'mdc_system') && ( 117 | <div className="mt-4"> 118 | <h4 className="text-md font-medium text-gray-800 mb-3">MDC Configuration</h4> 119 | <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> 120 | {databaseType === 'mdc_tenant' && connection.env?.HANA_DATABASE_NAME && ( 121 | <div className="p-3 bg-amber-50 rounded-lg border border-amber-200"> 122 | <label className="block text-sm font-medium text-amber-800 mb-1">Database Name</label> 123 | <p className="text-sm text-amber-900 font-mono break-all"> 124 | {connection.env.HANA_DATABASE_NAME} 125 | </p> 126 | </div> 127 | )} 128 | {connection.env?.HANA_INSTANCE_NUMBER && ( 129 | <div className="p-3 bg-amber-50 rounded-lg border border-amber-200"> 130 | <label className="block text-sm font-medium text-amber-800 mb-1">Instance Number</label> 131 | <p className="text-sm text-amber-900 font-mono"> 132 | {connection.env.HANA_INSTANCE_NUMBER} 133 | </p> 134 | </div> 135 | )} 136 | </div> 137 | </div> 138 | )} 139 | </div> 140 | 141 | {/* Security & SSL Configuration */} 142 | <div> 143 | <h3 className="text-lg font-medium text-gray-900 mb-3">Security & SSL Configuration</h3> 144 | <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> 145 | <div className="p-3 bg-gray-50 rounded-lg"> 146 | <label className="block text-sm font-medium text-gray-700 mb-1">SSL Enabled</label> 147 | <div className="flex items-center space-x-2"> 148 | <div className={`w-3 h-3 rounded-full ${ 149 | connection.env?.HANA_SSL === 'true' ? 'bg-green-500' : 'bg-red-500' 150 | }`}></div> 151 | <p className="text-sm text-gray-900"> 152 | {connection.env?.HANA_SSL === 'true' ? 'Enabled' : 'Disabled'} 153 | </p> 154 | </div> 155 | </div> 156 | <div className="p-3 bg-gray-50 rounded-lg"> 157 | <label className="block text-sm font-medium text-gray-700 mb-1">Encryption</label> 158 | <div className="flex items-center space-x-2"> 159 | <div className={`w-3 h-3 rounded-full ${ 160 | connection.env?.HANA_ENCRYPT === 'true' ? 'bg-green-500' : 'bg-red-500' 161 | }`}></div> 162 | <p className="text-sm text-gray-900"> 163 | {connection.env?.HANA_ENCRYPT === 'true' ? 'Enabled' : 'Disabled'} 164 | </p> 165 | </div> 166 | </div> 167 | <div className="p-3 bg-gray-50 rounded-lg"> 168 | <label className="block text-sm font-medium text-gray-700 mb-1">Certificate Validation</label> 169 | <div className="flex items-center space-x-2"> 170 | <div className={`w-3 h-3 rounded-full ${ 171 | connection.env?.HANA_VALIDATE_CERT === 'true' ? 'bg-green-500' : 'bg-yellow-500' 172 | }`}></div> 173 | <p className="text-sm text-gray-900"> 174 | {connection.env?.HANA_VALIDATE_CERT === 'true' ? 'Enabled' : 'Disabled'} 175 | </p> 176 | </div> 177 | </div> 178 | </div> 179 | </div> 180 | 181 | {/* Logging Configuration */} 182 | <div> 183 | <h3 className="text-lg font-medium text-gray-900 mb-3">Logging Configuration</h3> 184 | <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> 185 | <div className="p-3 bg-gray-50 rounded-lg"> 186 | <label className="block text-sm font-medium text-gray-700 mb-1">Log Level</label> 187 | <p className="text-sm text-gray-900 font-semibold uppercase"> 188 | {connection.env?.LOG_LEVEL || 'info'} 189 | </p> 190 | </div> 191 | <div className="p-3 bg-gray-50 rounded-lg"> 192 | <label className="block text-sm font-medium text-gray-700 mb-1">File Logging</label> 193 | <div className="flex items-center space-x-2"> 194 | <div className={`w-3 h-3 rounded-full ${ 195 | connection.env?.ENABLE_FILE_LOGGING === 'true' ? 'bg-green-500' : 'bg-red-500' 196 | }`}></div> 197 | <p className="text-sm text-gray-900"> 198 | {connection.env?.ENABLE_FILE_LOGGING === 'true' ? 'Enabled' : 'Disabled'} 199 | </p> 200 | </div> 201 | </div> 202 | <div className="p-3 bg-gray-50 rounded-lg"> 203 | <label className="block text-sm font-medium text-gray-700 mb-1">Console Logging</label> 204 | <div className="flex items-center space-x-2"> 205 | <div className={`w-3 h-3 rounded-full ${ 206 | connection.env?.ENABLE_CONSOLE_LOGGING === 'true' ? 'bg-green-500' : 'bg-red-500' 207 | }`}></div> 208 | <p className="text-sm text-gray-900"> 209 | {connection.env?.ENABLE_CONSOLE_LOGGING === 'true' ? 'Enabled' : 'Disabled'} 210 | </p> 211 | </div> 212 | </div> 213 | </div> 214 | </div> 215 | </div> 216 | 217 | {/* Action Buttons */} 218 | <div className="flex justify-end space-x-3 mt-4 pt-4 border-t border-gray-200"> 219 | <button 220 | onClick={onClose} 221 | className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors" 222 | > 223 | Close 224 | </button> 225 | </div> 226 | </motion.div> 227 | </div> 228 | ); 229 | }; 230 | 231 | export default ConnectionDetailsModal; 232 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/DatabaseListView.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState, useMemo } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import SearchAndFilter from './SearchAndFilter'; 4 | import EnhancedServerCard from './EnhancedServerCard'; 5 | import { cn } from '../utils/cn'; 6 | 7 | const DatabaseListView = ({ 8 | hanaServers, 9 | claudeServers, 10 | activeEnvironments, 11 | onEditServer, 12 | onAddToClaudeServer, 13 | onDeleteServer, 14 | onAddDatabase 15 | }) => { 16 | const [searchQuery, setSearchQuery] = useState(''); 17 | const [selectedDatabase, setSelectedDatabase] = useState(null); 18 | const [filters, setFilters] = useState({ 19 | status: 'all', 20 | sortBy: 'name', 21 | sortOrder: 'asc' 22 | }); 23 | 24 | // Selection handlers 25 | const handleDatabaseSelect = (databaseName) => { 26 | setSelectedDatabase(databaseName); 27 | }; 28 | 29 | const handleEditSelected = () => { 30 | if (selectedDatabase && hanaServers[selectedDatabase]) { 31 | onEditServer(hanaServers[selectedDatabase]); 32 | } 33 | }; 34 | 35 | const handleAddToClaudeSelected = () => { 36 | if (selectedDatabase) { 37 | onAddToClaudeServer(selectedDatabase); 38 | } 39 | }; 40 | 41 | const handleDeleteSelected = () => { 42 | if (selectedDatabase) { 43 | onDeleteServer(selectedDatabase); 44 | setSelectedDatabase(null); 45 | } 46 | }; 47 | 48 | // Calculate filter counts 49 | const filterCounts = useMemo(() => { 50 | const servers = Object.entries(hanaServers); 51 | const activeInClaude = Object.keys(activeEnvironments).length; 52 | return { 53 | total: servers.length, 54 | active: servers.filter(([name]) => claudeServers.some(cs => cs.name === name)).length, 55 | activeInClaude: activeInClaude, 56 | production: servers.filter(([, server]) => server.environments?.Production).length, 57 | development: servers.filter(([, server]) => server.environments?.Development).length, 58 | staging: servers.filter(([, server]) => server.environments?.Staging).length, 59 | activeFilter: filters.status 60 | }; 61 | }, [hanaServers, claudeServers, filters.status, activeEnvironments]); 62 | 63 | // Filter and sort servers 64 | const filteredServers = useMemo(() => { 65 | let filtered = Object.entries(hanaServers); 66 | 67 | // Apply search filter 68 | if (searchQuery) { 69 | filtered = filtered.filter(([name, server]) => 70 | name.toLowerCase().includes(searchQuery.toLowerCase()) || 71 | server.description?.toLowerCase().includes(searchQuery.toLowerCase()) 72 | ); 73 | } 74 | 75 | // Apply status filter 76 | if (filters.status !== 'all') { 77 | switch (filters.status) { 78 | case 'active': 79 | filtered = filtered.filter(([name]) => 80 | claudeServers.some(cs => cs.name === name) 81 | ); 82 | break; 83 | case 'production': 84 | case 'development': 85 | case 'staging': 86 | filtered = filtered.filter(([, server]) => 87 | server.environments?.[filters.status.charAt(0).toUpperCase() + filters.status.slice(1)] 88 | ); 89 | break; 90 | } 91 | } 92 | 93 | // Apply sorting 94 | filtered.sort(([nameA, serverA], [nameB, serverB]) => { 95 | let valueA, valueB; 96 | 97 | switch (filters.sortBy) { 98 | case 'name': 99 | valueA = nameA.toLowerCase(); 100 | valueB = nameB.toLowerCase(); 101 | break; 102 | case 'created': 103 | valueA = new Date(serverA.created || 0); 104 | valueB = new Date(serverB.created || 0); 105 | break; 106 | case 'modified': 107 | valueA = new Date(serverA.modified || 0); 108 | valueB = new Date(serverB.modified || 0); 109 | break; 110 | case 'environments': 111 | valueA = Object.keys(serverA.environments || {}).length; 112 | valueB = Object.keys(serverB.environments || {}).length; 113 | break; 114 | default: 115 | valueA = nameA.toLowerCase(); 116 | valueB = nameB.toLowerCase(); 117 | } 118 | 119 | if (filters.sortOrder === 'desc') { 120 | [valueA, valueB] = [valueB, valueA]; 121 | } 122 | 123 | if (valueA < valueB) return -1; 124 | if (valueA > valueB) return 1; 125 | return 0; 126 | }); 127 | 128 | return filtered; 129 | }, [hanaServers, claudeServers, searchQuery, filters]); 130 | 131 | 132 | 133 | const handleFilterChange = (newFilters) => { 134 | setFilters(prev => ({ ...prev, ...newFilters })); 135 | }; 136 | 137 | const handleClearFilters = () => { 138 | setFilters({ 139 | status: 'all', 140 | sortBy: 'name', 141 | sortOrder: 'asc' 142 | }); 143 | setSearchQuery(''); 144 | }; 145 | 146 | return ( 147 | <div className="p-6 space-y-6 bg-gray-100 rounded-2xl sm:rounded-3xl overflow-y-auto max-h-full database-list-scrollbar"> 148 | {/* Header */} 149 | <div className="flex items-center justify-between"> 150 | <div> 151 | <h1 className="text-2xl font-bold text-gray-900 mb-2">My Local Databases</h1> 152 | <p className="text-gray-600"> 153 | Manage your HANA database configurations 154 | </p> 155 | {filterCounts.activeInClaude > 0 && ( 156 | <div className="flex items-center space-x-2 mt-2"> 157 | <div className="flex items-center space-x-1 text-green-600 bg-green-50 px-3 py-1 rounded-full"> 158 | <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 159 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> 160 | </svg> 161 | <span className="text-sm font-medium"> 162 | {filterCounts.activeInClaude} environment{filterCounts.activeInClaude !== 1 ? 's' : ''} connected to Claude 163 | </span> 164 | </div> 165 | </div> 166 | )} 167 | </div> 168 | <button 169 | onClick={onAddDatabase} 170 | className="flex items-center px-4 py-2 bg-[#86a0ff] text-white text-sm font-medium rounded-lg hover:bg-[#7990e6] transition-colors shadow-sm hover:shadow-md" 171 | > 172 | <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 173 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> 174 | </svg> 175 | Add Database 176 | </button> 177 | </div> 178 | 179 | {/* Search and Filter Bar */} 180 | <div className="bg-white rounded-xl border border-gray-100 p-6"> 181 | <SearchAndFilter 182 | searchQuery={searchQuery} 183 | onSearchChange={setSearchQuery} 184 | filters={filters} 185 | onFiltersChange={setFilters} 186 | filterCounts={filterCounts} 187 | /> 188 | </div> 189 | 190 | {/* Top Bar with Actions */} 191 | <div className="bg-white rounded-xl border border-gray-200 p-6"> 192 | <div className="flex items-center justify-between"> 193 | <div className="flex items-center space-x-4"> 194 | <span className="text-sm font-medium text-gray-700"> 195 | {filteredServers.length} of {Object.keys(hanaServers).length} databases 196 | </span> 197 | </div> 198 | 199 | <div className="flex items-center space-x-3"> 200 | <button 201 | onClick={handleEditSelected} 202 | disabled={!selectedDatabase} 203 | className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 204 | > 205 | Edit 206 | </button> 207 | 208 | <button 209 | onClick={handleAddToClaudeSelected} 210 | disabled={!selectedDatabase} 211 | className="px-4 py-2 text-sm font-medium text-white bg-[#86a0ff] border border-[#86a0ff] rounded-lg hover:bg-[#7990e6] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 212 | > 213 | Add to Claude 214 | </button> 215 | 216 | <button 217 | onClick={handleDeleteSelected} 218 | disabled={!selectedDatabase} 219 | className="px-4 py-2 text-sm font-medium text-red-600 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" 220 | > 221 | Delete 222 | </button> 223 | </div> 224 | </div> 225 | </div> 226 | 227 | {/* Database Table */} 228 | <div className="bg-white rounded-xl border border-gray-200 overflow-hidden"> 229 | {filteredServers.length === 0 ? ( 230 | <div className="text-center py-12"> 231 | <svg className="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 232 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> 233 | </svg> 234 | <h3 className="text-lg font-medium text-gray-900 mb-2">No databases found</h3> 235 | <p className="text-gray-600 mb-4"> 236 | {searchQuery ? `No databases match "${searchQuery}"` : 'Get started by adding your first database'} 237 | </p> 238 | <button 239 | onClick={onAddDatabase} 240 | className="inline-flex items-center px-6 py-3 bg-[#86a0ff] text-white font-medium rounded-lg hover:bg-[#7990e6] transition-colors shadow-sm hover:shadow-md" 241 | > 242 | <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 243 | <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> 244 | </svg> 245 | Add Your First Database 246 | </button> 247 | </div> 248 | ) : ( 249 | <> 250 | {/* Table Header */} 251 | <div className="bg-gray-50 px-6 py-3 border-b border-gray-200"> 252 | <div className="grid grid-cols-12 gap-4 items-center"> 253 | <div className="col-span-1"> 254 | {/* Empty header for radio button column */} 255 | </div> 256 | <div className="col-span-4"> 257 | <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Database</h3> 258 | </div> 259 | <div className="col-span-2"> 260 | <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Active Environment</h3> 261 | </div> 262 | <div className="col-span-2"> 263 | <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Environments</h3> 264 | </div> 265 | <div className="col-span-3"> 266 | <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Description</h3> 267 | </div> 268 | </div> 269 | </div> 270 | 271 | {/* Database List */} 272 | <div className="divide-y divide-gray-200"> 273 | <AnimatePresence> 274 | {filteredServers.map(([name, server], index) => ( 275 | <motion.div 276 | key={name} 277 | initial={{ opacity: 0, x: -10 }} 278 | animate={{ opacity: 1, x: 0 }} 279 | exit={{ opacity: 0, x: -10 }} 280 | transition={{ duration: 0.2, delay: index * 0.02 }} 281 | > 282 | <EnhancedServerCard 283 | name={name} 284 | server={server} 285 | index={index} 286 | isSelected={selectedDatabase === name} 287 | activeEnvironment={activeEnvironments[name]} 288 | onSelect={handleDatabaseSelect} 289 | onEdit={() => onEditServer(server)} 290 | onAddToClaude={() => onAddToClaudeServer(name)} 291 | onDelete={() => onDeleteServer(name)} 292 | /> 293 | </motion.div> 294 | ))} 295 | </AnimatePresence> 296 | </div> 297 | </> 298 | )} 299 | </div> 300 | </div> 301 | ); 302 | }; 303 | 304 | export default DatabaseListView; 305 | ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/PathConfigModal.jsx: -------------------------------------------------------------------------------- ```javascript 1 | import { useState, useEffect } from 'react'; 2 | import { motion, AnimatePresence } from 'framer-motion'; 3 | import { XMarkIcon } from '@heroicons/react/24/outline'; 4 | import { cn } from '../utils/cn'; 5 | 6 | // Reusable styling constants (following the same pattern as BackupHistoryModal) 7 | const BUTTON_STYLES = { 8 | primary: "inline-flex items-center gap-2 px-4 py-2 bg-[#86a0ff] text-white rounded-lg text-sm font-medium hover:bg-[#7990e6] transition-colors", 9 | secondary: "px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" 10 | }; 11 | 12 | const MODAL_ANIMATIONS = { 13 | backdrop: { 14 | initial: { opacity: 0 }, 15 | animate: { opacity: 1 }, 16 | exit: { opacity: 0 } 17 | }, 18 | modal: { 19 | initial: { scale: 0.95, opacity: 0 }, 20 | animate: { scale: 1, opacity: 1 }, 21 | exit: { scale: 0.95, opacity: 0 } 22 | } 23 | }; 24 | 25 | // Default configuration paths for different operating systems 26 | const DEFAULT_PATHS = { 27 | windows: [ 28 | '%APPDATA%\\Claude\\claude_desktop_config.json', 29 | '%APPDATA%\\Claude\\desktop\\claude_desktop_config.json', 30 | '%LOCALAPPDATA%\\Claude\\claude_desktop_config.json', 31 | 'C:\\Users\\%USERNAME%\\AppData\\Roaming\\Claude\\claude_desktop_config.json', 32 | 'C:\\Users\\%USERNAME%\\AppData\\Local\\Claude\\claude_desktop_config.json' 33 | ], 34 | mac: [ 35 | '~/Library/Application Support/Claude/claude_desktop_config.json', 36 | '~/Library/Application Support/Claude/desktop/claude_desktop_config.json', 37 | '/Users/$USER/Library/Application Support/Claude/claude_desktop_config.json', 38 | '/Users/$USER/Library/Application Support/Claude/desktop/claude_desktop_config.json', 39 | '/Users/$USER/.config/claude/claude_desktop_config.json' 40 | ], 41 | linux: [ 42 | '~/.config/claude/claude_desktop_config.json', 43 | '/home/$USER/.config/claude/claude_desktop_config.json', 44 | '/home/$USER/.local/share/claude/claude_desktop_config.json' 45 | ] 46 | }; 47 | 48 | const PathConfigModal = ({ 49 | isOpen, 50 | onClose, 51 | onConfigPathChange, 52 | currentPath = '' 53 | }) => { 54 | const [pathInput, setPathInput] = useState(currentPath); 55 | const [isSubmitting, setIsSubmitting] = useState(false); 56 | const [detectedOS, setDetectedOS] = useState('mac'); 57 | 58 | // Detect OS 59 | useEffect(() => { 60 | const userAgent = navigator.userAgent; 61 | let os = 'mac'; 62 | if (userAgent.includes('Windows')) os = 'windows'; 63 | else if (userAgent.includes('Linux')) os = 'linux'; 64 | 65 | setDetectedOS(os); 66 | }, []); 67 | 68 | // Reset form when modal opens/closes 69 | useEffect(() => { 70 | if (isOpen) { 71 | setPathInput(currentPath); 72 | setIsSubmitting(false); 73 | } 74 | }, [isOpen, currentPath]); 75 | 76 | // Handle escape key 77 | useEffect(() => { 78 | if (!isOpen) return; 79 | const onKeyDown = (e) => { 80 | if (e.key === 'Escape') { 81 | onClose(); 82 | } 83 | }; 84 | window.addEventListener('keydown', onKeyDown); 85 | return () => window.removeEventListener('keydown', onKeyDown); 86 | }, [isOpen, onClose]); 87 | 88 | const selectPath = (path) => { 89 | // Replace environment variables with actual values for better user experience 90 | let resolvedPath = path; 91 | 92 | if (detectedOS === 'mac' || detectedOS === 'linux') { 93 | // For Mac/Linux, replace $USER with actual username if we can detect it 94 | // Try to get username from common sources 95 | let username = 'YourUsername'; 96 | 97 | // Try to get username from localStorage if previously set 98 | const savedUsername = localStorage.getItem('claude_username'); 99 | if (savedUsername) { 100 | username = savedUsername; 101 | } else { 102 | // Try to extract username from common patterns 103 | if (detectedOS === 'mac') { 104 | // For Mac, try to get username from common locations 105 | username = 'YourUsername'; 106 | } else if (detectedOS === 'linux') { 107 | username = 'YourUsername'; 108 | } 109 | } 110 | 111 | resolvedPath = path.replace(/\$USER/g, username); 112 | } 113 | 114 | setPathInput(resolvedPath); 115 | }; 116 | 117 | const handleSubmit = async () => { 118 | if (!pathInput.trim()) { 119 | alert('Please select or enter a configuration path'); 120 | return; 121 | } 122 | 123 | setIsSubmitting(true); 124 | try { 125 | if (onConfigPathChange) { 126 | await onConfigPathChange(pathInput.trim()); 127 | } 128 | onClose(); 129 | } catch (error) { 130 | console.error('Error updating config path:', error); 131 | alert('Failed to update configuration path. Please try again.'); 132 | } finally { 133 | setIsSubmitting(false); 134 | } 135 | }; 136 | 137 | if (!isOpen) return null; 138 | 139 | return ( 140 | <AnimatePresence> 141 | <motion.div 142 | {...MODAL_ANIMATIONS.backdrop} 143 | className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" 144 | onClick={onClose} 145 | > 146 | <motion.div 147 | {...MODAL_ANIMATIONS.modal} 148 | onClick={(e) => e.stopPropagation()} 149 | className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col" 150 | > 151 | {/* Header */} 152 | <div className="px-6 py-3 border-b border-gray-200"> 153 | <div className="flex items-center justify-between"> 154 | <div> 155 | <h2 className="text-xl font-semibold text-gray-900">Configure Claude Desktop Path</h2> 156 | <p className="text-sm text-gray-600">Select or enter the path to your Claude Desktop configuration file</p> 157 | </div> 158 | <button 159 | onClick={onClose} 160 | className="text-gray-400 hover:text-gray-600 transition-colors" 161 | > 162 | <XMarkIcon className="w-5 h-5" /> 163 | </button> 164 | </div> 165 | </div> 166 | 167 | {/* Content */} 168 | <div className="p-6 space-y-4 flex-1 overflow-y-auto"> 169 | {/* Path Input */} 170 | <div className="space-y-3"> 171 | <div className="space-y-2"> 172 | <label htmlFor="pathInput" className="text-sm font-medium text-gray-700"> 173 | Configuration Path 174 | </label> 175 | <input 176 | type="text" 177 | id="pathInput" 178 | value={pathInput} 179 | onChange={(e) => setPathInput(e.target.value)} 180 | placeholder="Select a path below or enter custom path" 181 | className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm" 182 | /> 183 | <p className="text-xs text-gray-500"> 184 | The selected path will be used to locate your Claude Desktop configuration 185 | </p> 186 | </div> 187 | 188 | {/* Selectable Path Locations */} 189 | <div className="space-y-2"> 190 | <h4 className="text-sm font-medium text-gray-700"> 191 | 📁 Common Claude Desktop Config Locations for {detectedOS === 'windows' ? 'Windows' : detectedOS === 'mac' ? 'macOS' : 'Linux'}: 192 | </h4> 193 | <div className="grid gap-2"> 194 | {DEFAULT_PATHS[detectedOS].map((path, index) => ( 195 | <div 196 | key={index} 197 | className={cn( 198 | "border rounded-lg p-3 transition-all duration-200", 199 | pathInput === path 200 | ? "border-blue-500 bg-blue-50" 201 | : "border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50" 202 | )} 203 | > 204 | <div className="flex items-center justify-between"> 205 | <div className="flex-1"> 206 | <code className="text-sm font-mono text-gray-700 break-all"> 207 | {path} 208 | </code> 209 | </div> 210 | <button 211 | onClick={() => selectPath(path)} 212 | className={cn( 213 | "ml-3 px-3 py-1.5 text-xs font-medium rounded-md transition-colors", 214 | pathInput === path 215 | ? "bg-blue-600 text-white" 216 | : "bg-gray-100 text-gray-700 hover:bg-blue-100 hover:text-blue-700" 217 | )} 218 | > 219 | {pathInput === path ? 'Selected' : 'Select'} 220 | </button> 221 | </div> 222 | </div> 223 | ))} 224 | </div> 225 | <p className="text-xs text-gray-500"> 226 | 💡 Click "Select" next to any path above to choose it, then click "Update Path" below to save 227 | </p> 228 | </div> 229 | </div> 230 | 231 | {/* Help Section */} 232 | <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg"> 233 | <h4 className="text-sm font-medium text-blue-900 mb-1.5">💡 How to find your config file:</h4> 234 | <div className="text-sm text-blue-700 space-y-0.5"> 235 | {detectedOS === 'windows' ? ( 236 | <> 237 | <p>• <strong>Windows:</strong> Check these locations:</p> 238 | <ul className="ml-4 space-y-0.5"> 239 | <li>• <code className="bg-blue-100 px-1 rounded">%APPDATA%\\Claude\\</code> (usually C:\Users\YourUsername\AppData\Roaming\Claude\)</li> 240 | <li>• <code className="bg-blue-100 px-1 rounded">%LOCALAPPDATA%\\Claude\\</code> (usually C:\Users\YourUsername\AppData\Local\Claude\)</li> 241 | <li>• <code className="bg-blue-100 px-1 rounded">C:\\Users\\YourUsername\\AppData\\Roaming\\Claude\\</code></li> 242 | </ul> 243 | </> 244 | ) : detectedOS === 'mac' ? ( 245 | <> 246 | <p>• <strong>macOS:</strong> Check these locations:</p> 247 | <ul className="ml-4 space-y-1"> 248 | <li>• <code className="bg-blue-100 px-1 rounded">~/Library/Application Support/Claude/</code></li> 249 | <li>• <code className="bg-blue-100 px-1 rounded">/Users/YourUsername/Library/Application Support/Claude/</code></li> 250 | </ul> 251 | </> 252 | ) : ( 253 | <> 254 | <p>• <strong>Linux:</strong> Check these locations:</p> 255 | <ul className="ml-4 space-y-1"> 256 | <li>• <code className="bg-blue-100 px-1 rounded">~/.config/claude/</code></li> 257 | <li>• <code className="bg-blue-100 px-1 rounded">/home/YourUsername/.config/claude/</code></li> 258 | </ul> 259 | </> 260 | )} 261 | <p className="mt-2">• Look for a file named <code className="bg-blue-100 px-1 rounded">claude_desktop_config.json</code></p> 262 | </div> 263 | </div> 264 | </div> 265 | 266 | {/* Footer */} 267 | <div className="flex items-center justify-end gap-3 p-4 border-t border-gray-200"> 268 | <button 269 | onClick={onClose} 270 | className={BUTTON_STYLES.secondary} 271 | > 272 | Cancel 273 | </button> 274 | <button 275 | onClick={handleSubmit} 276 | disabled={isSubmitting || !pathInput.trim()} 277 | className={cn( 278 | BUTTON_STYLES.primary, 279 | isSubmitting || !pathInput.trim() 280 | ? "opacity-50 cursor-not-allowed" 281 | : "" 282 | )} 283 | > 284 | {isSubmitting ? ( 285 | <> 286 | <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> 287 | Updating... 288 | </> 289 | ) : ( 290 | 'Update Path' 291 | )} 292 | </button> 293 | </div> 294 | </motion.div> 295 | </motion.div> 296 | </AnimatePresence> 297 | ); 298 | }; 299 | 300 | export default PathConfigModal; 301 | ```