mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: introduce runtime path (#132)
This commit is contained in:
@@ -12,16 +12,7 @@ import GroupsPage from './pages/GroupsPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import MarketPage from './pages/MarketPage';
|
||||
import LogsPage from './pages/LogsPage';
|
||||
|
||||
// Get base path from environment variable or default to empty string
|
||||
const getBasePath = (): string => {
|
||||
const basePath = import.meta.env.BASE_PATH || '';
|
||||
// Ensure the path starts with / if it's not empty and doesn't already start with /
|
||||
if (basePath && !basePath.startsWith('/')) {
|
||||
return '/' + basePath;
|
||||
}
|
||||
return basePath;
|
||||
};
|
||||
import { getBasePath } from './utils/runtime';
|
||||
|
||||
function App() {
|
||||
const basename = getBasePath();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ServerForm from './ServerForm'
|
||||
import { getApiUrl } from '../utils/api'
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
interface AddServerFormProps {
|
||||
onAdd: () => void
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Server } from '@/types'
|
||||
import { getApiUrl } from '../utils/runtime'
|
||||
import ServerForm from './ServerForm'
|
||||
|
||||
interface EditServerFormProps {
|
||||
@@ -17,7 +18,7 @@ const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
||||
try {
|
||||
setError(null)
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(`/api/servers/${server.name}`, {
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Group, ApiResponse } from '@/types';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
export const useGroupData = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MarketServer, ApiResponse } from '@/types';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
export const useMarketData = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Server, ApiResponse } from '@/types';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
// Configuration options
|
||||
const CONFIG = {
|
||||
@@ -204,7 +204,7 @@ export const useServerData = () => {
|
||||
try {
|
||||
// Fetch settings to get the full server config before editing
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(`/api/settings`, {
|
||||
const response = await fetch(getApiUrl('/settings'), {
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
},
|
||||
@@ -241,7 +241,7 @@ export const useServerData = () => {
|
||||
const handleServerRemove = async (serverName: string) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(`/api/servers/${serverName}`, {
|
||||
const response = await fetch(getApiUrl(`/servers/${serverName}`), {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'x-auth-token': token || '',
|
||||
@@ -265,7 +265,7 @@ export const useServerData = () => {
|
||||
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
||||
try {
|
||||
const token = localStorage.getItem('mcphub_token');
|
||||
const response = await fetch(`/api/servers/${server.name}/toggle`, {
|
||||
const response = await fetch(getApiUrl(`/servers/${server.name}/toggle`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ApiResponse } from '@/types';
|
||||
import { useToast } from '@/contexts/ToastContext';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
// Define types for the settings data
|
||||
interface RoutingConfig {
|
||||
|
||||
@@ -1,12 +1,45 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
// Import the i18n configuration
|
||||
import './i18n'
|
||||
import './i18n';
|
||||
import { loadRuntimeConfig } from './utils/runtime';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
// Load runtime configuration before starting the app
|
||||
async function initializeApp() {
|
||||
try {
|
||||
console.log('Loading runtime configuration...');
|
||||
const config = await loadRuntimeConfig();
|
||||
console.log('Runtime configuration loaded:', config);
|
||||
|
||||
// Store config in window object
|
||||
window.__MCPHUB_CONFIG__ = config;
|
||||
|
||||
// Start React app
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize app:', error);
|
||||
|
||||
// Fallback: start app with default config
|
||||
console.log('Starting app with default configuration...');
|
||||
window.__MCPHUB_CONFIG__ = {
|
||||
basePath: '',
|
||||
version: 'dev',
|
||||
name: 'mcphub',
|
||||
};
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the app
|
||||
initializeApp();
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
RegisterCredentials,
|
||||
ChangePasswordCredentials,
|
||||
} from '../types';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
// Token key in localStorage
|
||||
const TOKEN_KEY = 'mcphub_token';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getToken } from './authService'; // Import getToken function
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import { getApiUrl } from '../utils/runtime';
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
|
||||
15
frontend/src/types/runtime.ts
Normal file
15
frontend/src/types/runtime.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Global runtime configuration interface
|
||||
export interface RuntimeConfig {
|
||||
basePath: string;
|
||||
version: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// Extend Window interface to include runtime config
|
||||
declare global {
|
||||
interface Window {
|
||||
__MCPHUB_CONFIG__?: RuntimeConfig;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,27 +1,28 @@
|
||||
/**
|
||||
* API utility functions for constructing URLs with proper base path support
|
||||
*
|
||||
* @deprecated Use functions from utils/runtime.ts instead for runtime configuration support
|
||||
*/
|
||||
|
||||
import { getApiBaseUrl as getRuntimeApiBaseUrl, getApiUrl as getRuntimeApiUrl } from './runtime';
|
||||
|
||||
/**
|
||||
* Get the API base URL including base path and /api prefix
|
||||
* @returns The complete API base URL
|
||||
* @deprecated Use getApiBaseUrl from utils/runtime.ts instead
|
||||
*/
|
||||
export const getApiBaseUrl = (): string => {
|
||||
const basePath = import.meta.env.BASE_PATH || '';
|
||||
// Ensure the path starts with / if it's not empty and doesn't already start with /
|
||||
const normalizedBasePath = basePath && !basePath.startsWith('/') ? '/' + basePath : basePath;
|
||||
// Always append /api to the base path for API endpoints
|
||||
return normalizedBasePath + '/api';
|
||||
console.warn('getApiBaseUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
|
||||
return getRuntimeApiBaseUrl();
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a full API URL with the given endpoint
|
||||
* @param endpoint - The API endpoint (should start with /, e.g., '/auth/login')
|
||||
* @returns The complete API URL
|
||||
* @deprecated Use getApiUrl from utils/runtime.ts instead
|
||||
*/
|
||||
export const getApiUrl = (endpoint: string): string => {
|
||||
const baseUrl = getApiBaseUrl();
|
||||
// Ensure endpoint starts with /
|
||||
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
|
||||
return baseUrl + normalizedEndpoint;
|
||||
console.warn('getApiUrl from utils/api.ts is deprecated, use utils/runtime.ts instead');
|
||||
return getRuntimeApiUrl(endpoint);
|
||||
};
|
||||
|
||||
105
frontend/src/utils/runtime.ts
Normal file
105
frontend/src/utils/runtime.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { RuntimeConfig } from '../types/runtime';
|
||||
|
||||
/**
|
||||
* Get runtime configuration from window object
|
||||
*/
|
||||
export const getRuntimeConfig = (): RuntimeConfig => {
|
||||
return (
|
||||
window.__MCPHUB_CONFIG__ || {
|
||||
basePath: '',
|
||||
version: 'dev',
|
||||
name: 'mcphub',
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the base path from runtime configuration
|
||||
*/
|
||||
export const getBasePath = (): string => {
|
||||
const config = getRuntimeConfig();
|
||||
const basePath = config.basePath || '';
|
||||
|
||||
// Ensure the path starts with / if it's not empty and doesn't already start with /
|
||||
if (basePath && !basePath.startsWith('/')) {
|
||||
return '/' + basePath;
|
||||
}
|
||||
return basePath;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the API base URL including base path and /api prefix
|
||||
*/
|
||||
export const getApiBaseUrl = (): string => {
|
||||
const basePath = getBasePath();
|
||||
// Always append /api to the base path for API endpoints
|
||||
return basePath + '/api';
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct a full API URL with the given endpoint
|
||||
*/
|
||||
export const getApiUrl = (endpoint: string): string => {
|
||||
const baseUrl = getApiBaseUrl();
|
||||
// Ensure endpoint starts with /
|
||||
const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : '/' + endpoint;
|
||||
return baseUrl + normalizedEndpoint;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load runtime configuration from server
|
||||
*/
|
||||
export const loadRuntimeConfig = async (): Promise<RuntimeConfig> => {
|
||||
try {
|
||||
// For initial config load, we need to determine the correct path
|
||||
// Try different possible paths based on current location
|
||||
const currentPath = window.location.pathname;
|
||||
const possibleConfigPaths = [
|
||||
// If we're already on a subpath, try to use it
|
||||
currentPath.replace(/\/[^\/]*$/, '') + '/config',
|
||||
// Try root config
|
||||
'/config',
|
||||
// Try with potential base paths
|
||||
...(currentPath.includes('/')
|
||||
? [currentPath.split('/')[1] ? `/${currentPath.split('/')[1]}/config` : '/config']
|
||||
: ['/config']),
|
||||
];
|
||||
|
||||
for (const configPath of possibleConfigPaths) {
|
||||
try {
|
||||
const response = await fetch(configPath, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.data) {
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Continue to next path
|
||||
console.debug(`Failed to load config from ${configPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to default config
|
||||
console.warn('Could not load runtime config from server, using defaults');
|
||||
return {
|
||||
basePath: '',
|
||||
version: 'dev',
|
||||
name: 'mcphub',
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading runtime config:', error);
|
||||
return {
|
||||
basePath: '',
|
||||
version: 'dev',
|
||||
name: 'mcphub',
|
||||
};
|
||||
}
|
||||
};
|
||||
1
frontend/src/vite-env.d.ts
vendored
1
frontend/src/vite-env.d.ts
vendored
@@ -3,7 +3,6 @@
|
||||
interface ImportMeta {
|
||||
readonly env: {
|
||||
readonly PACKAGE_VERSION: string;
|
||||
readonly BASE_PATH?: string; // Add base path environment variable
|
||||
// Add other custom env variables here if needed
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
@@ -8,12 +8,13 @@ import { readFileSync } from 'fs';
|
||||
// Get package.json version
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8'));
|
||||
|
||||
// Get base path from environment variable
|
||||
const basePath = process.env.BASE_PATH || '';
|
||||
// For runtime configuration, we'll always use relative paths
|
||||
// BASE_PATH will be determined at runtime
|
||||
const basePath = '';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: basePath || './', // Use base path or relative paths for assets
|
||||
base: './', // Always use relative paths for runtime configuration
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -21,9 +22,9 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// Make package version and base path available as global variables
|
||||
// Make package version available as global variable
|
||||
// BASE_PATH will be loaded at runtime
|
||||
'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version),
|
||||
'import.meta.env.BASE_PATH': JSON.stringify(basePath),
|
||||
},
|
||||
build: {
|
||||
sourcemap: true, // Enable source maps for production build
|
||||
|
||||
Reference in New Issue
Block a user