@samanhappy/mcphub 0.0.9 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/.eslintrc.json +25 -0
- package/.github/workflows/build.yml +51 -0
- package/.github/workflows/release.yml +19 -0
- package/.prettierrc +7 -0
- package/Dockerfile +51 -0
- package/assets/amap-edit.png +0 -0
- package/assets/amap-result.png +0 -0
- package/assets/cherry-mcp.png +0 -0
- package/assets/cursor-mcp.png +0 -0
- package/assets/cursor-query.png +0 -0
- package/assets/cursor-tools.png +0 -0
- package/assets/dashboard.png +0 -0
- package/assets/dashboard.zh.png +0 -0
- package/assets/group.png +0 -0
- package/assets/group.zh.png +0 -0
- package/assets/market.zh.png +0 -0
- package/assets/wegroup.jpg +0 -0
- package/assets/wegroup.png +0 -0
- package/assets/wexin.png +0 -0
- package/bin/mcphub.js +3 -0
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/doc/intro.md +73 -0
- package/doc/intro2.md +232 -0
- package/entrypoint.sh +10 -0
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +13 -0
- package/frontend/postcss.config.js +6 -0
- package/frontend/src/App.tsx +44 -0
- package/frontend/src/components/AddGroupForm.tsx +132 -0
- package/frontend/src/components/AddServerForm.tsx +90 -0
- package/frontend/src/components/ChangePasswordForm.tsx +158 -0
- package/frontend/src/components/EditGroupForm.tsx +149 -0
- package/frontend/src/components/EditServerForm.tsx +76 -0
- package/frontend/src/components/GroupCard.tsx +143 -0
- package/frontend/src/components/MarketServerCard.tsx +153 -0
- package/frontend/src/components/MarketServerDetail.tsx +297 -0
- package/frontend/src/components/ProtectedRoute.tsx +27 -0
- package/frontend/src/components/ServerCard.tsx +230 -0
- package/frontend/src/components/ServerForm.tsx +276 -0
- package/frontend/src/components/icons/LucideIcons.tsx +14 -0
- package/frontend/src/components/layout/Content.tsx +17 -0
- package/frontend/src/components/layout/Header.tsx +61 -0
- package/frontend/src/components/layout/Sidebar.tsx +98 -0
- package/frontend/src/components/ui/Badge.tsx +33 -0
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +48 -0
- package/frontend/src/components/ui/Pagination.tsx +128 -0
- package/frontend/src/components/ui/Toast.tsx +96 -0
- package/frontend/src/components/ui/ToggleGroup.tsx +134 -0
- package/frontend/src/components/ui/ToolCard.tsx +38 -0
- package/frontend/src/contexts/AuthContext.tsx +159 -0
- package/frontend/src/contexts/ToastContext.tsx +60 -0
- package/frontend/src/hooks/useGroupData.ts +232 -0
- package/frontend/src/hooks/useMarketData.ts +410 -0
- package/frontend/src/hooks/useServerData.ts +306 -0
- package/frontend/src/hooks/useSettingsData.ts +131 -0
- package/frontend/src/i18n.ts +42 -0
- package/frontend/src/index.css +20 -0
- package/frontend/src/layouts/MainLayout.tsx +33 -0
- package/frontend/src/locales/en.json +214 -0
- package/frontend/src/locales/zh.json +214 -0
- package/frontend/src/main.tsx +12 -0
- package/frontend/src/pages/Dashboard.tsx +206 -0
- package/frontend/src/pages/GroupsPage.tsx +116 -0
- package/frontend/src/pages/LoginPage.tsx +104 -0
- package/frontend/src/pages/MarketPage.tsx +356 -0
- package/frontend/src/pages/ServersPage.tsx +144 -0
- package/frontend/src/pages/SettingsPage.tsx +149 -0
- package/frontend/src/services/authService.ts +141 -0
- package/frontend/src/types/index.ts +160 -0
- package/frontend/src/utils/cn.ts +10 -0
- package/frontend/tsconfig.json +31 -0
- package/frontend/tsconfig.node.json +10 -0
- package/frontend/vite.config.ts +26 -0
- package/googled76ca578b6543fbc.html +1 -0
- package/jest.config.js +10 -0
- package/mcp_settings.json +45 -0
- package/package.json +5 -8
- package/servers.json +74722 -0
- package/src/config/index.ts +46 -0
- package/src/controllers/authController.ts +179 -0
- package/src/controllers/groupController.ts +341 -0
- package/src/controllers/marketController.ts +154 -0
- package/src/controllers/serverController.ts +303 -0
- package/src/index.ts +17 -0
- package/src/middlewares/auth.ts +28 -0
- package/src/middlewares/index.ts +43 -0
- package/src/models/User.ts +103 -0
- package/src/routes/index.ts +96 -0
- package/src/server.ts +72 -0
- package/src/services/groupService.ts +232 -0
- package/src/services/marketService.ts +116 -0
- package/src/services/mcpService.ts +385 -0
- package/src/services/sseService.ts +119 -0
- package/src/types/index.ts +129 -0
- package/src/utils/migration.ts +52 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { useGroupData } from '@/hooks/useGroupData'
|
|
4
|
+
import { useServerData } from '@/hooks/useServerData'
|
|
5
|
+
import { GroupFormData, Server } from '@/types'
|
|
6
|
+
import { ToggleGroup } from './ui/ToggleGroup'
|
|
7
|
+
|
|
8
|
+
interface AddGroupFormProps {
|
|
9
|
+
onAdd: () => void
|
|
10
|
+
onCancel: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
|
|
14
|
+
const { t } = useTranslation()
|
|
15
|
+
const { createGroup } = useGroupData()
|
|
16
|
+
const { servers } = useServerData()
|
|
17
|
+
const [availableServers, setAvailableServers] = useState<Server[]>([])
|
|
18
|
+
const [error, setError] = useState<string | null>(null)
|
|
19
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
20
|
+
|
|
21
|
+
const [formData, setFormData] = useState<GroupFormData>({
|
|
22
|
+
name: '',
|
|
23
|
+
description: '',
|
|
24
|
+
servers: []
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
// Filter available servers (enabled only)
|
|
29
|
+
setAvailableServers(servers.filter(server => server.enabled !== false))
|
|
30
|
+
}, [servers])
|
|
31
|
+
|
|
32
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
33
|
+
const { name, value } = e.target
|
|
34
|
+
setFormData(prev => ({
|
|
35
|
+
...prev,
|
|
36
|
+
[name]: value
|
|
37
|
+
}))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
setIsSubmitting(true)
|
|
43
|
+
setError(null)
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (!formData.name.trim()) {
|
|
47
|
+
setError(t('groups.nameRequired'))
|
|
48
|
+
setIsSubmitting(false)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await createGroup(formData.name, formData.description, formData.servers)
|
|
53
|
+
|
|
54
|
+
if (!result) {
|
|
55
|
+
setError(t('groups.createError'))
|
|
56
|
+
setIsSubmitting(false)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onAdd()
|
|
61
|
+
} catch (err) {
|
|
62
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
63
|
+
setIsSubmitting(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
|
69
|
+
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
|
70
|
+
<div className="p-6">
|
|
71
|
+
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.addNew')}</h2>
|
|
72
|
+
|
|
73
|
+
{error && (
|
|
74
|
+
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
|
75
|
+
{error}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<form onSubmit={handleSubmit}>
|
|
80
|
+
<div className="mb-4">
|
|
81
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
|
82
|
+
{t('groups.name')} *
|
|
83
|
+
</label>
|
|
84
|
+
<input
|
|
85
|
+
type="text"
|
|
86
|
+
id="name"
|
|
87
|
+
name="name"
|
|
88
|
+
value={formData.name}
|
|
89
|
+
onChange={handleChange}
|
|
90
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
91
|
+
placeholder={t('groups.namePlaceholder')}
|
|
92
|
+
required
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<ToggleGroup
|
|
97
|
+
className="mb-6"
|
|
98
|
+
label={t('groups.servers')}
|
|
99
|
+
noOptionsText={t('groups.noServerOptions')}
|
|
100
|
+
values={formData.servers}
|
|
101
|
+
options={availableServers.map(server => ({
|
|
102
|
+
value: server.name,
|
|
103
|
+
label: server.name
|
|
104
|
+
}))}
|
|
105
|
+
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<div className="flex justify-end space-x-3">
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={onCancel}
|
|
112
|
+
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
|
113
|
+
disabled={isSubmitting}
|
|
114
|
+
>
|
|
115
|
+
{t('common.cancel')}
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
type="submit"
|
|
119
|
+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
|
120
|
+
disabled={isSubmitting}
|
|
121
|
+
>
|
|
122
|
+
{isSubmitting ? t('common.submitting') : t('common.create')}
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
</form>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default AddGroupForm
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import ServerForm from './ServerForm'
|
|
4
|
+
|
|
5
|
+
interface AddServerFormProps {
|
|
6
|
+
onAdd: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const AddServerForm = ({ onAdd }: AddServerFormProps) => {
|
|
10
|
+
const { t } = useTranslation()
|
|
11
|
+
const [modalVisible, setModalVisible] = useState(false)
|
|
12
|
+
const [error, setError] = useState<string | null>(null)
|
|
13
|
+
|
|
14
|
+
const toggleModal = () => {
|
|
15
|
+
setModalVisible(!modalVisible)
|
|
16
|
+
setError(null) // Clear any previous errors when toggling modal
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const handleSubmit = async (payload: any) => {
|
|
20
|
+
try {
|
|
21
|
+
setError(null)
|
|
22
|
+
const token = localStorage.getItem('mcphub_token');
|
|
23
|
+
const response = await fetch('/api/servers', {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
'x-auth-token': token || ''
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify(payload),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const result = await response.json()
|
|
33
|
+
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
// Use specific error message from the response if available
|
|
36
|
+
if (result && result.message) {
|
|
37
|
+
setError(result.message)
|
|
38
|
+
} else if (response.status === 400) {
|
|
39
|
+
setError(t('server.invalidData'))
|
|
40
|
+
} else if (response.status === 409) {
|
|
41
|
+
setError(t('server.alreadyExists', { serverName: payload.name }))
|
|
42
|
+
} else {
|
|
43
|
+
setError(t('server.addError'))
|
|
44
|
+
}
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setModalVisible(false)
|
|
49
|
+
onAdd()
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error('Error adding server:', err)
|
|
52
|
+
|
|
53
|
+
// Use friendly error messages based on error type
|
|
54
|
+
if (!navigator.onLine) {
|
|
55
|
+
setError(t('errors.network'))
|
|
56
|
+
} else if (err instanceof TypeError && (
|
|
57
|
+
err.message.includes('NetworkError') ||
|
|
58
|
+
err.message.includes('Failed to fetch')
|
|
59
|
+
)) {
|
|
60
|
+
setError(t('errors.serverConnection'))
|
|
61
|
+
} else {
|
|
62
|
+
setError(t('errors.serverAdd'))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<button
|
|
70
|
+
onClick={toggleModal}
|
|
71
|
+
className="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-4 rounded"
|
|
72
|
+
>
|
|
73
|
+
{t('server.addServer')}
|
|
74
|
+
</button>
|
|
75
|
+
|
|
76
|
+
{modalVisible && (
|
|
77
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
|
78
|
+
<ServerForm
|
|
79
|
+
onSubmit={handleSubmit}
|
|
80
|
+
onCancel={toggleModal}
|
|
81
|
+
modalTitle={t('server.addServer')}
|
|
82
|
+
formError={error}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default AddServerForm
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ChangePasswordCredentials } from '../types';
|
|
4
|
+
import { changePassword } from '../services/authService';
|
|
5
|
+
|
|
6
|
+
interface ChangePasswordFormProps {
|
|
7
|
+
onSuccess?: () => void;
|
|
8
|
+
onCancel?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const ChangePasswordForm: React.FC<ChangePasswordFormProps> = ({ onSuccess, onCancel }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
const [formData, setFormData] = useState<ChangePasswordCredentials>({
|
|
14
|
+
currentPassword: '',
|
|
15
|
+
newPassword: '',
|
|
16
|
+
});
|
|
17
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
18
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState<string | null>(null);
|
|
20
|
+
const [success, setSuccess] = useState(false);
|
|
21
|
+
|
|
22
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
23
|
+
const { name, value } = e.target;
|
|
24
|
+
if (name === 'confirmPassword') {
|
|
25
|
+
setConfirmPassword(value);
|
|
26
|
+
} else {
|
|
27
|
+
setFormData(prev => ({ ...prev, [name]: value }));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
32
|
+
e.preventDefault();
|
|
33
|
+
setError(null);
|
|
34
|
+
|
|
35
|
+
// Validate passwords match
|
|
36
|
+
if (formData.newPassword !== confirmPassword) {
|
|
37
|
+
setError(t('auth.passwordsNotMatch'));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setIsLoading(true);
|
|
42
|
+
try {
|
|
43
|
+
const response = await changePassword(formData);
|
|
44
|
+
|
|
45
|
+
if (response.success) {
|
|
46
|
+
setSuccess(true);
|
|
47
|
+
if (onSuccess) {
|
|
48
|
+
onSuccess();
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
setError(response.message || t('auth.changePasswordError'));
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
setError(t('auth.changePasswordError'));
|
|
55
|
+
} finally {
|
|
56
|
+
setIsLoading(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="p-6 bg-white rounded-lg shadow-md">
|
|
62
|
+
<h2 className="text-xl font-bold mb-4">{t('auth.changePassword')}</h2>
|
|
63
|
+
|
|
64
|
+
{success ? (
|
|
65
|
+
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
|
|
66
|
+
{t('auth.changePasswordSuccess')}
|
|
67
|
+
</div>
|
|
68
|
+
) : (
|
|
69
|
+
<form onSubmit={handleSubmit}>
|
|
70
|
+
{error && (
|
|
71
|
+
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
|
|
72
|
+
{error}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
|
|
76
|
+
<div className="mb-4">
|
|
77
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="currentPassword">
|
|
78
|
+
{t('auth.currentPassword')}
|
|
79
|
+
</label>
|
|
80
|
+
<input
|
|
81
|
+
type="password"
|
|
82
|
+
id="currentPassword"
|
|
83
|
+
name="currentPassword"
|
|
84
|
+
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
85
|
+
value={formData.currentPassword}
|
|
86
|
+
onChange={handleChange}
|
|
87
|
+
required
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="mb-4">
|
|
92
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="newPassword">
|
|
93
|
+
{t('auth.newPassword')}
|
|
94
|
+
</label>
|
|
95
|
+
<input
|
|
96
|
+
type="password"
|
|
97
|
+
id="newPassword"
|
|
98
|
+
name="newPassword"
|
|
99
|
+
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
100
|
+
value={formData.newPassword}
|
|
101
|
+
onChange={handleChange}
|
|
102
|
+
required
|
|
103
|
+
minLength={6}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="mb-6">
|
|
108
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="confirmPassword">
|
|
109
|
+
{t('auth.confirmPassword')}
|
|
110
|
+
</label>
|
|
111
|
+
<input
|
|
112
|
+
type="password"
|
|
113
|
+
id="confirmPassword"
|
|
114
|
+
name="confirmPassword"
|
|
115
|
+
className="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
116
|
+
value={confirmPassword}
|
|
117
|
+
onChange={handleChange}
|
|
118
|
+
required
|
|
119
|
+
minLength={6}
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="flex justify-end space-x-2">
|
|
124
|
+
{onCancel && (
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={onCancel}
|
|
128
|
+
disabled={isLoading}
|
|
129
|
+
className="py-2 px-4 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
130
|
+
>
|
|
131
|
+
{t('common.cancel')}
|
|
132
|
+
</button>
|
|
133
|
+
)}
|
|
134
|
+
<button
|
|
135
|
+
type="submit"
|
|
136
|
+
disabled={isLoading}
|
|
137
|
+
className="py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
138
|
+
>
|
|
139
|
+
{isLoading ? (
|
|
140
|
+
<span className="flex items-center">
|
|
141
|
+
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
142
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
143
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
144
|
+
</svg>
|
|
145
|
+
{t('common.save')}
|
|
146
|
+
</span>
|
|
147
|
+
) : (
|
|
148
|
+
t('common.save')
|
|
149
|
+
)}
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</form>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export default ChangePasswordForm;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Group, GroupFormData, Server } from '@/types'
|
|
4
|
+
import { useGroupData } from '@/hooks/useGroupData'
|
|
5
|
+
import { useServerData } from '@/hooks/useServerData'
|
|
6
|
+
import { ToggleGroup } from './ui/ToggleGroup'
|
|
7
|
+
|
|
8
|
+
interface EditGroupFormProps {
|
|
9
|
+
group: Group
|
|
10
|
+
onEdit: () => void
|
|
11
|
+
onCancel: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
|
|
15
|
+
const { t } = useTranslation()
|
|
16
|
+
const { updateGroup } = useGroupData()
|
|
17
|
+
const { servers } = useServerData()
|
|
18
|
+
const [availableServers, setAvailableServers] = useState<Server[]>([])
|
|
19
|
+
const [error, setError] = useState<string | null>(null)
|
|
20
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
21
|
+
|
|
22
|
+
const [formData, setFormData] = useState<GroupFormData>({
|
|
23
|
+
name: group.name,
|
|
24
|
+
description: group.description || '',
|
|
25
|
+
servers: group.servers || []
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
// Filter available servers (enabled only)
|
|
30
|
+
setAvailableServers(servers.filter(server => server.enabled !== false))
|
|
31
|
+
}, [servers])
|
|
32
|
+
|
|
33
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
34
|
+
const { name, value } = e.target
|
|
35
|
+
setFormData(prev => ({
|
|
36
|
+
...prev,
|
|
37
|
+
[name]: value
|
|
38
|
+
}))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleServerToggle = (serverName: string) => {
|
|
42
|
+
setFormData(prev => {
|
|
43
|
+
const isSelected = prev.servers.includes(serverName)
|
|
44
|
+
return {
|
|
45
|
+
...prev,
|
|
46
|
+
servers: isSelected
|
|
47
|
+
? prev.servers.filter(name => name !== serverName)
|
|
48
|
+
: [...prev.servers, serverName]
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
setIsSubmitting(true)
|
|
56
|
+
setError(null)
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (!formData.name.trim()) {
|
|
60
|
+
setError(t('groups.nameRequired'))
|
|
61
|
+
setIsSubmitting(false)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await updateGroup(group.id, {
|
|
66
|
+
name: formData.name,
|
|
67
|
+
description: formData.description,
|
|
68
|
+
servers: formData.servers
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (!result) {
|
|
72
|
+
setError(t('groups.updateError'))
|
|
73
|
+
setIsSubmitting(false)
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
onEdit()
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
80
|
+
setIsSubmitting(false)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
|
86
|
+
<div className="bg-white rounded-lg shadow-lg max-w-md w-full">
|
|
87
|
+
<div className="p-6">
|
|
88
|
+
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t('groups.edit')}</h2>
|
|
89
|
+
|
|
90
|
+
{error && (
|
|
91
|
+
<div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
|
|
92
|
+
{error}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<form onSubmit={handleSubmit}>
|
|
97
|
+
<div className="mb-4">
|
|
98
|
+
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="name">
|
|
99
|
+
{t('groups.name')} *
|
|
100
|
+
</label>
|
|
101
|
+
<input
|
|
102
|
+
type="text"
|
|
103
|
+
id="name"
|
|
104
|
+
name="name"
|
|
105
|
+
value={formData.name}
|
|
106
|
+
onChange={handleChange}
|
|
107
|
+
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
|
108
|
+
placeholder={t('groups.namePlaceholder')}
|
|
109
|
+
required
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<ToggleGroup
|
|
114
|
+
className="mb-6"
|
|
115
|
+
label={t('groups.servers')}
|
|
116
|
+
noOptionsText={t('groups.noServerOptions')}
|
|
117
|
+
values={formData.servers}
|
|
118
|
+
options={availableServers.map(server => ({
|
|
119
|
+
value: server.name,
|
|
120
|
+
label: server.name
|
|
121
|
+
}))}
|
|
122
|
+
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
<div className="flex justify-end space-x-3">
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
onClick={onCancel}
|
|
129
|
+
className="px-4 py-2 text-gray-600 hover:text-gray-800"
|
|
130
|
+
disabled={isSubmitting}
|
|
131
|
+
>
|
|
132
|
+
{t('common.cancel')}
|
|
133
|
+
</button>
|
|
134
|
+
<button
|
|
135
|
+
type="submit"
|
|
136
|
+
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
|
137
|
+
disabled={isSubmitting}
|
|
138
|
+
>
|
|
139
|
+
{isSubmitting ? t('common.submitting') : t('common.save')}
|
|
140
|
+
</button>
|
|
141
|
+
</div>
|
|
142
|
+
</form>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export default EditGroupForm
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Server } from '@/types'
|
|
4
|
+
import ServerForm from './ServerForm'
|
|
5
|
+
|
|
6
|
+
interface EditServerFormProps {
|
|
7
|
+
server: Server
|
|
8
|
+
onEdit: () => void
|
|
9
|
+
onCancel: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const EditServerForm = ({ server, onEdit, onCancel }: EditServerFormProps) => {
|
|
13
|
+
const { t } = useTranslation()
|
|
14
|
+
const [error, setError] = useState<string | null>(null)
|
|
15
|
+
|
|
16
|
+
const handleSubmit = async (payload: any) => {
|
|
17
|
+
try {
|
|
18
|
+
setError(null)
|
|
19
|
+
const token = localStorage.getItem('mcphub_token');
|
|
20
|
+
const response = await fetch(`/api/servers/${server.name}`, {
|
|
21
|
+
method: 'PUT',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'x-auth-token': token || ''
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify(payload),
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const result = await response.json()
|
|
30
|
+
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
// Use specific error message from the response if available
|
|
33
|
+
if (result && result.message) {
|
|
34
|
+
setError(result.message)
|
|
35
|
+
} else if (response.status === 404) {
|
|
36
|
+
setError(t('server.notFound', { serverName: server.name }))
|
|
37
|
+
} else if (response.status === 400) {
|
|
38
|
+
setError(t('server.invalidData'))
|
|
39
|
+
} else {
|
|
40
|
+
setError(t('server.updateError', { serverName: server.name }))
|
|
41
|
+
}
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onEdit()
|
|
46
|
+
} catch (err) {
|
|
47
|
+
console.error('Error updating server:', err)
|
|
48
|
+
|
|
49
|
+
// Use friendly error messages based on error type
|
|
50
|
+
if (!navigator.onLine) {
|
|
51
|
+
setError(t('errors.network'))
|
|
52
|
+
} else if (err instanceof TypeError && (
|
|
53
|
+
err.message.includes('NetworkError') ||
|
|
54
|
+
err.message.includes('Failed to fetch')
|
|
55
|
+
)) {
|
|
56
|
+
setError(t('errors.serverConnection'))
|
|
57
|
+
} else {
|
|
58
|
+
setError(t('errors.serverUpdate', { serverName: server.name }))
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
|
65
|
+
<ServerForm
|
|
66
|
+
onSubmit={handleSubmit}
|
|
67
|
+
onCancel={onCancel}
|
|
68
|
+
initialData={server}
|
|
69
|
+
modalTitle={t('server.editTitle', { serverName: server.name })}
|
|
70
|
+
formError={error}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default EditServerForm
|