Procházet zdrojové kódy

Merge pull request #10783 from growilabs/feat/94790-178338-duplicate-export-confirmation

feat: duplicate export confirmation
Yuki Takei před 1 měsícem
rodič
revize
566e860dfa

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

@@ -864,6 +864,9 @@
     "export_audit_log": "Export Audit Log",
     "export_audit_log": "Export Audit Log",
     "export_requested": "Export request accepted. You will be notified when the export is complete.",
     "export_requested": "Export request accepted. You will be notified when the export is complete.",
     "export_failed": "Failed to start export",
     "export_failed": "Failed to start export",
+    "duplicate_export_confirm": "An export with the same conditions is already in progress. Do you want to restart it?",
+    "restart_export": "Restart Export",
+    "confirm_export": "Confirm Export",
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }

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

@@ -863,6 +863,9 @@
     "export_audit_log": "Exporter le journal d'audit",
     "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_requested": "Demande d'exportation acceptée. Vous serez averti lorsque l'exportation sera terminée.",
     "export_failed": "Échec du démarrage de l'exportation",
     "export_failed": "Échec du démarrage de l'exportation",
+    "duplicate_export_confirm": "Une exportation avec les mêmes conditions est déjà en cours. Voulez-vous la redémarrer ?",
+    "restart_export": "Redémarrer l'exportation",
+    "confirm_export": "Confirmer l'exportation",
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }

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

@@ -873,6 +873,9 @@
     "export_audit_log": "監査ログのエクスポート",
     "export_audit_log": "監査ログのエクスポート",
     "export_requested": "エクスポートリクエストを受け付けました。完了後に通知されます。",
     "export_requested": "エクスポートリクエストを受け付けました。完了後に通知されます。",
     "export_failed": "エクスポートの開始に失敗しました",
     "export_failed": "エクスポートの開始に失敗しました",
+    "duplicate_export_confirm": "同じ条件のエクスポートが進行中です。やり直しますか?",
+    "restart_export": "やり直す",
+    "confirm_export": "エクスポートの確認",
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }

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

@@ -864,6 +864,9 @@
     "export_audit_log": "감사 로그 내보내기",
     "export_audit_log": "감사 로그 내보내기",
     "export_requested": "내보내기 요청이 접수되었습니다. 내보내기가 완료되면 알림을 받게 됩니다.",
     "export_requested": "내보내기 요청이 접수되었습니다. 내보내기가 완료되면 알림을 받게 됩니다.",
     "export_failed": "내보내기 시작에 실패했습니다",
     "export_failed": "내보내기 시작에 실패했습니다",
+    "duplicate_export_confirm": "동일한 조건의 내보내기가 이미 진행 중입니다. 다시 시작하시겠습니까?",
+    "restart_export": "내보내기 다시 시작",
+    "confirm_export": "내보내기 확인",
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }

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

@@ -873,6 +873,9 @@
     "export_audit_log": "导出审核日志",
     "export_audit_log": "导出审核日志",
     "export_requested": "导出请求已接受。导出完成后将通知您。",
     "export_requested": "导出请求已接受。导出完成后将通知您。",
     "export_failed": "导出启动失败",
     "export_failed": "导出启动失败",
+    "duplicate_export_confirm": "已有相同条件的导出正在进行中。是否要重新启动它?",
+    "restart_export": "重新启动导出",
+    "confirm_export": "确认导出",
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }

+ 39 - 35
apps/app/src/client/components/Admin/AuditLog/AuditLogExportModal.tsx

@@ -4,14 +4,14 @@ import { useAtomValue } from 'jotai';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
 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 type { SupportedActionType } from '~/interfaces/activity';
 import { auditLogAvailableActionsAtom } from '~/states/server-configurations';
 import { auditLogAvailableActionsAtom } from '~/states/server-configurations';
 
 
 import { DateRangePicker } from './DateRangePicker';
 import { DateRangePicker } from './DateRangePicker';
+import { DuplicateExportConfirmModal } from './DuplicateExportConfirmModal';
 import { SearchUsernameTypeahead } from './SearchUsernameTypeahead';
 import { SearchUsernameTypeahead } from './SearchUsernameTypeahead';
 import { SelectActionDropdown } from './SelectActionDropdown';
 import { SelectActionDropdown } from './SelectActionDropdown';
+import { useAuditLogExport } from './useAuditLogExport';
 
 
 type Props = {
 type Props = {
   isOpen: boolean;
   isOpen: boolean;
@@ -38,7 +38,6 @@ const AuditLogExportModalSubstance = ({
         auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
         auditLogAvailableActionsData?.map((action) => [action, true]) ?? [],
       ),
       ),
   );
   );
