This is page 2 of 2. Use http://codebase.md/hatrigt/hana-mcp-server?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/EnvironmentManager.jsx: -------------------------------------------------------------------------------- ```javascript import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { GradientButton } from './ui'; import { cn } from '../utils/cn'; const EnvironmentManager = ({ isOpen, onClose, onSave }) => { const [environments, setEnvironments] = useState([ { id: 'development', name: 'Development', color: 'blue', required: false }, { id: 'staging', name: 'Staging', color: 'amber', required: false }, { id: 'production', name: 'Production', color: 'green', required: false } ]); const [newEnvironmentName, setNewEnvironmentName] = useState(''); const [selectedColor, setSelectedColor] = useState('purple'); useEffect(() => { if (!isOpen) return; const onKeyDown = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [isOpen, onClose]); const colorOptions = [ { id: 'blue', name: 'Blue', class: 'bg-blue-500' }, { id: 'green', name: 'Green', class: 'bg-green-500' }, { id: 'amber', name: 'Amber', class: 'bg-amber-500' }, { id: 'purple', name: 'Purple', class: 'bg-purple-500' }, { id: 'indigo', name: 'Indigo', class: 'bg-indigo-500' }, { id: 'red', name: 'Red', class: 'bg-red-500' }, { id: 'pink', name: 'Pink', class: 'bg-pink-500' }, { id: 'teal', name: 'Teal', class: 'bg-teal-500' } ]; useEffect(() => { // Load existing environments from localStorage or API const savedEnvironments = localStorage.getItem('hana-environments'); if (savedEnvironments) { setEnvironments(JSON.parse(savedEnvironments)); } }, []); const addEnvironment = () => { if (!newEnvironmentName.trim()) return; const newEnv = { id: newEnvironmentName.toLowerCase().replace(/\s+/g, '-'), name: newEnvironmentName, color: selectedColor, required: false }; const updatedEnvironments = [...environments, newEnv]; setEnvironments(updatedEnvironments); setNewEnvironmentName(''); setSelectedColor('purple'); }; const removeEnvironment = (envId) => { setEnvironments(environments.filter(env => env.id !== envId)); }; const handleSave = () => { // Save to localStorage (in real app, this would be an API call) localStorage.setItem('hana-environments', JSON.stringify(environments)); onSave(environments); onClose(); }; if (!isOpen) return null; return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-2xl max-h-[80vh] overflow-y-auto" > <div className="flex items-center justify-between mb-6"> <h2 className="text-xl font-semibold text-gray-900">Manage Environments</h2> <button onClick={onClose} className="text-gray-400 hover:text-gray-600" > <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> {/* Current Environments */} <div className="mb-6"> <h3 className="text-lg font-medium text-gray-900 mb-3">Current Environments</h3> <div className="space-y-2"> {environments.map((env) => ( <div key={env.id} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg" > <div className="flex items-center space-x-3"> <div className={cn('w-4 h-4 rounded-full', `bg-${env.color}-500`)} /> <span className="font-medium text-gray-900">{env.name}</span> <span className="text-sm text-gray-500">({env.id})</span> </div> <button onClick={() => removeEnvironment(env.id)} className="text-red-600 hover:text-red-800 text-sm" > Remove </button> </div> ))} </div> </div> {/* Add New Environment */} <div className="mb-6"> <h3 className="text-lg font-medium text-gray-900 mb-3">Add New Environment</h3> <div className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> Environment Name </label> <input type="text" value={newEnvironmentName} onChange={(e) => setNewEnvironmentName(e.target.value)} placeholder="e.g., Pre-Production, QA, Testing" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-2"> Color </label> <div className="flex flex-wrap gap-2"> {colorOptions.map((color) => ( <button key={color.id} onClick={() => setSelectedColor(color.id)} className={cn( 'w-8 h-8 rounded-full border-2 transition-all', color.class, selectedColor === color.id ? 'border-gray-800 scale-110' : 'border-gray-300 hover:scale-105' )} title={color.name} /> ))} </div> </div> <GradientButton onClick={addEnvironment} disabled={!newEnvironmentName.trim()} className="w-full" > Add Environment </GradientButton> </div> </div> {/* Note */} <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6"> <div className="flex"> <svg className="w-5 h-5 text-blue-600 mr-2 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> <div> <p className="text-sm text-blue-800"> <strong>Note:</strong> Environments are optional for databases. You can configure any combination of environments for each database based on your needs. </p> </div> </div> </div> {/* Actions */} <div className="flex justify-end space-x-3"> <GradientButton variant="secondary" onClick={onClose}> Cancel </GradientButton> <GradientButton onClick={handleSave}> Save Changes </GradientButton> </div> </motion.div> </div> ); }; export default EnvironmentManager; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ClaudeDesktopView.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { motion } from 'framer-motion'; import ClaudeConfigTile from './ClaudeConfigTile'; import BackupHistoryModal from './BackupHistoryModal'; import { cn } from '../utils/cn'; import { ArchiveBoxIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; const ClaudeDesktopView = ({ claudeConfigPath, claudeServers, activeEnvironments, onSetupPath, onRemoveConnection, onViewConnection, onRefresh, onConfigPathChange }) => { const activeConnections = claudeServers.length; const [isRefreshing, setIsRefreshing] = useState(false); const [showBackupHistory, setShowBackupHistory] = useState(false); const handleRefresh = async () => { setIsRefreshing(true); try { await onRefresh(); } finally { setIsRefreshing(false); } }; return ( <div className="p-6 space-y-6 bg-gray-100 rounded-2xl sm:rounded-3xl"> <div className="flex items-center justify-between"> <div> <h1 className="text-2xl font-bold text-gray-900 mb-2">Claude Desktop Integration</h1> <p className="text-gray-600"> Manage your HANA database connections available in Claude Desktop </p> </div> <div className="flex items-center gap-3"> <button onClick={() => setShowBackupHistory(true)} 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" title="Manage configuration backups" > <ArchiveBoxIcon className="w-4 h-4 mr-2" /> Backups </button> <button onClick={handleRefresh} disabled={isRefreshing} className={cn( "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", isRefreshing ? "text-gray-400 cursor-not-allowed" : "text-gray-700 hover:bg-gray-200 hover:border-gray-300" )} title="Refresh configuration from Claude Desktop" > <ArrowPathIcon className={cn( "w-4 h-4 mr-2", isRefreshing && "animate-spin" )} /> {isRefreshing ? 'Refreshing...' : 'Refresh'} </button> </div> </div> {/* Configuration Status */} <ClaudeConfigTile claudeConfigPath={claudeConfigPath} claudeServers={claudeServers} onSetupPath={onSetupPath} onConfigPathChange={onConfigPathChange} /> {/* Active Database Connections */} <div className="bg-white rounded-xl border border-gray-200 p-6"> <div className="flex items-center justify-between mb-6"> <h2 className="text-lg font-semibold text-gray-900">Active Database Connections</h2> <div className="flex items-center"> <div className={cn( 'w-2 h-2 rounded-full mr-2', activeConnections > 0 ? 'bg-green-500' : 'bg-gray-300' )} /> <span className="text-sm text-gray-600"> {activeConnections} {activeConnections === 1 ? 'connection' : 'connections'} active </span> </div> </div> {activeConnections > 0 ? ( <div className="overflow-x-auto max-h-96 overflow-y-auto claude-table-scrollbar"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-100"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Database </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Environment </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> Actions </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {claudeServers.map((server) => ( <tr key={server.name} className="hover:bg-gray-50 cursor-pointer transition-colors" onClick={() => onViewConnection(server)} > <td className="px-6 py-4 whitespace-nowrap"> <div className="flex items-center"> <div className="h-8 w-8 bg-blue-50 rounded-full flex items-center justify-center"> <svg className="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> </div> <div className="ml-4"> <div className="text-sm font-medium text-gray-900">{server.name}</div> <div className="text-sm text-gray-500">{server.env.HANA_HOST}</div> </div> </div> </td> <td className="px-6 py-4 whitespace-nowrap"> <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> <span className="w-1.5 h-1.5 bg-green-600 rounded-full mr-1.5"></span> {server.env?.ENVIRONMENT || 'Development'} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <button onClick={(e) => { e.stopPropagation(); onViewConnection(server); }} className="text-blue-600 hover:text-blue-900 mr-3" > View </button> <button onClick={(e) => { e.stopPropagation(); onRemoveConnection(server.name); }} className="text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 px-2 py-1 rounded transition-colors" > Remove </button> </td> </tr> ))} </tbody> </table> </div> ) : ( <div className="text-center py-12"> <div className="mx-auto w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mb-4"> <svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> </div> <h3 className="text-lg font-medium text-gray-900 mb-2">No active connections</h3> <p className="text-gray-600 mb-4"> You haven't added any HANA databases to Claude Desktop yet </p> <button onClick={onSetupPath} 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" > <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> Setup Claude Desktop </button> </div> )} </div> {/* Backup History Modal */} <BackupHistoryModal isOpen={showBackupHistory} onClose={() => setShowBackupHistory(false)} /> </div> ); }; export default ClaudeDesktopView; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/SearchAndFilter.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { cn } from '../utils/cn'; const SearchAndFilter = ({ searchQuery, onSearchChange, filters, onFilterChange, onClearFilters, placeholder = "Search databases..." }) => { const [isFilterOpen, setIsFilterOpen] = useState(false); const [sortBy, setSortBy] = useState('name'); const [sortOrder, setSortOrder] = useState('asc'); const filterOptions = [ { id: 'all', label: 'All Databases', count: filters.total || 0 }, { id: 'active', label: 'Active in Claude', count: filters.activeInClaude || 0, highlight: true }, { id: 'production', label: 'Production', count: filters.production || 0 }, { id: 'development', label: 'Development', count: filters.development || 0 }, { id: 'staging', label: 'Staging', count: filters.staging || 0 } ]; const sortOptions = [ { id: 'name', label: 'Name' }, { id: 'created', label: 'Date Created' }, { id: 'modified', label: 'Last Modified' }, { id: 'environments', label: 'Environment Count' } ]; const handleSortChange = (newSortBy) => { if (sortBy === newSortBy) { setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); } else { setSortBy(newSortBy); setSortOrder('asc'); } // Call parent callback if provided if (onFilterChange) { onFilterChange({ sortBy: newSortBy, sortOrder: sortOrder === 'asc' ? 'desc' : 'asc' }); } }; return ( <div className="p-4"> <div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4"> {/* Search Input */} <div className="flex-1 relative"> <div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none"> <svg className="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> </svg> </div> <input type="text" value={searchQuery} onChange={(e) => onSearchChange(e.target.value)} placeholder={placeholder} 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" /> {searchQuery && ( <button onClick={() => onSearchChange('')} className="absolute inset-y-0 right-0 pr-4 flex items-center group" > <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"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> )} </div> <div className="flex items-center gap-4"> {/* Claude Integration Status */} {filters.activeInClaude > 0 && ( <div className="flex items-center space-x-2 px-3 py-2 bg-green-50 border border-green-200 rounded-lg"> <svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <span className="text-sm font-medium text-green-700"> {filters.activeInClaude} Claude </span> </div> )} {/* Filter Toggle */} <button onClick={() => setIsFilterOpen(!isFilterOpen)} className={cn( 'flex items-center px-4 py-2 border rounded-lg transition-colors', isFilterOpen ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-gray-100 bg-white text-gray-700 hover:bg-gray-50' )} > <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> Filters <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"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> {/* Sort Dropdown */} <div className="relative"> <select value={`${sortBy}-${sortOrder}`} onChange={(e) => { const [newSortBy, newSortOrder] = e.target.value.split('-'); setSortBy(newSortBy); setSortOrder(newSortOrder); if (onFilterChange) { onFilterChange({ sortBy: newSortBy, sortOrder: newSortOrder }); } }} 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" > {sortOptions.map(option => ( <option key={`${option.id}-asc`} value={`${option.id}-asc`}>{option.label} (A-Z)</option> ))} {sortOptions.map(option => ( <option key={`${option.id}-desc`} value={`${option.id}-desc`}>{option.label} (Z-A)</option> ))} </select> <div className="absolute inset-y-0 right-0 flex items-center px-3 pointer-events-none"> <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </div> </div> </div> </div> {/* Filter Panel */} <AnimatePresence> {isFilterOpen && ( <motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} className="overflow-hidden" > <div className="pt-4 mt-4 border-t border-gray-200"> <div className="flex items-center justify-between mb-3"> <h3 className="text-sm font-medium text-gray-900">Filter by Status</h3> <button onClick={onClearFilters} className="text-sm text-blue-600 hover:text-blue-800" > Clear all </button> </div> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3"> {filterOptions.map(option => ( <button key={option.id} onClick={() => onFilterChange && onFilterChange({ status: option.id })} className={cn( 'flex items-center justify-between p-3 rounded-lg border transition-colors text-left', filters.activeFilter === option.id ? option.highlight ? 'border-green-300 bg-green-50 text-green-700' : 'border-blue-300 bg-blue-50 text-blue-700' : option.highlight ? 'border-green-200 bg-white text-green-700 hover:bg-green-50' : 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50' )} > <span className="text-sm font-medium flex items-center space-x-1"> {option.highlight && ( <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> )} <span>{option.label}</span> </span> <span className={cn( "text-xs px-2 py-0.5 rounded-full", option.highlight ? filters.activeFilter === option.id ? 'bg-green-200 text-green-700' : 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-600' )}> {option.count} </span> </button> ))} </div> </div> </motion.div> )} </AnimatePresence> </div> ); }; export default SearchAndFilter; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/layout/VerticalSidebar.jsx: -------------------------------------------------------------------------------- ```javascript import { useState } from 'react'; import { motion } from 'framer-motion'; import { cn } from '../../utils/cn'; const VerticalSidebar = ({ activeView, onViewChange, databaseCount, activeConnections, claudeConfigured }) => { const [collapsed, setCollapsed] = useState(false); const navigationItems = [ { id: 'dashboard', label: 'Dashboard', icon: ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ), description: 'Overview & insights' }, { id: 'databases', label: 'My Local Databases', icon: ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ), description: 'Manage configurations', count: databaseCount, hasSubmenu: true }, { id: 'claude', label: 'Claude Desktop', icon: ( <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ), description: 'Integration status', count: activeConnections, status: claudeConfigured ? 'online' : 'offline' } ]; return ( <motion.div className={cn( 'bg-white border border-gray-200 flex flex-col h-full rounded-xl shadow-lg overflow-hidden', collapsed ? 'w-12 sm:w-16' : 'w-56 sm:w-64' )} initial={false} animate={{ width: collapsed ? 64 : 256, transition: { type: "spring", stiffness: 300, damping: 30, mass: 0.8 } }} transition={{ type: "spring", stiffness: 300, damping: 30, mass: 0.8 }} > {/* Header */} <div className="p-3 sm:p-4 border-b border-gray-200 rounded-t-xl"> <div className="flex items-center justify-between"> <motion.div className="flex items-center space-x-3" initial={false} animate={{ opacity: collapsed ? 0 : 1, x: collapsed ? -20 : 0, transition: { duration: 0.2, delay: collapsed ? 0 : 0.1 } }} style={{ display: collapsed ? 'none' : 'flex' }} > <img src="/logo.png" alt="HANA MCP Logo" className="w-8 h-8 flex-shrink-0" /> <div> <h2 className="text-lg font-semibold text-gray-900">HANA MCP</h2> <p className="text-xs text-gray-500">Database Manager</p> </div> </motion.div> <motion.div className="flex flex-col items-center w-full space-y-2" initial={false} animate={{ opacity: collapsed ? 1 : 0, scale: collapsed ? 1 : 0.8, transition: { duration: 0.2, delay: collapsed ? 0.1 : 0 } }} style={{ display: collapsed ? 'flex' : 'none' }} > <img src="/logo.png" alt="HANA MCP Logo" className="w-6 h-6" /> <motion.button onClick={() => setCollapsed(!collapsed)} className="p-1 rounded-lg hover:bg-gray-100 transition-colors" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} title="Expand sidebar" > <motion.svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" animate={{ rotate: collapsed ? 180 : 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" /> </motion.svg> </motion.button> </motion.div> <motion.button onClick={() => setCollapsed(!collapsed)} className={cn( "p-1.5 rounded-lg hover:bg-gray-100 transition-colors", collapsed && "hidden" )} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > <motion.svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" animate={{ rotate: collapsed ? 180 : 0 }} transition={{ duration: 0.3, ease: "easeInOut" }} > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /> </motion.svg> </motion.button> </div> </div> {/* Navigation */} <nav className="flex-1 p-1 sm:p-2"> <ul className="space-y-1"> {navigationItems.map((item) => ( <li key={item.id}> <button onClick={() => onViewChange(item.id)} className={cn( 'w-full flex items-center p-3 rounded-lg text-left transition-all duration-200', 'hover:bg-gray-50 group', activeView === item.id ? 'bg-blue-50 text-blue-700 border border-blue-200' : 'text-gray-700 hover:text-gray-900' )} > <div className={cn( 'flex-shrink-0', activeView === item.id ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-600' )}> {item.icon} </div> <motion.div className="ml-3 flex-1 min-w-0" initial={false} animate={{ opacity: collapsed ? 0 : 1, x: collapsed ? -10 : 0, transition: { duration: 0.2, delay: collapsed ? 0 : 0.1 } }} style={{ display: collapsed ? 'none' : 'block' }} > <div className="flex items-center justify-between"> <span className="text-sm font-medium truncate"> {item.label} </span> {item.count !== undefined && item.count > 0 && ( <motion.span className={cn( 'ml-2 px-2 py-0.5 text-xs font-medium rounded-full', activeView === item.id ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600' )} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2 }} > {item.count} </motion.span> )} {item.status && ( <motion.div className={cn( 'ml-2 w-2 h-2 rounded-full', item.status === 'online' ? 'bg-green-500' : 'bg-gray-300' )} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2 }} /> )} </div> <p className="text-xs text-gray-500 truncate"> {item.description} </p> </motion.div> </button> </li> ))} </ul> </nav> {/* Quick Actions */} <motion.div className="p-3 sm:p-4 border-t border-gray-200 rounded-b-xl" initial={false} animate={{ opacity: collapsed ? 0 : 1, y: collapsed ? 20 : 0, transition: { duration: 0.2, delay: collapsed ? 0 : 0.15 } }} style={{ display: collapsed ? 'none' : 'block' }} > <motion.button onClick={() => onViewChange('add-database')} 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" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> Add Database </motion.button> </motion.div> </motion.div> ); }; export default VerticalSidebar; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/DashboardView.jsx: -------------------------------------------------------------------------------- ```javascript import { useState, useEffect } from 'react'; import { motion } from 'framer-motion'; import { MetricCard } from './ui'; import { cn } from '../utils/cn'; import EnvironmentManager from './EnvironmentManager'; const DashboardView = ({ hanaServers, claudeServers, activeEnvironments, onQuickAction }) => { const [showEnvironmentManager, setShowEnvironmentManager] = useState(false); // Calculate insights const totalDatabases = Object.keys(hanaServers).length; const activeConnections = claudeServers.length; const totalEnvironments = Object.values(hanaServers).reduce((total, server) => { return total + Object.keys(server.environments || {}).length; }, 0); const environmentBreakdown = Object.values(hanaServers).reduce((breakdown, server) => { Object.keys(server.environments || {}).forEach(env => { breakdown[env] = (breakdown[env] || 0) + 1; }); return breakdown; }, {}); // Calculate real connection status const connectionStatus = activeConnections > 0 ? 'Connected' : 'Not Connected'; const configuredDatabases = Object.keys(hanaServers).filter(key => Object.keys(hanaServers[key].environments || {}).length > 0 ).length; const quickActions = [ { id: 'add-database', title: 'Add New Database', description: 'Configure a new HANA database connection', icon: ( <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> ), color: 'blue', enabled: true }, { id: 'manage-databases', title: 'Manage Databases', description: 'View and configure your database connections', icon: ( <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ), color: 'green', enabled: totalDatabases > 0 }, { id: 'claude-integration', title: 'Claude Integration', description: 'Manage Claude Desktop integration', icon: ( <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> ), color: 'purple', enabled: true } ].filter(action => action.enabled); const getStatusIcon = (status) => { switch (status) { case 'success': return <div className="w-2 h-2 bg-green-500 rounded-full" />; case 'warning': return <div className="w-2 h-2 bg-yellow-500 rounded-full" />; case 'error': return <div className="w-2 h-2 bg-red-500 rounded-full" />; default: return <div className="w-2 h-2 bg-blue-500 rounded-full" />; } }; return ( <div className="p-4 space-y-4 bg-gray-100 rounded-2xl sm:rounded-3xl"> {/* Welcome Header */} <div className="mb-4"> <h1 className="text-xl font-bold text-gray-900 mb-1">Dashboard</h1> </div> {/* Key Metrics */} <div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <MetricCard title="Total Databases" value={totalDatabases} /> <MetricCard title="Active Connections" value={activeConnections} /> <MetricCard title="Total Environments" value={totalEnvironments} /> <MetricCard title="Configured Databases" value={configuredDatabases} /> </div> {/* Quick Actions */} <div className="bg-white rounded-xl border border-gray-200 p-4"> <h2 className="text-lg font-semibold text-gray-900 mb-3">Quick Actions</h2> <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> {quickActions.map((action) => ( <button key={action.id} onClick={() => onQuickAction(action.id)} 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" > <div className="w-10 h-10 rounded-lg flex items-center justify-center mb-2 transition-colors text-gray-900 group-hover:text-[#86a0ff]"> {action.icon} </div> <h3 className="font-semibold text-gray-900 mb-1 text-sm">{action.title}</h3> <p className="text-xs text-gray-900 text-center">{action.description}</p> </button> ))} </div> </div> {/* System Status */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-4"> <div className="bg-white rounded-xl border border-gray-200 p-4"> <h2 className="text-lg font-semibold text-gray-900 mb-3">System Status</h2> <div className="space-y-2"> <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> <div className="flex items-center"> <div className={cn( 'w-3 h-3 rounded-full mr-3', totalDatabases > 0 ? 'bg-green-500' : 'bg-gray-400' )} /> <span className="text-sm font-medium text-gray-900">Database Connections</span> </div> <span className="text-sm text-gray-600"> {totalDatabases > 0 ? `${totalDatabases} configured` : 'No databases'} </span> </div> <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> <div className="flex items-center"> <div className={cn( 'w-3 h-3 rounded-full mr-3', activeConnections > 0 ? 'bg-green-500' : 'bg-gray-400' )} /> <span className="text-sm font-medium text-gray-900">Claude Integration</span> </div> <span className="text-sm text-gray-600"> {activeConnections > 0 ? `${activeConnections} active` : 'Not connected'} </span> </div> <div className="flex items-center justify-between p-2 bg-gray-100 rounded-lg"> <div className="flex items-center"> <div className={cn( 'w-3 h-3 rounded-full mr-3', totalEnvironments > 0 ? 'bg-green-500' : 'bg-gray-400' )} /> <span className="text-sm font-medium text-gray-900">Environment Setup</span> </div> <span className="text-sm text-gray-600"> {totalEnvironments > 0 ? `${totalEnvironments} environments` : 'No environments'} </span> </div> </div> </div> {/* Environment Breakdown */} <div className="bg-white rounded-xl border border-gray-200 p-4"> <div className="flex items-center justify-between mb-3"> <h2 className="text-lg font-semibold text-gray-900">Environment Distribution</h2> <button onClick={() => setShowEnvironmentManager(true)} className="text-sm text-blue-600 hover:text-blue-800 flex items-center" > <svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> </svg> Manage Environments </button> </div> <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3"> {Object.entries(environmentBreakdown).map(([env, count]) => ( <div key={env} className="text-center p-3 bg-gray-100 rounded-lg"> <div className="text-xl font-bold text-gray-900">{count}</div> <div className="text-xs text-gray-600">{env}</div> </div> ))} {Object.keys(environmentBreakdown).length === 0 && ( <div className="col-span-full text-center text-gray-500 py-6"> <svg className="w-10 h-10 mx-auto text-gray-300 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> <p className="text-sm">No environments configured yet</p> <button onClick={() => setShowEnvironmentManager(true)} className="mt-2 text-blue-600 hover:text-blue-800 text-xs" > Click to add environments </button> </div> )} </div> </div> </div> {/* Claude Integration Status */} <div className="bg-white rounded-xl border border-gray-200 p-4"> <div className="flex items-center justify-between mb-3"> <h2 className="text-lg font-semibold text-gray-900">Claude Desktop Integration</h2> <div className="flex items-center"> <div className={cn( 'w-2 h-2 rounded-full mr-2', activeConnections > 0 ? 'bg-green-500' : 'bg-gray-300' )} /> <span className="text-sm text-gray-600"> {activeConnections > 0 ? 'Connected' : 'Disconnected'} </span> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="p-3 bg-green-50 rounded-lg"> <div className="text-base font-semibold text-green-900">{activeConnections}</div> <div className="text-xs text-green-700">Active Connections</div> </div> <div className="p-3 bg-blue-50 rounded-lg"> <div className="text-base font-semibold text-blue-900">{Math.max(0, totalDatabases - activeConnections)}</div> <div className="text-xs text-blue-700">Available to Add</div> </div> </div> </div> {/* Environment Manager Modal */} <EnvironmentManager isOpen={showEnvironmentManager} onClose={() => setShowEnvironmentManager(false)} onSave={(environments) => { // This would update the environments in the app state // In a real app, you'd update the global state here }} /> </div> ); }; export default DashboardView; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/index.css: -------------------------------------------------------------------------------- ```css @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); @tailwind base; @tailwind components; @tailwind utilities; /* Professional Light Theme Base Styles */ @layer base { * { @apply border-gray-200; } html { @apply scroll-smooth overflow-hidden; } body { @apply bg-gray-100 text-gray-900 font-sans; @apply h-screen overflow-hidden; font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Enhanced Typography System */ h1, .h1 { @apply text-4xl font-bold text-gray-900 leading-tight tracking-tight; } h2, .h2 { @apply text-3xl font-semibold text-gray-800 leading-tight tracking-tight; } h3, .h3 { @apply text-2xl font-semibold text-gray-800 leading-snug tracking-tight; } h4, .h4 { @apply text-xl font-medium text-gray-700 leading-snug; } h5, .h5 { @apply text-lg font-medium text-gray-700 leading-normal; } h6, .h6 { @apply text-base font-medium text-gray-700 leading-normal; } /* Body text styles */ p, .body-text { @apply text-base text-gray-600 leading-relaxed; } .body-text-sm { @apply text-sm text-gray-600 leading-relaxed; } .body-text-lg { @apply text-lg text-gray-600 leading-relaxed; } /* Special text styles */ .display-text { @apply text-5xl font-black text-gray-900 leading-none tracking-tight; } .hero-text { @apply text-6xl font-black text-gray-900 leading-none tracking-tight; } .caption-text { @apply text-sm font-medium text-gray-500 leading-tight; } .label-text { @apply text-sm font-semibold text-gray-700 uppercase tracking-wide; } /* Link styles */ a { @apply text-blue-600 hover:text-blue-700 transition-colors duration-200; } /* Custom scrollbar */ ::-webkit-scrollbar { @apply w-2; } ::-webkit-scrollbar-track { @apply bg-gray-100; } ::-webkit-scrollbar-thumb { @apply bg-gray-300 rounded-full; } ::-webkit-scrollbar-thumb:hover { @apply bg-gray-400; } /* Enhanced scrollbar for modal content */ .modal-scrollbar::-webkit-scrollbar { @apply w-3; } .modal-scrollbar::-webkit-scrollbar-track { @apply bg-gray-100 rounded-lg; } .modal-scrollbar::-webkit-scrollbar-thumb { @apply bg-gray-300 rounded-lg; } .modal-scrollbar::-webkit-scrollbar-thumb:hover { @apply bg-gray-500; } .modal-scrollbar::-webkit-scrollbar-corner { @apply bg-transparent; } } @layer components { /* Professional Light Card Base */ .glass-card { @apply bg-white rounded-2xl border border-gray-200; @apply shadow-sm shadow-gray-100/50 transition-all duration-300; @apply hover:shadow-md hover:shadow-gray-200/60 hover:-translate-y-0.5; } .glass-card-primary { @apply bg-blue-50 border-blue-200/60; } .glass-card-success { @apply bg-emerald-50 border-emerald-200/60; } .glass-card-warning { @apply bg-amber-50 border-amber-200/60; } .glass-card-danger { @apply bg-red-50 border-red-200/60; } /* Professional Buttons - Matching Image Theme */ .btn-gradient { @apply relative inline-flex items-center justify-center gap-2; @apply rounded-lg font-semibold px-6 py-2.5; @apply transition-all duration-200 overflow-hidden; @apply hover:-translate-y-0.5 active:translate-y-0 active:scale-95; @apply disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none; } .btn-primary { @apply bg-[#86a0ff] text-white; @apply shadow-sm hover:shadow-md hover:bg-[#7990e6]; @apply hover:shadow-[#86a0ff]/20; } .btn-secondary { @apply bg-gray-100 border border-gray-200 text-gray-700; @apply shadow-sm hover:shadow-md hover:bg-gray-200; @apply hover:border-gray-300; } .btn-success { @apply bg-emerald-600 text-white; @apply shadow-sm hover:shadow-md hover:bg-emerald-700; @apply hover:shadow-emerald-200/60; } .btn-danger { @apply bg-red-600 text-white; @apply shadow-sm hover:shadow-md hover:bg-red-700; @apply hover:shadow-red-200/60; } .btn-warning { @apply bg-amber-500 text-white; @apply shadow-sm hover:shadow-md hover:bg-amber-600; @apply hover:shadow-amber-200/60; } .btn-light { @apply bg-gray-100 border border-gray-200 text-gray-700; @apply shadow-sm hover:shadow-md hover:bg-gray-200; @apply hover:border-gray-300; } /* Form Elements */ .form-input { @apply w-full px-4 py-3 bg-white border border-gray-300; @apply rounded-lg text-gray-900 placeholder-gray-500; @apply transition-all duration-200 font-medium; @apply focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20; @apply focus:outline-none; } .form-select { @apply form-input; @apply appearance-none bg-no-repeat bg-right bg-[length:16px]; @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")]; } .form-checkbox { @apply w-4 h-4 text-blue-600 bg-white border-gray-300; @apply rounded focus:ring-blue-500/20 focus:ring-2; } /* Status Badges - Matching Image Colors */ .status-badge { @apply inline-flex items-center gap-2 px-3 py-1 rounded-full; @apply text-sm font-semibold border; } .status-online { @apply bg-emerald-100 text-emerald-800 border-emerald-200; } .status-offline { @apply bg-gray-100 text-gray-700 border-gray-200; } .status-warning { @apply bg-amber-100 text-amber-800 border-amber-200; } .status-error { @apply bg-red-100 text-red-800 border-red-200; } .status-pending { @apply bg-amber-100 text-amber-800 border-amber-200; } .status-done { @apply bg-blue-100 text-blue-800 border-blue-200; } /* Environment Badges */ .env-production { @apply bg-red-100 border-red-300 text-red-800; } .env-development { @apply bg-blue-100 border-blue-300 text-blue-800; } .env-staging { @apply bg-amber-100 border-amber-300 text-amber-800; } /* Dashboard-specific typography classes */ .metric-value { @apply text-3xl font-bold text-gray-900 leading-none tracking-tight; } .metric-label { @apply text-sm font-semibold text-gray-600 uppercase tracking-wide; } .metric-description { @apply text-sm font-medium text-gray-500 leading-relaxed; } .card-title { @apply text-lg font-semibold text-gray-800 leading-tight; } .card-subtitle { @apply text-sm font-medium text-gray-600 leading-relaxed; } .table-header { @apply text-sm font-semibold text-gray-700 uppercase tracking-wide; } .table-cell { @apply text-sm font-medium text-gray-600 leading-relaxed; } } @layer utilities { .text-gradient { @apply bg-gradient-to-r from-[#86a0ff] to-[#7990e6] bg-clip-text text-transparent; } .border-gradient { @apply bg-gradient-to-r from-blue-200/50 to-blue-300/50; @apply border border-transparent bg-clip-padding; } /* Animation utilities */ .animate-in { @apply animate-fade-in; } .animate-up { @apply animate-slide-up; } .animate-glow { @apply animate-pulse-glow; } .animate-float { @apply animate-float; } /* Typography utilities */ .font-thin { font-weight: 100; } .font-extralight { font-weight: 200; } .font-light { font-weight: 300; } .font-normal { font-weight: 400; } .font-medium { font-weight: 500; } .font-semibold { font-weight: 600; } .font-bold { font-weight: 700; } .font-extrabold { font-weight: 800; } .font-black { font-weight: 900; } /* Text truncation utilities */ .line-clamp-1 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1; } .line-clamp-2 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } .line-clamp-3 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; } } /* Custom background patterns */ .bg-dots { background-image: radial-gradient(circle, rgba(99, 102, 241, 0.1) 1px, transparent 1px); background-size: 20px 20px; } /* Glass window specific styles */ .glass-window { backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); background: rgba(255, 255, 255, 0.8); border: 1px solid rgba(255, 255, 255, 0.2); } /* Perfect centering for glass window */ .glass-window-container { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; } .glass-window-content { width: 100%; max-width: 110rem; /* 8xl - maximum width */ height: calc(100vh - 2rem); margin: 0 auto; box-sizing: border-box; } /* Ensure content is contained within the glass window */ .glass-window-content * { box-sizing: border-box; } /* Prevent content overflow */ .glass-window-content > div { max-width: 100%; overflow: visible; } /* Custom scrollbar for database list */ .database-list-scrollbar { scrollbar-width: thin; scrollbar-color: rgba(156, 163, 175, 0.5) transparent; } .database-list-scrollbar::-webkit-scrollbar { width: 6px; } .database-list-scrollbar::-webkit-scrollbar-track { background: transparent; } .database-list-scrollbar::-webkit-scrollbar-thumb { background: rgba(156, 163, 175, 0.5); border-radius: 3px; } .database-list-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(156, 163, 175, 0.7); } /* Custom scrollbar for Claude table */ .claude-table-scrollbar { scrollbar-width: thin; scrollbar-color: rgba(156, 163, 175, 0.5) transparent; } .claude-table-scrollbar::-webkit-scrollbar { width: 6px; } .claude-table-scrollbar::-webkit-scrollbar-track { background: transparent; } .claude-table-scrollbar::-webkit-scrollbar-thumb { background: rgba(156, 163, 175, 0.5); border-radius: 3px; } .claude-table-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(156, 163, 175, 0.7); } .bg-grid { background-image: linear-gradient(rgba(99, 102, 241, 0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(99, 102, 241, 0.05) 1px, transparent 1px); background-size: 20px 20px; } /* Loading spinner */ @keyframes spin { to { transform: rotate(360deg); } } .loading-spinner { @apply inline-block w-6 h-6 border-2 border-gray-200 border-t-blue-600 rounded-full; animation: spin 1s linear infinite; } /* Shine effect for buttons */ .btn-shine::before { content: ''; @apply absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent; @apply opacity-0 transition-all duration-500 -skew-x-12; transform: translateX(-100%); } .btn-shine:hover::before { @apply opacity-100; transform: translateX(100%); } ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ConnectionDetailsModal.jsx: -------------------------------------------------------------------------------- ```javascript import { useEffect } from 'react'; import { motion } from 'framer-motion'; import { EnvironmentBadge } from './ui/StatusBadge'; import { DatabaseTypeBadge } from './ui'; import { detectDatabaseType, getDatabaseTypeDisplayName } from '../utils/databaseTypes'; const ConnectionDetailsModal = ({ isOpen, onClose, connection }) => { useEffect(() => { if (!isOpen) return; const onKeyDown = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [isOpen, onClose]); if (!isOpen || !connection) return null; // Detect database type from connection data const databaseType = detectDatabaseType(connection.env || {}); return ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <motion.div initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} className="bg-white rounded-xl shadow-2xl p-6 w-full max-w-4xl max-h-[90vh] overflow-hidden" > <div className="flex items-center justify-between mb-4"> <div className="flex items-center space-x-3"> <div className="h-10 w-10 bg-blue-50 rounded-full flex items-center justify-center"> <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"> <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" /> </svg> </div> <div> <h2 className="text-xl font-semibold text-gray-900">{connection.name}</h2> <p className="text-sm text-gray-500">Database Connection Details</p> </div> </div> <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors" > <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> </div> {/* Connection Status */} <div className="mb-4"> <div className="flex items-center justify-between p-4 bg-green-50 rounded-lg border border-green-200"> <div className="flex items-center space-x-3"> <div className="w-3 h-3 bg-green-500 rounded-full"></div> <div> <p className="text-sm font-medium text-green-800">Connected to Claude Desktop</p> <p className="text-xs text-green-600">Active and available for use</p> </div> </div> <EnvironmentBadge environment={connection.env?.ENVIRONMENT || connection.environment || 'Development'} active={true} size="sm" /> </div> </div> {/* Database Type Information */} <div className="mb-4"> <div className="flex items-center justify-between p-4 bg-blue-50 rounded-lg border border-blue-200"> <div className="flex items-center space-x-3"> <DatabaseTypeBadge type={databaseType} size="md" /> <div> <p className="text-sm font-medium text-blue-800">Database Type</p> <p className="text-xs text-blue-600">{getDatabaseTypeDisplayName(databaseType)}</p> </div> </div> </div> </div> {/* Connection Configuration */} <div className="space-y-4"> {/* Basic Connection Settings */} <div> <h3 className="text-lg font-medium text-gray-900 mb-3">Connection Configuration</h3> <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Host</label> <p className="text-sm text-gray-900 font-mono break-all"> {connection.env?.HANA_HOST || 'Not configured'} </p> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Port</label> <p className="text-sm text-gray-900 font-mono"> {connection.env?.HANA_PORT || '443'} </p> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">User</label> <p className="text-sm text-gray-900 font-mono break-all"> {connection.env?.HANA_USER || 'Not set'} </p> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Schema</label> <p className="text-sm text-gray-900 font-mono break-all"> {connection.env?.HANA_SCHEMA || 'Not set'} </p> </div> </div> {/* MDC-specific fields - show conditionally */} {(databaseType === 'mdc_tenant' || databaseType === 'mdc_system') && ( <div className="mt-4"> <h4 className="text-md font-medium text-gray-800 mb-3">MDC Configuration</h4> <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> {databaseType === 'mdc_tenant' && connection.env?.HANA_DATABASE_NAME && ( <div className="p-3 bg-amber-50 rounded-lg border border-amber-200"> <label className="block text-sm font-medium text-amber-800 mb-1">Database Name</label> <p className="text-sm text-amber-900 font-mono break-all"> {connection.env.HANA_DATABASE_NAME} </p> </div> )} {connection.env?.HANA_INSTANCE_NUMBER && ( <div className="p-3 bg-amber-50 rounded-lg border border-amber-200"> <label className="block text-sm font-medium text-amber-800 mb-1">Instance Number</label> <p className="text-sm text-amber-900 font-mono"> {connection.env.HANA_INSTANCE_NUMBER} </p> </div> )} </div> </div> )} </div> {/* Security & SSL Configuration */} <div> <h3 className="text-lg font-medium text-gray-900 mb-3">Security & SSL Configuration</h3> <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">SSL Enabled</label> <div className="flex items-center space-x-2"> <div className={`w-3 h-3 rounded-full ${ connection.env?.HANA_SSL === 'true' ? 'bg-green-500' : 'bg-red-500' }`}></div> <p className="text-sm text-gray-900"> {connection.env?.HANA_SSL === 'true' ? 'Enabled' : 'Disabled'} </p> </div> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Encryption</label> <div className="flex items-center space-x-2"> <div className={`w-3 h-3 rounded-full ${ connection.env?.HANA_ENCRYPT === 'true' ? 'bg-green-500' : 'bg-red-500' }`}></div> <p className="text-sm text-gray-900"> {connection.env?.HANA_ENCRYPT === 'true' ? 'Enabled' : 'Disabled'} </p> </div> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Certificate Validation</label> <div className="flex items-center space-x-2"> <div className={`w-3 h-3 rounded-full ${ connection.env?.HANA_VALIDATE_CERT === 'true' ? 'bg-green-500' : 'bg-yellow-500' }`}></div> <p className="text-sm text-gray-900"> {connection.env?.HANA_VALIDATE_CERT === 'true' ? 'Enabled' : 'Disabled'} </p> </div> </div> </div> </div> {/* Logging Configuration */} <div> <h3 className="text-lg font-medium text-gray-900 mb-3">Logging Configuration</h3> <div className="grid grid-cols-1 md:grid-cols-3 gap-3"> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Log Level</label> <p className="text-sm text-gray-900 font-semibold uppercase"> {connection.env?.LOG_LEVEL || 'info'} </p> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">File Logging</label> <div className="flex items-center space-x-2"> <div className={`w-3 h-3 rounded-full ${ connection.env?.ENABLE_FILE_LOGGING === 'true' ? 'bg-green-500' : 'bg-red-500' }`}></div> <p className="text-sm text-gray-900"> {connection.env?.ENABLE_FILE_LOGGING === 'true' ? 'Enabled' : 'Disabled'} </p> </div> </div> <div className="p-3 bg-gray-50 rounded-lg"> <label className="block text-sm font-medium text-gray-700 mb-1">Console Logging</label> <div className="flex items-center space-x-2"> <div className={`w-3 h-3 rounded-full ${ connection.env?.ENABLE_CONSOLE_LOGGING === 'true' ? 'bg-green-500' : 'bg-red-500' }`}></div> <p className="text-sm text-gray-900"> {connection.env?.ENABLE_CONSOLE_LOGGING === 'true' ? 'Enabled' : 'Disabled'} </p> </div> </div> </div> </div> </div> {/* Action Buttons */} <div className="flex justify-end space-x-3 mt-4 pt-4 border-t border-gray-200"> <button onClick={onClose} 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" > Close </button> </div> </motion.div> </div> ); }; export default ConnectionDetailsModal; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/DatabaseListView.jsx: -------------------------------------------------------------------------------- ```javascript import { useState, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import SearchAndFilter from './SearchAndFilter'; import EnhancedServerCard from './EnhancedServerCard'; import { cn } from '../utils/cn'; const DatabaseListView = ({ hanaServers, claudeServers, activeEnvironments, onEditServer, onAddToClaudeServer, onDeleteServer, onAddDatabase }) => { const [searchQuery, setSearchQuery] = useState(''); const [selectedDatabase, setSelectedDatabase] = useState(null); const [filters, setFilters] = useState({ status: 'all', sortBy: 'name', sortOrder: 'asc' }); // Selection handlers const handleDatabaseSelect = (databaseName) => { setSelectedDatabase(databaseName); }; const handleEditSelected = () => { if (selectedDatabase && hanaServers[selectedDatabase]) { onEditServer(hanaServers[selectedDatabase]); } }; const handleAddToClaudeSelected = () => { if (selectedDatabase) { onAddToClaudeServer(selectedDatabase); } }; const handleDeleteSelected = () => { if (selectedDatabase) { onDeleteServer(selectedDatabase); setSelectedDatabase(null); } }; // Calculate filter counts const filterCounts = useMemo(() => { const servers = Object.entries(hanaServers); const activeInClaude = Object.keys(activeEnvironments).length; return { total: servers.length, active: servers.filter(([name]) => claudeServers.some(cs => cs.name === name)).length, activeInClaude: activeInClaude, production: servers.filter(([, server]) => server.environments?.Production).length, development: servers.filter(([, server]) => server.environments?.Development).length, staging: servers.filter(([, server]) => server.environments?.Staging).length, activeFilter: filters.status }; }, [hanaServers, claudeServers, filters.status, activeEnvironments]); // Filter and sort servers const filteredServers = useMemo(() => { let filtered = Object.entries(hanaServers); // Apply search filter if (searchQuery) { filtered = filtered.filter(([name, server]) => name.toLowerCase().includes(searchQuery.toLowerCase()) || server.description?.toLowerCase().includes(searchQuery.toLowerCase()) ); } // Apply status filter if (filters.status !== 'all') { switch (filters.status) { case 'active': filtered = filtered.filter(([name]) => claudeServers.some(cs => cs.name === name) ); break; case 'production': case 'development': case 'staging': filtered = filtered.filter(([, server]) => server.environments?.[filters.status.charAt(0).toUpperCase() + filters.status.slice(1)] ); break; } } // Apply sorting filtered.sort(([nameA, serverA], [nameB, serverB]) => { let valueA, valueB; switch (filters.sortBy) { case 'name': valueA = nameA.toLowerCase(); valueB = nameB.toLowerCase(); break; case 'created': valueA = new Date(serverA.created || 0); valueB = new Date(serverB.created || 0); break; case 'modified': valueA = new Date(serverA.modified || 0); valueB = new Date(serverB.modified || 0); break; case 'environments': valueA = Object.keys(serverA.environments || {}).length; valueB = Object.keys(serverB.environments || {}).length; break; default: valueA = nameA.toLowerCase(); valueB = nameB.toLowerCase(); } if (filters.sortOrder === 'desc') { [valueA, valueB] = [valueB, valueA]; } if (valueA < valueB) return -1; if (valueA > valueB) return 1; return 0; }); return filtered; }, [hanaServers, claudeServers, searchQuery, filters]); const handleFilterChange = (newFilters) => { setFilters(prev => ({ ...prev, ...newFilters })); }; const handleClearFilters = () => { setFilters({ status: 'all', sortBy: 'name', sortOrder: 'asc' }); setSearchQuery(''); }; return ( <div className="p-6 space-y-6 bg-gray-100 rounded-2xl sm:rounded-3xl overflow-y-auto max-h-full database-list-scrollbar"> {/* Header */} <div className="flex items-center justify-between"> <div> <h1 className="text-2xl font-bold text-gray-900 mb-2">My Local Databases</h1> <p className="text-gray-600"> Manage your HANA database configurations </p> {filterCounts.activeInClaude > 0 && ( <div className="flex items-center space-x-2 mt-2"> <div className="flex items-center space-x-1 text-green-600 bg-green-50 px-3 py-1 rounded-full"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <span className="text-sm font-medium"> {filterCounts.activeInClaude} environment{filterCounts.activeInClaude !== 1 ? 's' : ''} connected to Claude </span> </div> </div> )} </div> <button onClick={onAddDatabase} 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" > <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> Add Database </button> </div> {/* Search and Filter Bar */} <div className="bg-white rounded-xl border border-gray-100 p-6"> <SearchAndFilter searchQuery={searchQuery} onSearchChange={setSearchQuery} filters={filters} onFiltersChange={setFilters} filterCounts={filterCounts} /> </div> {/* Top Bar with Actions */} <div className="bg-white rounded-xl border border-gray-200 p-6"> <div className="flex items-center justify-between"> <div className="flex items-center space-x-4"> <span className="text-sm font-medium text-gray-700"> {filteredServers.length} of {Object.keys(hanaServers).length} databases </span> </div> <div className="flex items-center space-x-3"> <button onClick={handleEditSelected} disabled={!selectedDatabase} 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" > Edit </button> <button onClick={handleAddToClaudeSelected} disabled={!selectedDatabase} 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" > Add to Claude </button> <button onClick={handleDeleteSelected} disabled={!selectedDatabase} 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" > Delete </button> </div> </div> </div> {/* Database Table */} <div className="bg-white rounded-xl border border-gray-200 overflow-hidden"> {filteredServers.length === 0 ? ( <div className="text-center py-12"> <svg className="w-16 h-16 mx-auto text-gray-300 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> </svg> <h3 className="text-lg font-medium text-gray-900 mb-2">No databases found</h3> <p className="text-gray-600 mb-4"> {searchQuery ? `No databases match "${searchQuery}"` : 'Get started by adding your first database'} </p> <button onClick={onAddDatabase} 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" > <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" /> </svg> Add Your First Database </button> </div> ) : ( <> {/* Table Header */} <div className="bg-gray-50 px-6 py-3 border-b border-gray-200"> <div className="grid grid-cols-12 gap-4 items-center"> <div className="col-span-1"> {/* Empty header for radio button column */} </div> <div className="col-span-4"> <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Database</h3> </div> <div className="col-span-2"> <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Active Environment</h3> </div> <div className="col-span-2"> <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Environments</h3> </div> <div className="col-span-3"> <h3 className="text-sm font-semibold text-gray-700 uppercase tracking-wide">Description</h3> </div> </div> </div> {/* Database List */} <div className="divide-y divide-gray-200"> <AnimatePresence> {filteredServers.map(([name, server], index) => ( <motion.div key={name} initial={{ opacity: 0, x: -10 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -10 }} transition={{ duration: 0.2, delay: index * 0.02 }} > <EnhancedServerCard name={name} server={server} index={index} isSelected={selectedDatabase === name} activeEnvironment={activeEnvironments[name]} onSelect={handleDatabaseSelect} onEdit={() => onEditServer(server)} onAddToClaude={() => onAddToClaudeServer(name)} onDelete={() => onDeleteServer(name)} /> </motion.div> ))} </AnimatePresence> </div> </> )} </div> </div> ); }; export default DatabaseListView; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/PathConfigModal.jsx: -------------------------------------------------------------------------------- ```javascript import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { XMarkIcon } from '@heroicons/react/24/outline'; import { cn } from '../utils/cn'; // Reusable styling constants (following the same pattern as BackupHistoryModal) const BUTTON_STYLES = { 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", secondary: "px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" }; const MODAL_ANIMATIONS = { backdrop: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }, modal: { initial: { scale: 0.95, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.95, opacity: 0 } } }; // Default configuration paths for different operating systems const DEFAULT_PATHS = { windows: [ '%APPDATA%\\Claude\\claude_desktop_config.json', '%APPDATA%\\Claude\\desktop\\claude_desktop_config.json', '%LOCALAPPDATA%\\Claude\\claude_desktop_config.json', 'C:\\Users\\%USERNAME%\\AppData\\Roaming\\Claude\\claude_desktop_config.json', 'C:\\Users\\%USERNAME%\\AppData\\Local\\Claude\\claude_desktop_config.json' ], mac: [ '~/Library/Application Support/Claude/claude_desktop_config.json', '~/Library/Application Support/Claude/desktop/claude_desktop_config.json', '/Users/$USER/Library/Application Support/Claude/claude_desktop_config.json', '/Users/$USER/Library/Application Support/Claude/desktop/claude_desktop_config.json', '/Users/$USER/.config/claude/claude_desktop_config.json' ], linux: [ '~/.config/claude/claude_desktop_config.json', '/home/$USER/.config/claude/claude_desktop_config.json', '/home/$USER/.local/share/claude/claude_desktop_config.json' ] }; const PathConfigModal = ({ isOpen, onClose, onConfigPathChange, currentPath = '' }) => { const [pathInput, setPathInput] = useState(currentPath); const [isSubmitting, setIsSubmitting] = useState(false); const [detectedOS, setDetectedOS] = useState('mac'); // Detect OS useEffect(() => { const userAgent = navigator.userAgent; let os = 'mac'; if (userAgent.includes('Windows')) os = 'windows'; else if (userAgent.includes('Linux')) os = 'linux'; setDetectedOS(os); }, []); // Reset form when modal opens/closes useEffect(() => { if (isOpen) { setPathInput(currentPath); setIsSubmitting(false); } }, [isOpen, currentPath]); // Handle escape key useEffect(() => { if (!isOpen) return; const onKeyDown = (e) => { if (e.key === 'Escape') { onClose(); } }; window.addEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown); }, [isOpen, onClose]); const selectPath = (path) => { // Replace environment variables with actual values for better user experience let resolvedPath = path; if (detectedOS === 'mac' || detectedOS === 'linux') { // For Mac/Linux, replace $USER with actual username if we can detect it // Try to get username from common sources let username = 'YourUsername'; // Try to get username from localStorage if previously set const savedUsername = localStorage.getItem('claude_username'); if (savedUsername) { username = savedUsername; } else { // Try to extract username from common patterns if (detectedOS === 'mac') { // For Mac, try to get username from common locations username = 'YourUsername'; } else if (detectedOS === 'linux') { username = 'YourUsername'; } } resolvedPath = path.replace(/\$USER/g, username); } setPathInput(resolvedPath); }; const handleSubmit = async () => { if (!pathInput.trim()) { alert('Please select or enter a configuration path'); return; } setIsSubmitting(true); try { if (onConfigPathChange) { await onConfigPathChange(pathInput.trim()); } onClose(); } catch (error) { console.error('Error updating config path:', error); alert('Failed to update configuration path. Please try again.'); } finally { setIsSubmitting(false); } }; if (!isOpen) return null; return ( <AnimatePresence> <motion.div {...MODAL_ANIMATIONS.backdrop} className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" onClick={onClose} > <motion.div {...MODAL_ANIMATIONS.modal} onClick={(e) => e.stopPropagation()} className="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col" > {/* Header */} <div className="px-6 py-3 border-b border-gray-200"> <div className="flex items-center justify-between"> <div> <h2 className="text-xl font-semibold text-gray-900">Configure Claude Desktop Path</h2> <p className="text-sm text-gray-600">Select or enter the path to your Claude Desktop configuration file</p> </div> <button onClick={onClose} className="text-gray-400 hover:text-gray-600 transition-colors" > <XMarkIcon className="w-5 h-5" /> </button> </div> </div> {/* Content */} <div className="p-6 space-y-4 flex-1 overflow-y-auto"> {/* Path Input */} <div className="space-y-3"> <div className="space-y-2"> <label htmlFor="pathInput" className="text-sm font-medium text-gray-700"> Configuration Path </label> <input type="text" id="pathInput" value={pathInput} onChange={(e) => setPathInput(e.target.value)} placeholder="Select a path below or enter custom path" 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" /> <p className="text-xs text-gray-500"> The selected path will be used to locate your Claude Desktop configuration </p> </div> {/* Selectable Path Locations */} <div className="space-y-2"> <h4 className="text-sm font-medium text-gray-700"> 📁 Common Claude Desktop Config Locations for {detectedOS === 'windows' ? 'Windows' : detectedOS === 'mac' ? 'macOS' : 'Linux'}: </h4> <div className="grid gap-2"> {DEFAULT_PATHS[detectedOS].map((path, index) => ( <div key={index} className={cn( "border rounded-lg p-3 transition-all duration-200", pathInput === path ? "border-blue-500 bg-blue-50" : "border-gray-200 bg-white hover:border-blue-300 hover:bg-blue-50" )} > <div className="flex items-center justify-between"> <div className="flex-1"> <code className="text-sm font-mono text-gray-700 break-all"> {path} </code> </div> <button onClick={() => selectPath(path)} className={cn( "ml-3 px-3 py-1.5 text-xs font-medium rounded-md transition-colors", pathInput === path ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-700 hover:bg-blue-100 hover:text-blue-700" )} > {pathInput === path ? 'Selected' : 'Select'} </button> </div> </div> ))} </div> <p className="text-xs text-gray-500"> 💡 Click "Select" next to any path above to choose it, then click "Update Path" below to save </p> </div> </div> {/* Help Section */} <div className="p-3 bg-blue-50 border border-blue-200 rounded-lg"> <h4 className="text-sm font-medium text-blue-900 mb-1.5">💡 How to find your config file:</h4> <div className="text-sm text-blue-700 space-y-0.5"> {detectedOS === 'windows' ? ( <> <p>• <strong>Windows:</strong> Check these locations:</p> <ul className="ml-4 space-y-0.5"> <li>• <code className="bg-blue-100 px-1 rounded">%APPDATA%\\Claude\\</code> (usually C:\Users\YourUsername\AppData\Roaming\Claude\)</li> <li>• <code className="bg-blue-100 px-1 rounded">%LOCALAPPDATA%\\Claude\\</code> (usually C:\Users\YourUsername\AppData\Local\Claude\)</li> <li>• <code className="bg-blue-100 px-1 rounded">C:\\Users\\YourUsername\\AppData\\Roaming\\Claude\\</code></li> </ul> </> ) : detectedOS === 'mac' ? ( <> <p>• <strong>macOS:</strong> Check these locations:</p> <ul className="ml-4 space-y-1"> <li>• <code className="bg-blue-100 px-1 rounded">~/Library/Application Support/Claude/</code></li> <li>• <code className="bg-blue-100 px-1 rounded">/Users/YourUsername/Library/Application Support/Claude/</code></li> </ul> </> ) : ( <> <p>• <strong>Linux:</strong> Check these locations:</p> <ul className="ml-4 space-y-1"> <li>• <code className="bg-blue-100 px-1 rounded">~/.config/claude/</code></li> <li>• <code className="bg-blue-100 px-1 rounded">/home/YourUsername/.config/claude/</code></li> </ul> </> )} <p className="mt-2">• Look for a file named <code className="bg-blue-100 px-1 rounded">claude_desktop_config.json</code></p> </div> </div> </div> {/* Footer */} <div className="flex items-center justify-end gap-3 p-4 border-t border-gray-200"> <button onClick={onClose} className={BUTTON_STYLES.secondary} > Cancel </button> <button onClick={handleSubmit} disabled={isSubmitting || !pathInput.trim()} className={cn( BUTTON_STYLES.primary, isSubmitting || !pathInput.trim() ? "opacity-50 cursor-not-allowed" : "" )} > {isSubmitting ? ( <> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> Updating... </> ) : ( 'Update Path' )} </button> </div> </motion.div> </motion.div> </AnimatePresence> ); }; export default PathConfigModal; ``` -------------------------------------------------------------------------------- /docs/hana_mcp_architecture.svg: -------------------------------------------------------------------------------- ``` <svg viewBox="0 0 1400 700" xmlns="http://www.w3.org/2000/svg"> <!-- Clean background --> <rect width="1400" height="700" fill="#ffffff"/> <!-- Title --> <text x="700" y="40" text-anchor="middle" font-family="Arial, sans-serif" font-size="28" font-weight="bold" fill="#1a1a1a"> HANA MCP Server Architecture </text> <text x="700" y="65" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#666666"> Enterprise AI-Database Integration Platform </text> <!-- Client Applications Layer --> <rect x="80" y="100" width="220" height="130" rx="8" fill="#f8f9fa" stroke="#dee2e6" stroke-width="2"/> <text x="190" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#212529"> MCP Clients </text> <rect x="100" y="145" width="180" height="65" rx="4" fill="#ffffff" stroke="#e9ecef" stroke-width="1"/> <text x="190" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> • Claude Desktop </text> <text x="190" y="185" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> • VSCode Extensions </text> <text x="190" y="205" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#495057"> • Custom AI Applications </text> <!-- MCP Protocol Bridge --> <rect x="380" y="140" width="140" height="50" rx="25" fill="#e9ecef" stroke="#ced4da" stroke-width="1"/> <text x="450" y="160" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#495057"> MCP Protocol </text> <text x="450" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d"> JSON-RPC </text> <!-- HANA MCP Server Core --> <rect x="600" y="80" width="260" height="170" rx="8" fill="#343a40" stroke="#495057" stroke-width="2"/> <text x="730" y="110" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#ffffff"> HANA MCP Server </text> <text x="730" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#f8f9fa"> Enterprise Database Gateway </text> <!-- Server components --> <rect x="620" y="145" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> <text x="670" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Connection</text> <text x="670" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Manager</text> <rect x="740" y="145" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> <text x="790" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Schema</text> <text x="790" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Inspector</text> <rect x="620" y="200" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> <text x="670" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Query</text> <text x="670" y="230" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Engine</text> <rect x="740" y="200" width="100" height="40" rx="4" fill="#495057" stroke="#6c757d" stroke-width="1"/> <text x="790" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#ffffff">Tool</text> <text x="790" y="230" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#e9ecef">Handler</text> <!-- SAP HANA Database --> <rect x="940" y="80" width="220" height="170" rx="8" fill="#212529" stroke="#343a40" stroke-width="2"/> <text x="1050" y="110" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="#ffffff"> SAP HANA </text> <text x="1050" y="130" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#f8f9fa"> Enterprise Database </text> <!-- Database features --> <rect x="960" y="145" width="180" height="85" rx="4" fill="#343a40" stroke="#495057" stroke-width="1"/> <text x="1050" y="165" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#ffffff">In-Memory Processing</text> <text x="1050" y="185" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Columnar Store Engine</text> <text x="1050" y="200" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Real-time Analytics</text> <text x="1050" y="215" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#e9ecef">• Enterprise Security</text> <!-- Available MCP Tools Section --> <text x="700" y="310" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="bold" fill="#212529"> Available MCP Tools </text> <!-- Connection Tools --> <rect x="80" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="90" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> <text x="180" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Connection Tools</text> <text x="90" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_test_connection</text> <text x="90" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_show_config</text> <text x="90" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_show_env_vars</text> <text x="90" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Database connectivity & setup</text> <!-- Schema Tools --> <rect x="300" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="310" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> <text x="400" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Schema Tools</text> <text x="310" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_schemas</text> <text x="310" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_tables</text> <text x="310" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_describe_table</text> <text x="310" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Metadata & structure discovery</text> <!-- Index Tools --> <rect x="520" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="530" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> <text x="620" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Index Tools</text> <text x="530" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_list_indexes</text> <text x="530" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_describe_index</text> <text x="530" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Performance optimization</text> <text x="530" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Index management & analysis</text> <!-- Query Tools --> <rect x="740" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="750" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> <text x="840" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Query Tools</text> <text x="750" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• hana_execute_query</text> <text x="750" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Parameterized queries</text> <text x="750" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• Custom SQL execution</text> <text x="750" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Data retrieval & analysis</text> <!-- Browser Control Tools --> <rect x="960" y="340" width="200" height="110" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="970" y="350" width="180" height="25" rx="3" fill="#f8f9fa"/> <text x="1060" y="367" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529">Browser Control</text> <text x="970" y="385" font-family="Arial, sans-serif" font-size="10" fill="#495057">• open_url</text> <text x="970" y="400" font-family="Arial, sans-serif" font-size="10" fill="#495057">• get_page_content</text> <text x="970" y="415" font-family="Arial, sans-serif" font-size="10" fill="#495057">• execute_javascript</text> <text x="970" y="435" font-family="Arial, sans-serif" font-size="9" fill="#6c757d">Web automation & integration</text> <!-- Configuration and Benefits Section --> <!-- Configuration --> <rect x="180" y="500" width="350" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="190" y="510" width="330" height="25" rx="3" fill="#f8f9fa"/> <text x="355" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> Configuration & Security </text> <text x="200" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Environment Variables (HANA_HOST, HANA_USER, etc.)</text> <text x="200" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• SSL/TLS Encryption & Certificate Validation</text> <text x="200" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Connection Pooling & Resource Management</text> <!-- Key Benefits --> <rect x="580" y="500" width="350" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="590" y="510" width="330" height="25" rx="3" fill="#f8f9fa"/> <text x="755" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> Enterprise Benefits </text> <text x="600" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Seamless AI-Database Integration</text> <text x="600" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Enterprise-Grade Security & Compliance</text> <text x="600" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Real-time Analytics & Decision Support</text> <!-- Performance Metrics --> <rect x="980" y="500" width="240" height="120" rx="6" fill="#ffffff" stroke="#dee2e6" stroke-width="1"/> <rect x="990" y="510" width="220" height="25" rx="3" fill="#f8f9fa"/> <text x="1100" y="527" text-anchor="middle" font-family="Arial, sans-serif" font-size="15" font-weight="bold" fill="#212529"> Performance </text> <text x="1000" y="550" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Sub-second Query Response</text> <text x="1000" y="570" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Concurrent User Support</text> <text x="1000" y="590" font-family="Arial, sans-serif" font-size="11" fill="#495057">• Optimized Memory Usage</text> <!-- Simple Data Flow Arrows --> <polygon points="300,165 380,165 375,160 375,170" fill="#6c757d"/> <polygon points="520,165 600,165 595,160 595,170" fill="#6c757d"/> <polygon points="860,165 940,165 935,160 935,170" fill="#6c757d"/> <!-- Flow labels --> <text x="340" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">JSON-RPC</text> <text x="560" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Tool Calls</text> <text x="900" y="150" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">SQL Queries</text> <!-- Footer --> <text x="700" y="660" text-anchor="middle" font-family="Arial, sans-serif" font-size="13" font-weight="bold" fill="#212529"> Enterprise AI-Database Integration Platform </text> <text x="700" y="680" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6c757d"> Enabling secure, scalable AI-powered database interactions through standardized protocols </text> </svg> ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/MainApp.jsx: -------------------------------------------------------------------------------- ```javascript import { useState, useEffect } from 'react'; import axios from 'axios'; import toast from 'react-hot-toast'; import { motion, AnimatePresence } from 'framer-motion'; // Import components import VerticalSidebar from './layout/VerticalSidebar'; import DashboardView from './DashboardView'; import DatabaseListView from './DatabaseListView'; import ClaudeConfigTile from './ClaudeConfigTile' import ClaudeDesktopView from './ClaudeDesktopView' import ConnectionDetailsModal from './ConnectionDetailsModal'; // Import existing components import ConfigurationModal from './ConfigurationModal'; import EnvironmentSelector from './EnvironmentSelector'; import PathSetupModal from './PathSetupModal'; import { LoadingOverlay, GlassWindow } from './ui'; const API_BASE = 'http://localhost:3001/api'; const MainApp = () => { // State management const [activeView, setActiveView] = useState('dashboard'); const [hanaServers, setHanaServers] = useState({}); const [claudeServers, setClaudeServers] = useState([]); const [claudeConfigPath, setClaudeConfigPath] = useState(null); const [activeEnvironments, setActiveEnvironments] = useState({}); // UI State const [isConfigModalOpen, setIsConfigModalOpen] = useState(false); const [isPathSetupOpen, setIsPathSetupOpen] = useState(false) const [isConnectionDetailsOpen, setIsConnectionDetailsOpen] = useState(false) const [selectedConnection, setSelectedConnection] = useState(null); const [isEnvironmentSelectorOpen, setIsEnvironmentSelectorOpen] = useState(false); const [selectedServer, setSelectedServer] = useState(null); const [deploymentTarget, setDeploymentTarget] = useState(null); const [activeTab, setActiveTab] = useState('Production'); const [isLoading, setIsLoading] = useState(false); // Form data for multi-environment configuration const [formData, setFormData] = useState({ name: '', description: '', environments: { Production: { HANA_HOST: '', HANA_PORT: '443', HANA_USER: '', HANA_PASSWORD: '', HANA_SCHEMA: '', HANA_SSL: 'true', HANA_ENCRYPT: 'true', HANA_VALIDATE_CERT: 'true', LOG_LEVEL: 'info', ENABLE_FILE_LOGGING: 'true', ENABLE_CONSOLE_LOGGING: 'false' }, Development: { HANA_HOST: '', HANA_PORT: '443', HANA_USER: '', HANA_PASSWORD: '', HANA_SCHEMA: '', HANA_SSL: 'true', HANA_ENCRYPT: 'true', HANA_VALIDATE_CERT: 'false', LOG_LEVEL: 'debug', ENABLE_FILE_LOGGING: 'true', ENABLE_CONSOLE_LOGGING: 'true' }, Staging: { HANA_HOST: '', HANA_PORT: '443', HANA_USER: '', HANA_PASSWORD: '', HANA_SCHEMA: '', HANA_SSL: 'true', HANA_ENCRYPT: 'true', HANA_VALIDATE_CERT: 'true', LOG_LEVEL: 'info', ENABLE_FILE_LOGGING: 'true', ENABLE_CONSOLE_LOGGING: 'false' } } }); const [pathInput, setPathInput] = useState(''); // Load data on component mount useEffect(() => { loadData(); }, []); // Auto-refresh Claude data when Claude tab is opened (only if not initial load) useEffect(() => { if (activeView === 'claude' && claudeServers.length > 0) { // Only refresh if we already have data (prevents race condition on first load) // Silent refresh (no toast notification) refreshClaudeData(false); } }, [activeView]); const loadData = async () => { try { setIsLoading(true); await Promise.all([ loadHanaServers(), loadClaudeServers(), loadClaudeConfigPath(), loadActiveEnvironments() ]); } catch (error) { console.error('Error loading data:', error); toast.error('Failed to load data'); } finally { setIsLoading(false); } }; const refreshClaudeData = async (showToast = true) => { try { await Promise.all([ loadClaudeServers(), loadActiveEnvironments() ]); if (showToast) { toast.success('Configuration refreshed'); } } catch (error) { console.error('Error refreshing Claude data:', error); if (showToast) { toast.error('Failed to refresh configuration'); } } }; const loadHanaServers = async () => { try { const response = await axios.get(`${API_BASE}/hana-servers`); setHanaServers(response.data); } catch (error) { console.error('Error loading HANA servers:', error); } }; const loadClaudeServers = async () => { try { const response = await axios.get(`${API_BASE}/claude`); setClaudeServers(response.data); } catch (error) { console.error('Error loading Claude servers:', error); } }; const loadClaudeConfigPath = async () => { try { const response = await axios.get(`${API_BASE}/claude/config-path`); setClaudeConfigPath(response.data.configPath); if (!response.data.configPath) { setPathInput(response.data.defaultPath || ''); setIsPathSetupOpen(true); } } catch (error) { console.error('Error loading Claude config path:', error); } }; const loadActiveEnvironments = async () => { try { const response = await axios.get(`${API_BASE}/claude/active-environments`); setActiveEnvironments(response.data); } catch (error) { console.error('Error loading active environments:', error); } }; // Form handlers const handleFormChange = (environment, field, value) => { setFormData(prev => ({ ...prev, environments: { ...prev.environments, [environment]: { ...(prev.environments[environment] || {}), [field]: value } } })); }; const handleServerInfoChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); }; // Handle environment-specific updates const handleEnvironmentUpdate = (environments) => { setFormData(prev => ({ ...prev, environments: environments })); }; // Navigation handlers const handleViewChange = (view) => { setActiveView(view); // Handle special actions if (view === 'add-database') { openConfigModal(); return; } }; const handleQuickAction = (actionId) => { switch (actionId) { case 'add-database': openConfigModal(); break; case 'manage-databases': setActiveView('databases'); break; case 'claude-integration': setActiveView('claude'); break; default: console.warn(`Unknown quick action: ${actionId}`); } }; const handleBulkAction = (action, selectedItems) => { switch (action) { case 'deploy': toast.success(`Adding ${selectedItems.length} database(s) to Claude`); break; case 'test': toast.success(`Testing connections for ${selectedItems.length} database(s)`); break; case 'export': toast.success(`Exporting ${selectedItems.length} database configuration(s)`); break; default: console.warn(`Unknown bulk action: ${action}`); } }; const handleConfigPathChange = async (newPath) => { try { // Update the config path via API await axios.post(`${API_BASE}/claude/config-path`, { configPath: newPath }); // Update local state setClaudeConfigPath(newPath); toast.success('Configuration path updated successfully'); // Refresh Claude data to reflect changes await refreshClaudeData(false); } catch (error) { console.error('Error updating config path:', error); toast.error('Failed to update configuration path'); } }; // Modal handlers const openConfigModal = (server = null) => { if (server) { setFormData(server); setSelectedServer(server); // Set active tab to first available environment when editing const envKeys = Object.keys(server.environments || {}); setActiveTab(envKeys.length > 0 ? envKeys[0] : null); } else { // Reset form for new server setFormData({ name: '', description: '', environments: {} }); setSelectedServer(null); setActiveTab(null); } setIsConfigModalOpen(true); }; const closeConfigModal = () => { setIsConfigModalOpen(false); setSelectedServer(null); setActiveTab(null); }; // Server operations const saveServer = async () => { try { setIsLoading(true); if (selectedServer) { await axios.put(`${API_BASE}/hana-servers/${selectedServer.name}`, formData); toast.success('Database updated successfully'); } else { await axios.post(`${API_BASE}/hana-servers`, formData); toast.success('Database created successfully'); } closeConfigModal(); await loadHanaServers(); } catch (error) { console.error('Error saving server:', error); toast.error(error.response?.data?.error || 'Failed to save database'); } finally { setIsLoading(false); } }; const deleteServer = async (serverName) => { try { setIsLoading(true); await axios.delete(`${API_BASE}/hana-servers/${serverName}`); toast.success('Database deleted successfully'); await loadHanaServers(); } catch (error) { console.error('Error deleting server:', error); toast.error('Failed to delete database'); } finally { setIsLoading(false); } }; // Claude operations const openEnvironmentSelector = (serverName) => { setDeploymentTarget(serverName); setIsEnvironmentSelectorOpen(true); }; const deployToClaude = async (environment) => { try { setIsLoading(true); await axios.post(`${API_BASE}/apply-to-claude`, { serverName: deploymentTarget, environment: environment }); toast.success(`Added ${deploymentTarget} (${environment}) to Claude Desktop configuration`); setIsEnvironmentSelectorOpen(false); setDeploymentTarget(null); await loadClaudeServers(); await loadActiveEnvironments(); } catch (error) { console.error('Error adding to Claude:', error); toast.error(error.response?.data?.error || 'Failed to add to Claude configuration'); } finally { setIsLoading(false); } }; const removeFromClaude = async (serverName) => { try { setIsLoading(true); await axios.delete(`${API_BASE}/claude/${encodeURIComponent(serverName)}`); toast.success(`Removed ${serverName} from Claude Desktop`); await loadClaudeServers(); await loadActiveEnvironments(); } catch (error) { console.error('Error removing from Claude:', error); toast.error('Failed to remove from Claude'); } finally { setIsLoading(false); } }; // Claude path operations const saveClaudePath = async () => { try { setIsLoading(true); await axios.post(`${API_BASE}/claude/config-path`, { configPath: pathInput }); setClaudeConfigPath(pathInput); setIsPathSetupOpen(false); toast.success('Claude config path saved successfully'); await loadClaudeServers(); } catch (error) { console.error('Error saving Claude path:', error); toast.error('Failed to save Claude config path'); } finally { setIsLoading(false); } }; // Render main content based on active view const renderMainContent = () => { switch (activeView) { case 'dashboard': return ( <DashboardView hanaServers={hanaServers} claudeServers={claudeServers} activeEnvironments={activeEnvironments} onQuickAction={handleQuickAction} /> ); case 'databases': return ( <DatabaseListView hanaServers={hanaServers} claudeServers={claudeServers} activeEnvironments={activeEnvironments} onEditServer={openConfigModal} onAddToClaudeServer={openEnvironmentSelector} onDeleteServer={deleteServer} onBulkAction={handleBulkAction} onAddDatabase={() => openConfigModal()} /> ); case 'claude': return ( <ClaudeDesktopView claudeConfigPath={claudeConfigPath} claudeServers={claudeServers} activeEnvironments={activeEnvironments} onSetupPath={() => setIsPathSetupOpen(true)} onRemoveConnection={removeFromClaude} onViewConnection={(connection) => { setSelectedConnection(connection); setIsConnectionDetailsOpen(true); }} onRefresh={refreshClaudeData} onConfigPathChange={handleConfigPathChange} /> ); default: return ( <DashboardView hanaServers={hanaServers} claudeServers={claudeServers} activeEnvironments={activeEnvironments} onQuickAction={handleQuickAction} /> ); } }; return ( <GlassWindow maxWidth="full" maxHeight="full"> <div className="flex h-full bg-transparent p-3 sm:p-4 overflow-hidden"> {/* Loading Overlay */} {isLoading && ( <LoadingOverlay message="Processing your request..." /> )} {/* Floating Sidebar */} <div className="flex-shrink-0 h-full"> <VerticalSidebar activeView={activeView} onViewChange={handleViewChange} databaseCount={Object.keys(hanaServers).length} activeConnections={claudeServers.length} claudeConfigured={!!claudeConfigPath} /> </div> {/* Main Content */} <div className="flex-1 flex flex-col overflow-hidden bg-white/50 backdrop-blur-sm rounded-r-2xl sm:rounded-r-3xl ml-3"> {/* Main Content Area */} <main className="flex-1 overflow-hidden p-4 sm:p-6 pb-8"> <AnimatePresence mode="wait"> <motion.div key={activeView} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} transition={{ duration: 0.2 }} className="h-full" > {renderMainContent()} </motion.div> </AnimatePresence> </main> </div> </div> {/* Modals */} {isConfigModalOpen && ( <ConfigurationModal isOpen={isConfigModalOpen} onClose={closeConfigModal} server={selectedServer} formData={formData} activeTab={activeTab} setActiveTab={setActiveTab} onFormChange={handleFormChange} onServerInfoChange={handleServerInfoChange} onSave={saveServer} isLoading={isLoading} /> )} {isEnvironmentSelectorOpen && deploymentTarget && ( <EnvironmentSelector isOpen={isEnvironmentSelectorOpen} onClose={() => { setIsEnvironmentSelectorOpen(false); setDeploymentTarget(null); }} serverName={deploymentTarget} environments={hanaServers[deploymentTarget]?.environments || {}} activeEnvironment={activeEnvironments[deploymentTarget]} onDeploy={deployToClaude} isLoading={isLoading} /> )} {isPathSetupOpen && ( <PathSetupModal isOpen={isPathSetupOpen} onClose={() => setIsPathSetupOpen(false)} pathInput={pathInput} setPathInput={setPathInput} onSave={saveClaudePath} isLoading={isLoading} /> )} <ConnectionDetailsModal isOpen={isConnectionDetailsOpen} onClose={() => { setIsConnectionDetailsOpen(false); setSelectedConnection(null); }} connection={selectedConnection} /> </GlassWindow> ); }; export default MainApp; ``` -------------------------------------------------------------------------------- /hana-mcp-ui/server/index.js: -------------------------------------------------------------------------------- ```javascript import express from 'express'; import cors from 'cors'; import fs from 'fs-extra'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { homedir } from 'os'; import { existsSync } from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const app = express(); const PORT = process.env.PORT || 3001; // Middleware app.use(cors()); app.use(express.json()); // Storage paths - ensure they work regardless of where the server is started from const UI_ROOT = dirname(__dirname); // This is the hana-mcp-ui directory const DATA_DIR = join(UI_ROOT, 'data'); const SERVERS_FILE = join(DATA_DIR, 'hana-servers.json'); const CONFIG_FILE = join(DATA_DIR, 'config.json'); const BACKUPS_DIR = join(DATA_DIR, 'backups'); const BACKUP_HISTORY_FILE = join(DATA_DIR, 'backup-history.json'); // Ensure data directory exists fs.ensureDirSync(DATA_DIR); fs.ensureDirSync(BACKUPS_DIR); // Default Claude config paths by OS const getDefaultClaudeConfigPath = () => { const platform = process.platform; switch (platform) { case 'darwin': return join(homedir(), 'Library/Application Support/Claude/claude_desktop_config.json'); case 'win32': return join(homedir(), 'AppData/Roaming/Claude/claude_desktop_config.json'); default: return join(homedir(), '.config/claude/claude_desktop_config.json'); } }; // Helper functions const loadServers = async () => { try { if (await fs.pathExists(SERVERS_FILE)) { return await fs.readJson(SERVERS_FILE); } return {}; } catch (error) { console.error('Error loading servers:', error); return {}; } }; const saveServers = async (servers) => { try { await fs.writeJson(SERVERS_FILE, servers, { spaces: 2 }); } catch (error) { console.error('Error saving servers:', error); throw error; } }; const loadConfig = async () => { try { if (await fs.pathExists(CONFIG_FILE)) { return await fs.readJson(CONFIG_FILE); } return {}; } catch (error) { console.error('Error loading config:', error); return {}; } }; const saveConfig = async (config) => { try { await fs.writeJson(CONFIG_FILE, config, { spaces: 2 }); } catch (error) { console.error('Error saving config:', error); throw error; } }; const loadClaudeConfig = async (configPath) => { try { if (await fs.pathExists(configPath)) { return await fs.readJson(configPath); } return { mcpServers: {} }; } catch (error) { console.error('Error loading Claude config:', error); return { mcpServers: {} }; } }; const saveClaudeConfig = async (configPath, config, skipBackup = false) => { try { // Create backup before saving (unless explicitly skipped) if (!skipBackup && await fs.pathExists(configPath)) { await createBackup(configPath, 'Auto backup before save'); } await fs.ensureDir(dirname(configPath)); await fs.writeJson(configPath, config, { spaces: 2 }); } catch (error) { console.error('Error saving Claude config:', error); throw error; } }; // Helper function to identify HANA MCP servers const isHanaMcpServer = (server) => { // Must have the correct command if (server.command !== 'hana-mcp-server') { return false; } // Must have HANA-specific environment variables for a complete HANA server if (!server.env) { return false; } const hasHanaHost = server.env.HANA_HOST; const hasHanaUser = server.env.HANA_USER; const hasHanaSchema = server.env.HANA_SCHEMA; // Must have all core HANA environment variables return hasHanaHost && hasHanaUser && hasHanaSchema; }; // Helper function to create composite server name const createCompositeServerName = (serverName, environment) => { return `${serverName} - ${environment}`; }; // Helper function to parse composite server name const parseCompositeServerName = (compositeName) => { const parts = compositeName.split(' - '); if (parts.length >= 2) { const environment = parts.pop(); // Last part is environment const serverName = parts.join(' - '); // Everything else is server name return { serverName, environment }; } return { serverName: compositeName, environment: null }; }; // Helper function to filter only HANA MCP servers from Claude config const filterHanaMcpServers = (mcpServers) => { const hanaServers = {}; for (const [name, server] of Object.entries(mcpServers || {})) { if (isHanaMcpServer(server)) { hanaServers[name] = server; } } return hanaServers; }; // Helper function to merge HANA servers while preserving non-HANA servers const mergeWithPreservation = (originalConfig, hanaServers) => { const newConfig = { ...originalConfig }; // Start with original mcpServers newConfig.mcpServers = { ...originalConfig.mcpServers }; // add new HANA servers newConfig.mcpServers = { ...newConfig.mcpServers, ...hanaServers }; return newConfig; }; // Backup management functions const loadBackupHistory = async () => { try { if (await fs.pathExists(BACKUP_HISTORY_FILE)) { return await fs.readJson(BACKUP_HISTORY_FILE); } return []; } catch (error) { console.error('Error loading backup history:', error); return []; } }; const saveBackupHistory = async (history) => { try { await fs.writeJson(BACKUP_HISTORY_FILE, history, { spaces: 2 }); } catch (error) { console.error('Error saving backup history:', error); throw error; } }; const createBackup = async (configPath, reason = 'Manual backup') => { try { if (!await fs.pathExists(configPath)) { throw new Error('Config file does not exist'); } const timestamp = new Date().toISOString(); const backupId = `backup_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`; const backupFileName = `${backupId}.json`; const backupFilePath = join(BACKUPS_DIR, backupFileName); // Read and backup the config const config = await fs.readJson(configPath); await fs.writeJson(backupFilePath, config, { spaces: 2 }); // Create backup metadata const backupEntry = { id: backupId, timestamp, fileName: backupFileName, reason, size: JSON.stringify(config).length, mcpServerCount: Object.keys(config.mcpServers || {}).length, hanaServerCount: Object.keys(filterHanaMcpServers(config.mcpServers || {})).length }; // Update history const history = await loadBackupHistory(); history.unshift(backupEntry); // Add to beginning (most recent first) // Keep only last 50 backups if (history.length > 50) { const oldBackups = history.splice(50); // Delete old backup files for (const oldBackup of oldBackups) { const oldBackupPath = join(BACKUPS_DIR, oldBackup.fileName); if (await fs.pathExists(oldBackupPath)) { await fs.remove(oldBackupPath); } } } await saveBackupHistory(history); return backupEntry; } catch (error) { console.error('Error creating backup:', error); throw error; } }; const restoreBackup = async (backupId, configPath) => { try { const history = await loadBackupHistory(); const backup = history.find(b => b.id === backupId); if (!backup) { throw new Error('Backup not found'); } const backupFilePath = join(BACKUPS_DIR, backup.fileName); if (!await fs.pathExists(backupFilePath)) { throw new Error('Backup file not found'); } // Create a backup of current state before restoring await createBackup(configPath, `Before restoring to ${backup.timestamp}`); // Restore the backup const backupConfig = await fs.readJson(backupFilePath); await saveClaudeConfig(configPath, backupConfig, true); // Skip backup when restoring return backup; } catch (error) { console.error('Error restoring backup:', error); throw error; } }; // API Routes // Get Claude Desktop config path app.get('/api/claude/config-path', async (req, res) => { try { const config = await loadConfig(); res.json({ configPath: config.claudeConfigPath || null, defaultPath: getDefaultClaudeConfigPath() }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Set Claude Desktop config path app.post('/api/claude/config-path', async (req, res) => { try { const { configPath } = req.body; if (!configPath) { return res.status(400).json({ error: 'Config path is required' }); } // Validate path exists or can be created const dir = dirname(configPath); await fs.ensureDir(dir); const config = await loadConfig(); config.claudeConfigPath = configPath; await saveConfig(config); res.json({ success: true, configPath }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get all local HANA servers app.get('/api/hana-servers', async (req, res) => { try { const servers = await loadServers(); res.json(servers); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create new HANA server app.post('/api/hana-servers', async (req, res) => { try { const serverConfig = req.body; if (!serverConfig.name) { return res.status(400).json({ error: 'Server name is required' }); } const servers = await loadServers(); if (servers[serverConfig.name]) { return res.status(409).json({ error: 'Server with this name already exists' }); } // Add metadata serverConfig.created = new Date().toISOString(); serverConfig.modified = new Date().toISOString(); serverConfig.version = '1.0.0'; servers[serverConfig.name] = serverConfig; await saveServers(servers); res.status(201).json(serverConfig); } catch (error) { res.status(500).json({ error: error.message }); } }); // Update HANA server app.put('/api/hana-servers/:name', async (req, res) => { try { const { name } = req.params; const updatedConfig = req.body; const servers = await loadServers(); if (!servers[name]) { return res.status(404).json({ error: 'Server not found' }); } // Preserve creation date, update modified date updatedConfig.created = servers[name].created; updatedConfig.modified = new Date().toISOString(); servers[name] = updatedConfig; await saveServers(servers); res.json(updatedConfig); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete HANA server app.delete('/api/hana-servers/:name', async (req, res) => { try { const { name } = req.params; const servers = await loadServers(); if (!servers[name]) { return res.status(404).json({ error: 'Server not found' }); } delete servers[name]; await saveServers(servers); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Apply server to Claude Desktop app.post('/api/apply-to-claude', async (req, res) => { try { const { serverName, environment } = req.body; if (!serverName || !environment) { return res.status(400).json({ error: 'Server name and environment are required' }); } const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const servers = await loadServers(); const server = servers[serverName]; if (!server) { return res.status(404).json({ error: 'Server not found' }); } // Find environment with case-insensitive matching let envConfig = server.environments?.[environment]; let actualEnvironmentName = environment; if (!envConfig) { // Try case-insensitive matching const envKeys = Object.keys(server.environments || {}); const matchingKey = envKeys.find(key => key.toLowerCase() === environment.toLowerCase()); if (matchingKey) { envConfig = server.environments[matchingKey]; actualEnvironmentName = matchingKey; } else { return res.status(404).json({ error: 'Environment not found' }); } } const claudeConfig = await loadClaudeConfig(claudeConfigPath); // create a new HANA server const newHanaServer = { [serverName]: { command: 'hana-mcp-server', env: envConfig } }; // Merge while preserving non-HANA servers const updatedConfig = mergeWithPreservation(claudeConfig, newHanaServer); await saveClaudeConfig(claudeConfigPath, updatedConfig); res.json({ success: true, serverName, environment: actualEnvironmentName }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Remove server from Claude Desktop app.delete('/api/claude/:serverName', async (req, res) => { try { const { serverName } = req.params; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const serverToDelete = claudeConfig.mcpServers[serverName]; if (!serverToDelete) { return res.status(404).json({ error: 'Server not found in Claude config' }); } // Only delete if it's a HANA MCP server if (!isHanaMcpServer(serverToDelete)) { return res.status(400).json({ error: 'Cannot delete non-HANA MCP server' }); } delete claudeConfig.mcpServers[serverName]; await saveClaudeConfig(claudeConfigPath, claudeConfig); res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get Claude Desktop servers app.get('/api/claude', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.json([]); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const claudeServers = []; // Filter only HANA MCP servers const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); for (const [serverName, server] of Object.entries(hanaServers)) { const serverData = { name: serverName, environment: server.env?.ENVIRONMENT || 'Development', env: server.env || {} }; claudeServers.push(serverData); } res.json(claudeServers); } catch (error) { console.error('Error loading Claude servers:', error); res.status(500).json({ error: error.message }); } }); // Get active environments app.get('/api/claude/active-environments', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.json({}); } const claudeConfig = await loadClaudeConfig(claudeConfigPath); const servers = await loadServers(); const activeEnvironments = {}; // Filter only HANA MCP servers const hanaServers = filterHanaMcpServers(claudeConfig.mcpServers); for (const [serverName, claudeServer] of Object.entries(hanaServers)) { if (servers[serverName]) { // Store the active environment for this server activeEnvironments[serverName] = claudeServer.env?.ENVIRONMENT || 'Development'; } } res.json(activeEnvironments); } catch (error) { res.status(500).json({ error: error.message }); } }); // Validate connection app.post('/api/validate-connection', async (req, res) => { try { const config = req.body; // Basic validation const required = ['HANA_HOST', 'HANA_USER', 'HANA_PASSWORD', 'HANA_SCHEMA']; for (const field of required) { if (!config[field]) { return res.status(400).json({ valid: false, error: `${field} is required` }); } } // For now, just validate required fields // In a real implementation, you could test the actual connection res.json({ valid: true }); } catch (error) { res.status(500).json({ valid: false, error: error.message }); } }); // Backup Management APIs // Get backup history app.get('/api/claude/backups', async (req, res) => { try { const history = await loadBackupHistory(); res.json(history); } catch (error) { res.status(500).json({ error: error.message }); } }); // Create manual backup app.post('/api/claude/backups', async (req, res) => { try { const { reason = 'Manual backup' } = req.body; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const backup = await createBackup(claudeConfigPath, reason); res.json(backup); } catch (error) { res.status(500).json({ error: error.message }); } }); // Restore backup app.post('/api/claude/backups/:backupId/restore', async (req, res) => { try { const { backupId } = req.params; const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; if (!claudeConfigPath) { return res.status(400).json({ error: 'Claude config path not set' }); } const backup = await restoreBackup(backupId, claudeConfigPath); res.json({ success: true, backup }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete backup app.delete('/api/claude/backups/:backupId', async (req, res) => { try { const { backupId } = req.params; const history = await loadBackupHistory(); const backupIndex = history.findIndex(b => b.id === backupId); if (backupIndex === -1) { return res.status(404).json({ error: 'Backup not found' }); } const backup = history[backupIndex]; const backupFilePath = join(BACKUPS_DIR, backup.fileName); // Remove from history history.splice(backupIndex, 1); await saveBackupHistory(history); // Delete backup file if (await fs.pathExists(backupFilePath)) { await fs.remove(backupFilePath); } res.json({ success: true }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Get backup details app.get('/api/claude/backups/:backupId', async (req, res) => { try { const { backupId } = req.params; const history = await loadBackupHistory(); const backup = history.find(b => b.id === backupId); if (!backup) { return res.status(404).json({ error: 'Backup not found' }); } const backupFilePath = join(BACKUPS_DIR, backup.fileName); if (!await fs.pathExists(backupFilePath)) { return res.status(404).json({ error: 'Backup file not found' }); } const backupConfig = await fs.readJson(backupFilePath); res.json({ ...backup, config: backupConfig }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Health check app.get('/api/status', async (req, res) => { try { const config = await loadConfig(); const claudeConfigPath = config.claudeConfigPath; let claudeConfigExists = false; if (claudeConfigPath) { claudeConfigExists = await fs.pathExists(claudeConfigPath); } res.json({ status: 'healthy', version: '1.0.0', claudeConfigPath, claudeConfigExists, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: error.message }); } }); app.listen(PORT, () => { console.log(`🚀 HANA MCP UI Backend running on port ${PORT}`); }); ``` -------------------------------------------------------------------------------- /hana-mcp-ui/src/components/ConfigurationModal.jsx: -------------------------------------------------------------------------------- ```javascript import { motion } from 'framer-motion' import { useState, useEffect } from 'react' import { XMarkIcon, ServerIcon, PlusIcon, PencilIcon, ExclamationTriangleIcon, TrashIcon } from '@heroicons/react/24/outline' import toast from 'react-hot-toast' import { detectDatabaseType, shouldShowMDCFields, validateForDatabaseType } from '../utils/databaseTypes' const ConfigurationModal = ({ isOpen, onClose, server, formData, activeTab, setActiveTab, onFormChange, onServerInfoChange, onSave, isLoading }) => { const [availableEnvironments, setAvailableEnvironments] = useState([ { id: 'development', name: 'Development', color: 'blue' }, { id: 'staging', name: 'Staging', color: 'amber' }, { id: 'production', name: 'Production', color: 'green' } ]) const [selectedEnvironments, setSelectedEnvironments] = useState(new Set()) const [showEnvironmentSelector, setShowEnvironmentSelector] = useState(false) const [validationErrors, setValidationErrors] = useState({}) const [showDeleteConfirm, setShowDeleteConfirm] = useState(null) useEffect(() => { // Load saved environments const savedEnvironments = localStorage.getItem('hana-environments') if (savedEnvironments) { setAvailableEnvironments(JSON.parse(savedEnvironments)) } // Set selected environments based on formData.environments if (formData && formData.environments) { const envKeys = Object.keys(formData.environments) setSelectedEnvironments(new Set(envKeys)) } else { // Clear selected environments for new server setSelectedEnvironments(new Set()) } }, [formData, isOpen]) useEffect(() => { if (!isOpen) return const onKeyDown = (e) => { if (e.key === 'Escape') { onClose() } } window.addEventListener('keydown', onKeyDown) return () => window.removeEventListener('keydown', onKeyDown) }, [isOpen, onClose]) // validation function with database type support const validateEnvironment = (envId, envData) => { const detectedType = detectDatabaseType(envData) const manualType = envData.HANA_CONNECTION_TYPE || 'auto' const dbType = manualType === 'auto' ? detectedType : manualType const validation = validateForDatabaseType(envData, dbType) return validation.errors } // Validate all environments const validateAllEnvironments = () => { const allErrors = {} if (formData.environments) { Object.keys(formData.environments).forEach(envId => { const envErrors = validateEnvironment(envId, formData.environments[envId]) if (Object.keys(envErrors).length > 0) { allErrors[envId] = envErrors } }) } setValidationErrors(allErrors) return Object.keys(allErrors).length === 0 } // Check if current environment has validation errors const getCurrentEnvironmentErrors = () => { if (!activeTab || !validationErrors[activeTab]) return {} return validationErrors[activeTab] } // Handle form change with validation const handleFormChange = (environment, field, value) => { onFormChange(environment, field, value) // Clear validation error for this field if it exists if (validationErrors[environment] && validationErrors[environment][field]) { const newErrors = { ...validationErrors } delete newErrors[environment][field] if (Object.keys(newErrors[environment]).length === 0) { delete newErrors[environment] } setValidationErrors(newErrors) } } const addEnvironment = (envId) => { const newSelected = new Set(selectedEnvironments) newSelected.add(envId) setSelectedEnvironments(newSelected) // Initialize the environment in formData if it doesn't exist if (!formData.environments || !formData.environments[envId]) { onFormChange(envId, 'ENVIRONMENT', envId.toUpperCase()) } // Make the newly added environment active setActiveTab(envId) setShowEnvironmentSelector(false) } const removeEnvironment = (envId) => { const newSelected = new Set(selectedEnvironments) newSelected.delete(envId) // Remove the environment from formData as well if (formData.environments && formData.environments[envId]) { const newFormData = { ...formData } delete newFormData.environments[envId] // Update the parent's formData onServerInfoChange('environments', newFormData.environments) } // Clear validation errors for this environment if (validationErrors[envId]) { const newErrors = { ...validationErrors } delete newErrors[envId] setValidationErrors(newErrors) } // If we removed the active tab, switch to the first available if (activeTab === envId) { const remaining = Array.from(newSelected) setActiveTab(remaining.length > 0 ? remaining[0] : null) } setSelectedEnvironments(newSelected) setShowDeleteConfirm(null) } const getAvailableEnvironmentsToAdd = () => { const available = availableEnvironments.filter(env => !selectedEnvironments.has(env.id)) return available } // Handle save with validation const handleSave = () => { if (!formData.name.trim()) { toast.error('Server name is required') return } // Validate all environments if (!validateAllEnvironments()) { toast.error('Please fill in all required fields for selected environments') return } onSave() } if (!isOpen) return null return ( <motion.div className='fixed inset-0 bg-gray-900/20 backdrop-blur-sm z-50 flex items-center justify-center p-4' initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} > <motion.div className='bg-white rounded-2xl shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden border border-gray-200 flex flex-col' initial={{ scale: 0.9, opacity: 0, y: 20 }} animate={{ scale: 1, opacity: 1, y: 0 }} exit={{ scale: 0.9, opacity: 0, y: 20 }} transition={{ type: 'spring', stiffness: 300, damping: 25 }} onClick={(e) => e.stopPropagation()} > {/* Fixed Header */} <div className='sticky top-0 z-10 px-8 py-6 border-b border-gray-100 bg-white rounded-t-2xl'> <div className='flex items-center justify-between'> <div className='flex items-center gap-4'> <div className='p-3 bg-gray-100 rounded-xl'> {server ? <PencilIcon className='w-5 h-5 text-gray-600' /> : <PlusIcon className='w-5 h-5 text-gray-600' />} </div> <div> <h2 className='text-2xl font-bold text-gray-900 leading-tight'> {server ? 'Edit HANA Server' : 'Add HANA Server'} </h2> <p className='text-base text-gray-600 mt-2 font-medium'> {server ? 'Update database connection settings' : 'Configure a new database connection'} </p> </div> </div> <button onClick={onClose} className='p-3 rounded-xl text-gray-400 hover:text-gray-600 hover:bg-gray-50 transition-colors' > <XMarkIcon className='w-5 h-5' /> </button> </div> </div> {/* Scrollable Body */} <div className='flex-1 overflow-y-auto p-8'> {/* Server Info */} <div className='mb-8'> <h3 className='text-xl font-bold text-gray-900 mb-6 flex items-center gap-3'> <ServerIcon className='w-5 h-5 text-gray-600' /> Server Information </h3> <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> <div> <label className='block text-base font-semibold text-gray-800 mb-3'> Server Name <span className='text-red-500'>*</span> </label> <input type='text' value={formData.name} onChange={(e) => onServerInfoChange('name', e.target.value)} placeholder='e.g. Production HANA' disabled={!!server} className={`w-full px-4 py-3 border border-gray-300 rounded-xl text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors ${ server ? 'bg-gray-100 text-gray-600 cursor-not-allowed' : 'text-gray-400' }`} /> </div> <div> <label className='block text-base font-semibold text-gray-800 mb-3'> Description </label> <input type='text' value={formData.description} onChange={(e) => onServerInfoChange('description', e.target.value)} placeholder='Optional description' 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' /> </div> </div> </div> {/* Environment Configuration */} <div className='mb-10'> <div className='flex items-center justify-between mb-6'> <h3 className='text-xl font-bold text-gray-900'>Environment Configuration</h3> <span className='text-sm px-4 py-2 bg-gray-100 text-gray-600 rounded-lg font-semibold'>Optional</span> </div> <p className='text-base text-gray-600 mb-6 font-medium'> Configure for specific environments: </p> {/* Configured Environments */} {selectedEnvironments.size > 0 && ( <div className='space-y-3 mb-6'> {Array.from(selectedEnvironments).map((envId) => { const env = availableEnvironments.find(e => e.id === envId) const hasErrors = validationErrors[envId] && Object.keys(validationErrors[envId]).length > 0 const isDeleteConfirm = showDeleteConfirm === envId return ( <div key={envId} className={`flex items-center justify-between p-4 border rounded-xl group transition-all ${ hasErrors ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-gray-50' }`} > <div className='flex items-center gap-4'> <div className={`w-4 h-4 rounded-full bg-${env?.color}-500`}></div> <div className='flex items-center gap-3'> <h4 className='text-base font-semibold text-gray-900'>{env?.name}</h4> {hasErrors && ( <div className='flex items-center gap-1 text-red-600'> <ExclamationTriangleIcon className='w-4 h-4' /> <span className='text-sm font-medium'> {Object.keys(validationErrors[envId]).length} required field(s) missing </span> </div> )} </div> </div> <div className='flex items-center gap-2'> {isDeleteConfirm ? ( <> <button type='button' onClick={() => setShowDeleteConfirm(null)} className='px-3 py-1 text-sm font-medium text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors' > Cancel </button> <button type='button' onClick={() => removeEnvironment(envId)} className='px-3 py-1 text-sm font-medium text-white bg-red-600 border border-red-600 rounded-lg hover:bg-red-700 transition-colors' > Delete </button> </> ) : ( <button type='button' onClick={() => setShowDeleteConfirm(envId)} className='p-2 text-gray-400 hover:text-red-500 transition-colors opacity-0 group-hover:opacity-100' title='Delete environment configuration' > <TrashIcon className='w-4 h-4' /> </button> )} </div> </div> ) })} </div> )} {/* Add Environment Button */} {getAvailableEnvironmentsToAdd().length > 0 && ( <button type='button' onClick={() => setShowEnvironmentSelector(true)} className='w-full p-6 border-2 border-dashed border-gray-300 rounded-xl hover:border-[#86a0ff] hover:bg-[#86a0ff]/5 transition-colors flex items-center justify-center gap-4 text-gray-600 hover:text-[#86a0ff] group' > <PlusIcon className='w-5 h-5 group-hover:scale-110 transition-transform' /> <span className='text-lg font-bold'>Add Environment</span> </button> )} {/* Environment Tabs - Only show if environments are selected */} {selectedEnvironments.size > 0 && ( <> <div className='mt-8 mb-6'> <div className='flex border-b border-gray-200 overflow-x-auto'> {Array.from(selectedEnvironments).map((envId) => { const env = availableEnvironments.find(e => e.id === envId) const hasErrors = validationErrors[envId] && Object.keys(validationErrors[envId]).length > 0 return ( <button key={envId} type='button' onClick={() => setActiveTab(envId)} className={`px-6 py-3 font-semibold text-sm transition-colors border-b-2 whitespace-nowrap flex items-center gap-2 ${ activeTab === envId ? 'text-blue-600 border-blue-600' : 'text-gray-500 border-transparent hover:text-gray-700' }`} > <div className='flex items-center gap-3'> <div className={`w-3 h-3 rounded-full bg-${env?.color}-500`}></div> {env?.name || envId} </div> {hasErrors && ( <div className='w-2 h-2 rounded-full bg-red-500'></div> )} </button> ) })} </div> </div> {/* Tab Content - Only show if an environment is selected */} {activeTab && selectedEnvironments.has(activeTab) && ( <motion.div key={activeTab} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.2 }} > <EnvironmentForm environment={activeTab} data={formData.environments[activeTab] || {}} onChange={handleFormChange} errors={getCurrentEnvironmentErrors()} /> </motion.div> )} </> )} {/* No environments selected - encouraging message */} {selectedEnvironments.size === 0 && ( <div className='text-center py-12 bg-gray-50 border border-gray-200 rounded-xl'> <div className='w-12 h-12 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center'> <svg className='w-6 h-6 text-gray-600' fill='none' stroke='currentColor' viewBox='0 0 24 24'> <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' /> </svg> </div> <p className='text-lg font-bold text-gray-800'>No environments configured</p> <p className='text-base text-gray-600 mt-2'>Add environment-specific settings or skip for a basic connection</p> </div> )} </div> </div> {/* Fixed Footer */} <div className='sticky bottom-0 z-10 px-8 py-6 border-t border-gray-100 bg-gray-50 rounded-b-2xl flex justify-end gap-4'> <button onClick={onClose} disabled={isLoading} className='px-8 py-3 text-base font-semibold text-gray-700 bg-white border border-gray-300 rounded-xl hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-gray-400 disabled:opacity-50 transition-colors shadow-sm hover:shadow-md' > Cancel </button> <button onClick={handleSave} disabled={isLoading} className='px-10 py-3 text-base font-semibold text-white bg-[#86a0ff] border border-transparent rounded-xl hover:bg-[#7990e6] focus:outline-none focus:ring-2 focus:ring-[#86a0ff] disabled:opacity-50 min-w-[160px] transition-colors shadow-md hover:shadow-lg' > {isLoading ? 'Saving...' : (server ? 'Update Server' : 'Add Server')} </button> </div> </motion.div> {/* Environment Selector Modal */} {showEnvironmentSelector && ( <div className='absolute inset-0 bg-black/20 flex items-center justify-center p-4 z-10' onClick={() => setShowEnvironmentSelector(false)} > <motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.15 }} className='bg-white rounded-xl shadow-xl max-w-md w-full border border-gray-200' onClick={(e) => e.stopPropagation()} > <div className='p-6 border-b border-gray-200'> <div className='flex items-center justify-between'> <h3 className='text-xl font-bold text-gray-900'>Add Environment</h3> <button onClick={() => setShowEnvironmentSelector(false)} className='p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-colors' > <XMarkIcon className='w-5 h-5' /> </button> </div> <p className='text-base text-gray-600 mt-2 font-medium'>Select an environment for which you want to configure the connection</p> </div> <div className='p-6 max-h-80 overflow-y-auto'> <div className='space-y-3'> {getAvailableEnvironmentsToAdd().map((env) => ( <button key={env.id} type='button' onClick={(e) => { e.preventDefault() e.stopPropagation() addEnvironment(env.id) }} className='w-full p-5 text-left border border-gray-200 rounded-xl hover:bg-gray-50 hover:border-[#86a0ff] hover:shadow-sm transition-all duration-200 group' > <div className='flex items-center gap-4'> <div className={`w-5 h-5 rounded-full bg-${env.color}-500 shadow-sm`}></div> <div className='flex-1'> <h4 className='text-lg font-bold text-gray-900 group-hover:text-[#86a0ff] transition-colors'>{env.name}</h4> </div> <svg className='w-5 h-5 text-gray-400 group-hover:text-[#86a0ff] transition-colors' fill='none' stroke='currentColor' viewBox='0 0 24 24'> <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M12 4v16m8-8H4' /> </svg> </div> </button> ))} {getAvailableEnvironmentsToAdd().length === 0 && ( <div className='text-center py-8 text-gray-500'> <svg className='w-12 h-12 mx-auto mb-4 text-gray-400' fill='none' stroke='currentColor' viewBox='0 0 24 24'> <path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' /> </svg> <p className='text-lg font-semibold text-gray-600'>All available environments are already configured</p> </div> )} </div> </div> </motion.div> </div> )} </motion.div> ) } // Toggle Switch Component const ToggleSwitch = ({ label, value, onChange, description }) => { const isEnabled = value === 'true' || value === true return ( <div className="flex items-center justify-between"> <div className="flex-1"> <label className="block text-base font-semibold text-gray-700">{label}</label> {description && <p className="text-sm text-gray-500 mt-1 font-medium">{description}</p>} </div> <button type="button" onClick={() => onChange(!isEnabled ? 'true' : 'false')} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:ring-offset-2 ${ isEnabled ? 'bg-[#86a0ff]' : 'bg-gray-200' }`} > <span className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${ isEnabled ? 'translate-x-6' : 'translate-x-1' }`} /> </button> </div> ) } // Environment Form Component const EnvironmentForm = ({ environment, data = {}, onChange, errors = {} }) => { // Get environment display name const getEnvironmentDisplayName = (envId) => { const envMap = { 'development': 'DEVELOPMENT', 'staging': 'STAGING', 'production': 'PRODUCTION', 'testing': 'TESTING', 'qa': 'QA' } return envMap[envId] || envId.toUpperCase() } // Ensure ENVIRONMENT parameter is automatically set const environmentValue = data.ENVIRONMENT || getEnvironmentDisplayName(environment) // Database type state - default to single_container if not specified const [manualType, setManualType] = useState(data.HANA_CONNECTION_TYPE || 'single_container') // Note: We no longer use auto-detect in the UI, users must explicitly select database type // Database type options for radio buttons const databaseTypeOptions = [ { label: 'Single-Container Database', value: 'single_container', description: 'Basic HANA database - HOST:PORT connection', required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'SCHEMA'] }, { label: 'MDC System Database', value: 'mdc_system', description: 'Multi-tenant system database - HOST:PORT;INSTANCE', required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'INSTANCE_NUMBER'] }, { label: 'MDC Tenant Database', value: 'mdc_tenant', description: 'Multi-tenant tenant database - HOST:PORT + DATABASE_NAME', required: ['HOST', 'PORT', 'USER', 'PASSWORD', 'INSTANCE_NUMBER', 'DATABASE_NAME'] } ] // Auto-set default values when component renders useEffect(() => { const defaults = { ENVIRONMENT: environmentValue, HANA_PORT: '443', HANA_SSL: 'true', HANA_ENCRYPT: 'true', HANA_VALIDATE_CERT: 'true', HANA_CONNECTION_TYPE: 'auto', LOG_LEVEL: 'info', ENABLE_FILE_LOGGING: 'true', ENABLE_CONSOLE_LOGGING: 'false' } // Set any missing default values Object.entries(defaults).forEach(([key, defaultValue]) => { if (!data[key]) { onChange(environment, key, defaultValue) } }) }, [environment, data, environmentValue, onChange]) // Handle connection type change const handleConnectionTypeChange = (e) => { const newType = e.target.value setManualType(newType) onChange(environment, 'HANA_CONNECTION_TYPE', newType) } // Helper function to render input field with error handling const renderInputField = (field, label, type = 'text', placeholder = '', required = false) => { const hasError = errors[field] return ( <div> <label className={`block text-base font-semibold mb-3 ${ hasError ? 'text-red-700' : 'text-gray-800' }`}> {label} {required && <span className='text-red-500'>*</span>} </label> <input type={type} value={data[field] || ''} onChange={(e) => onChange(environment, field, e.target.value)} placeholder={placeholder} className={`w-full px-4 py-3 border rounded-xl text-gray-900 placeholder-gray-400 text-base focus:outline-none focus:ring-2 transition-colors ${ hasError ? 'border-red-300 focus:ring-red-500 focus:border-red-500 bg-red-50' : 'border-gray-300 focus:ring-[#86a0ff] focus:border-[#86a0ff]' }`} /> {hasError && ( <p className='text-sm text-red-600 mt-1 font-medium flex items-center gap-1'> <ExclamationTriangleIcon className='w-3 h-3' /> {hasError} </p> )} </div> ) } return ( <div className='space-y-8'> {/* Connection Settings */} <div> <h4 className='text-lg font-bold text-gray-900 mb-6'>Connection Settings</h4> <div className='grid grid-cols-1 lg:grid-cols-3 gap-6'> {renderInputField('HANA_HOST', 'Host', 'text', 'your-hana-host.com', true)} {renderInputField('HANA_PORT', 'Port', 'number', '443')} {renderInputField('HANA_SCHEMA', 'Schema', 'text', 'your-schema', true)} </div> {/* Database Type Selection */} <div className='mt-6'> <label className='block text-base font-semibold mb-3 text-gray-800'> Database Type </label> <div className='space-y-3'> {databaseTypeOptions.map((option) => ( <label key={option.value} className={` flex items-start gap-4 p-4 rounded-xl border-2 cursor-pointer transition-all duration-200 ${manualType === option.value ? 'border-[#86a0ff] bg-blue-50 shadow-md' : 'border-gray-200 bg-white hover:border-gray-300 hover:shadow-sm' } `} > <input type="radio" name="databaseType" value={option.value} checked={manualType === option.value} onChange={handleConnectionTypeChange} className="mt-1 w-4 h-4 text-[#86a0ff] border-gray-300 focus:ring-[#86a0ff] focus:ring-2" /> <div className="flex-1"> <div className="mb-1"> <span className="font-semibold text-gray-900">{option.label}</span> </div> <p className="text-sm text-gray-600 mb-2">{option.description}</p> <div> <p className="text-xs text-gray-500 mb-1">Required fields:</p> <div className="flex flex-wrap gap-1"> {option.required.map((field) => ( <span key={field} className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700" > {field.replace('HANA_', '')} </span> ))} </div> </div> </div> </label> ))} </div> </div> {/* MDC-specific fields - show conditionally */} {shouldShowMDCFields(detectDatabaseType(data), manualType) && ( <div className='mt-6'> <h5 className='text-base font-semibold text-gray-800 mb-2'>MDC Configuration</h5> <p className='text-sm text-gray-600 mb-4'> {manualType === 'mdc_system' ? 'MDC System Database requires instance number for connection string format: HOST:PORT;INSTANCE' : 'MDC Tenant Database requires both instance number and database name for connection' } </p> <div className='grid grid-cols-1 md:grid-cols-2 gap-6'> {renderInputField('HANA_INSTANCE_NUMBER', 'Instance Number', 'number', '10', true)} {manualType === 'mdc_tenant' && renderInputField('HANA_DATABASE_NAME', 'Database Name', 'text', 'HQQ', true)} </div> </div> )} <div className='grid grid-cols-1 md:grid-cols-2 gap-6 mt-6'> {renderInputField('HANA_USER', 'Username', 'text', 'your-username', true)} {renderInputField('HANA_PASSWORD', 'Password', 'password', '••••••••', true)} </div> </div> {/* Security & SSL Configuration */} <div> <h4 className='text-lg font-bold text-gray-900 mb-6'>Security & SSL</h4> <div className='bg-gray-50 rounded-xl p-6 space-y-6'> <ToggleSwitch label="Enable SSL" description="Use SSL/TLS for secure connection" value={data.HANA_SSL || 'true'} onChange={(value) => onChange(environment, 'HANA_SSL', value)} /> <ToggleSwitch label="Encrypt Connection" description="Encrypt data transmission" value={data.HANA_ENCRYPT || 'true'} onChange={(value) => onChange(environment, 'HANA_ENCRYPT', value)} /> <ToggleSwitch label="Validate Certificate" description="Verify SSL certificate authenticity" value={data.HANA_VALIDATE_CERT || 'false'} onChange={(value) => onChange(environment, 'HANA_VALIDATE_CERT', value)} /> </div> </div> {/* Logging Configuration */} <div> <h4 className='text-lg font-bold text-gray-900 mb-6'>Logging Configuration</h4> <div className='grid grid-cols-1 md:grid-cols-3 gap-6'> <div> <label className='block text-base font-semibold text-gray-800 mb-3'>Log Level</label> <select value={data.LOG_LEVEL || 'info'} onChange={(e) => onChange(environment, 'LOG_LEVEL', e.target.value)} className='w-full px-4 py-3 border border-gray-300 rounded-xl text-gray-900 text-base focus:outline-none focus:ring-2 focus:ring-[#86a0ff] focus:border-[#86a0ff] transition-colors' > <option value='error'>Error</option> <option value='warn'>Warning</option> <option value='info'>Info</option> <option value='debug'>Debug</option> </select> </div> <div className='flex items-end'> <div className='w-full'> <ToggleSwitch label="File Logging" description="Save logs to file" value={data.ENABLE_FILE_LOGGING || 'true'} onChange={(value) => onChange(environment, 'ENABLE_FILE_LOGGING', value)} /> </div> </div> <div className='flex items-end'> <div className='w-full'> <ToggleSwitch label="Console Logging" description="Display logs in console" value={data.ENABLE_CONSOLE_LOGGING || 'false'} onChange={(value) => onChange(environment, 'ENABLE_CONSOLE_LOGGING', value)} /> </div> </div> </div> </div> </div> ) } export default ConfigurationModal ```