Преглед изворни кода

can request an export by the export button

ryosei-f пре 1 месец
родитељ
комит
3f00039fd2

+ 1 - 0
apps/app/public/static/locales/en_US/admin.json

@@ -861,6 +861,7 @@
     "action_list": "Action List",
     "disable_mode_explanation": "Audit log is currently disabled. To enable it, set the environment variable <code>AUDIT_LOG_ENABLED</code> to true.",
     "export": "Export",
+    "export_audit_log": "Export Audit Log",
     "export_requested": "Export request accepted. You will be notified when the export is complete.",
     "export_failed": "Failed to start export",
     "docs_url": {

+ 1 - 0
apps/app/public/static/locales/fr_FR/admin.json

@@ -860,6 +860,7 @@
     "action_list": "Liste d'actions",
     "disable_mode_explanation": "Cette fonctionnalité est désactivée. Afin de l'activer, mettre à jour <code>AUDIT_LOG_ENABLED</code> pour true.",
     "export": " Exporter",
+    "export_audit_log": "Exporter le journal d'audit",
     "export_requested": " Demande d'exportation acceptée. Vous serez averti lorsque l'exportation sera terminée.",
     "export_failed": "Échec du démarrage de l'exportation",
     "docs_url": {

+ 1 - 0
apps/app/public/static/locales/ja_JP/admin.json

@@ -870,6 +870,7 @@
     "action_list": "アクション一覧",
     "disable_mode_explanation": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。",
     "export": "エクスポート",
+    "export_audit_log": "監査ログのエクスポート",
     "export_requested": "エクスポートリクエストを受け付けました。完了後に通知されます。",
     "export_failed": "エクスポートの開始に失敗しました",
     "docs_url": {

+ 1 - 0
apps/app/public/static/locales/ko_KR/admin.json

@@ -861,6 +861,7 @@
     "action_list": "작업 목록",
     "disable_mode_explanation": "감사 로그가 현재 비활성화되어 있습니다. 활성화하려면 환경 변수 <code>AUDIT_LOG_ENABLED</code>를 true로 설정하십시오.",
     "export": "내보내기",
+    "export_audit_log": "감사 로그 내보내기",
     "export_requested": "내보내기 요청이 접수되었습니다. 내보내기가 완료되면 알림을 받게 됩니다.",
     "export_failed": "내보내기 시작에 실패했습니다",
     "docs_url": {

+ 1 - 0
apps/app/public/static/locales/zh_CN/admin.json

@@ -870,6 +870,7 @@
     "action_list": "行动清单",
     "disable_mode_explanation": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。",
     "export": "匯出",
+    "export_audit_log": "匯出稽核記錄",
     "export_requested": "匯出請求已接受。匯出完成時將通知您。",
     "export_failed": "無法啟動匯出",
     "docs_url": {

+ 177 - 0
apps/app/src/client/components/Admin/AuditLog/AuditLogExportModal.tsx

@@ -0,0 +1,177 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { LoadingSpinner } from '@growi/ui/dist/components';
+import { useAtomValue } from 'jotai';
+import { useTranslation } from 'react-i18next';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
+
+import type { IClearable } from '~/client/interfaces/clearable';
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import type { SupportedActionType } from '~/interfaces/activity';
+import { auditLogAvailableActionsAtom } from '~/states/server-configurations';
+
+import { DateRangePicker } from './DateRangePicker';
+import { SearchUsernameTypeahead } from './SearchUsernameTypeahead';
+import { SelectActionDropdown } from './SelectActionDropdown';
+
+type Props = {
+  isOpen: boolean;
+  onClose: () => void;
+};
+
+export const AuditLogExportModal = ({
+  isOpen,
+  onClose,
+}: Props): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  const typeaheadRef = useRef<IClearable>(null);
+
+  const auditLogAvailableActionsData = useAtomValue(
+    auditLogAvailableActionsAtom,
+  );
+
+  const [startDate, setStartDate] = useState<Date | null>(null);
+  const [endDate, setEndDate] = useState<Date | null>(null);
+  const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
+  const [actionMap, setActionMap] = useState(
+    new Map<SupportedActionType, boolean>(),
+  );
+  const [isExporting, setIsExporting] = useState<boolean>(false);
+
+  useEffect(() => {
+    if (isOpen) {
+      setStartDate(null);
+      setEndDate(null);
+      setSelectedUsernames([]);
+      setActionMap(
+        new Map<SupportedActionType, boolean>(
+          auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
+        ),
+      );
+      setIsExporting(false);
+      typeaheadRef.current?.clear();
+    }
+  }, [isOpen, auditLogAvailableActionsData]);
+
+  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
+    setStartDate(dateList[0]);
+    setEndDate(dateList[1]);
+  }, []);
+
+  const actionCheckboxChangedHandler = useCallback(
+    (action: SupportedActionType) => {
+      actionMap.set(action, !actionMap.get(action));
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap],
+  );
+
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions: SupportedActionType[], isChecked: boolean) => {
+      actions.forEach((action) => {
+        actionMap.set(action, isChecked);
+      });
+      setActionMap(new Map(actionMap.entries()));
+    },
+    [actionMap],
+  );
+
+  const setUsernamesHandler = useCallback((usernames: string[]) => {
+    setSelectedUsernames(usernames);
+  }, []);
+
+  const exportHandler = useCallback(async () => {
+    setIsExporting(true);
+    try {
+      const selectedActionList = Array.from(actionMap.entries())
+        .filter((v) => v[1])
+        .map((v) => v[0]);
+
+      const filters: {
+        actions?: SupportedActionType[];
+        dateFrom?: Date;
+        dateTo?: Date;
+        // TODO: Add users filter after implementing username-to-userId conversion
+      } = {};
+
+      if (selectedActionList.length > 0) {
+        filters.actions = selectedActionList;
+      }
+      if (startDate != null) {
+        filters.dateFrom = startDate;
+      }
+      if (endDate != null) {
+        filters.dateTo = endDate;
+      }
+
+      await apiv3Post('/audit-log-bulk-export', { filters });
+      toastSuccess(t('audit_log_management.export_requested'));
+      onClose();
+    } catch {
+      toastError(t('audit_log_management.export_failed'));
+    } finally {
+      setIsExporting(false);
+    }
+  }, [actionMap, startDate, endDate, t, onClose]);
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose}>
+      <ModalHeader tag="h4" toggle={onClose}>
+        {t('audit_log_management.export_audit_log')}
+      </ModalHeader>
+
+      <ModalBody>
+        <div className="mb-3">
+          <div className="form-label">{t('audit_log_management.username')}</div>
+          <SearchUsernameTypeahead
+            ref={typeaheadRef}
+            onChange={setUsernamesHandler}
+          />
+        </div>
+
+        <div className="mb-3">
+          <div className="form-label">{t('audit_log_management.date')}</div>
+          <DateRangePicker
+            startDate={startDate}
+            endDate={endDate}
+            onChange={datePickerChangedHandler}
+          />
+        </div>
+
+        <div className="mb-3">
+          <div className="form-label">{t('audit_log_management.action')}</div>
+          <SelectActionDropdown
+            actionMap={actionMap}
+            availableActions={auditLogAvailableActionsData || []}
+            onChangeAction={actionCheckboxChangedHandler}
+            onChangeMultipleAction={multipleActionCheckboxChangedHandler}
+          />
+        </div>
+      </ModalBody>
+
+      <ModalFooter>
+        <button
+          type="button"
+          className="btn btn-outline-secondary"
+          onClick={onClose}
+        >
+          {t('export_management.cancel')}
+        </button>
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={exportHandler}
+          disabled={isExporting}
+        >
+          {isExporting ? (
+            <LoadingSpinner className="me-1 fs-3" />
+          ) : (
+            <span className="material-symbols-outlined me-1">download</span>
+          )}
+          {t('audit_log_management.export')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};

