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

Merge pull request #17 from hakumizuki/feat/add-options-map-to-g2g-data-transfer

feat: add options map to g2g data transfer
Haku Mizuki 3 лет назад
Родитель
Сommit
f4e4d2a3c0

+ 50 - 21
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',
@@ -19,13 +20,28 @@ const G2GDataTransfer = (): JSX.Element => {
   const { data: socket } = useAdminSocket();
   const { t } = useTranslation();
 
-  const [collections, setCollections] = useState<any[]>([]);
-  const [isExportModalOpen, setExportModalOpen] = useState(false);
+  const [startTransferKey, setStartTransferKey] = useState('');
+  const [collections, setCollections] = useState<string[]>([]);
+  const [selectedCollections, setSelectedCollections] = useState<Set<string>>(new Set());
+  const [optionsMap, setOptionsMap] = useState<any>({});
+  const [isShowExportForm, setShowExportForm] = useState(false);
   const [isExporting, setExporting] = useState(false);
   // TODO: データのエクスポートが完了したことが分かるようにする
   const [isExported, setExported] = useState(false);
 
-  const fetchData = useCallback(async() => {
+  const updateSelectedCollections = (newSelectedCollections: Set<string>) => {
+    setSelectedCollections(newSelectedCollections);
+  };
+
+  const updateOptionsMap = (newOptionsMap: any) => {
+    setOptionsMap(newOptionsMap);
+  };
+
+  const onChangeTransferKeyHandler = useCallback((e) => {
+    setStartTransferKey(e.target.value);
+  }, []);
+
+  const setCollectionsAndSelectedCollections = useCallback(async() => {
     const [{ data: collectionsData }, { data: statusData }] = await Promise.all([
       apiv3Get<{collections: any[]}>('/mongo/collections', {}),
       apiv3Get<{status: { zipFileStats: any[], isExporting: boolean, progressList: any[] }}>('/export/status', {}),
@@ -37,6 +53,7 @@ const G2GDataTransfer = (): JSX.Element => {
     });
 
     setCollections(filteredCollections);
+    setSelectedCollections(new Set(filteredCollections));
     setExporting(statusData.status.isExporting);
   }, []);
 
@@ -73,30 +90,50 @@ const G2GDataTransfer = (): JSX.Element => {
     generateTransferKeyWithThrottle();
   }, [generateTransferKeyWithThrottle]);
 
-  const transferData = () => {
-    // データ移行の処理
-  };
+  const startTransfer = (e) => {
+    e.preventDefault();
 
-  const exportingRequestedHandler = useCallback(() => {}, []);
+    // console.log(startTransferKey);
+    // console.log(selectedCollections);
+    // console.log(optionsMap);
+  };
 
   useEffect(() => {
-    fetchData();
+    setCollectionsAndSelectedCollections();
 
     setupWebsocketEventHandler();
-  }, [fetchData, setupWebsocketEventHandler]);
+  }, [setCollectionsAndSelectedCollections, setupWebsocketEventHandler]);
 
   return (
     <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>
 
-      <form onSubmit={transferData}>
+      {collections.length !== 0 && (
+        <div className={isShowExportForm ? '' : 'd-none'}>
+          <G2GDataTransferExportForm
+            allCollectionNames={collections}
+            selectedCollections={selectedCollections}
+            updateSelectedCollections={updateSelectedCollections}
+            optionsMap={optionsMap}
+            updateOptionsMap={updateOptionsMap}
+          />
+        </div>
+      )}
+
+      <form onSubmit={startTransfer}>
         <div className="form-group row mt-3">
           <div className="col-9">
-            <input className="form-control" type="text" placeholder={t('admin:g2g_data_transfer.paste_transfer_key')} />
+            <input
+              className="form-control"
+              type="text"
+              placeholder={t('admin:g2g_data_transfer.paste_transfer_key')}
+              onChange={onChangeTransferKeyHandler}
+              required
+            />
           </div>
           <div className="col-3">
             <button type="submit" className="btn btn-primary w-100">{t('admin:g2g_data_transfer.start_transfer')}</button>
@@ -125,14 +162,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>
   );
 };

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

