AuditLogExportModal.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. import { useCallback, useEffect, useRef, useState } from 'react';
  2. import { LoadingSpinner } from '@growi/ui/dist/components';
  3. import { useAtomValue } from 'jotai';
  4. import { useTranslation } from 'react-i18next';
  5. import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
  6. import type { IClearable } from '~/client/interfaces/clearable';
  7. import { apiv3Post } from '~/client/util/apiv3-client';
  8. import { toastError, toastSuccess } from '~/client/util/toastr';
  9. import type { SupportedActionType } from '~/interfaces/activity';
  10. import { auditLogAvailableActionsAtom } from '~/states/server-configurations';
  11. import { DateRangePicker } from './DateRangePicker';
  12. import { SearchUsernameTypeahead } from './SearchUsernameTypeahead';
  13. import { SelectActionDropdown } from './SelectActionDropdown';
  14. type Props = {
  15. isOpen: boolean;
  16. onClose: () => void;
  17. };
  18. export const AuditLogExportModal = ({
  19. isOpen,
  20. onClose,
  21. }: Props): JSX.Element => {
  22. const { t } = useTranslation('admin');
  23. const typeaheadRef = useRef<IClearable>(null);
  24. const auditLogAvailableActionsData = useAtomValue(
  25. auditLogAvailableActionsAtom,
  26. );
  27. const [startDate, setStartDate] = useState<Date | null>(null);
  28. const [endDate, setEndDate] = useState<Date | null>(null);
  29. const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
  30. const [actionMap, setActionMap] = useState(
  31. new Map<SupportedActionType, boolean>(),
  32. );
  33. const [isExporting, setIsExporting] = useState<boolean>(false);
  34. useEffect(() => {
  35. if (isOpen) {
  36. setStartDate(null);
  37. setEndDate(null);
  38. setSelectedUsernames([]);
  39. setActionMap(
  40. new Map<SupportedActionType, boolean>(
  41. auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
  42. ),
  43. );
  44. setIsExporting(false);
  45. typeaheadRef.current?.clear();
  46. }
  47. }, [isOpen, auditLogAvailableActionsData]);
  48. const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
  49. setStartDate(dateList[0]);
  50. setEndDate(dateList[1]);
  51. }, []);
  52. const actionCheckboxChangedHandler = useCallback(
  53. (action: SupportedActionType) => {
  54. actionMap.set(action, !actionMap.get(action));
  55. setActionMap(new Map(actionMap.entries()));
  56. },
  57. [actionMap],
  58. );
  59. const multipleActionCheckboxChangedHandler = useCallback(
  60. (actions: SupportedActionType[], isChecked: boolean) => {
  61. actions.forEach((action) => {
  62. actionMap.set(action, isChecked);
  63. });
  64. setActionMap(new Map(actionMap.entries()));
  65. },
  66. [actionMap],
  67. );
  68. const setUsernamesHandler = useCallback((usernames: string[]) => {
  69. setSelectedUsernames(usernames);
  70. }, []);
  71. const exportHandler = useCallback(async () => {
  72. setIsExporting(true);
  73. try {
  74. const selectedActionList = Array.from(actionMap.entries())
  75. .filter((v) => v[1])
  76. .map((v) => v[0]);
  77. const filters: {
  78. actions?: SupportedActionType[];
  79. dateFrom?: Date;
  80. dateTo?: Date;
  81. // TODO: Add users filter after implementing username-to-userId conversion
  82. } = {};
  83. if (selectedActionList.length > 0) {
  84. filters.actions = selectedActionList;
  85. }
  86. if (startDate != null) {
  87. filters.dateFrom = startDate;
  88. }
  89. if (endDate != null) {
  90. filters.dateTo = endDate;
  91. }
  92. await apiv3Post('/audit-log-bulk-export', { filters });
  93. toastSuccess(t('audit_log_management.export_requested'));
  94. onClose();
  95. } catch {
  96. toastError(t('audit_log_management.export_failed'));
  97. } finally {
  98. setIsExporting(false);
  99. }
  100. }, [actionMap, startDate, endDate, t, onClose]);
  101. return (
  102. <Modal isOpen={isOpen} toggle={onClose}>
  103. <ModalHeader tag="h4" toggle={onClose}>
  104. {t('audit_log_management.export_audit_log')}
  105. </ModalHeader>
  106. <ModalBody>
  107. <div className="mb-3">
  108. <div className="form-label">{t('audit_log_management.username')}</div>
  109. <SearchUsernameTypeahead
  110. ref={typeaheadRef}
  111. onChange={setUsernamesHandler}
  112. />
  113. </div>
  114. <div className="mb-3">
  115. <div className="form-label">{t('audit_log_management.date')}</div>
  116. <DateRangePicker
  117. startDate={startDate}
  118. endDate={endDate}
  119. onChange={datePickerChangedHandler}
  120. />
  121. </div>
  122. <div className="mb-3">
  123. <div className="form-label">{t('audit_log_management.action')}</div>
  124. <SelectActionDropdown
  125. actionMap={actionMap}
  126. availableActions={auditLogAvailableActionsData || []}
  127. onChangeAction={actionCheckboxChangedHandler}
  128. onChangeMultipleAction={multipleActionCheckboxChangedHandler}
  129. />
  130. </div>
  131. </ModalBody>
  132. <ModalFooter>
  133. <button
  134. type="button"
  135. className="btn btn-outline-secondary"
  136. onClick={onClose}
  137. >
  138. {t('export_management.cancel')}
  139. </button>
  140. <button
  141. type="button"
  142. className="btn btn-primary"
  143. onClick={exportHandler}
  144. disabled={isExporting}
  145. >
  146. {isExporting ? (
  147. <LoadingSpinner className="me-1 fs-3" />
  148. ) : (
  149. <span className="material-symbols-outlined me-1">download</span>
  150. )}
  151. {t('audit_log_management.export')}
  152. </button>
  153. </ModalFooter>
  154. </Modal>
  155. );
  156. };