@samanhappy/mcphub 0.0.10 → 0.0.12
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/package.json +8 -5
- package/.env.example +0 -2
- package/.eslintrc.json +0 -25
- package/.github/workflows/build.yml +0 -51
- package/.github/workflows/release.yml +0 -19
- package/.prettierrc +0 -7
- package/Dockerfile +0 -51
- 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 +0 -3
- package/doc/intro.md +0 -73
- package/doc/intro2.md +0 -232
- package/entrypoint.sh +0 -10
- package/frontend/favicon.ico +0 -0
- package/frontend/index.html +0 -13
- package/frontend/postcss.config.js +0 -6
- package/frontend/src/App.tsx +0 -44
- package/frontend/src/components/AddGroupForm.tsx +0 -132
- package/frontend/src/components/AddServerForm.tsx +0 -90
- package/frontend/src/components/ChangePasswordForm.tsx +0 -158
- package/frontend/src/components/EditGroupForm.tsx +0 -149
- package/frontend/src/components/EditServerForm.tsx +0 -76
- package/frontend/src/components/GroupCard.tsx +0 -143
- package/frontend/src/components/MarketServerCard.tsx +0 -153
- package/frontend/src/components/MarketServerDetail.tsx +0 -297
- package/frontend/src/components/ProtectedRoute.tsx +0 -27
- package/frontend/src/components/ServerCard.tsx +0 -230
- package/frontend/src/components/ServerForm.tsx +0 -276
- package/frontend/src/components/icons/LucideIcons.tsx +0 -14
- package/frontend/src/components/layout/Content.tsx +0 -17
- package/frontend/src/components/layout/Header.tsx +0 -61
- package/frontend/src/components/layout/Sidebar.tsx +0 -98
- package/frontend/src/components/ui/Badge.tsx +0 -33
- package/frontend/src/components/ui/Button.tsx +0 -0
- package/frontend/src/components/ui/DeleteDialog.tsx +0 -48
- package/frontend/src/components/ui/Pagination.tsx +0 -128
- package/frontend/src/components/ui/Toast.tsx +0 -96
- package/frontend/src/components/ui/ToggleGroup.tsx +0 -134
- package/frontend/src/components/ui/ToolCard.tsx +0 -38
- package/frontend/src/contexts/AuthContext.tsx +0 -159
- package/frontend/src/contexts/ToastContext.tsx +0 -60
- package/frontend/src/hooks/useGroupData.ts +0 -232
- package/frontend/src/hooks/useMarketData.ts +0 -410
- package/frontend/src/hooks/useServerData.ts +0 -306
- package/frontend/src/hooks/useSettingsData.ts +0 -131
- package/frontend/src/i18n.ts +0 -42
- package/frontend/src/index.css +0 -20
- package/frontend/src/layouts/MainLayout.tsx +0 -33
- package/frontend/src/locales/en.json +0 -214
- package/frontend/src/locales/zh.json +0 -214
- package/frontend/src/main.tsx +0 -12
- package/frontend/src/pages/Dashboard.tsx +0 -206
- package/frontend/src/pages/GroupsPage.tsx +0 -116
- package/frontend/src/pages/LoginPage.tsx +0 -104
- package/frontend/src/pages/MarketPage.tsx +0 -356
- package/frontend/src/pages/ServersPage.tsx +0 -144
- package/frontend/src/pages/SettingsPage.tsx +0 -149
- package/frontend/src/services/authService.ts +0 -141
- package/frontend/src/types/index.ts +0 -160
- package/frontend/src/utils/cn.ts +0 -10
- package/frontend/tsconfig.json +0 -31
- package/frontend/tsconfig.node.json +0 -10
- package/frontend/vite.config.ts +0 -26
- package/googled76ca578b6543fbc.html +0 -1
- package/jest.config.js +0 -10
- package/mcp_settings.json +0 -45
- package/servers.json +0 -74722
- package/src/config/index.ts +0 -46
- package/src/controllers/authController.ts +0 -179
- package/src/controllers/groupController.ts +0 -341
- package/src/controllers/marketController.ts +0 -154
- package/src/controllers/serverController.ts +0 -303
- package/src/index.ts +0 -17
- package/src/middlewares/auth.ts +0 -28
- package/src/middlewares/index.ts +0 -43
- package/src/models/User.ts +0 -103
- package/src/routes/index.ts +0 -96
- package/src/server.ts +0 -72
- package/src/services/groupService.ts +0 -232
- package/src/services/marketService.ts +0 -116
- package/src/services/mcpService.ts +0 -385
- package/src/services/sseService.ts +0 -119
- package/src/types/index.ts +0 -129
- package/src/utils/migration.ts +0 -52
- package/tsconfig.json +0 -17
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { useServerData } from '@/hooks/useServerData';
|
|
4
|
-
import { ServerStatus } from '@/types';
|
|
5
|
-
|
|
6
|
-
const DashboardPage: React.FC = () => {
|
|
7
|
-
const { t } = useTranslation();
|
|
8
|
-
const { servers, error, setError, isLoading } = useServerData();
|
|
9
|
-
|
|
10
|
-
// Calculate server statistics
|
|
11
|
-
const serverStats = {
|
|
12
|
-
total: servers.length,
|
|
13
|
-
online: servers.filter(server => server.status === 'connected').length,
|
|
14
|
-
offline: servers.filter(server => server.status === 'disconnected').length,
|
|
15
|
-
connecting: servers.filter(server => server.status === 'connecting').length
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Map status to translation keys
|
|
19
|
-
const statusTranslations = {
|
|
20
|
-
connected: 'status.online',
|
|
21
|
-
disconnected: 'status.offline',
|
|
22
|
-
connecting: 'status.connecting'
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Calculate percentage for each status (for dashboard display)
|
|
26
|
-
const getStatusPercentage = (status: ServerStatus) => {
|
|
27
|
-
if (servers.length === 0) return 0;
|
|
28
|
-
return Math.round((servers.filter(server => server.status === status).length / servers.length) * 100);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<div>
|
|
33
|
-
<h1 className="text-2xl font-bold text-gray-900 mb-8">{t('pages.dashboard.title')}</h1>
|
|
34
|
-
|
|
35
|
-
{error && (
|
|
36
|
-
<div className="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-sm">
|
|
37
|
-
<div className="flex items-center justify-between">
|
|
38
|
-
<div>
|
|
39
|
-
<h3 className="text-red-600 text-lg font-medium">{t('app.error')}</h3>
|
|
40
|
-
<p className="text-gray-600 mt-1">{error}</p>
|
|
41
|
-
</div>
|
|
42
|
-
<button
|
|
43
|
-
onClick={() => setError(null)}
|
|
44
|
-
className="ml-4 text-gray-500 hover:text-gray-700"
|
|
45
|
-
aria-label={t('app.closeButton')}
|
|
46
|
-
>
|
|
47
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
48
|
-
<path fillRule="evenodd" d="M4.293 4.293a1 1 011.414 0L10 8.586l4.293-4.293a1 1 111.414 1.414L11.414 10l4.293 4.293a1 1 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 01-1.414-1.414L8.586 10 4.293 5.707a1 1 010-1.414z" clipRule="evenodd" />
|
|
49
|
-
</svg>
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
)}
|
|
54
|
-
|
|
55
|
-
{isLoading ? (
|
|
56
|
-
<div className="bg-white shadow rounded-lg p-6 flex items-center justify-center">
|
|
57
|
-
<div className="flex flex-col items-center">
|
|
58
|
-
<svg className="animate-spin h-10 w-10 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
59
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
60
|
-
<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>
|
|
61
|
-
</svg>
|
|
62
|
-
<p className="text-gray-600">{t('app.loading')}</p>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
) : (
|
|
66
|
-
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
67
|
-
{/* Total servers */}
|
|
68
|
-
<div className="bg-white rounded-lg shadow p-6">
|
|
69
|
-
<div className="flex items-center">
|
|
70
|
-
<div className="p-3 rounded-full bg-blue-100 text-blue-800">
|
|
71
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
72
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
|
73
|
-
</svg>
|
|
74
|
-
</div>
|
|
75
|
-
<div className="ml-4">
|
|
76
|
-
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.totalServers')}</h2>
|
|
77
|
-
<p className="text-3xl font-bold text-gray-900">{serverStats.total}</p>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{/* Online servers */}
|
|
83
|
-
<div className="bg-white rounded-lg shadow p-6">
|
|
84
|
-
<div className="flex items-center">
|
|
85
|
-
<div className="p-3 rounded-full bg-green-100 text-green-800">
|
|
86
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
87
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
88
|
-
</svg>
|
|
89
|
-
</div>
|
|
90
|
-
<div className="ml-4">
|
|
91
|
-
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.onlineServers')}</h2>
|
|
92
|
-
<p className="text-3xl font-bold text-gray-900">{serverStats.online}</p>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
<div className="mt-4 h-2 bg-gray-200 rounded-full">
|
|
96
|
-
<div
|
|
97
|
-
className="h-full bg-green-500 rounded-full"
|
|
98
|
-
style={{ width: `${getStatusPercentage('connected')}%` }}
|
|
99
|
-
></div>
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
{/* Offline servers */}
|
|
104
|
-
<div className="bg-white rounded-lg shadow p-6">
|
|
105
|
-
<div className="flex items-center">
|
|
106
|
-
<div className="p-3 rounded-full bg-red-100 text-red-800">
|
|
107
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
108
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
109
|
-
</svg>
|
|
110
|
-
</div>
|
|
111
|
-
<div className="ml-4">
|
|
112
|
-
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.offlineServers')}</h2>
|
|
113
|
-
<p className="text-3xl font-bold text-gray-900">{serverStats.offline}</p>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div className="mt-4 h-2 bg-gray-200 rounded-full">
|
|
117
|
-
<div
|
|
118
|
-
className="h-full bg-red-500 rounded-full"
|
|
119
|
-
style={{ width: `${getStatusPercentage('disconnected')}%` }}
|
|
120
|
-
></div>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
{/* Connecting servers */}
|
|
125
|
-
<div className="bg-white rounded-lg shadow p-6">
|
|
126
|
-
<div className="flex items-center">
|
|
127
|
-
<div className="p-3 rounded-full bg-yellow-100 text-yellow-800">
|
|
128
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
129
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
130
|
-
</svg>
|
|
131
|
-
</div>
|
|
132
|
-
<div className="ml-4">
|
|
133
|
-
<h2 className="text-xl font-semibold text-gray-700">{t('pages.dashboard.connectingServers')}</h2>
|
|
134
|
-
<p className="text-3xl font-bold text-gray-900">{serverStats.connecting}</p>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
<div className="mt-4 h-2 bg-gray-200 rounded-full">
|
|
138
|
-
<div
|
|
139
|
-
className="h-full bg-yellow-500 rounded-full"
|
|
140
|
-
style={{ width: `${getStatusPercentage('connecting')}%` }}
|
|
141
|
-
></div>
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
146
|
-
|
|
147
|
-
{/* Recent activity list */}
|
|
148
|
-
{servers.length > 0 && !isLoading && (
|
|
149
|
-
<div className="mt-8">
|
|
150
|
-
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('pages.dashboard.recentServers')}</h2>
|
|
151
|
-
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
152
|
-
<table className="min-w-full divide-y divide-gray-200">
|
|
153
|
-
<thead className="bg-gray-50">
|
|
154
|
-
<tr>
|
|
155
|
-
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
156
|
-
{t('server.name')}
|
|
157
|
-
</th>
|
|
158
|
-
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
159
|
-
{t('server.status')}
|
|
160
|
-
</th>
|
|
161
|
-
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
162
|
-
{t('server.tools')}
|
|
163
|
-
</th>
|
|
164
|
-
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
165
|
-
{t('server.enabled')}
|
|
166
|
-
</th>
|
|
167
|
-
</tr>
|
|
168
|
-
</thead>
|
|
169
|
-
<tbody className="bg-white divide-y divide-gray-200">
|
|
170
|
-
{servers.slice(0, 5).map((server, index) => (
|
|
171
|
-
<tr key={index}>
|
|
172
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
173
|
-
{server.name}
|
|
174
|
-
</td>
|
|
175
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
176
|
-
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${server.status === 'connected'
|
|
177
|
-
? 'bg-green-100 text-green-800'
|
|
178
|
-
: server.status === 'disconnected'
|
|
179
|
-
? 'bg-red-100 text-red-800'
|
|
180
|
-
: 'bg-yellow-100 text-yellow-800'
|
|
181
|
-
}`}>
|
|
182
|
-
{t(statusTranslations[server.status] || server.status)}
|
|
183
|
-
</span>
|
|
184
|
-
</td>
|
|
185
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
186
|
-
{server.tools?.length || 0}
|
|
187
|
-
</td>
|
|
188
|
-
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
189
|
-
{server.enabled !== false ? (
|
|
190
|
-
<span className="text-green-600">✓</span>
|
|
191
|
-
) : (
|
|
192
|
-
<span className="text-red-600">✗</span>
|
|
193
|
-
)}
|
|
194
|
-
</td>
|
|
195
|
-
</tr>
|
|
196
|
-
))}
|
|
197
|
-
</tbody>
|
|
198
|
-
</table>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
)}
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
export default DashboardPage;
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Group } from '@/types';
|
|
4
|
-
import { useGroupData } from '@/hooks/useGroupData';
|
|
5
|
-
import { useServerData } from '@/hooks/useServerData';
|
|
6
|
-
import AddGroupForm from '@/components/AddGroupForm';
|
|
7
|
-
import EditGroupForm from '@/components/EditGroupForm';
|
|
8
|
-
import GroupCard from '@/components/GroupCard';
|
|
9
|
-
|
|
10
|
-
const GroupsPage: React.FC = () => {
|
|
11
|
-
const { t } = useTranslation();
|
|
12
|
-
const {
|
|
13
|
-
groups,
|
|
14
|
-
loading: groupsLoading,
|
|
15
|
-
error: groupError,
|
|
16
|
-
setError: setGroupError,
|
|
17
|
-
deleteGroup,
|
|
18
|
-
triggerRefresh
|
|
19
|
-
} = useGroupData();
|
|
20
|
-
const { servers } = useServerData();
|
|
21
|
-
|
|
22
|
-
const [editingGroup, setEditingGroup] = useState<Group | null>(null);
|
|
23
|
-
const [showAddForm, setShowAddForm] = useState(false);
|
|
24
|
-
|
|
25
|
-
const handleEditClick = (group: Group) => {
|
|
26
|
-
setEditingGroup(group);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const handleEditComplete = () => {
|
|
30
|
-
setEditingGroup(null);
|
|
31
|
-
triggerRefresh(); // Refresh the groups list after editing
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const handleDeleteGroup = async (groupId: string) => {
|
|
35
|
-
const success = await deleteGroup(groupId);
|
|
36
|
-
if (!success) {
|
|
37
|
-
setGroupError(t('groups.deleteError'));
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const handleAddGroup = () => {
|
|
42
|
-
setShowAddForm(true);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const handleAddComplete = () => {
|
|
46
|
-
setShowAddForm(false);
|
|
47
|
-
triggerRefresh(); // Refresh the groups list after adding
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div>
|
|
52
|
-
<div className="flex justify-between items-center mb-8">
|
|
53
|
-
<h1 className="text-2xl font-bold text-gray-900">{t('pages.groups.title')}</h1>
|
|
54
|
-
<div className="flex space-x-4">
|
|
55
|
-
<button
|
|
56
|
-
onClick={handleAddGroup}
|
|
57
|
-
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center"
|
|
58
|
-
>
|
|
59
|
-
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
|
60
|
-
<path fillRule="evenodd" d="M10 3a1 1 0 00-1 1v5H4a1 1 0 100 2h5v5a1 1 0 102 0v-5h5a1 1 0 100-2h-5V4a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
61
|
-
</svg>
|
|
62
|
-
{t('groups.add')}
|
|
63
|
-
</button>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{groupError && (
|
|
68
|
-
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-6">
|
|
69
|
-
<p>{groupError}</p>
|
|
70
|
-
</div>
|
|
71
|
-
)}
|
|
72
|
-
|
|
73
|
-
{groupsLoading ? (
|
|
74
|
-
<div className="bg-white shadow rounded-lg p-6">
|
|
75
|
-
<div className="flex flex-col items-center justify-center">
|
|
76
|
-
<svg className="animate-spin h-10 w-10 text-blue-500 mb-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
77
|
-
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
78
|
-
<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>
|
|
79
|
-
</svg>
|
|
80
|
-
<p className="text-gray-600">{t('app.loading')}</p>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
) : groups.length === 0 ? (
|
|
84
|
-
<div className="bg-white shadow rounded-lg p-6">
|
|
85
|
-
<p className="text-gray-600">{t('groups.noGroups')}</p>
|
|
86
|
-
</div>
|
|
87
|
-
) : (
|
|
88
|
-
<div className="space-y-6">
|
|
89
|
-
{groups.map((group) => (
|
|
90
|
-
<GroupCard
|
|
91
|
-
key={group.id}
|
|
92
|
-
group={group}
|
|
93
|
-
servers={servers}
|
|
94
|
-
onEdit={handleEditClick}
|
|
95
|
-
onDelete={handleDeleteGroup}
|
|
96
|
-
/>
|
|
97
|
-
))}
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
|
|
101
|
-
{showAddForm && (
|
|
102
|
-
<AddGroupForm onAdd={handleAddComplete} onCancel={handleAddComplete} />
|
|
103
|
-
)}
|
|
104
|
-
|
|
105
|
-
{editingGroup && (
|
|
106
|
-
<EditGroupForm
|
|
107
|
-
group={editingGroup}
|
|
108
|
-
onEdit={handleEditComplete}
|
|
109
|
-
onCancel={() => setEditingGroup(null)}
|
|
110
|
-
/>
|
|
111
|
-
)}
|
|
112
|
-
</div>
|
|
113
|
-
);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
export default GroupsPage;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { useNavigate } from 'react-router-dom';
|
|
3
|
-
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { useAuth } from '../contexts/AuthContext';
|
|
5
|
-
|
|
6
|
-
const LoginPage: React.FC = () => {
|
|
7
|
-
const { t } = useTranslation();
|
|
8
|
-
const [username, setUsername] = useState('');
|
|
9
|
-
const [password, setPassword] = useState('');
|
|
10
|
-
const [error, setError] = useState<string | null>(null);
|
|
11
|
-
const [loading, setLoading] = useState(false);
|
|
12
|
-
const { login } = useAuth();
|
|
13
|
-
const navigate = useNavigate();
|
|
14
|
-
|
|
15
|
-
const handleSubmit = async (e: React.FormEvent) => {
|
|
16
|
-
e.preventDefault();
|
|
17
|
-
setError(null);
|
|
18
|
-
setLoading(true);
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
if (!username || !password) {
|
|
22
|
-
setError(t('auth.emptyFields'));
|
|
23
|
-
setLoading(false);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const success = await login(username, password);
|
|
28
|
-
|
|
29
|
-
if (success) {
|
|
30
|
-
navigate('/');
|
|
31
|
-
} else {
|
|
32
|
-
setError(t('auth.loginFailed'));
|
|
33
|
-
}
|
|
34
|
-
} catch (err) {
|
|
35
|
-
setError(t('auth.loginError'));
|
|
36
|
-
} finally {
|
|
37
|
-
setLoading(false);
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
43
|
-
<div className="max-w-md w-full space-y-8">
|
|
44
|
-
<div>
|
|
45
|
-
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
|
46
|
-
{t('auth.loginTitle')}
|
|
47
|
-
</h2>
|
|
48
|
-
</div>
|
|
49
|
-
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
|
50
|
-
<div className="rounded-md shadow-sm -space-y-px">
|
|
51
|
-
<div>
|
|
52
|
-
<label htmlFor="username" className="sr-only">
|
|
53
|
-
{t('auth.username')}
|
|
54
|
-
</label>
|
|
55
|
-
<input
|
|
56
|
-
id="username"
|
|
57
|
-
name="username"
|
|
58
|
-
type="text"
|
|
59
|
-
autoComplete="username"
|
|
60
|
-
required
|
|
61
|
-
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
|
62
|
-
placeholder={t('auth.username')}
|
|
63
|
-
value={username}
|
|
64
|
-
onChange={(e) => setUsername(e.target.value)}
|
|
65
|
-
/>
|
|
66
|
-
</div>
|
|
67
|
-
<div>
|
|
68
|
-
<label htmlFor="password" className="sr-only">
|
|
69
|
-
{t('auth.password')}
|
|
70
|
-
</label>
|
|
71
|
-
<input
|
|
72
|
-
id="password"
|
|
73
|
-
name="password"
|
|
74
|
-
type="password"
|
|
75
|
-
autoComplete="current-password"
|
|
76
|
-
required
|
|
77
|
-
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
|
|
78
|
-
placeholder={t('auth.password')}
|
|
79
|
-
value={password}
|
|
80
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
81
|
-
/>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
|
|
85
|
-
{error && (
|
|
86
|
-
<div className="text-red-500 text-sm text-center">{error}</div>
|
|
87
|
-
)}
|
|
88
|
-
|
|
89
|
-
<div>
|
|
90
|
-
<button
|
|
91
|
-
type="submit"
|
|
92
|
-
disabled={loading}
|
|
93
|
-
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
|
94
|
-
>
|
|
95
|
-
{loading ? t('auth.loggingIn') : t('auth.login')}
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
</form>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export default LoginPage;
|