AuditLogManagement.tsx 9.1 KB

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