Просмотр исходного кода

Merge pull request #10769 from growilabs/feat/94790-172039-administrators-can-request-export-by-clicking-the-export-button

feat: administrator can request an export from the audit log bulk export modal
Yuki Takei 1 месяц назад
Родитель
Сommit
081e5e81b7

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

@@ -860,6 +860,10 @@
     "available_action_list_explanation": "List of actions that can be searched/viewed in the current settings",
     "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": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }

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

@@ -859,6 +859,10 @@
     "available_action_list_explanation": "Liste des actions pouvant être recherchées/vues",
     "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": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }

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

@@ -869,6 +869,10 @@
     "available_action_list_explanation": "現在の設定で検索 / 表示 可能なアクション一覧です",
     "action_list": "アクション一覧",
     "disable_mode_explanation": "現在、監査ログは無効になっています。有効にする場合は環境変数 <code>AUDIT_LOG_ENABLED</code> を true に設定してください。",
+    "export": "エクスポート",
+    "export_audit_log": "監査ログのエクスポート",
+    "export_requested": "エクスポートリクエストを受け付けました。完了後に通知されます。",
+    "export_failed": "エクスポートの開始に失敗しました",
     "docs_url": {
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }

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

@@ -860,6 +860,10 @@
     "available_action_list_explanation": "현재 설정에서 검색/볼 수 있는 작업 목록",
     "action_list": "작업 목록",
     "disable_mode_explanation": "감사 로그가 현재 비활성화되어 있습니다. 활성화하려면 환경 변수 <code>AUDIT_LOG_ENABLED</code>를 true로 설정하십시오.",
+    "export": "내보내기",
+    "export_audit_log": "감사 로그 내보내기",
+    "export_requested": "내보내기 요청이 접수되었습니다. 내보내기가 완료되면 알림을 받게 됩니다.",
+    "export_failed": "내보내기 시작에 실패했습니다",
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }

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

@@ -869,6 +869,10 @@
     "available_action_list_explanation": "在当前配置中可以搜索/查看的行动列表",
     "action_list": "行动清单",
     "disable_mode_explanation": "审计日志当前已禁用。 要启用它,请将环境变量 <code>AUDIT_LOG_ENABLED</code> 设置为 true。",
+    "export": "导出",
+    "export_audit_log": "导出审核日志",
+    "export_requested": "导出请求已接受。导出完成后将通知您。",
+    "export_failed": "导出启动失败",
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }

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

@@ -0,0 +1,177 @@
+import { useCallback, 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 { 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;
+};
+
+const AuditLogExportModalSubstance = ({
+  onClose,
+}: {
+  onClose: () => void;
+}): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  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>(
+        auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
+      ),
+  );
+  const [isExporting, setIsExporting] = useState<boolean>(false);
+
+  const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
+    setStartDate(dateList[0]);
+    setEndDate(dateList[1]);
+  }, []);
+
+  const actionCheckboxChangedHandler = useCallback(
+    (action: SupportedActionType) => {
+      setActionMap((prev) => {
+        const next = new Map(prev);
+        next.set(action, !next.get(action));
+        return next;
+      });
+    },
+    [],
+  );
+
+  const multipleActionCheckboxChangedHandler = useCallback(
+    (actions: SupportedActionType[], isChecked: boolean) => {
+      setActionMap((prev) => {
+        const next = new Map(prev);
+        actions.forEach((action) => {
+          next.set(action, isChecked);
+        });
+        return next;
+      });
+    },
+    [],
+  );
+
+  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 (
+    <>
+      <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 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>
+    </>
+  );
+};
+
+export const AuditLogExportModal = ({
+  isOpen,
+  onClose,
+}: Props): JSX.Element => {
+  return (
+    <Modal isOpen={isOpen} toggle={onClose}>
+      {isOpen && <AuditLogExportModalSubstance onClose={onClose} />}
+    </Modal>
+  );
+};

+ 19 - 0
apps/app/src/client/components/Admin/AuditLogManagement.tsx

@@ -18,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';
@@ -185,6 +186,8 @@ export const AuditLogManagement: FC = () => {
     setActivePageNumber(jumpPageNumber);
   }, [jumpPageNumber]);
 
+  const [isExportModalOpen, setIsExportModalOpen] = useState<boolean>(false);
+
   const startIndex = activityList.length === 0 ? 0 : offset + 1;
   const endIndex = activityList.length === 0 ? 0 : offset + activityList.length;
 
@@ -267,6 +270,17 @@ export const AuditLogManagement: FC = () => {
                 {t('admin:audit_log_management.clear')}
               </button>
             </div>
+
+            <div className="col-12">
+              <button
+                type="button"
+                className="btn btn-outline-secondary"
+                onClick={() => setIsExportModalOpen(true)}
+              >
+                <span className="material-symbols-outlined me-1">download</span>
+                {t('admin:audit_log_management.export')}
+              </button>
+            </div>
           </div>
 
           <p className="ms-2">
@@ -315,6 +329,11 @@ export const AuditLogManagement: FC = () => {
               </button>
             </div>
           </div>
+
+          <AuditLogExportModal
+            isOpen={isExportModalOpen}
+            onClose={() => setIsExportModalOpen(false)}
+          />
         </>
       )}
     </div>