mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat(cluster): add cluster configuration options and update system settings
This commit is contained in:
@@ -5,6 +5,7 @@ export const PERMISSIONS = {
|
||||
SETTINGS_SKIP_AUTH: 'settings:skip_auth',
|
||||
SETTINGS_INSTALL_CONFIG: 'settings:install_config',
|
||||
SETTINGS_EXPORT_CONFIG: 'settings:export_config',
|
||||
SETTINGS_CLUSTER_CONFIG: 'settings:cluster_config',
|
||||
} as const;
|
||||
|
||||
export default PERMISSIONS;
|
||||
|
||||
@@ -34,6 +34,35 @@ interface MCPRouterConfig {
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
interface ClusterNodeConfig {
|
||||
id?: string;
|
||||
name?: string;
|
||||
coordinatorUrl: string;
|
||||
heartbeatInterval?: number;
|
||||
registerOnStartup?: boolean;
|
||||
}
|
||||
|
||||
interface ClusterCoordinatorConfig {
|
||||
nodeTimeout?: number;
|
||||
cleanupInterval?: number;
|
||||
stickySessionTimeout?: number;
|
||||
}
|
||||
|
||||
interface ClusterStickySessionConfig {
|
||||
enabled: boolean;
|
||||
strategy: 'consistent-hash' | 'cookie' | 'header';
|
||||
cookieName?: string;
|
||||
headerName?: string;
|
||||
}
|
||||
|
||||
interface ClusterConfig {
|
||||
enabled: boolean;
|
||||
mode: 'standalone' | 'node' | 'coordinator';
|
||||
node?: ClusterNodeConfig;
|
||||
coordinator?: ClusterCoordinatorConfig;
|
||||
stickySession?: ClusterStickySessionConfig;
|
||||
}
|
||||
|
||||
interface SystemSettings {
|
||||
systemConfig?: {
|
||||
routing?: RoutingConfig;
|
||||
@@ -41,6 +70,7 @@ interface SystemSettings {
|
||||
smartRouting?: SmartRoutingConfig;
|
||||
mcpRouter?: MCPRouterConfig;
|
||||
nameSeparator?: string;
|
||||
cluster?: ClusterConfig;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +115,27 @@ export const useSettingsData = () => {
|
||||
baseUrl: 'https://api.mcprouter.to/v1',
|
||||
});
|
||||
|
||||
const [clusterConfig, setClusterConfig] = useState<ClusterConfig>({
|
||||
enabled: false,
|
||||
mode: 'standalone',
|
||||
node: {
|
||||
coordinatorUrl: '',
|
||||
heartbeatInterval: 5000,
|
||||
registerOnStartup: true,
|
||||
},
|
||||
coordinator: {
|
||||
nodeTimeout: 15000,
|
||||
cleanupInterval: 30000,
|
||||
stickySessionTimeout: 3600000,
|
||||
},
|
||||
stickySession: {
|
||||
enabled: true,
|
||||
strategy: 'consistent-hash',
|
||||
cookieName: 'MCPHUB_NODE',
|
||||
headerName: 'X-MCPHub-Node',
|
||||
},
|
||||
});
|
||||
|
||||
const [nameSeparator, setNameSeparator] = useState<string>('-');
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -141,6 +192,28 @@ export const useSettingsData = () => {
|
||||
if (data.success && data.data?.systemConfig?.nameSeparator !== undefined) {
|
||||
setNameSeparator(data.data.systemConfig.nameSeparator);
|
||||
}
|
||||
if (data.success && data.data?.systemConfig?.cluster) {
|
||||
setClusterConfig({
|
||||
enabled: data.data.systemConfig.cluster.enabled ?? false,
|
||||
mode: data.data.systemConfig.cluster.mode || 'standalone',
|
||||
node: data.data.systemConfig.cluster.node || {
|
||||
coordinatorUrl: '',
|
||||
heartbeatInterval: 5000,
|
||||
registerOnStartup: true,
|
||||
},
|
||||
coordinator: data.data.systemConfig.cluster.coordinator || {
|
||||
nodeTimeout: 15000,
|
||||
cleanupInterval: 30000,
|
||||
stickySessionTimeout: 3600000,
|
||||
},
|
||||
stickySession: data.data.systemConfig.cluster.stickySession || {
|
||||
enabled: true,
|
||||
strategy: 'consistent-hash',
|
||||
cookieName: 'MCPHUB_NODE',
|
||||
headerName: 'X-MCPHub-Node',
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch settings:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to fetch settings');
|
||||
@@ -420,6 +493,39 @@ export const useSettingsData = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update cluster configuration
|
||||
const updateClusterConfig = async (updates: Partial<ClusterConfig>) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const data = await apiPut('/system-config', {
|
||||
cluster: updates,
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
setClusterConfig({
|
||||
...clusterConfig,
|
||||
...updates,
|
||||
});
|
||||
showToast(t('settings.systemConfigUpdated'));
|
||||
return true;
|
||||
} else {
|
||||
showToast(data.message || t('errors.failedToUpdateSystemConfig'));
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update cluster config:', error);
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Failed to update cluster config';
|
||||
setError(errorMessage);
|
||||
showToast(errorMessage);
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const exportMCPSettings = async (serverName?: string) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -455,6 +561,7 @@ export const useSettingsData = () => {
|
||||
installConfig,
|
||||
smartRoutingConfig,
|
||||
mcpRouterConfig,
|
||||
clusterConfig,
|
||||
nameSeparator,
|
||||
loading,
|
||||
error,
|
||||
@@ -468,6 +575,7 @@ export const useSettingsData = () => {
|
||||
updateRoutingConfigBatch,
|
||||
updateMCPRouterConfig,
|
||||
updateMCPRouterConfigBatch,
|
||||
updateClusterConfig,
|
||||
updateNameSeparator,
|
||||
exportMCPSettings,
|
||||
};
|
||||
|
||||
@@ -1,55 +1,99 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import ChangePasswordForm from '@/components/ChangePasswordForm'
|
||||
import { Switch } from '@/components/ui/ToggleGroup'
|
||||
import { useSettingsData } from '@/hooks/useSettingsData'
|
||||
import { useToast } from '@/contexts/ToastContext'
|
||||
import { generateRandomKey } from '@/utils/key'
|
||||
import { PermissionChecker } from '@/components/PermissionChecker'
|
||||
import { PERMISSIONS } from '@/constants/permissions'
|
||||
import { Copy, Check, Download } from 'lucide-react'
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import ChangePasswordForm from '@/components/ChangePasswordForm';
|
||||
import { Switch } from '@/components/ui/ToggleGroup';
|
||||
import { useSettingsData } from '@/hooks/useSettingsData';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { generateRandomKey } from '@/utils/key';
|
||||
import { PermissionChecker } from '@/components/PermissionChecker';
|
||||
import { PERMISSIONS } from '@/constants/permissions';
|
||||
import { Copy, Check, Download } from 'lucide-react';
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { showToast } = useToast()
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [installConfig, setInstallConfig] = useState<{
|
||||
pythonIndexUrl: string
|
||||
npmRegistry: string
|
||||
baseUrl: string
|
||||
pythonIndexUrl: string;
|
||||
npmRegistry: string;
|
||||
baseUrl: string;
|
||||
}>({
|
||||
pythonIndexUrl: '',
|
||||
npmRegistry: '',
|
||||
baseUrl: 'http://localhost:3000',
|
||||
})
|
||||
});
|
||||
|
||||
const [tempSmartRoutingConfig, setTempSmartRoutingConfig] = useState<{
|
||||
dbUrl: string
|
||||
openaiApiBaseUrl: string
|
||||
openaiApiKey: string
|
||||
openaiApiEmbeddingModel: string
|
||||
dbUrl: string;
|
||||
openaiApiBaseUrl: string;
|
||||
openaiApiKey: string;
|
||||
openaiApiEmbeddingModel: string;
|
||||
}>({
|
||||
dbUrl: '',
|
||||
openaiApiBaseUrl: '',
|
||||
openaiApiKey: '',
|
||||
openaiApiEmbeddingModel: '',
|
||||
})
|
||||
});
|
||||
|
||||
const [tempMCPRouterConfig, setTempMCPRouterConfig] = useState<{
|
||||
apiKey: string
|
||||
referer: string
|
||||
title: string
|
||||
baseUrl: string
|
||||
apiKey: string;
|
||||
referer: string;
|
||||
title: string;
|
||||
baseUrl: string;
|
||||
}>({
|
||||
apiKey: '',
|
||||
referer: 'https://www.mcphubx.com',
|
||||
title: 'MCPHub',
|
||||
baseUrl: 'https://api.mcprouter.to/v1',
|
||||
})
|
||||
});
|
||||
|
||||
const [tempNameSeparator, setTempNameSeparator] = useState<string>('-')
|
||||
const [tempNameSeparator, setTempNameSeparator] = useState<string>('-');
|
||||
|
||||
const [tempClusterConfig, setTempClusterConfig] = useState<{
|
||||
enabled: boolean;
|
||||
mode: 'standalone' | 'node' | 'coordinator';
|
||||
node: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
coordinatorUrl: string;
|
||||
heartbeatInterval?: number;
|
||||
registerOnStartup?: boolean;
|
||||
};
|
||||
coordinator: {
|
||||
nodeTimeout?: number;
|
||||
cleanupInterval?: number;
|
||||
stickySessionTimeout?: number;
|
||||
};
|
||||
stickySession: {
|
||||
enabled: boolean;
|
||||
strategy: 'consistent-hash' | 'cookie' | 'header';
|
||||
cookieName?: string;
|
||||
headerName?: string;
|
||||
};
|
||||
}>({
|
||||
enabled: false,
|
||||
mode: 'standalone',
|
||||
node: {
|
||||
id: '',
|
||||
name: '',
|
||||
coordinatorUrl: '',
|
||||
heartbeatInterval: 5000,
|
||||
registerOnStartup: true,
|
||||
},
|
||||
coordinator: {
|
||||
nodeTimeout: 15000,
|
||||
cleanupInterval: 30000,
|
||||
stickySessionTimeout: 3600000,
|
||||
},
|
||||
stickySession: {
|
||||
enabled: true,
|
||||
strategy: 'consistent-hash',
|
||||
cookieName: 'MCPHUB_NODE',
|
||||
headerName: 'X-MCPHub-Node',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
routingConfig,
|
||||
@@ -58,6 +102,7 @@ const SettingsPage: React.FC = () => {
|
||||
installConfig: savedInstallConfig,
|
||||
smartRoutingConfig,
|
||||
mcpRouterConfig,
|
||||
clusterConfig,
|
||||
nameSeparator,
|
||||
loading,
|
||||
updateRoutingConfig,
|
||||
@@ -66,16 +111,17 @@ const SettingsPage: React.FC = () => {
|
||||
updateSmartRoutingConfig,
|
||||
updateSmartRoutingConfigBatch,
|
||||
updateMCPRouterConfig,
|
||||
updateClusterConfig,
|
||||
updateNameSeparator,
|
||||
exportMCPSettings,
|
||||
} = useSettingsData()
|
||||
} = useSettingsData();
|
||||
|
||||
// Update local installConfig when savedInstallConfig changes
|
||||
useEffect(() => {
|
||||
if (savedInstallConfig) {
|
||||
setInstallConfig(savedInstallConfig)
|
||||
setInstallConfig(savedInstallConfig);
|
||||
}
|
||||
}, [savedInstallConfig])
|
||||
}, [savedInstallConfig]);
|
||||
|
||||
// Update local tempSmartRoutingConfig when smartRoutingConfig changes
|
||||
useEffect(() => {
|
||||
@@ -85,9 +131,9 @@ const SettingsPage: React.FC = () => {
|
||||
openaiApiBaseUrl: smartRoutingConfig.openaiApiBaseUrl || '',
|
||||
openaiApiKey: smartRoutingConfig.openaiApiKey || '',
|
||||
openaiApiEmbeddingModel: smartRoutingConfig.openaiApiEmbeddingModel || '',
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [smartRoutingConfig])
|
||||
}, [smartRoutingConfig]);
|
||||
|
||||
// Update local tempMCPRouterConfig when mcpRouterConfig changes
|
||||
useEffect(() => {
|
||||
@@ -97,24 +143,53 @@ const SettingsPage: React.FC = () => {
|
||||
referer: mcpRouterConfig.referer || 'https://www.mcphubx.com',
|
||||
title: mcpRouterConfig.title || 'MCPHub',
|
||||
baseUrl: mcpRouterConfig.baseUrl || 'https://api.mcprouter.to/v1',
|
||||
})
|
||||
});
|
||||
}
|
||||
}, [mcpRouterConfig])
|
||||
}, [mcpRouterConfig]);
|
||||
|
||||
// Update local tempNameSeparator when nameSeparator changes
|
||||
useEffect(() => {
|
||||
setTempNameSeparator(nameSeparator)
|
||||
}, [nameSeparator])
|
||||
setTempNameSeparator(nameSeparator);
|
||||
}, [nameSeparator]);
|
||||
|
||||
// Update local tempClusterConfig when clusterConfig changes
|
||||
useEffect(() => {
|
||||
if (clusterConfig) {
|
||||
setTempClusterConfig({
|
||||
enabled: clusterConfig.enabled ?? false,
|
||||
mode: clusterConfig.mode || 'standalone',
|
||||
node: clusterConfig.node || {
|
||||
id: '',
|
||||
name: '',
|
||||
coordinatorUrl: '',
|
||||
heartbeatInterval: 5000,
|
||||
registerOnStartup: true,
|
||||
},
|
||||
coordinator: clusterConfig.coordinator || {
|
||||
nodeTimeout: 15000,
|
||||
cleanupInterval: 30000,
|
||||
stickySessionTimeout: 3600000,
|
||||
},
|
||||
stickySession: clusterConfig.stickySession || {
|
||||
enabled: true,
|
||||
strategy: 'consistent-hash',
|
||||
cookieName: 'MCPHUB_NODE',
|
||||
headerName: 'X-MCPHub-Node',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [clusterConfig]);
|
||||
|
||||
const [sectionsVisible, setSectionsVisible] = useState({
|
||||
routingConfig: false,
|
||||
installConfig: false,
|
||||
smartRoutingConfig: false,
|
||||
mcpRouterConfig: false,
|
||||
clusterConfig: false,
|
||||
nameSeparator: false,
|
||||
password: false,
|
||||
exportConfig: false,
|
||||
})
|
||||
});
|
||||
|
||||
const toggleSection = (
|
||||
section:
|
||||
@@ -122,6 +197,7 @@ const SettingsPage: React.FC = () => {
|
||||
| 'installConfig'
|
||||
| 'smartRoutingConfig'
|
||||
| 'mcpRouterConfig'
|
||||
| 'clusterConfig'
|
||||
| 'nameSeparator'
|
||||
| 'password'
|
||||
| 'exportConfig',
|
||||
@@ -129,8 +205,8 @@ const SettingsPage: React.FC = () => {
|
||||
setSectionsVisible((prev) => ({
|
||||
...prev,
|
||||
[section]: !prev[section],
|
||||
}))
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRoutingConfigChange = async (
|
||||
key:
|
||||
@@ -144,39 +220,39 @@ const SettingsPage: React.FC = () => {
|
||||
// If enableBearerAuth is turned on and there's no key, generate one first
|
||||
if (key === 'enableBearerAuth' && value === true) {
|
||||
if (!tempRoutingConfig.bearerAuthKey && !routingConfig.bearerAuthKey) {
|
||||
const newKey = generateRandomKey()
|
||||
handleBearerAuthKeyChange(newKey)
|
||||
const newKey = generateRandomKey();
|
||||
handleBearerAuthKeyChange(newKey);
|
||||
|
||||
// Update both enableBearerAuth and bearerAuthKey in a single call
|
||||
const success = await updateRoutingConfigBatch({
|
||||
enableBearerAuth: true,
|
||||
bearerAuthKey: newKey,
|
||||
})
|
||||
});
|
||||
|
||||
if (success) {
|
||||
// Update tempRoutingConfig to reflect the saved values
|
||||
setTempRoutingConfig((prev) => ({
|
||||
...prev,
|
||||
bearerAuthKey: newKey,
|
||||
}))
|
||||
}));
|
||||
}
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await updateRoutingConfig(key, value)
|
||||
}
|
||||
await updateRoutingConfig(key, value);
|
||||
};
|
||||
|
||||
const handleBearerAuthKeyChange = (value: string) => {
|
||||
setTempRoutingConfig((prev) => ({
|
||||
...prev,
|
||||
bearerAuthKey: value,
|
||||
}))
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const saveBearerAuthKey = async () => {
|
||||
await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey)
|
||||
}
|
||||
await updateRoutingConfig('bearerAuthKey', tempRoutingConfig.bearerAuthKey);
|
||||
};
|
||||
|
||||
const handleInstallConfigChange = (
|
||||
key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl',
|
||||
@@ -185,12 +261,12 @@ const SettingsPage: React.FC = () => {
|
||||
setInstallConfig({
|
||||
...installConfig,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveInstallConfig = async (key: 'pythonIndexUrl' | 'npmRegistry' | 'baseUrl') => {
|
||||
await updateInstallConfig(key, installConfig[key])
|
||||
}
|
||||
await updateInstallConfig(key, installConfig[key]);
|
||||
};
|
||||
|
||||
const handleSmartRoutingConfigChange = (
|
||||
key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
|
||||
@@ -199,14 +275,14 @@ const SettingsPage: React.FC = () => {
|
||||
setTempSmartRoutingConfig({
|
||||
...tempSmartRoutingConfig,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveSmartRoutingConfig = async (
|
||||
key: 'dbUrl' | 'openaiApiBaseUrl' | 'openaiApiKey' | 'openaiApiEmbeddingModel',
|
||||
) => {
|
||||
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key])
|
||||
}
|
||||
await updateSmartRoutingConfig(key, tempSmartRoutingConfig[key]);
|
||||
};
|
||||
|
||||
const handleMCPRouterConfigChange = (
|
||||
key: 'apiKey' | 'referer' | 'title' | 'baseUrl',
|
||||
@@ -215,141 +291,141 @@ const SettingsPage: React.FC = () => {
|
||||
setTempMCPRouterConfig({
|
||||
...tempMCPRouterConfig,
|
||||
[key]: value,
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const saveMCPRouterConfig = async (key: 'apiKey' | 'referer' | 'title' | 'baseUrl') => {
|
||||
await updateMCPRouterConfig(key, tempMCPRouterConfig[key])
|
||||
}
|
||||
await updateMCPRouterConfig(key, tempMCPRouterConfig[key]);
|
||||
};
|
||||
|
||||
const saveNameSeparator = async () => {
|
||||
await updateNameSeparator(tempNameSeparator)
|
||||
}
|
||||
await updateNameSeparator(tempNameSeparator);
|
||||
};
|
||||
|
||||
const handleSmartRoutingEnabledChange = async (value: boolean) => {
|
||||
// If enabling Smart Routing, validate required fields and save any unsaved changes
|
||||
if (value) {
|
||||
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl
|
||||
const currentDbUrl = tempSmartRoutingConfig.dbUrl || smartRoutingConfig.dbUrl;
|
||||
const currentOpenaiApiKey =
|
||||
tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey
|
||||
tempSmartRoutingConfig.openaiApiKey || smartRoutingConfig.openaiApiKey;
|
||||
|
||||
if (!currentDbUrl || !currentOpenaiApiKey) {
|
||||
const missingFields = []
|
||||
if (!currentDbUrl) missingFields.push(t('settings.dbUrl'))
|
||||
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'))
|
||||
const missingFields = [];
|
||||
if (!currentDbUrl) missingFields.push(t('settings.dbUrl'));
|
||||
if (!currentOpenaiApiKey) missingFields.push(t('settings.openaiApiKey'));
|
||||
|
||||
showToast(
|
||||
t('settings.smartRoutingValidationError', {
|
||||
fields: missingFields.join(', '),
|
||||
}),
|
||||
)
|
||||
return
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare updates object with unsaved changes and enabled status
|
||||
const updates: any = { enabled: value }
|
||||
const updates: any = { enabled: value };
|
||||
|
||||
// Check for unsaved changes and include them in the batch update
|
||||
if (tempSmartRoutingConfig.dbUrl !== smartRoutingConfig.dbUrl) {
|
||||
updates.dbUrl = tempSmartRoutingConfig.dbUrl
|
||||
updates.dbUrl = tempSmartRoutingConfig.dbUrl;
|
||||
}
|
||||
if (tempSmartRoutingConfig.openaiApiBaseUrl !== smartRoutingConfig.openaiApiBaseUrl) {
|
||||
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl
|
||||
updates.openaiApiBaseUrl = tempSmartRoutingConfig.openaiApiBaseUrl;
|
||||
}
|
||||
if (tempSmartRoutingConfig.openaiApiKey !== smartRoutingConfig.openaiApiKey) {
|
||||
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey
|
||||
updates.openaiApiKey = tempSmartRoutingConfig.openaiApiKey;
|
||||
}
|
||||
if (
|
||||
tempSmartRoutingConfig.openaiApiEmbeddingModel !==
|
||||
smartRoutingConfig.openaiApiEmbeddingModel
|
||||
) {
|
||||
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel
|
||||
updates.openaiApiEmbeddingModel = tempSmartRoutingConfig.openaiApiEmbeddingModel;
|
||||
}
|
||||
|
||||
// Save all changes in a single batch update
|
||||
await updateSmartRoutingConfigBatch(updates)
|
||||
await updateSmartRoutingConfigBatch(updates);
|
||||
} else {
|
||||
// If disabling, just update the enabled status
|
||||
await updateSmartRoutingConfig('enabled', value)
|
||||
await updateSmartRoutingConfig('enabled', value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
setTimeout(() => {
|
||||
navigate('/')
|
||||
}, 2000)
|
||||
}
|
||||
navigate('/');
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const [copiedConfig, setCopiedConfig] = useState(false)
|
||||
const [mcpSettingsJson, setMcpSettingsJson] = useState<string>('')
|
||||
const [copiedConfig, setCopiedConfig] = useState(false);
|
||||
const [mcpSettingsJson, setMcpSettingsJson] = useState<string>('');
|
||||
|
||||
const fetchMcpSettings = async () => {
|
||||
try {
|
||||
const result = await exportMCPSettings()
|
||||
console.log('Fetched MCP settings:', result)
|
||||
const configJson = JSON.stringify(result.data, null, 2)
|
||||
setMcpSettingsJson(configJson)
|
||||
const result = await exportMCPSettings();
|
||||
console.log('Fetched MCP settings:', result);
|
||||
const configJson = JSON.stringify(result.data, null, 2);
|
||||
setMcpSettingsJson(configJson);
|
||||
} catch (error) {
|
||||
console.error('Error fetching MCP settings:', error)
|
||||
showToast(t('settings.exportError') || 'Failed to fetch settings', 'error')
|
||||
console.error('Error fetching MCP settings:', error);
|
||||
showToast(t('settings.exportError') || 'Failed to fetch settings', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (sectionsVisible.exportConfig && !mcpSettingsJson) {
|
||||
fetchMcpSettings()
|
||||
fetchMcpSettings();
|
||||
}
|
||||
}, [sectionsVisible.exportConfig])
|
||||
}, [sectionsVisible.exportConfig]);
|
||||
|
||||
const handleCopyConfig = async () => {
|
||||
if (!mcpSettingsJson) return
|
||||
if (!mcpSettingsJson) return;
|
||||
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(mcpSettingsJson)
|
||||
setCopiedConfig(true)
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
setTimeout(() => setCopiedConfig(false), 2000)
|
||||
await navigator.clipboard.writeText(mcpSettingsJson);
|
||||
setCopiedConfig(true);
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
setTimeout(() => setCopiedConfig(false), 2000);
|
||||
} else {
|
||||
// Fallback for HTTP or unsupported clipboard API
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = mcpSettingsJson
|
||||
textArea.style.position = 'fixed'
|
||||
textArea.style.left = '-9999px'
|
||||
document.body.appendChild(textArea)
|
||||
textArea.focus()
|
||||
textArea.select()
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = mcpSettingsJson;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy')
|
||||
setCopiedConfig(true)
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success')
|
||||
setTimeout(() => setCopiedConfig(false), 2000)
|
||||
document.execCommand('copy');
|
||||
setCopiedConfig(true);
|
||||
showToast(t('common.copySuccess') || 'Copied to clipboard', 'success');
|
||||
setTimeout(() => setCopiedConfig(false), 2000);
|
||||
} catch (err) {
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
console.error('Copy to clipboard failed:', err)
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error');
|
||||
console.error('Copy to clipboard failed:', err);
|
||||
}
|
||||
document.body.removeChild(textArea)
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error copying configuration:', error)
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error')
|
||||
console.error('Error copying configuration:', error);
|
||||
showToast(t('common.copyFailed') || 'Copy failed', 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadConfig = () => {
|
||||
if (!mcpSettingsJson) return
|
||||
if (!mcpSettingsJson) return;
|
||||
|
||||
const blob = new Blob([mcpSettingsJson], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'mcp_settings.json'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success')
|
||||
}
|
||||
const blob = new Blob([mcpSettingsJson], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = 'mcp_settings.json';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
showToast(t('settings.exportSuccess') || 'Settings exported successfully', 'success');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto">
|
||||
@@ -563,6 +639,432 @@ const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</PermissionChecker>
|
||||
|
||||
{/* Cluster Configuration Settings */}
|
||||
<PermissionChecker permissions={PERMISSIONS.SETTINGS_CLUSTER_CONFIG}>
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 page-card dashboard-card">
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer transition-colors duration-200 hover:text-blue-600"
|
||||
onClick={() => toggleSection('clusterConfig')}
|
||||
>
|
||||
<h2 className="font-semibold text-gray-800">{t('settings.clusterConfig')}</h2>
|
||||
<span className="text-gray-500 transition-transform duration-200">
|
||||
{sectionsVisible.clusterConfig ? '▼' : '►'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{sectionsVisible.clusterConfig && (
|
||||
<div className="space-y-4 mt-4">
|
||||
{/* Enable Cluster Mode */}
|
||||
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-md">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-700">{t('settings.clusterEnabled')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.clusterEnabledDescription')}</p>
|
||||
</div>
|
||||
<Switch
|
||||
disabled={loading}
|
||||
checked={tempClusterConfig.enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setTempClusterConfig((prev) => ({ ...prev, enabled: checked }));
|
||||
updateClusterConfig({ enabled: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cluster Mode Selection */}
|
||||
{tempClusterConfig.enabled && (
|
||||
<div className="p-3 bg-gray-50 rounded-md">
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-700">{t('settings.clusterMode')}</h3>
|
||||
<p className="text-sm text-gray-500">{t('settings.clusterModeDescription')}</p>
|
||||
</div>
|
||||
<select
|
||||
value={tempClusterConfig.mode}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value as 'standalone' | 'node' | 'coordinator';
|
||||
setTempClusterConfig((prev) => ({ ...prev, mode }));
|
||||
updateClusterConfig({ mode });
|
||||
}}
|
||||
className="mt-1 block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="standalone">{t('settings.clusterModeStandalone')}</option>
|
||||
<option value="node">{t('settings.clusterModeNode')}</option>
|
||||
<option value="coordinator">{t('settings.clusterModeCoordinator')}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Node Configuration */}
|
||||
{tempClusterConfig.enabled && tempClusterConfig.mode === 'node' && (
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-md space-y-3">
|
||||
<h3 className="font-semibold text-gray-800 mb-2">{t('settings.nodeConfig')}</h3>
|
||||
|
||||
{/* Coordinator URL */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.coordinatorUrl')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.coordinatorUrlDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={tempClusterConfig.node.coordinatorUrl}
|
||||
onChange={(e) => {
|
||||
const coordinatorUrl = e.target.value;
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
node: { ...prev.node, coordinatorUrl },
|
||||
}));
|
||||
}}
|
||||
onBlur={() => updateClusterConfig({ node: { ...tempClusterConfig.node } })}
|
||||
placeholder={t('settings.coordinatorUrlPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Node ID */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.nodeId')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">{t('settings.nodeIdDescription')}</p>
|
||||
<input
|
||||
type="text"
|
||||
value={tempClusterConfig.node.id || ''}
|
||||
onChange={(e) => {
|
||||
const id = e.target.value;
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
node: { ...prev.node, id },
|
||||
}));
|
||||
}}
|
||||
onBlur={() => updateClusterConfig({ node: { ...tempClusterConfig.node } })}
|
||||
placeholder={t('settings.nodeIdPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Node Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.nodeName')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.nodeNameDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={tempClusterConfig.node.name || ''}
|
||||
onChange={(e) => {
|
||||
const name = e.target.value;
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
node: { ...prev.node, name },
|
||||
}));
|
||||
}}
|
||||
onBlur={() => updateClusterConfig({ node: { ...tempClusterConfig.node } })}
|
||||
placeholder={t('settings.nodeNamePlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Heartbeat Interval */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.heartbeatInterval')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.heartbeatIntervalDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
value={tempClusterConfig.node.heartbeatInterval || 5000}
|
||||
onChange={(e) => {
|
||||
const heartbeatInterval = parseInt(e.target.value);
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
node: { ...prev.node, heartbeatInterval },
|
||||
}));
|
||||
}}
|
||||
onBlur={() => updateClusterConfig({ node: { ...tempClusterConfig.node } })}
|
||||
placeholder={t('settings.heartbeatIntervalPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
min="1000"
|
||||
step="1000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Register on Startup */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{t('settings.registerOnStartup')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500">
|
||||
{t('settings.registerOnStartupDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
disabled={loading}
|
||||
checked={tempClusterConfig.node.registerOnStartup ?? true}
|
||||
onCheckedChange={(checked) => {
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
node: { ...prev.node, registerOnStartup: checked },
|
||||
}));
|
||||
updateClusterConfig({
|
||||
node: { ...tempClusterConfig.node, registerOnStartup: checked },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Coordinator Configuration */}
|
||||
{tempClusterConfig.enabled && tempClusterConfig.mode === 'coordinator' && (
|
||||
<div className="p-3 bg-purple-50 border border-purple-200 rounded-md space-y-3">
|
||||
<h3 className="font-semibold text-gray-800 mb-2">
|
||||
{t('settings.coordinatorConfig')}
|
||||
</h3>
|
||||
|
||||
{/* Node Timeout */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.nodeTimeout')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.nodeTimeoutDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
value={tempClusterConfig.coordinator.nodeTimeout || 15000}
|
||||
onChange={(e) => {
|
||||
const nodeTimeout = parseInt(e.target.value);
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
coordinator: { ...prev.coordinator, nodeTimeout },
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
updateClusterConfig({ coordinator: { ...tempClusterConfig.coordinator } })
|
||||
}
|
||||
placeholder={t('settings.nodeTimeoutPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
min="5000"
|
||||
step="1000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cleanup Interval */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.cleanupInterval')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.cleanupIntervalDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
value={tempClusterConfig.coordinator.cleanupInterval || 30000}
|
||||
onChange={(e) => {
|
||||
const cleanupInterval = parseInt(e.target.value);
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
coordinator: { ...prev.coordinator, cleanupInterval },
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
updateClusterConfig({ coordinator: { ...tempClusterConfig.coordinator } })
|
||||
}
|
||||
placeholder={t('settings.cleanupIntervalPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
min="10000"
|
||||
step="5000"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sticky Session Timeout */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.stickySessionTimeout')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.stickySessionTimeoutDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
value={tempClusterConfig.coordinator.stickySessionTimeout || 3600000}
|
||||
onChange={(e) => {
|
||||
const stickySessionTimeout = parseInt(e.target.value);
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
coordinator: { ...prev.coordinator, stickySessionTimeout },
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
updateClusterConfig({ coordinator: { ...tempClusterConfig.coordinator } })
|
||||
}
|
||||
placeholder={t('settings.stickySessionTimeoutPlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
min="60000"
|
||||
step="60000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sticky Session Configuration */}
|
||||
{tempClusterConfig.enabled &&
|
||||
(tempClusterConfig.mode === 'coordinator' || tempClusterConfig.mode === 'node') && (
|
||||
<div className="p-3 bg-green-50 border border-green-200 rounded-md space-y-3">
|
||||
<h3 className="font-semibold text-gray-800 mb-2">
|
||||
{t('settings.stickySessionConfig')}
|
||||
</h3>
|
||||
|
||||
{/* Enable Sticky Sessions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
{t('settings.stickySessionEnabled')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500">
|
||||
{t('settings.stickySessionEnabledDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
disabled={loading}
|
||||
checked={tempClusterConfig.stickySession.enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
stickySession: { ...prev.stickySession, enabled: checked },
|
||||
}));
|
||||
updateClusterConfig({
|
||||
stickySession: { ...tempClusterConfig.stickySession, enabled: checked },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tempClusterConfig.stickySession.enabled && (
|
||||
<>
|
||||
{/* Session Strategy */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.stickySessionStrategy')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.stickySessionStrategyDescription')}
|
||||
</p>
|
||||
<select
|
||||
value={tempClusterConfig.stickySession.strategy}
|
||||
onChange={(e) => {
|
||||
const strategy = e.target.value as
|
||||
| 'consistent-hash'
|
||||
| 'cookie'
|
||||
| 'header';
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
stickySession: { ...prev.stickySession, strategy },
|
||||
}));
|
||||
updateClusterConfig({
|
||||
stickySession: { ...tempClusterConfig.stickySession, strategy },
|
||||
});
|
||||
}}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="consistent-hash">
|
||||
{t('settings.stickySessionStrategyConsistentHash')}
|
||||
</option>
|
||||
<option value="cookie">
|
||||
{t('settings.stickySessionStrategyCookie')}
|
||||
</option>
|
||||
<option value="header">
|
||||
{t('settings.stickySessionStrategyHeader')}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Cookie Name (only for cookie strategy) */}
|
||||
{tempClusterConfig.stickySession.strategy === 'cookie' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.cookieName')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.cookieNameDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={tempClusterConfig.stickySession.cookieName || 'MCPHUB_NODE'}
|
||||
onChange={(e) => {
|
||||
const cookieName = e.target.value;
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
stickySession: { ...prev.stickySession, cookieName },
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
updateClusterConfig({
|
||||
stickySession: { ...tempClusterConfig.stickySession },
|
||||
})
|
||||
}
|
||||
placeholder={t('settings.cookieNamePlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Name (only for header strategy) */}
|
||||
{tempClusterConfig.stickySession.strategy === 'header' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{t('settings.headerName')}
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mb-2">
|
||||
{t('settings.headerNameDescription')}
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={tempClusterConfig.stickySession.headerName || 'X-MCPHub-Node'}
|
||||
onChange={(e) => {
|
||||
const headerName = e.target.value;
|
||||
setTempClusterConfig((prev) => ({
|
||||
...prev,
|
||||
stickySession: { ...prev.stickySession, headerName },
|
||||
}));
|
||||
}}
|
||||
onBlur={() =>
|
||||
updateClusterConfig({
|
||||
stickySession: { ...tempClusterConfig.stickySession },
|
||||
})
|
||||
}
|
||||
placeholder={t('settings.headerNamePlaceholder')}
|
||||
className="block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PermissionChecker>
|
||||
|
||||
{/* System Settings */}
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card">
|
||||
<div
|
||||
@@ -794,7 +1296,10 @@ const SettingsPage: React.FC = () => {
|
||||
</PermissionChecker>
|
||||
|
||||
{/* Change Password */}
|
||||
<div className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card" data-section="password">
|
||||
<div
|
||||
className="bg-white shadow rounded-lg py-4 px-6 mb-6 dashboard-card"
|
||||
data-section="password"
|
||||
>
|
||||
<div
|
||||
className="flex justify-between items-center cursor-pointer"
|
||||
onClick={() => toggleSection('password')}
|
||||
@@ -864,7 +1369,7 @@ const SettingsPage: React.FC = () => {
|
||||
</div>
|
||||
</PermissionChecker>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage
|
||||
export default SettingsPage;
|
||||
|
||||
@@ -574,6 +574,53 @@
|
||||
"systemSettings": "System Settings",
|
||||
"nameSeparatorLabel": "Name Separator",
|
||||
"nameSeparatorDescription": "Character used to separate server name and tool/prompt name (default: -)",
|
||||
"clusterConfig": "Cluster Configuration",
|
||||
"clusterEnabled": "Enable Cluster Mode",
|
||||
"clusterEnabledDescription": "Enable distributed cluster deployment for high availability and scalability",
|
||||
"clusterMode": "Cluster Mode",
|
||||
"clusterModeDescription": "Select the operating mode for this instance",
|
||||
"clusterModeStandalone": "Standalone",
|
||||
"clusterModeNode": "Node",
|
||||
"clusterModeCoordinator": "Coordinator",
|
||||
"nodeConfig": "Node Configuration",
|
||||
"nodeId": "Node ID",
|
||||
"nodeIdDescription": "Unique identifier for this node (auto-generated if not provided)",
|
||||
"nodeIdPlaceholder": "e.g. node-1",
|
||||
"nodeName": "Node Name",
|
||||
"nodeNameDescription": "Human-readable name for this node (defaults to hostname)",
|
||||
"nodeNamePlaceholder": "e.g. mcp-node-1",
|
||||
"coordinatorUrl": "Coordinator URL",
|
||||
"coordinatorUrlDescription": "URL of the coordinator node to register with",
|
||||
"coordinatorUrlPlaceholder": "http://coordinator:3000",
|
||||
"heartbeatInterval": "Heartbeat Interval (ms)",
|
||||
"heartbeatIntervalDescription": "Interval in milliseconds between heartbeat signals (default: 5000)",
|
||||
"heartbeatIntervalPlaceholder": "5000",
|
||||
"registerOnStartup": "Register on Startup",
|
||||
"registerOnStartupDescription": "Automatically register with coordinator when node starts (default: true)",
|
||||
"coordinatorConfig": "Coordinator Configuration",
|
||||
"nodeTimeout": "Node Timeout (ms)",
|
||||
"nodeTimeoutDescription": "Time in milliseconds before marking a node as unhealthy (default: 15000)",
|
||||
"nodeTimeoutPlaceholder": "15000",
|
||||
"cleanupInterval": "Cleanup Interval (ms)",
|
||||
"cleanupIntervalDescription": "Interval for cleaning up inactive nodes in milliseconds (default: 30000)",
|
||||
"cleanupIntervalPlaceholder": "30000",
|
||||
"stickySessionTimeout": "Sticky Session Timeout (ms)",
|
||||
"stickySessionTimeoutDescription": "Session timeout in milliseconds (default: 3600000 = 1 hour)",
|
||||
"stickySessionTimeoutPlaceholder": "3600000",
|
||||
"stickySessionConfig": "Sticky Session Configuration",
|
||||
"stickySessionEnabled": "Enable Sticky Sessions",
|
||||
"stickySessionEnabledDescription": "Enable session affinity to route requests from the same client to the same node",
|
||||
"stickySessionStrategy": "Session Strategy",
|
||||
"stickySessionStrategyDescription": "Strategy for maintaining session affinity",
|
||||
"stickySessionStrategyConsistentHash": "Consistent Hash",
|
||||
"stickySessionStrategyCookie": "Cookie",
|
||||
"stickySessionStrategyHeader": "Header",
|
||||
"cookieName": "Cookie Name",
|
||||
"cookieNameDescription": "Cookie name for cookie-based sticky sessions (default: MCPHUB_NODE)",
|
||||
"cookieNamePlaceholder": "MCPHUB_NODE",
|
||||
"headerName": "Header Name",
|
||||
"headerNameDescription": "Header name for header-based sticky sessions (default: X-MCPHub-Node)",
|
||||
"headerNamePlaceholder": "X-MCPHub-Node",
|
||||
"restartRequired": "Configuration saved. It is recommended to restart the application to ensure all services load the new settings correctly.",
|
||||
"exportMcpSettings": "Export Settings",
|
||||
"mcpSettingsJson": "MCP Settings JSON",
|
||||
|
||||
@@ -574,6 +574,53 @@
|
||||
"systemSettings": "Paramètres système",
|
||||
"nameSeparatorLabel": "Séparateur de noms",
|
||||
"nameSeparatorDescription": "Caractère utilisé pour séparer le nom du serveur et le nom de l'outil/prompt (par défaut : -)",
|
||||
"clusterConfig": "Configuration du cluster",
|
||||
"clusterEnabled": "Activer le mode cluster",
|
||||
"clusterEnabledDescription": "Activer le déploiement en cluster distribué pour la haute disponibilité et l'évolutivité",
|
||||
"clusterMode": "Mode cluster",
|
||||
"clusterModeDescription": "Sélectionnez le mode de fonctionnement pour cette instance",
|
||||
"clusterModeStandalone": "Autonome",
|
||||
"clusterModeNode": "Nœud",
|
||||
"clusterModeCoordinator": "Coordinateur",
|
||||
"nodeConfig": "Configuration du nœud",
|
||||
"nodeId": "ID du nœud",
|
||||
"nodeIdDescription": "Identifiant unique pour ce nœud (généré automatiquement si non fourni)",
|
||||
"nodeIdPlaceholder": "ex. node-1",
|
||||
"nodeName": "Nom du nœud",
|
||||
"nodeNameDescription": "Nom lisible par l'homme pour ce nœud (par défaut, nom d'hôte)",
|
||||
"nodeNamePlaceholder": "ex. mcp-node-1",
|
||||
"coordinatorUrl": "URL du coordinateur",
|
||||
"coordinatorUrlDescription": "URL du nœud coordinateur auquel s'inscrire",
|
||||
"coordinatorUrlPlaceholder": "http://coordinator:3000",
|
||||
"heartbeatInterval": "Intervalle de battement de cœur (ms)",
|
||||
"heartbeatIntervalDescription": "Intervalle en millisecondes entre les signaux de battement de cœur (par défaut : 5000)",
|
||||
"heartbeatIntervalPlaceholder": "5000",
|
||||
"registerOnStartup": "S'inscrire au démarrage",
|
||||
"registerOnStartupDescription": "S'inscrire automatiquement auprès du coordinateur au démarrage du nœud (par défaut : true)",
|
||||
"coordinatorConfig": "Configuration du coordinateur",
|
||||
"nodeTimeout": "Délai d'expiration du nœud (ms)",
|
||||
"nodeTimeoutDescription": "Temps en millisecondes avant de marquer un nœud comme non sain (par défaut : 15000)",
|
||||
"nodeTimeoutPlaceholder": "15000",
|
||||
"cleanupInterval": "Intervalle de nettoyage (ms)",
|
||||
"cleanupIntervalDescription": "Intervalle de nettoyage des nœuds inactifs en millisecondes (par défaut : 30000)",
|
||||
"cleanupIntervalPlaceholder": "30000",
|
||||
"stickySessionTimeout": "Délai d'expiration de la session persistante (ms)",
|
||||
"stickySessionTimeoutDescription": "Délai d'expiration de la session en millisecondes (par défaut : 3600000 = 1 heure)",
|
||||
"stickySessionTimeoutPlaceholder": "3600000",
|
||||
"stickySessionConfig": "Configuration de la session persistante",
|
||||
"stickySessionEnabled": "Activer les sessions persistantes",
|
||||
"stickySessionEnabledDescription": "Activer l'affinité de session pour acheminer les requêtes du même client vers le même nœud",
|
||||
"stickySessionStrategy": "Stratégie de session",
|
||||
"stickySessionStrategyDescription": "Stratégie pour maintenir l'affinité de session",
|
||||
"stickySessionStrategyConsistentHash": "Hachage cohérent",
|
||||
"stickySessionStrategyCookie": "Cookie",
|
||||
"stickySessionStrategyHeader": "En-tête",
|
||||
"cookieName": "Nom du cookie",
|
||||
"cookieNameDescription": "Nom du cookie pour les sessions persistantes basées sur les cookies (par défaut : MCPHUB_NODE)",
|
||||
"cookieNamePlaceholder": "MCPHUB_NODE",
|
||||
"headerName": "Nom de l'en-tête",
|
||||
"headerNameDescription": "Nom de l'en-tête pour les sessions persistantes basées sur les en-têtes (par défaut : X-MCPHub-Node)",
|
||||
"headerNamePlaceholder": "X-MCPHub-Node",
|
||||
"restartRequired": "Configuration enregistrée. Il est recommandé de redémarrer l'application pour s'assurer que tous les services chargent correctement les nouveaux paramètres.",
|
||||
"exportMcpSettings": "Exporter les paramètres",
|
||||
"mcpSettingsJson": "JSON des paramètres MCP",
|
||||
|
||||
@@ -576,6 +576,53 @@
|
||||
"systemSettings": "系统设置",
|
||||
"nameSeparatorLabel": "名称分隔符",
|
||||
"nameSeparatorDescription": "用于分隔服务器名称和工具/提示名称(默认:-)",
|
||||
"clusterConfig": "集群配置",
|
||||
"clusterEnabled": "启用集群模式",
|
||||
"clusterEnabledDescription": "启用分布式集群部署,实现高可用和可扩展性",
|
||||
"clusterMode": "集群模式",
|
||||
"clusterModeDescription": "选择此实例的运行模式",
|
||||
"clusterModeStandalone": "独立模式",
|
||||
"clusterModeNode": "节点模式",
|
||||
"clusterModeCoordinator": "协调器模式",
|
||||
"nodeConfig": "节点配置",
|
||||
"nodeId": "节点 ID",
|
||||
"nodeIdDescription": "节点的唯一标识符(如果未提供则自动生成)",
|
||||
"nodeIdPlaceholder": "例如: node-1",
|
||||
"nodeName": "节点名称",
|
||||
"nodeNameDescription": "节点的可读名称(默认为主机名)",
|
||||
"nodeNamePlaceholder": "例如: mcp-node-1",
|
||||
"coordinatorUrl": "协调器地址",
|
||||
"coordinatorUrlDescription": "要注册的协调器节点的地址",
|
||||
"coordinatorUrlPlaceholder": "http://coordinator:3000",
|
||||
"heartbeatInterval": "心跳间隔(毫秒)",
|
||||
"heartbeatIntervalDescription": "心跳信号的发送间隔,单位为毫秒(默认:5000)",
|
||||
"heartbeatIntervalPlaceholder": "5000",
|
||||
"registerOnStartup": "启动时注册",
|
||||
"registerOnStartupDescription": "节点启动时自动向协调器注册(默认:true)",
|
||||
"coordinatorConfig": "协调器配置",
|
||||
"nodeTimeout": "节点超时(毫秒)",
|
||||
"nodeTimeoutDescription": "将节点标记为不健康之前的超时时间,单位为毫秒(默认:15000)",
|
||||
"nodeTimeoutPlaceholder": "15000",
|
||||
"cleanupInterval": "清理间隔(毫秒)",
|
||||
"cleanupIntervalDescription": "清理非活动节点的间隔时间,单位为毫秒(默认:30000)",
|
||||
"cleanupIntervalPlaceholder": "30000",
|
||||
"stickySessionTimeout": "会话超时(毫秒)",
|
||||
"stickySessionTimeoutDescription": "会话的超时时间,单位为毫秒(默认:3600000 = 1 小时)",
|
||||
"stickySessionTimeoutPlaceholder": "3600000",
|
||||
"stickySessionConfig": "会话保持配置",
|
||||
"stickySessionEnabled": "启用会话保持",
|
||||
"stickySessionEnabledDescription": "启用会话亲和性,将来自同一客户端的请求路由到同一节点",
|
||||
"stickySessionStrategy": "会话策略",
|
||||
"stickySessionStrategyDescription": "维护会话亲和性的策略",
|
||||
"stickySessionStrategyConsistentHash": "一致性哈希",
|
||||
"stickySessionStrategyCookie": "Cookie",
|
||||
"stickySessionStrategyHeader": "Header",
|
||||
"cookieName": "Cookie 名称",
|
||||
"cookieNameDescription": "基于 Cookie 的会话保持使用的 Cookie 名称(默认:MCPHUB_NODE)",
|
||||
"cookieNamePlaceholder": "MCPHUB_NODE",
|
||||
"headerName": "Header 名称",
|
||||
"headerNameDescription": "基于 Header 的会话保持使用的 Header 名称(默认:X-MCPHub-Node)",
|
||||
"headerNamePlaceholder": "X-MCPHub-Node",
|
||||
"restartRequired": "配置已保存。为确保所有服务正确加载新设置,建议重启应用。",
|
||||
"exportMcpSettings": "导出配置",
|
||||
"mcpSettingsJson": "MCP 配置 JSON",
|
||||
|
||||
@@ -508,7 +508,7 @@ export const updateToolDescription = async (req: Request, res: Response): Promis
|
||||
|
||||
export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
try {
|
||||
const { routing, install, smartRouting, mcpRouter, nameSeparator } = req.body;
|
||||
const { routing, install, smartRouting, mcpRouter, nameSeparator, cluster } = req.body;
|
||||
const currentUser = (req as any).user;
|
||||
|
||||
if (
|
||||
@@ -533,7 +533,8 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
typeof mcpRouter.referer !== 'string' &&
|
||||
typeof mcpRouter.title !== 'string' &&
|
||||
typeof mcpRouter.baseUrl !== 'string')) &&
|
||||
typeof nameSeparator !== 'string'
|
||||
typeof nameSeparator !== 'string' &&
|
||||
!cluster
|
||||
) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
@@ -610,6 +611,13 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!settings.systemConfig.cluster) {
|
||||
settings.systemConfig.cluster = {
|
||||
enabled: false,
|
||||
mode: 'standalone',
|
||||
};
|
||||
}
|
||||
|
||||
if (routing) {
|
||||
if (typeof routing.enableGlobalRoute === 'boolean') {
|
||||
settings.systemConfig.routing.enableGlobalRoute = routing.enableGlobalRoute;
|
||||
@@ -719,6 +727,88 @@ export const updateSystemConfig = (req: Request, res: Response): void => {
|
||||
settings.systemConfig.nameSeparator = nameSeparator;
|
||||
}
|
||||
|
||||
if (cluster) {
|
||||
if (typeof cluster.enabled === 'boolean') {
|
||||
settings.systemConfig.cluster.enabled = cluster.enabled;
|
||||
}
|
||||
if (
|
||||
typeof cluster.mode === 'string' &&
|
||||
['standalone', 'node', 'coordinator'].includes(cluster.mode)
|
||||
) {
|
||||
settings.systemConfig.cluster.mode = cluster.mode as 'standalone' | 'node' | 'coordinator';
|
||||
}
|
||||
|
||||
// Node configuration
|
||||
if (cluster.node) {
|
||||
if (!settings.systemConfig.cluster.node) {
|
||||
settings.systemConfig.cluster.node = {
|
||||
coordinatorUrl: '',
|
||||
};
|
||||
}
|
||||
if (typeof cluster.node.id === 'string') {
|
||||
settings.systemConfig.cluster.node.id = cluster.node.id;
|
||||
}
|
||||
if (typeof cluster.node.name === 'string') {
|
||||
settings.systemConfig.cluster.node.name = cluster.node.name;
|
||||
}
|
||||
if (typeof cluster.node.coordinatorUrl === 'string') {
|
||||
settings.systemConfig.cluster.node.coordinatorUrl = cluster.node.coordinatorUrl;
|
||||
}
|
||||
if (typeof cluster.node.heartbeatInterval === 'number') {
|
||||
settings.systemConfig.cluster.node.heartbeatInterval = cluster.node.heartbeatInterval;
|
||||
}
|
||||
if (typeof cluster.node.registerOnStartup === 'boolean') {
|
||||
settings.systemConfig.cluster.node.registerOnStartup = cluster.node.registerOnStartup;
|
||||
}
|
||||
}
|
||||
|
||||
// Coordinator configuration
|
||||
if (cluster.coordinator) {
|
||||
if (!settings.systemConfig.cluster.coordinator) {
|
||||
settings.systemConfig.cluster.coordinator = {};
|
||||
}
|
||||
if (typeof cluster.coordinator.nodeTimeout === 'number') {
|
||||
settings.systemConfig.cluster.coordinator.nodeTimeout = cluster.coordinator.nodeTimeout;
|
||||
}
|
||||
if (typeof cluster.coordinator.cleanupInterval === 'number') {
|
||||
settings.systemConfig.cluster.coordinator.cleanupInterval =
|
||||
cluster.coordinator.cleanupInterval;
|
||||
}
|
||||
if (typeof cluster.coordinator.stickySessionTimeout === 'number') {
|
||||
settings.systemConfig.cluster.coordinator.stickySessionTimeout =
|
||||
cluster.coordinator.stickySessionTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
// Sticky session configuration
|
||||
if (cluster.stickySession) {
|
||||
if (!settings.systemConfig.cluster.stickySession) {
|
||||
settings.systemConfig.cluster.stickySession = {
|
||||
enabled: true,
|
||||
strategy: 'consistent-hash',
|
||||
};
|
||||
}
|
||||
if (typeof cluster.stickySession.enabled === 'boolean') {
|
||||
settings.systemConfig.cluster.stickySession.enabled = cluster.stickySession.enabled;
|
||||
}
|
||||
if (
|
||||
typeof cluster.stickySession.strategy === 'string' &&
|
||||
['consistent-hash', 'cookie', 'header'].includes(cluster.stickySession.strategy)
|
||||
) {
|
||||
settings.systemConfig.cluster.stickySession.strategy = cluster.stickySession.strategy as
|
||||
| 'consistent-hash'
|
||||
| 'cookie'
|
||||
| 'header';
|
||||
}
|
||||
if (typeof cluster.stickySession.cookieName === 'string') {
|
||||
settings.systemConfig.cluster.stickySession.cookieName = cluster.stickySession.cookieName;
|
||||
}
|
||||
if (typeof cluster.stickySession.headerName === 'string') {
|
||||
settings.systemConfig.cluster.stickySession.headerName = cluster.stickySession.headerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (saveSettings(settings, currentUser)) {
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user