Browse Source

feat: show detailed progress

mizozobu 3 years ago
parent
commit
3c599231ca

+ 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": "G2G transfer successfully completed",
     "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": "G2G transfer successfully completed",
     "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"

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

@@ -3,18 +3,56 @@ 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 { 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',
 ];
 
+/**
+ * Get the union type of all the values in an object, array or array-like type `T`
+ * @see {@link https://github.com/piotrwitek/utility-types/blob/df2502ef504c4ba8bd9de81a45baef112b7921d0/src/mapped-types.ts#L589-L597}
+ */
+type ValuesType<
+  T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>
+> = T extends ReadonlyArray<any>
+  ? T[number]
+  : T extends ArrayLike<any>
+  ? T[number]
+  : T extends object
+  ? T[keyof T]
+  : never;
+
+/**
+ * G2G transfer progress status master
+ */
+const G2G_PROGRESS_STATUS = {
+  PENDING: 'PENDING',
+  IN_PROGRESS: 'IN_PROGRESS',
+  COMPLETED: 'COMPLETED',
+  ERROR: 'ERROR',
+} as const;
+
+/**
+ * G2G transfer progress status
+ */
+type G2GProgressStatus = ValuesType<typeof G2G_PROGRESS_STATUS>
+
+/**
+ * G2G transfer progress
+ */
+interface G2GProgress {
+  mongo: G2GProgressStatus;
+  attachments: G2GProgressStatus;
+}
+
 const G2GDataTransfer = (): JSX.Element => {
   const { data: socket } = useAdminSocket();
   const { t } = useTranslation();
@@ -25,7 +63,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 +97,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:g2gProgress', (g2gProgress: G2GProgress) => {
+        setG2GProgress(g2gProgress);
 
-      socket.on('admin:onStartTransferAttachments', () => {
-        setStatusMessage(t('Transferring attachment files ...'));
+        if (g2gProgress.mongo === G2G_PROGRESS_STATUS.COMPLETED && g2gProgress.attachments === G2G_PROGRESS_STATUS.COMPLETED) {
+          toastSuccess(t('admin:g2g:transfer_success'));
+        }
       });
 
-      socket.on('admin:onFinishTransfer', () => {
+      socket.on('admin:g2gError', ({ key }) => {
         setTransferring(false);
-        setStatusMessage(t('Successfully transferred GROWI. Now you can use new GROWI !'));
-      });
-
-      socket.on('admin:onG2gError', ({ 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 +127,7 @@ const G2GDataTransfer = (): JSX.Element => {
 
   const startTransfer = useCallback(async(e) => {
     e.preventDefault();
+    setTransferring(true);
 
     try {
       await apiv3Post('/g2g-transfer/transfer', {
@@ -108,13 +139,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 +187,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>

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

@@ -0,0 +1,64 @@
+import React, { type ComponentPropsWithoutRef } from 'react';
+
+/**
+ * Get the union type of all the values in an object, array or array-like type `T`
+ * @see {@link https://github.com/piotrwitek/utility-types/blob/df2502ef504c4ba8bd9de81a45baef112b7921d0/src/mapped-types.ts#L589-L597}
+ */
+type ValuesType<
+ T extends ReadonlyArray<any> | ArrayLike<any> | Record<any, any>
+> = T extends ReadonlyArray<any>
+ ? T[number]
+ : T extends ArrayLike<any>
+ ? T[number]
+ : T extends object
+ ? T[keyof T]
+ : never;
+
+/**
+ * G2G transfer progress status master
+ */
+const G2G_PROGRESS_STATUS = {
+  PENDING: 'PENDING',
+  IN_PROGRESS: 'IN_PROGRESS',
+  COMPLETED: 'COMPLETED',
+  ERROR: 'ERROR',
+} as const;
+
+/**
+ * G2G transfer progress status
+ */
+type G2GProgressStatus = ValuesType<typeof G2G_PROGRESS_STATUS>
+
+/**
+ * Props for {@link G2GDataTransfer}
+ */
+interface Props extends ComponentPropsWithoutRef<'i'>{
+  status: G2GProgressStatus;
+}
+
+/**
+ * Icon for G2G transfer status
+ */
+const G2GDataTransferStatusIcon = ({ status, className, ...props }: Props): JSX.Element | null => {
+  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;

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

@@ -31,6 +31,16 @@ export const uploadConfigKeys = [
   'gcs:useOnlyEnvVarsForSomeOptions',
 ];
 
+/**
+* G2G transfer progress status master
+*/
+const G2G_PROGRESS_STATUS = {
+  PENDING: 'PENDING',
+  IN_PROGRESS: 'IN_PROGRESS',
+  COMPLETED: 'COMPLETED',
+  ERROR: 'ERROR',
+} as const;
+
 /**
  * Data used for comparing to/from GROWI information
  */
@@ -304,7 +314,12 @@ export class G2GTransferPusherService implements Pusher {
   public async startTransfer(tk: TransferKey, user: any, toGROWIInfo: IDataGROWIInfo, collections: string[], optionsMap: any, 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;
 
@@ -322,7 +337,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;
     }
 
@@ -341,24 +360,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);
     }
     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,
+      });
+    }
   }
 
   /**