useUsersData.jsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import { useState, useEffect } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { API, showError, showSuccess } from '../../helpers';
  18. import { ITEMS_PER_PAGE } from '../../constants';
  19. import { useTableCompactMode } from '../common/useTableCompactMode';
  20. export const useUsersData = () => {
  21. const { t } = useTranslation();
  22. const [compactMode, setCompactMode] = useTableCompactMode('users');
  23. // State management
  24. const [users, setUsers] = useState([]);
  25. const [loading, setLoading] = useState(true);
  26. const [activePage, setActivePage] = useState(1);
  27. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  28. const [searching, setSearching] = useState(false);
  29. const [groupOptions, setGroupOptions] = useState([]);
  30. const [userCount, setUserCount] = useState(0);
  31. // Modal states
  32. const [showAddUser, setShowAddUser] = useState(false);
  33. const [showEditUser, setShowEditUser] = useState(false);
  34. const [editingUser, setEditingUser] = useState({
  35. id: undefined,
  36. });
  37. // Form initial values
  38. const formInitValues = {
  39. searchKeyword: '',
  40. searchGroup: '',
  41. };
  42. // Form API reference
  43. const [formApi, setFormApi] = useState(null);
  44. // Get form values helper function
  45. const getFormValues = () => {
  46. const formValues = formApi ? formApi.getValues() : {};
  47. return {
  48. searchKeyword: formValues.searchKeyword || '',
  49. searchGroup: formValues.searchGroup || '',
  50. };
  51. };
  52. // Set user format with key field
  53. const setUserFormat = (users) => {
  54. for (let i = 0; i < users.length; i++) {
  55. users[i].key = users[i].id;
  56. }
  57. setUsers(users);
  58. };
  59. // Load users data
  60. const loadUsers = async (startIdx, pageSize) => {
  61. setLoading(true);
  62. const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`);
  63. const { success, message, data } = res.data;
  64. if (success) {
  65. const newPageData = data.items;
  66. setActivePage(data.page);
  67. setUserCount(data.total);
  68. setUserFormat(newPageData);
  69. } else {
  70. showError(message);
  71. }
  72. setLoading(false);
  73. };
  74. // Search users with keyword and group
  75. const searchUsers = async (
  76. startIdx,
  77. pageSize,
  78. searchKeyword = null,
  79. searchGroup = null,
  80. ) => {
  81. // If no parameters passed, get values from form
  82. if (searchKeyword === null || searchGroup === null) {
  83. const formValues = getFormValues();
  84. searchKeyword = formValues.searchKeyword;
  85. searchGroup = formValues.searchGroup;
  86. }
  87. if (searchKeyword === '' && searchGroup === '') {
  88. // If keyword is blank, load files instead
  89. await loadUsers(startIdx, pageSize);
  90. return;
  91. }
  92. setSearching(true);
  93. const res = await API.get(
  94. `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`,
  95. );
  96. const { success, message, data } = res.data;
  97. if (success) {
  98. const newPageData = data.items;
  99. setActivePage(data.page);
  100. setUserCount(data.total);
  101. setUserFormat(newPageData);
  102. } else {
  103. showError(message);
  104. }
  105. setSearching(false);
  106. };
  107. // Manage user operations (promote, demote, enable, disable, delete)
  108. const manageUser = async (userId, action, record) => {
  109. // Trigger loading state to force table re-render
  110. setLoading(true);
  111. const res = await API.post('/api/user/manage', {
  112. id: userId,
  113. action,
  114. });
  115. const { success, message } = res.data;
  116. if (success) {
  117. showSuccess('操作成功完成!');
  118. const user = res.data.data;
  119. // Create a new array and new object to ensure React detects changes
  120. const newUsers = users.map((u) => {
  121. if (u.id === userId) {
  122. if (action === 'delete') {
  123. return { ...u, DeletedAt: new Date() };
  124. }
  125. return { ...u, status: user.status, role: user.role };
  126. }
  127. return u;
  128. });
  129. setUsers(newUsers);
  130. } else {
  131. showError(message);
  132. }
  133. setLoading(false);
  134. };
  135. const resetUserPasskey = async (user) => {
  136. if (!user) {
  137. return;
  138. }
  139. try {
  140. const res = await API.delete(`/api/user/${user.id}/reset_passkey`);
  141. const { success, message } = res.data;
  142. if (success) {
  143. showSuccess(t('Passkey 已重置'));
  144. } else {
  145. showError(message || t('操作失败,请重试'));
  146. }
  147. } catch (error) {
  148. showError(t('操作失败,请重试'));
  149. }
  150. };
  151. const resetUserTwoFA = async (user) => {
  152. if (!user) {
  153. return;
  154. }
  155. try {
  156. const res = await API.delete(`/api/user/${user.id}/2fa`);
  157. const { success, message } = res.data;
  158. if (success) {
  159. showSuccess(t('二步验证已重置'));
  160. } else {
  161. showError(message || t('操作失败,请重试'));
  162. }
  163. } catch (error) {
  164. showError(t('操作失败,请重试'));
  165. }
  166. };
  167. // Handle page change
  168. const handlePageChange = (page) => {
  169. setActivePage(page);
  170. const { searchKeyword, searchGroup } = getFormValues();
  171. if (searchKeyword === '' && searchGroup === '') {
  172. loadUsers(page, pageSize).then();
  173. } else {
  174. searchUsers(page, pageSize, searchKeyword, searchGroup).then();
  175. }
  176. };
  177. // Handle page size change
  178. const handlePageSizeChange = async (size) => {
  179. localStorage.setItem('page-size', size + '');
  180. setPageSize(size);
  181. setActivePage(1);
  182. loadUsers(activePage, size)
  183. .then()
  184. .catch((reason) => {
  185. showError(reason);
  186. });
  187. };
  188. // Handle table row styling for disabled/deleted users
  189. const handleRow = (record, index) => {
  190. if (record.DeletedAt !== null || record.status !== 1) {
  191. return {
  192. style: {
  193. background: 'var(--semi-color-disabled-border)',
  194. },
  195. };
  196. } else {
  197. return {};
  198. }
  199. };
  200. // Refresh data
  201. const refresh = async (page = activePage) => {
  202. const { searchKeyword, searchGroup } = getFormValues();
  203. if (searchKeyword === '' && searchGroup === '') {
  204. await loadUsers(page, pageSize);
  205. } else {
  206. await searchUsers(page, pageSize, searchKeyword, searchGroup);
  207. }
  208. };
  209. // Fetch groups data
  210. const fetchGroups = async () => {
  211. try {
  212. let res = await API.get(`/api/group/`);
  213. if (res === undefined) {
  214. return;
  215. }
  216. setGroupOptions(
  217. res.data.data.map((group) => ({
  218. label: group,
  219. value: group,
  220. })),
  221. );
  222. } catch (error) {
  223. showError(error.message);
  224. }
  225. };
  226. // Modal control functions
  227. const closeAddUser = () => {
  228. setShowAddUser(false);
  229. };
  230. const closeEditUser = () => {
  231. setShowEditUser(false);
  232. setEditingUser({
  233. id: undefined,
  234. });
  235. };
  236. // Initialize data on component mount
  237. useEffect(() => {
  238. loadUsers(0, pageSize)
  239. .then()
  240. .catch((reason) => {
  241. showError(reason);
  242. });
  243. fetchGroups().then();
  244. }, []);
  245. return {
  246. // Data state
  247. users,
  248. loading,
  249. activePage,
  250. pageSize,
  251. userCount,
  252. searching,
  253. groupOptions,
  254. // Modal state
  255. showAddUser,
  256. showEditUser,
  257. editingUser,
  258. setShowAddUser,
  259. setShowEditUser,
  260. setEditingUser,
  261. // Form state
  262. formInitValues,
  263. formApi,
  264. setFormApi,
  265. // UI state
  266. compactMode,
  267. setCompactMode,
  268. // Actions
  269. loadUsers,
  270. searchUsers,
  271. manageUser,
  272. resetUserPasskey,
  273. resetUserTwoFA,
  274. handlePageChange,
  275. handlePageSizeChange,
  276. handleRow,
  277. refresh,
  278. closeAddUser,
  279. closeEditUser,
  280. getFormValues,
  281. // Translation
  282. t,
  283. };
  284. };