AuditLogManagement.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. import type React from 'react';
  2. import type { FC } from 'react';
  3. import { useCallback, useRef, useState } from 'react';
  4. import { LoadingSpinner } from '@growi/ui/dist/components';
  5. import { format } from 'date-fns/format';
  6. import { useAtomValue } from 'jotai';
  7. import { useTranslation } from 'react-i18next';
  8. import type { IClearable } from '~/client/interfaces/clearable';
  9. import { toastError } from '~/client/util/toastr';
  10. import type { SupportedActionType } from '~/interfaces/activity';
  11. import {
  12. auditLogAvailableActionsAtom,
  13. auditLogEnabledAtom,
  14. } from '~/states/server-configurations';
  15. import { useSWRxActivity } from '~/stores/activity';
  16. import PaginationWrapper from '../PaginationWrapper';
  17. import { ActivityTable } from './AuditLog/ActivityTable';
  18. import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
  19. import { AuditLogSettings } from './AuditLog/AuditLogSettings';
  20. import { DateRangePicker } from './AuditLog/DateRangePicker';
  21. import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
  22. import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
  23. const formatDate = (date: Date | null) => {
  24. if (date == null) {
  25. return '';
  26. }
  27. return format(new Date(date), 'yyyy-MM-dd');
  28. };
  29. const PAGING_LIMIT = 10;
  30. export const AuditLogManagement: FC = () => {
  31. const { t } = useTranslation('admin');
  32. const typeaheadRef = useRef<IClearable>(null);
  33. const auditLogAvailableActionsData = useAtomValue(
  34. auditLogAvailableActionsAtom,
  35. );
  36. /*
  37. * State
  38. */
  39. const [isSettingPage, setIsSettingPage] = useState<boolean>(false);
  40. const [activePageNumber, setActivePageNumber] = useState<number>(1);
  41. const [jumpPageNumber, setJumpPageNumber] = useState<number>(1);
  42. const offset = (activePageNumber - 1) * PAGING_LIMIT;
  43. const [startDate, setStartDate] = useState<Date | null>(null);
  44. const [endDate, setEndDate] = useState<Date | null>(null);
  45. const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
  46. const [actionMap, setActionMap] = useState(
  47. new Map<SupportedActionType, boolean>(
  48. auditLogAvailableActionsData != null
  49. ? auditLogAvailableActionsData.map((action) => [action, true])
  50. : [],
  51. ),
  52. );
  53. /*
  54. * Fetch
  55. */
  56. const selectedDate = {
  57. startDate: formatDate(startDate),
  58. endDate: formatDate(endDate),
  59. };
  60. const selectedActionList = Array.from(actionMap.entries())
  61. .filter((v) => v[1])
  62. .map((v) => v[0]);
  63. const searchFilter = {
  64. actions: selectedActionList,
  65. dates: selectedDate,
  66. usernames: selectedUsernames,
  67. };
  68. const {
  69. data: activityData,
  70. mutate: mutateActivity,
  71. error,
  72. } = useSWRxActivity(PAGING_LIMIT, offset, searchFilter);
  73. const activityList = activityData?.docs != null ? activityData.docs : [];
  74. const totalActivityNum =
  75. activityData?.totalDocs != null ? activityData.totalDocs : 0;
  76. const totalPagingPages =
  77. activityData?.totalPages != null ? activityData.totalPages : 0;
  78. const isLoading = activityData === undefined && error == null;
  79. if (error != null) {
  80. toastError('Failed to get Audit Log');
  81. }
  82. const auditLogEnabled = useAtomValue(auditLogEnabledAtom);
  83. /*
  84. * Functions
  85. */
  86. const setActivePageHandler = useCallback((selectedPageNum: number) => {
  87. setActivePageNumber(selectedPageNum);
  88. }, []);
  89. const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
  90. setActivePageNumber(1);
  91. setStartDate(dateList[0]);
  92. setEndDate(dateList[1]);
  93. }, []);
  94. const actionCheckboxChangedHandler = useCallback(
  95. (action: SupportedActionType) => {
  96. setActivePageNumber(1);
  97. actionMap.set(action, !actionMap.get(action));
  98. setActionMap(new Map(actionMap.entries()));
  99. },
  100. [actionMap],
  101. );
  102. const multipleActionCheckboxChangedHandler = useCallback(
  103. (actions: SupportedActionType[], isChecked) => {
  104. setActivePageNumber(1);
  105. actions.forEach((action) => {
  106. actionMap.set(action, isChecked);
  107. });
  108. setActionMap(new Map(actionMap.entries()));
  109. },
  110. [actionMap],
  111. );
  112. const setUsernamesHandler = useCallback((usernames: string[]) => {
  113. setActivePageNumber(1);
  114. setSelectedUsernames(usernames);
  115. }, []);
  116. const clearButtonPushedHandler = useCallback(() => {
  117. setActivePageNumber(1);
  118. setStartDate(null);
  119. setEndDate(null);
  120. setSelectedUsernames([]);
  121. typeaheadRef.current?.clear();
  122. if (auditLogAvailableActionsData != null) {
  123. setActionMap(
  124. new Map<SupportedActionType, boolean>(
  125. auditLogAvailableActionsData.map((action) => [action, true]),
  126. ),
  127. );
  128. }
  129. }, [auditLogAvailableActionsData]);
  130. const reloadButtonPushedHandler = useCallback(() => {
  131. setActivePageNumber(1);
  132. mutateActivity();
  133. }, [mutateActivity]);
  134. const jumpPageInputChangeHandler = useCallback(
  135. (e: React.ChangeEvent<HTMLInputElement>) => {
  136. const inputNumber = Number(e.target.value);
  137. const isNan = Number.isNaN(inputNumber);
  138. if (!isNan) {
  139. // eslint-disable-next-line no-nested-ternary
  140. const jumpPageNumber =
  141. inputNumber > totalPagingPages
  142. ? totalPagingPages
  143. : inputNumber <= 0
  144. ? activePageNumber
  145. : inputNumber;
  146. setJumpPageNumber(jumpPageNumber);
  147. } else {
  148. setJumpPageNumber(activePageNumber);
  149. }
  150. },
  151. [totalPagingPages, activePageNumber],
  152. );
  153. const jumpPageInputKeyDownHandler = useCallback(
  154. (e) => {
  155. if (e.key === 'Enter') {
  156. setActivePageNumber(jumpPageNumber);
  157. }
  158. },
  159. [jumpPageNumber],
  160. );
  161. const jumpPageButtonPushedHandler = useCallback(() => {
  162. setActivePageNumber(jumpPageNumber);
  163. }, [jumpPageNumber]);
  164. const startIndex = activityList.length === 0 ? 0 : offset + 1;
  165. const endIndex = activityList.length === 0 ? 0 : offset + activityList.length;
  166. if (!auditLogEnabled) {
  167. return <AuditLogDisableMode />;
  168. }
  169. return (
  170. <div data-testid="admin-auditlog">
  171. <button
  172. type="button"
  173. className="btn btn-outline-secondary mb-4"
  174. onClick={() => setIsSettingPage(!isSettingPage)}
  175. >
  176. {isSettingPage ? (
  177. <>
  178. <span className="material-symbols-outlined">arrow_left_alt</span>
  179. {t('admin:audit_log_management.return')}
  180. </>
  181. ) : (
  182. <>
  183. <span className="material-symbols-outlined">settings</span>
  184. {t('admin:audit_log_management.settings')}
  185. </>
  186. )}
  187. </button>
  188. <h2 className="admin-setting-header mb-3">
  189. <span>
  190. {isSettingPage
  191. ? t('audit_log_management.audit_log_settings')
  192. : t('audit_log_management.audit_log')}
  193. </span>
  194. {!isSettingPage && (
  195. <button
  196. type="button"
  197. className="btn btn-sm ms-auto grw-btn-reload"
  198. onClick={reloadButtonPushedHandler}
  199. >
  200. <span className="material-symbols-outlined">refresh</span>
  201. </button>
  202. )}
  203. </h2>
  204. {isSettingPage ? (
  205. <AuditLogSettings />
  206. ) : (
  207. <>
  208. <div className="row row-cols-lg-auto mb-3 g-3">
  209. <div className="col-12">
  210. <SearchUsernameTypeahead
  211. ref={typeaheadRef}
  212. onChange={setUsernamesHandler}
  213. />
  214. </div>
  215. <div className="col-12">
  216. <DateRangePicker
  217. startDate={startDate}
  218. endDate={endDate}
  219. onChange={datePickerChangedHandler}
  220. />
  221. </div>
  222. <div className="col-12">
  223. <SelectActionDropdown
  224. actionMap={actionMap}
  225. availableActions={auditLogAvailableActionsData || []}
  226. onChangeAction={actionCheckboxChangedHandler}
  227. onChangeMultipleAction={multipleActionCheckboxChangedHandler}
  228. />
  229. </div>
  230. <div className="col-12">
  231. <button
  232. type="button"
  233. className="btn btn-link"
  234. onClick={clearButtonPushedHandler}
  235. >
  236. {t('admin:audit_log_management.clear')}
  237. </button>
  238. </div>
  239. </div>
  240. <p className="ms-2">
  241. <strong>{startIndex}</strong> - <strong>{endIndex}</strong> of{' '}
  242. <strong>{totalActivityNum}</strong>
  243. </p>
  244. {isLoading ? (
  245. <div className="text-muted text-center mb-5">
  246. <LoadingSpinner className="me-1 fs-3" />
  247. </div>
  248. ) : (
  249. <ActivityTable activityList={activityList} />
  250. )}
  251. <div className="d-flex flex-row justify-content-center">
  252. <PaginationWrapper
  253. activePage={activePageNumber}
  254. changePage={setActivePageHandler}
  255. totalItemsCount={totalActivityNum}
  256. pagingLimit={PAGING_LIMIT}
  257. align="center"
  258. size="sm"
  259. />
  260. <div className="admin-audit-log ms-3">
  261. <label
  262. htmlFor="jumpPageInput"
  263. className="form-label me-1 text-secondary"
  264. >
  265. Jump To Page
  266. </label>
  267. <input
  268. id="jumpPageInput"
  269. type="text"
  270. className="jump-page-input"
  271. onChange={jumpPageInputChangeHandler}
  272. onKeyDown={jumpPageInputKeyDownHandler}
  273. />
  274. <button
  275. className="btn btn-sm"
  276. type="button"
  277. onClick={jumpPageButtonPushedHandler}
  278. >
  279. <b>Go</b>
  280. </button>
  281. </div>
  282. </div>
  283. </>
  284. )}
  285. </div>
  286. );
  287. };