Sfoglia il codice sorgente

add G2GDataTransferExportForm

atsuki-t 3 anni fa
parent
commit
8d9dcdc84b

+ 10 - 13
packages/app/src/components/Admin/G2GDataTransfer.tsx

@@ -7,9 +7,10 @@ import { useGenerateTransferKeyWithThrottle } from '~/client/services/g2g-transf
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { useAdminSocket } from '~/stores/socket-io';
 
+
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 
-import SelectCollectionsModal from './ExportArchiveData/SelectCollectionsModal';
+import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 
 const IGNORED_COLLECTION_NAMES = [
   'sessions', 'rlflx', 'activities',
@@ -20,7 +21,7 @@ const G2GDataTransfer = (): JSX.Element => {
   const { t } = useTranslation();
 
   const [collections, setCollections] = useState<any[]>([]);
-  const [isExportModalOpen, setExportModalOpen] = useState(false);
+  const [isShowExportForm, setShowExportForm] = useState(false);
   const [isExporting, setExporting] = useState(false);
   // TODO: データのエクスポートが完了したことが分かるようにする
   const [isExported, setExported] = useState(false);
@@ -77,8 +78,6 @@ const G2GDataTransfer = (): JSX.Element => {
     // データ移行の処理
   };
 
-  const exportingRequestedHandler = useCallback(() => {}, []);
-
   useEffect(() => {
     fetchData();
 
@@ -89,10 +88,16 @@ const G2GDataTransfer = (): JSX.Element => {
     <div data-testid="admin-export-archive-data">
       <h2 className="border-bottom">{t('admin:g2g_data_transfer.transfer_data_to_another_growi')}</h2>
 
-      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isExporting} onClick={() => setExportModalOpen(true)}>
+      <button type="button" className="btn btn-outline-secondary mt-4" disabled={isExporting} onClick={() => setShowExportForm(!isShowExportForm)}>
         {t('admin:g2g_data_transfer.advanced_options')}
       </button>
 
+      {collections.length !== 0 && (
+        <div className={isShowExportForm ? '' : 'd-none'}>
+          <G2GDataTransferExportForm allCollectionNames={collections} />
+        </div>
+      )}
+
       <form onSubmit={transferData}>
         <div className="form-group row mt-3">
           <div className="col-9">
@@ -125,14 +130,6 @@ const G2GDataTransfer = (): JSX.Element => {
         <p className="mb-1">{t('admin:g2g_data_transfer.once_transfer_key_used')}</p>
         <p className="mb-0">{t('admin:g2g_data_transfer.transfer_to_growi_cloud')}</p>
       </div>
-
-      <SelectCollectionsModal
-        isOpen={isExportModalOpen}
-        onExportingRequested={exportingRequestedHandler}
-        onClose={() => setExportModalOpen(false)}
-        collections={collections}
-        isAllChecked
-      />
     </div>
   );
 };

+ 279 - 0
packages/app/src/components/Admin/G2GDataTransferExportForm.tsx

@@ -0,0 +1,279 @@
+import React, { useState, useEffect, useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+
+import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
+import ImportOptionForPages from '~/models/admin/import-option-for-pages';
+import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
+import { useAdminSocket } from '~/stores/socket-io';
+
+import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
+import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
+
+const GROUPS_PAGE = [
+  'pages', 'revisions', 'tags', 'pagetagrelations',
+];
+const GROUPS_USER = [
+  'users', 'externalaccounts', 'usergroups', 'usergrouprelations',
+];
+const GROUPS_CONFIG = [
+  'configs', 'updateposts', 'globalnotificationsettings',
+];
+const ALL_GROUPED_COLLECTIONS = GROUPS_PAGE.concat(GROUPS_USER).concat(GROUPS_CONFIG);
+
+const IMPORT_OPTION_CLASS_MAPPING = {
+  pages: ImportOptionForPages,
+  revisions: ImportOptionForRevisions,
+};
+
+type Props = {
+  allCollectionNames: string[],
+};
+
+const G2GDataTransferExportForm = (props: Props): JSX.Element => {
+  const { data: socket } = useAdminSocket();
+  const { t } = useTranslation('admin');
+
+  const { allCollectionNames } = props;
+
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set(allCollectionNames));
+  const [optionsMap, setOptionsMap] = useState<any>({});
+  const [isImporting, setImporting] = useState(false);
+  const [isImported, setImported] = useState(false);
+  const [progressMap, setProgressMap] = useState<any>({});
+  const [errorsMap, setErrorsMap] = useState<any>([]);
+  const [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
+  const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
+  const [isErrorsViewerOpen, setErrorsViewerOpen] = useState(false);
+  const [collectionNameForErrorsViewer, setCollectionNameForErrorsViewer] = useState<any>();
+
+  const checkAll = useCallback(() => {
+    setSelectedCollections(new Set(allCollectionNames));
+  }, [allCollectionNames]);
+
+  const uncheckAll = useCallback(() => {
+    setSelectedCollections(new Set());
+  }, []);
+
+  const ImportItems = ({ collectionNames }): JSX.Element => {
+    const toggleCheckbox = (collectionName, bool) => {
+      const collections = new Set(selectedCollections);
+      if (bool) {
+        collections.add(collectionName);
+      }
+      else {
+        collections.delete(collectionName);
+      }
+
+      setSelectedCollections(collections);
+
+      // this.validate();
+    };
+
+    const updateOption = (collectionName, data) => {
+      const options = optionsMap[collectionName];
+
+      // merge
+      Object.assign(options, data);
+
+      optionsMap[collectionName] = options;
+      setOptionsMap({ ...optionsMap, collectionName: options });
+    };
+
+    const openConfigurationModal = (collectionName) => {
+      setConfigurationModalOpen(true);
+      setCollectionNameForConfiguration(collectionName);
+    };
+
+    const showErrorsViewer = (collectionName) => {
+      setErrorsViewerOpen(true);
+      setCollectionNameForErrorsViewer(collectionName);
+    };
+
+    return (
+      <div className="row">
+        {collectionNames.map((collectionName) => {
+          const collectionProgress = progressMap[collectionName];
+          const errors = errorsMap[collectionName];
+          const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
+
+          if (optionsMap[collectionName] == null) {
+            return <></>;
+          }
+
+          return (
+            <div className="col-md-6 my-1" key={collectionName}>
+              <ImportCollectionItem
+                isImporting={isImporting}
+                isImported={collectionProgress ? isImported : false}
+                insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
+                modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
+                errorsCount={errors ? errors.length : 0}
+                collectionName={collectionName}
+                isSelected={selectedCollections.has(collectionName)}
+                option={optionsMap[collectionName]}
+                isConfigButtonAvailable={isConfigButtonAvailable}
+                // events
+                onChange={toggleCheckbox}
+                onOptionChange={updateOption}
+                onConfigButtonClicked={openConfigurationModal}
+                onErrorLinkClicked={showErrorsViewer}
+              />
+            </div>
+          );
+        })}
+      </div>
+    );
+  };
+
+  const WarnForGroups = ({ errors, key }): JSX.Element => {
+    if (errors.length === 0) {
+      return <></>;
+    }
+
+    return (
+      <div key={key} className="alert alert-warning">
+        <ul>
+          {errors.map((error, index) => {
+            // eslint-disable-next-line react/no-array-index-key
+            return <li key={`${key}-${index}`}>{error}</li>;
+          })}
+        </ul>
+      </div>
+    );
+  };
+
+  const GroupImportItems = ({ groupList, groupName, errors }): JSX.Element => {
+    const collectionNames = groupList.filter((groupCollectionName) => {
+      return allCollectionNames.includes(groupCollectionName);
+    });
+
+    if (collectionNames.length === 0) {
+      return <></>;
+    }
+
+    return (
+      <div className="mt-4">
+        <legend>{groupName} Collections</legend>
+        <ImportItems collectionNames={collectionNames} />
+        <WarnForGroups errors={errors} key={`warnFor${groupName}`} />
+      </div>
+    );
+  };
+
+  const OtherImportItems = (): JSX.Element => {
+    const collectionNames = allCollectionNames.filter((collectionName) => {
+      return !ALL_GROUPED_COLLECTIONS.includes(collectionName);
+    });
+
+    // TODO: エラー対応
+    return <GroupImportItems groupList={collectionNames} groupName='Other' errors={[]} />;
+  };
+
+  const setInitialOptionsMap = () => {
+    const initialOptionsMap = {};
+    allCollectionNames.forEach((collectionName) => {
+      const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
+        ? MODE_RESTRICTED_COLLECTION[collectionName][0]
+        : DEFAULT_MODE;
+      const ImportOption = IMPORT_OPTION_CLASS_MAPPING[collectionName] || GrowiArchiveImportOption;
+      initialOptionsMap[collectionName] = new ImportOption(initialMode);
+    });
+    setOptionsMap(initialOptionsMap);
+  };
+
+  const setupWebsocketEventHandler = () => {
+    if (socket != null) {
+      socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
+        setImporting(true);
+        setProgressMap((prev) => {
+          return { ...prev, collectionName: collectionProgress };
+        });
+        const errors = errorsMap[collectionName] || [];
+        setErrorsMap((prev) => {
+          return { ...prev, collectionName: errors.concat(appendedErrors) };
+        });
+      });
+
+      // websocket event
+      socket.on('admin:onTerminateForImport', () => {
+        setImporting(false);
+        setImported(true);
+      });
+
+      // websocket event
+      socket.on('admin:onErrorForImport', () => {
+        setImporting(false);
+        setImported(false);
+      });
+    }
+  };
+
+  const teardownWebsocketEventHandler = () => {
+    if (socket != null) {
+      socket.removeAllListeners('admin:onProgressForImport');
+      socket.removeAllListeners('admin:onTerminateForImport');
+    }
+  };
+
+  useEffect(() => {
+    setInitialOptionsMap();
+    setupWebsocketEventHandler();
+    teardownWebsocketEventHandler();
+  }, []);
+
+  return (
+    <>
+      <form className="form-inline mt-4">
+        <div className="form-group">
+          <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={checkAll}>
+            <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}
+          </button>
+        </div>
+        <div className="form-group">
+          <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={uncheckAll}>
+            <i className="fa fa-square-o"></i> {t('admin:export_management.uncheck_all')}
+          </button>
+        </div>
+      </form>
+
+      <div className="card well small my-4">
+        <ul>
+          <li>{t('admin:importer_management.growi_settings.description_of_import_mode.about')}</li>
+          <ul>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.insert')}</li>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.upsert')}</li>
+            <li>{t('admin:importer_management.growi_settings.description_of_import_mode.flash_and_insert')}</li>
+          </ul>
+        </ul>
+      </div>
+
+      {/* TODO: エラー追加 */}
+      <GroupImportItems groupList={GROUPS_PAGE} groupName='Page' errors={[]} />
+      <GroupImportItems groupList={GROUPS_USER} groupName='User' errors={[]} />
+      <GroupImportItems groupList={GROUPS_CONFIG} groupName='Config' errors={[]} />
+      <OtherImportItems />
+
+      {/* <div className="mt-4 text-center">
+        <button type="button" className="btn btn-outline-secondary mx-1" onClick={this.props.onDiscard}>
+          {t('admin:importer_management.growi_settings.discard')}
+        </button>
+        <button type="button" className="btn btn-primary mx-1" onClick={this.import} disabled={!canImport || isImporting}>
+          {t('admin:importer_management.import')}
+        </button>
+      </div> */}
+
+      {/* <ImportCollectionConfigurationModal
+        isOpen={isConfigurationModalOpen}
+        onClose={() => this.setState({ isConfigurationModalOpen: false })}
+        onOptionChange={this.updateOption}
+        collectionName={collections}
+        option={optionsMap[collections]}
+      /> */}
+
+      {/* {this.renderErrorsViewer()} */}
+    </>
+  );
+};
+
+export default G2GDataTransferExportForm;