-  const [isExporting, setIsExporting] = useState<boolean>(false);
 
 
   const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
   const datePickerChangedHandler = useCallback((dateList: Date[] | null[]) => {
     setStartDate(dateList[0]);
     setStartDate(dateList[0]);
@@ -73,39 +72,38 @@ const AuditLogExportModalSubstance = ({
     setSelectedUsernames(usernames);
     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);
+  const buildFilters = useCallback(() => {
+    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;
     }
     }
-  }, [actionMap, startDate, endDate, t, onClose]);
+
+    return filters;
+  }, [actionMap, startDate, endDate]);
+
+  const {
+    isExporting,
+    isDuplicateConfirmOpen,
+    exportHandler,
+    restartExportHandler,
+    closeDuplicateConfirm,
+  } = useAuditLogExport(buildFilters, onClose);
 
 
   return (
   return (
     <>
     <>
@@ -161,6 +159,12 @@ const AuditLogExportModalSubstance = ({
           {t('audit_log_management.export')}
           {t('audit_log_management.export')}
         </button>
         </button>
       </ModalFooter>
       </ModalFooter>
+
+      <DuplicateExportConfirmModal
+        isOpen={isDuplicateConfirmOpen}
+        onClose={closeDuplicateConfirm}
+        onRestart={restartExportHandler}
+      />
     </>
     </>
   );
   );
 };
 };

+ 39 - 0
apps/app/src/client/components/Admin/AuditLog/DuplicateExportConfirmModal.tsx

@@ -0,0 +1,39 @@
+import { useTranslation } from 'react-i18next';
+import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
+
+type Props = {
+  isOpen: boolean;
+  onClose: () => void;
+  onRestart: () => void;
+};
+
+export const DuplicateExportConfirmModal = ({
+  isOpen,
+  onClose,
+  onRestart,
+}: Props): JSX.Element => {
+  const { t } = useTranslation('admin');
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose}>
+      <ModalHeader tag="h4" toggle={onClose}>
+        {t('audit_log_management.confirm_export')}
+      </ModalHeader>
+      <ModalBody>
+        {t('audit_log_management.duplicate_export_confirm')}
+      </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={onRestart}>
+          {t('audit_log_management.restart_export')}
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};

+ 67 - 0
apps/app/src/client/components/Admin/AuditLog/useAuditLogExport.ts

@@ -0,0 +1,67 @@
+import { useCallback, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import type { IAuditLogBulkExportFilters } from '~/features/audit-log-bulk-export/interfaces/audit-log-bulk-export';
+
+export const useAuditLogExport = (
+  buildFilters: () => IAuditLogBulkExportFilters,
+  onClose: () => void,
+) => {
+  const { t } = useTranslation('admin');
+
+  const [isExporting, setIsExporting] = useState(false);
+  const [isDuplicateConfirmOpen, setIsDuplicateConfirmOpen] = useState(false);
+
+  const exportHandler = useCallback(async () => {
+    setIsExporting(true);
+    try {
+      const filters = buildFilters();
+      await apiv3Post('/audit-log-bulk-export', { filters });
+      toastSuccess(t('audit_log_management.export_requested'));
+      onClose();
+    } catch (errs) {
+      const isDuplicate =
+        Array.isArray(errs) &&
+        errs.some(
+          (e) => e.code === 'audit_log_bulk_export.duplicate_export_job_error',
+        );
+
+      if (isDuplicate) {
+        setIsDuplicateConfirmOpen(true);
+      } else {
+        toastError(t('audit_log_management.export_failed'));
+      }
+    } finally {
+      setIsExporting(false);
+    }
+  }, [buildFilters, t, onClose]);
+
+  const restartExportHandler = useCallback(async () => {
+    setIsDuplicateConfirmOpen(false);
+    setIsExporting(true);
+    try {
+      const filters = buildFilters();
+      await apiv3Post('/audit-log-bulk-export', { filters, restartJob: true });
+      toastSuccess(t('audit_log_management.export_requested'));
+      onClose();
+    } catch {
+      toastError(t('audit_log_management.export_failed'));
+    } finally {
+      setIsExporting(false);
+    }
+  }, [buildFilters, t, onClose]);
+
+  const closeDuplicateConfirm = useCallback(() => {
+    setIsDuplicateConfirmOpen(false);
+  }, []);
+
+  return {
+    isExporting,
+    isDuplicateConfirmOpen,
+    exportHandler,
+    restartExportHandler,
+    closeDuplicateConfirm,
+  };
+};