2
0

AuditLogManagement.tsx 10 KB

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