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

Merge pull request #6901 from mizozobu/feat/g2g-show-progress

feat: show detailed progress
Haku Mizuki 3 лет назад
Родитель
Сommit
296e4b854c

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

@@ -1043,6 +1043,7 @@
     "paste_transfer_key": "Paste transter key here"
   },
   "g2g": {
+    "transfer_success": "Completed GROWI to GROWI transfer successfully",
     "error_upload_attachment": "Failed to upload attachment <code>%s</code>",
     "error_generate_growi_archive": "Failed to generate GROWI archive file",
     "error_send_growi_archive": "Failed to send GROWI archive file to new GROWI"

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

@@ -1042,6 +1042,7 @@
     "ADMIN_SEARCH_INDICES_REBUILD": "Elasticsearch のインデックスのリビルド"
   },
   "g2g": {
+    "transfer_success": "G2G移行が完了しました",
     "error_upload_attachment": "添付ファイルのアップロードに失敗しました",
     "error_generate_growi_archive": "GROWI アーカイブファイルの作成に失敗しました",
     "error_send_growi_archive": "GROWI アーカイブファイルの送信に失敗しました"

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

@@ -1008,6 +1008,7 @@
     "ADMIN_SEARCH_INDICES_REBUILD": "重建 Elasticsearch 索引"
   },
   "g2g": {
+    "transfer_success": "Completed GROWI to GROWI transfer successfully",
     "error_upload_attachment": "Failed to upload attachment <code>%s</code>",
     "error_generate_growi_archive": "Failed to generate GROWI archive file",
     "error_send_growi_archive": "Failed to send GROWI archive file to new GROWI"

+ 32 - 35
packages/app/src/components/Admin/G2GDataTransfer.tsx

@@ -3,13 +3,15 @@ import React, { useCallback, useEffect, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useGenerateTransferKeyWithThrottle } from '~/client/services/g2g-transfer';
-import { toastError } from '~/client/util/apiNotification';
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
+import { G2G_PROGRESS_STATUS, type G2GProgress } from '~/interfaces/g2g-transfer';
 import { useAdminSocket } from '~/stores/socket-io';
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
+import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 
 const IGNORED_COLLECTION_NAMES = [
   'sessions', 'rlflx', 'activities', 'attachmentFiles.files', 'attachmentFiles.chunks',
@@ -25,7 +27,10 @@ const G2GDataTransfer = (): JSX.Element => {
   const [optionsMap, setOptionsMap] = useState<any>({});
   const [isShowExportForm, setShowExportForm] = useState(false);
   const [isTransferring, setTransferring] = useState(false);
-  const [statusMessage, setStatusMessage] = useState<string | undefined>(undefined);
+  const [g2gProgress, setG2GProgress] = useState<G2GProgress>({
+    mongo: G2G_PROGRESS_STATUS.PENDING,
+    attachments: G2G_PROGRESS_STATUS.PENDING,
+  });
 
   const updateSelectedCollections = (newSelectedCollections: Set<string>) => {
     setSelectedCollections(newSelectedCollections);
@@ -56,36 +61,25 @@ const G2GDataTransfer = (): JSX.Element => {
 
   const setupWebsocketEventHandler = useCallback(() => {
     if (socket != null) {
-      socket.on('admin:onStartTransferMongoData', () => {
-        setTransferring(true);
-        setStatusMessage(t('Transferring DB data ...'));
-      });
-
-      socket.on('admin:onStartTransferAttachments', () => {
-        setStatusMessage(t('Transferring attachment files ...'));
-      });
+      socket.on('admin:g2gProgress', (g2gProgress: G2GProgress) => {
+        setG2GProgress(g2gProgress);
 
-      socket.on('admin:onFinishTransfer', () => {
-        setTransferring(false);
-        setStatusMessage(t('Successfully transferred GROWI. Now you can use new GROWI !'));
+        if (g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED && g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED) {
+          toastSuccess(t('admin:g2g:transfer_success'));
+        }
       });
 
-      socket.on('admin:onG2gError', ({ key }) => {
+      socket.on('admin:g2gError', ({ key }) => {
         setTransferring(false);
-        setStatusMessage(t(key));
+        toastError(t(key));
       });
     }
-  }, [socket, t]);
+  }, [socket, t, setTransferring, setG2GProgress]);
 
   const cleanUpWebsocketEventHandler = useCallback(() => {
     if (socket != null) {
-      socket.off('admin:onStartTransferMongoData');
-
-      socket.off('admin:onStartTransferAttachments');
-
-      socket.off('admin:onFinishTransfer');
-
-      socket.off('admin:onG2gError');
+      socket.off('admin:g2gProgress');
+      socket.off('admin:g2gError');
     }
   }, [socket]);
 
@@ -97,6 +91,7 @@ const G2GDataTransfer = (): JSX.Element => {
 
   const startTransfer = useCallback(async(e) => {
     e.preventDefault();
+    setTransferring(true);
 
     try {
       await apiv3Post('/g2g-transfer/transfer', {
@@ -108,13 +103,16 @@ const G2GDataTransfer = (): JSX.Element => {
     catch (errs) {
       toastError(errs);
     }
-  }, [startTransferKey, selectedCollections, optionsMap]);
+  }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
 
   useEffect(() => {
     setCollectionsAndSelectedCollections();
-
     setupWebsocketEventHandler();
-  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler]);
+
+    return () => {
+      cleanUpWebsocketEventHandler();
+    };
+  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler, cleanUpWebsocketEventHandler]);
 
   return (
     <div data-testid="admin-export-archive-data">
@@ -153,16 +151,15 @@ const G2GDataTransfer = (): JSX.Element => {
         </div>
       </form>
 
-
-      {statusMessage != null && (
-        <>
-          <div className='alert alert-info d-flex align-items-center'>
-            {isTransferring && (
-              <i className="fa fa-2x fa-spinner fa-pulse mr-2"></i>
-            )}
-            <p className="mb-0">{statusMessage}</p>
+      {isTransferring && (
+        <div className='border rounded p-4'>
+          <div>
+            <G2GDataTransferStatusIcon className='mr-2 mb-2' status={g2gProgress.mongo} /> MongoDB
+          </div>
+          <div>
+            <G2GDataTransferStatusIcon className='mr-2' status={g2gProgress.attachments} /> Attachments
           </div>
-        </>
+        </div>
       )}
 
       <h2 className="border-bottom mt-5">{t('admin:g2g_data_transfer.transfer_data_to_this_growi')}</h2>

+ 37 - 0
packages/app/src/components/Admin/G2GDataTransferStatusIcon.tsx

@@ -0,0 +1,37 @@
+import React, { type ComponentPropsWithoutRef } from 'react';
+
+import { G2G_PROGRESS_STATUS, type G2GProgressStatus } from '~/interfaces/g2g-transfer';
+
+/**
+ * Props for {@link G2GDataTransferStatusIcon}
+ */
+interface Props extends ComponentPropsWithoutRef<'i'>{
+  status: G2GProgressStatus;
+}
+
+/**
+ * Icon for G2G transfer status
+ */
+const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.Element => {
+  if (status === G2G_PROGRESS_STATUS.IN_PROGRESS) {
+    return (
+      <i className={`fa fa-spinner fa-pulse fa-fw ${className}`} aria-label="in progress" {...props} />
+    );
+  }
+
+  if (status === G2G_PROGRESS_STATUS.COMPLETED) {
+    return (
+      <i className={`fa fa-check-circle-o fa-fw text-info ${className}`} aria-label="completed" {...props} />
+    );
+  }
+
+  if (status === G2G_PROGRESS_STATUS.ERROR) {
+    return (
+      <i className={`fa fa-exclamation-circle fa-fw text-danger ${className}`} aria-label="error" {...props} />
+    );
+  }
+
+  return <i className={`fa fa-circle-o fa-fw ${className}`} aria-label="pending" {...props} />;
+};
+
+export default G2GDataTransferStatusIcon;

+ 22 - 0
packages/app/src/interfaces/g2g-transfer.ts

@@ -0,0 +1,22 @@
+/**
+ * G2G transfer progress status master
+ */
+export const G2G_PROGRESS_STATUS = {
+  PENDING: 'PENDING',
+  IN_PROGRESS: 'IN_PROGRESS',
+  COMPLETED: 'COMPLETED',
+  ERROR: 'ERROR',
+} as const;
+
+/**
+ * G2G transfer progress status
+ */
+export type G2GProgressStatus = typeof G2G_PROGRESS_STATUS[keyof typeof G2G_PROGRESS_STATUS];
+
+/**
+ * G2G transfer progress
+ */
+export interface G2GProgress {
+ mongo: G2GProgressStatus;
+ attachments: G2GProgressStatus;
+}

+ 34 - 8
packages/app/src/server/service/g2g-transfer.ts

@@ -7,6 +7,7 @@ import rawAxios from 'axios';
 import FormData from 'form-data';
 import { Types as MongooseTypes } from 'mongoose';
 
+import { G2G_PROGRESS_STATUS } from '~/interfaces/g2g-transfer';
 import TransferKeyModel from '~/server/models/transfer-key';
 import { createBatchStream } from '~/server/util/batch-stream';
 import axios from '~/utils/axios';
@@ -327,7 +328,12 @@ export class G2GTransferPusherService implements Pusher {
   public async startTransfer(tk: TransferKey, user: any, toGROWIInfo: IDataGROWIInfo, collections: string[], optionsMap: any, attachmentIdsFromNewGrowi: string[], shouldEmit = true): Promise<void> {
     const socket = this.crowi.socketIoService.getAdminSocket();
 
-    if (shouldEmit) socket.emit('admin:onStartTransferMongoData', {});
+    if (shouldEmit) {
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.IN_PROGRESS,
+        attachments: G2G_PROGRESS_STATUS.PENDING,
+      });
+    }
 
     const targetConfigKeys = uploadConfigKeys;
 
@@ -345,7 +351,11 @@ export class G2GTransferPusherService implements Pusher {
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:onG2gError', { message: 'Failed to generate GROWI archive file', key: 'error_generate_growi_archive' });
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.ERROR,
+        attachments: G2G_PROGRESS_STATUS.PENDING,
+      });
+      socket.emit('admin:g2gError', { message: 'Failed to generate GROWI archive file', key: 'admin:g2g:error_generate_growi_archive' });
       throw err;
     }
 
@@ -364,24 +374,40 @@ export class G2GTransferPusherService implements Pusher {
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:onG2gError', { message: 'Failed to send GROWI archive file to new GROWI', key: 'error_send_growi_archive' });
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.ERROR,
+        attachments: G2G_PROGRESS_STATUS.PENDING,
+      });
+      socket.emit('admin:g2gError', { message: 'Failed to send GROWI archive file to new GROWI', key: 'admin:g2g:error_send_growi_archive' });
       throw err;
     }
 
-    if (shouldEmit) socket.emit('admin:onStartTransferAttachments', {});
+    if (shouldEmit) {
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.COMPLETED,
+        attachments: G2G_PROGRESS_STATUS.IN_PROGRESS,
+      });
+    }
 
     try {
       await this.transferAttachments(tk, attachmentIdsFromNewGrowi);
     }
     catch (err) {
       logger.error(err);
-      socket.emit('admin:onG2gError', { message: 'Failed to transfer attachments', key: 'error_upload_attachment' });
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.COMPLETED,
+        attachments: G2G_PROGRESS_STATUS.ERROR,
+      });
+      socket.emit('admin:g2gError', { message: 'Failed to transfer attachments', key: 'admin:g2g:error_upload_attachment' });
       throw err;
     }
 
-    if (shouldEmit) socket.emit('admin:onFinishTransfer', {});
-
-    return;
+    if (shouldEmit) {
+      socket.emit('admin:g2gProgress', {
+        mongo: G2G_PROGRESS_STATUS.COMPLETED,
+        attachments: G2G_PROGRESS_STATUS.COMPLETED,
+      });
+    }
   }
 
   /**