@samanhappy/mcphub 0.0.7 → 0.0.8
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 +4 -1
- 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/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 -18
- 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,159 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
2
|
-
import { AuthState, IUser } from '../types';
|
|
3
|
-
import * as authService from '../services/authService';
|
|
4
|
-
|
|
5
|
-
// Initial auth state
|
|
6
|
-
const initialState: AuthState = {
|
|
7
|
-
token: null,
|
|
8
|
-
isAuthenticated: false,
|
|
9
|
-
loading: true,
|
|
10
|
-
user: null,
|
|
11
|
-
error: null,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// Create auth context
|
|
15
|
-
const AuthContext = createContext<{
|
|
16
|
-
auth: AuthState;
|
|
17
|
-
login: (username: string, password: string) => Promise<boolean>;
|
|
18
|
-
register: (username: string, password: string, isAdmin?: boolean) => Promise<boolean>;
|
|
19
|
-
logout: () => void;
|
|
20
|
-
}>({
|
|
21
|
-
auth: initialState,
|
|
22
|
-
login: async () => false,
|
|
23
|
-
register: async () => false,
|
|
24
|
-
logout: () => {},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Auth provider component
|
|
28
|
-
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
29
|
-
const [auth, setAuth] = useState<AuthState>(initialState);
|
|
30
|
-
|
|
31
|
-
// Load user if token exists
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
const loadUser = async () => {
|
|
34
|
-
const token = authService.getToken();
|
|
35
|
-
|
|
36
|
-
if (!token) {
|
|
37
|
-
setAuth({
|
|
38
|
-
...initialState,
|
|
39
|
-
loading: false,
|
|
40
|
-
});
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const response = await authService.getCurrentUser();
|
|
46
|
-
|
|
47
|
-
if (response.success && response.user) {
|
|
48
|
-
setAuth({
|
|
49
|
-
token,
|
|
50
|
-
isAuthenticated: true,
|
|
51
|
-
loading: false,
|
|
52
|
-
user: response.user,
|
|
53
|
-
error: null,
|
|
54
|
-
});
|
|
55
|
-
} else {
|
|
56
|
-
authService.removeToken();
|
|
57
|
-
setAuth({
|
|
58
|
-
...initialState,
|
|
59
|
-
loading: false,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
} catch (error) {
|
|
63
|
-
authService.removeToken();
|
|
64
|
-
setAuth({
|
|
65
|
-
...initialState,
|
|
66
|
-
loading: false,
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
loadUser();
|
|
72
|
-
}, []);
|
|
73
|
-
|
|
74
|
-
// Login function
|
|
75
|
-
const login = async (username: string, password: string): Promise<boolean> => {
|
|
76
|
-
try {
|
|
77
|
-
const response = await authService.login({ username, password });
|
|
78
|
-
|
|
79
|
-
if (response.success && response.token && response.user) {
|
|
80
|
-
setAuth({
|
|
81
|
-
token: response.token,
|
|
82
|
-
isAuthenticated: true,
|
|
83
|
-
loading: false,
|
|
84
|
-
user: response.user,
|
|
85
|
-
error: null,
|
|
86
|
-
});
|
|
87
|
-
return true;
|
|
88
|
-
} else {
|
|
89
|
-
setAuth({
|
|
90
|
-
...initialState,
|
|
91
|
-
loading: false,
|
|
92
|
-
error: response.message || 'Authentication failed',
|
|
93
|
-
});
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
} catch (error) {
|
|
97
|
-
setAuth({
|
|
98
|
-
...initialState,
|
|
99
|
-
loading: false,
|
|
100
|
-
error: 'Authentication failed',
|
|
101
|
-
});
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Register function
|
|
107
|
-
const register = async (
|
|
108
|
-
username: string,
|
|
109
|
-
password: string,
|
|
110
|
-
isAdmin = false
|
|
111
|
-
): Promise<boolean> => {
|
|
112
|
-
try {
|
|
113
|
-
const response = await authService.register({ username, password, isAdmin });
|
|
114
|
-
|
|
115
|
-
if (response.success && response.token && response.user) {
|
|
116
|
-
setAuth({
|
|
117
|
-
token: response.token,
|
|
118
|
-
isAuthenticated: true,
|
|
119
|
-
loading: false,
|
|
120
|
-
user: response.user,
|
|
121
|
-
error: null,
|
|
122
|
-
});
|
|
123
|
-
return true;
|
|
124
|
-
} else {
|
|
125
|
-
setAuth({
|
|
126
|
-
...initialState,
|
|
127
|
-
loading: false,
|
|
128
|
-
error: response.message || 'Registration failed',
|
|
129
|
-
});
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
} catch (error) {
|
|
133
|
-
setAuth({
|
|
134
|
-
...initialState,
|
|
135
|
-
loading: false,
|
|
136
|
-
error: 'Registration failed',
|
|
137
|
-
});
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Logout function
|
|
143
|
-
const logout = (): void => {
|
|
144
|
-
authService.logout();
|
|
145
|
-
setAuth({
|
|
146
|
-
...initialState,
|
|
147
|
-
loading: false,
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
return (
|
|
152
|
-
<AuthContext.Provider value={{ auth, login, register, logout }}>
|
|
153
|
-
{children}
|
|
154
|
-
</AuthContext.Provider>
|
|
155
|
-
);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Custom hook to use auth context
|
|
159
|
-
export const useAuth = () => useContext(AuthContext);
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
|
2
|
-
import Toast, { ToastType } from '@/components/ui/Toast';
|
|
3
|
-
|
|
4
|
-
interface ToastContextProps {
|
|
5
|
-
showToast: (message: string, type?: ToastType, duration?: number) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const ToastContext = createContext<ToastContextProps | undefined>(undefined);
|
|
9
|
-
|
|
10
|
-
export const useToast = () => {
|
|
11
|
-
const context = useContext(ToastContext);
|
|
12
|
-
if (!context) {
|
|
13
|
-
throw new Error('useToast must be used within a ToastProvider');
|
|
14
|
-
}
|
|
15
|
-
return context;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
interface ToastProviderProps {
|
|
19
|
-
children: ReactNode;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
|
23
|
-
const [toast, setToast] = useState<{
|
|
24
|
-
message: string;
|
|
25
|
-
type: ToastType;
|
|
26
|
-
visible: boolean;
|
|
27
|
-
duration: number;
|
|
28
|
-
}>({
|
|
29
|
-
message: '',
|
|
30
|
-
type: 'info',
|
|
31
|
-
visible: false,
|
|
32
|
-
duration: 3000,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const showToast = (message: string, type: ToastType = 'info', duration: number = 3000) => {
|
|
36
|
-
setToast({
|
|
37
|
-
message,
|
|
38
|
-
type,
|
|
39
|
-
visible: true,
|
|
40
|
-
duration,
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const hideToast = () => {
|
|
45
|
-
setToast((prev) => ({ ...prev, visible: false }));
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<ToastContext.Provider value={{ showToast }}>
|
|
50
|
-
{children}
|
|
51
|
-
<Toast
|
|
52
|
-
message={toast.message}
|
|
53
|
-
type={toast.type}
|
|
54
|
-
duration={toast.duration}
|
|
55
|
-
onClose={hideToast}
|
|
56
|
-
visible={toast.visible}
|
|
57
|
-
/>
|
|
58
|
-
</ToastContext.Provider>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { Group, ApiResponse } from '@/types';
|
|
4
|
-
|
|
5
|
-
export const useGroupData = () => {
|
|
6
|
-
const { t } = useTranslation();
|
|
7
|
-
const [groups, setGroups] = useState<Group[]>([]);
|
|
8
|
-
const [loading, setLoading] = useState(true);
|
|
9
|
-
const [error, setError] = useState<string | null>(null);
|
|
10
|
-
const [refreshKey, setRefreshKey] = useState(0);
|
|
11
|
-
|
|
12
|
-
const fetchGroups = useCallback(async () => {
|
|
13
|
-
try {
|
|
14
|
-
setLoading(true);
|
|
15
|
-
const token = localStorage.getItem('mcphub_token');
|
|
16
|
-
const response = await fetch('/api/groups', {
|
|
17
|
-
headers: {
|
|
18
|
-
'x-auth-token': token || ''
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
if (!response.ok) {
|
|
23
|
-
throw new Error(`Status: ${response.status}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const data: ApiResponse<Group[]> = await response.json();
|
|
27
|
-
|
|
28
|
-
if (data && data.success && Array.isArray(data.data)) {
|
|
29
|
-
setGroups(data.data);
|
|
30
|
-
} else {
|
|
31
|
-
console.error('Invalid group data format:', data);
|
|
32
|
-
setGroups([]);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
setError(null);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.error('Error fetching groups:', err);
|
|
38
|
-
setError(err instanceof Error ? err.message : 'Failed to fetch groups');
|
|
39
|
-
setGroups([]);
|
|
40
|
-
} finally {
|
|
41
|
-
setLoading(false);
|
|
42
|
-
}
|
|
43
|
-
}, []);
|
|
44
|
-
|
|
45
|
-
// Trigger a refresh of the groups data
|
|
46
|
-
const triggerRefresh = useCallback(() => {
|
|
47
|
-
setRefreshKey(prev => prev + 1);
|
|
48
|
-
}, []);
|
|
49
|
-
|
|
50
|
-
// Create a new group with server associations
|
|
51
|
-
const createGroup = async (name: string, description?: string, servers: string[] = []) => {
|
|
52
|
-
try {
|
|
53
|
-
const token = localStorage.getItem('mcphub_token');
|
|
54
|
-
const response = await fetch('/api/groups', {
|
|
55
|
-
method: 'POST',
|
|
56
|
-
headers: {
|
|
57
|
-
'Content-Type': 'application/json',
|
|
58
|
-
'x-auth-token': token || ''
|
|
59
|
-
},
|
|
60
|
-
body: JSON.stringify({ name, description, servers }),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const result: ApiResponse<Group> = await response.json();
|
|
64
|
-
|
|
65
|
-
if (!response.ok) {
|
|
66
|
-
setError(result.message || t('groups.createError'));
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
triggerRefresh();
|
|
71
|
-
return result.data || null;
|
|
72
|
-
} catch (err) {
|
|
73
|
-
setError(err instanceof Error ? err.message : 'Failed to create group');
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Update an existing group with server associations
|
|
79
|
-
const updateGroup = async (id: string, data: { name?: string; description?: string; servers?: string[] }) => {
|
|
80
|
-
try {
|
|
81
|
-
const token = localStorage.getItem('mcphub_token');
|
|
82
|
-
const response = await fetch(`/api/groups/${id}`, {
|
|
83
|
-
method: 'PUT',
|
|
84
|
-
headers: {
|
|
85
|
-
'Content-Type': 'application/json',
|
|
86
|
-
'x-auth-token': token || ''
|
|
87
|
-
},
|
|
88
|
-
body: JSON.stringify(data),
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const result: ApiResponse<Group> = await response.json();
|
|
92
|
-
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
setError(result.message || t('groups.updateError'));
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
triggerRefresh();
|
|
99
|
-
return result.data || null;
|
|
100
|
-
} catch (err) {
|
|
101
|
-
setError(err instanceof Error ? err.message : 'Failed to update group');
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Update servers in a group (for batch updates)
|
|
107
|
-
const updateGroupServers = async (groupId: string, servers: string[]) => {
|
|
108
|
-
try {
|
|
109
|
-
const token = localStorage.getItem('mcphub_token');
|
|
110
|
-
const response = await fetch(`/api/groups/${groupId}/servers/batch`, {
|
|
111
|
-
method: 'PUT',
|
|
112
|
-
headers: {
|
|
113
|
-
'Content-Type': 'application/json',
|
|
114
|
-
'x-auth-token': token || ''
|
|
115
|
-
},
|
|
116
|
-
body: JSON.stringify({ servers }),
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const result: ApiResponse<Group> = await response.json();
|
|
120
|
-
|
|
121
|
-
if (!response.ok) {
|
|
122
|
-
setError(result.message || t('groups.updateError'));
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
triggerRefresh();
|
|
127
|
-
return result.data || null;
|
|
128
|
-
} catch (err) {
|
|
129
|
-
setError(err instanceof Error ? err.message : 'Failed to update group servers');
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
// Delete a group
|
|
135
|
-
const deleteGroup = async (id: string) => {
|
|
136
|
-
try {
|
|
137
|
-
const token = localStorage.getItem('mcphub_token');
|
|
138
|
-
const response = await fetch(`/api/groups/${id}`, {
|
|
139
|
-
method: 'DELETE',
|
|
140
|
-
headers: {
|
|
141
|
-
'x-auth-token': token || ''
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const result = await response.json();
|
|
146
|
-
|
|
147
|
-
if (!response.ok) {
|
|
148
|
-
setError(result.message || t('groups.deleteError'));
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
triggerRefresh();
|
|
153
|
-
return true;
|
|
154
|
-
} catch (err) {
|
|
155
|
-
setError(err instanceof Error ? err.message : 'Failed to delete group');
|
|
156
|
-
return false;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
// Add server to a group
|
|
161
|
-
const addServerToGroup = async (groupId: string, serverName: string) => {
|
|
162
|
-
try {
|
|
163
|
-
const token = localStorage.getItem('mcphub_token');
|
|
164
|
-
const response = await fetch(`/api/groups/${groupId}/servers`, {
|
|
165
|
-
method: 'POST',
|
|
166
|
-
headers: {
|
|
167
|
-
'Content-Type': 'application/json',
|
|
168
|
-
'x-auth-token': token || ''
|
|
169
|
-
},
|
|
170
|
-
body: JSON.stringify({ serverName }),
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
const result: ApiResponse<Group> = await response.json();
|
|
174
|
-
|
|
175
|
-
if (!response.ok) {
|
|
176
|
-
setError(result.message || t('groups.serverAddError'));
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
triggerRefresh();
|
|
181
|
-
return result.data || null;
|
|
182
|
-
} catch (err) {
|
|
183
|
-
setError(err instanceof Error ? err.message : 'Failed to add server to group');
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// Remove server from group
|
|
189
|
-
const removeServerFromGroup = async (groupId: string, serverName: string) => {
|
|
190
|
-
try {
|
|
191
|
-
const token = localStorage.getItem('mcphub_token');
|
|
192
|
-
const response = await fetch(`/api/groups/${groupId}/servers/${serverName}`, {
|
|
193
|
-
method: 'DELETE',
|
|
194
|
-
headers: {
|
|
195
|
-
'x-auth-token': token || ''
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const result: ApiResponse<Group> = await response.json();
|
|
200
|
-
|
|
201
|
-
if (!response.ok) {
|
|
202
|
-
setError(result.message || t('groups.serverRemoveError'));
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
triggerRefresh();
|
|
207
|
-
return result.data || null;
|
|
208
|
-
} catch (err) {
|
|
209
|
-
setError(err instanceof Error ? err.message : 'Failed to remove server from group');
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// Fetch groups when the component mounts or refreshKey changes
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
fetchGroups();
|
|
217
|
-
}, [fetchGroups, refreshKey]);
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
groups,
|
|
221
|
-
loading,
|
|
222
|
-
error,
|
|
223
|
-
setError,
|
|
224
|
-
triggerRefresh,
|
|
225
|
-
createGroup,
|
|
226
|
-
updateGroup,
|
|
227
|
-
updateGroupServers,
|
|
228
|
-
deleteGroup,
|
|
229
|
-
addServerToGroup,
|
|
230
|
-
removeServerFromGroup
|
|
231
|
-
};
|
|
232
|
-
};
|