@@ -0,0 +1,304 @@
+import React, {
+  useState, useEffect, useCallback, useMemo,
+} 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[],
+  selectedCollections: Set<string>,
+  updateSelectedCollections: (newSelectedCollections: Set<string>) => void,
+  optionsMap: any,
+  updateOptionsMap: (newOptionsMap: any) => void,
+};
+
+const G2GDataTransferExportForm = (props: Props): JSX.Element => {
+  // const { data: socket } = useAdminSocket();
+  const { t } = useTranslation('admin');
+
+  const {
+    allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
+  } = props;
+
+  // 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 checkAll = useCallback(() => {
+    updateSelectedCollections(new Set(allCollectionNames));
+  }, [allCollectionNames, updateSelectedCollections]);
+
+  const uncheckAll = useCallback(() => {
+    updateSelectedCollections(new Set());
+  }, [updateSelectedCollections]);
+
+  const updateOption = (collectionName, data) => {
+    const options = optionsMap[collectionName];
+
+    // merge
+    Object.assign(options, data);
+
+    const updatedOptionsMap = {};
+    updatedOptionsMap[collectionName] = options;
+    updateOptionsMap((prev) => {
+      return { ...prev, updatedOptionsMap };
+    });
+  };
+
+  const ImportItems = ({ collectionNames }): JSX.Element => {
+    const toggleCheckbox = (collectionName, bool) => {
+      const collections = new Set(selectedCollections);
+      if (bool) {
+        collections.add(collectionName);
+      }
+      else {
+        collections.delete(collectionName);
+      }
+
+      updateSelectedCollections(collections);
+
+      // TODO: validation
+      // this.validate();
+    };
+
+    const openConfigurationModal = (collectionName) => {
+      setConfigurationModalOpen(true);
+      setCollectionNameForConfiguration(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}
+                isImporting={0}
+                isImported={false}
+                insertedCount={0}
+                modifiedCount={0}
+                errorsCount={0}
+                collectionName={collectionName}
+                isSelected={selectedCollections.has(collectionName)}
+                option={optionsMap[collectionName]}
+                isConfigButtonAvailable={isConfigButtonAvailable}
+                // events
+                onChange={toggleCheckbox}
+                onOptionChange={updateOption}
+                onConfigButtonClicked={openConfigurationModal}
+                // TODO: show progress
+                isHideProgress
+              />
+            </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={[]} />;
+  };
+
+  // TODO: モーダルを表示できるようにする
+  const configurationModal = useMemo(() => {
+    console.log(isConfigurationModalOpen);
+    if (collectionNameForConfiguration == null) {
+      return <></>;
+    }
+
+    return (
+      <ImportCollectionConfigurationModal
+        isOpen={isConfigurationModalOpen}
+        onClose={setConfigurationModalOpen(false)}
+        onOptionChange={updateOption}
+        collectionName={collectionNameForConfiguration}
+        option={optionsMap[collectionNameForConfiguration]}
+      />
+    );
+  }, [collectionNameForConfiguration, isConfigurationModalOpen]);
+
+  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);
+    });
+    updateOptionsMap(initialOptionsMap);
+  };
+
+  // TODO: use Socket
+
+  // setupWebsocketEventHandler() {
+  //   const socket = this.props.adminSocketIoContainer.getSocket();
+
+  //   // websocket event
+  //   // eslint-disable-next-line object-curly-newline
+  //   socket.on('admin:onProgressForImport', ({ collectionName, collectionProgress, appendedErrors }) => {
+  //     const { progressMap, errorsMap } = this.state;
+  //     progressMap[collectionName] = collectionProgress;
+
+  //     const errors = errorsMap[collectionName] || [];
+  //     errorsMap[collectionName] = errors.concat(appendedErrors);
+
+  //     this.setState({
+  //       isImporting: true,
+  //       progressMap,
+  //       errorsMap,
+  //     });
+  //   });
+
+  //   // websocket event
+  //   socket.on('admin:onTerminateForImport', () => {
+  //     this.setState({
+  //       isImporting: false,
+  //       isImported: true,
+  //     });
+
+  //     toastSuccess(undefined, 'Import process has completed.');
+  //   });
+
+  //   // websocket event
+  //   socket.on('admin:onErrorForImport', (err) => {
+  //     this.setState({
+  //       isImporting: false,
+  //       isImported: false,
+  //     });
+
+  //     toastError(err, 'Import process has failed.');
+  //   });
+  // }
+
+  // teardownWebsocketEventHandler() {
+  //   const socket = this.props.adminSocketIoContainer.getSocket();
+
+  //   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 />
+
+      {configurationModal}
+    </>
+  );
+};
+
+export default G2GDataTransferExportForm;

+ 4 - 2
packages/app/src/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -17,6 +17,7 @@ export const DEFAULT_MODE = 'insert';
 export const MODE_RESTRICTED_COLLECTION = {
   configs: ['flushAndInsert'],
   users: ['insert', 'upsert'],
+  pages: ['upsert', 'flushAndInsert'],
 };
 
 export default class ImportCollectionItem extends React.Component {
@@ -194,7 +195,7 @@ export default class ImportCollectionItem extends React.Component {
 
   render() {
     const {
-      isSelected,
+      isSelected, isHideProgress,
     } = this.props;
 
     return (
@@ -210,7 +211,7 @@ export default class ImportCollectionItem extends React.Component {
             </span>
           </div>
         </div>
-        {isSelected && (
+        {isSelected && !isHideProgress && (
           <>
             {this.renderProgressBar()}
             <div className="card-body">{this.renderBody()}</div>
@@ -225,6 +226,7 @@ export default class ImportCollectionItem extends React.Component {
 ImportCollectionItem.propTypes = {
   collectionName: PropTypes.string.isRequired,
   isSelected: PropTypes.bool.isRequired,
+  isHideProgress: PropTypes.bool,
   option: PropTypes.instanceOf(GrowiArchiveImportOption).isRequired,
 
   isImporting: PropTypes.bool.isRequired,