+ 10 - 40
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -7,8 +7,7 @@ import { useAtomValue } from 'jotai';
 import { useTranslation } from 'react-i18next';
 
 import type { IClearable } from '~/client/interfaces/clearable';
-import { apiv3Post } from '~/client/util/apiv3-client';
-import { toastError, toastSuccess } from '~/client/util/toastr';
+import { toastError } from '~/client/util/toastr';
 import type { SupportedActionType } from '~/interfaces/activity';
 import {
   auditLogAvailableActionsAtom,
@@ -19,6 +18,7 @@ import { useSWRxActivity } from '~/stores/activity';
 import PaginationWrapper from '../PaginationWrapper';
 import { ActivityTable } from './AuditLog/ActivityTable';
 import { AuditLogDisableMode } from './AuditLog/AuditLogDisableMode';
+import { AuditLogExportModal } from './AuditLog/AuditLogExportModal';
 import { AuditLogSettings } from './AuditLog/AuditLogSettings';
 import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
@@ -186,35 +186,7 @@ export const AuditLogManagement: FC = () => {
     setActivePageNumber(jumpPageNumber);
   }, [jumpPageNumber]);
 
-  const [isExporting, setIsExporting] = useState<boolean>(false);
-
-  const exportHandler = useCallback(async () => {
-    setIsExporting(true);
-    try {
-      const filters: {
-        actions?: SupportedActionType[];
-        dateFrom?: Date;
-        dateTo?: Date;
-      } = {};
-
-      if (selectedActionList.length > 0) {
-        filters.actions = selectedActionList;
-      }
-      if (startDate != null) {
-        filters.dateFrom = startDate;
-      }
-      if (endDate != null) {
-        filters.dateTo = endDate;
-      }
-
-      await apiv3Post('/audit-log-bulk-export', { filters });
-      toastSuccess(t('audit_log_management.export_requested'));
-    } catch {
-      toastError(t('audit_log_management.export_failed'));
-    } finally {
-      setIsExporting(false);
-    }
-  }, [selectedActionList, startDate, endDate, t]);
+  const [isExportModalOpen, setIsExportModalOpen] = useState<boolean>(false);
 
   const startIndex = activityList.length === 0 ? 0 : offset + 1;
   const endIndex = activityList.length === 0 ? 0 : offset + activityList.length;
@@ -303,16 +275,9 @@ export const AuditLogManagement: FC = () => {
               <button
                 type="button"
                 className="btn btn-outline-secondary"
-                onClick={exportHandler}
-                disabled={isExporting}
+                onClick={() => setIsExportModalOpen(true)}
               >
-                {isExporting ? (
-                  <LoadingSpinner className="me-1 fs-3" />
-                ) : (
-                  <span className="material-symbols-outlined me-1">
-                    download
-                  </span>
-                )}
+                <span className="material-symbols-outlined me-1">download</span>
                 {t('admin:audit_log_management.export')}
               </button>
             </div>
@@ -364,6 +329,11 @@ export const AuditLogManagement: FC = () => {
               </button>
             </div>
           </div>
+
+          <AuditLogExportModal
+            isOpen={isExportModalOpen}
+            onClose={() => setIsExportModalOpen(false)}
+          />
         </>
       )}
     </div>