Browse Source

Merge pull request #7261 from weseek/feat/ui-admin-g2g-transfer-advanced-options

imprv: UI admin g2g transfer advanced options
Haku Mizuki 3 years ago
parent
commit
c4c8ad560b

+ 31 - 38
packages/app/src/components/Admin/App/AwsSetting.jsx → packages/app/src/components/Admin/App/AwsSetting.tsx

@@ -1,20 +1,25 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
+export type AwsSettingMoleculeProps = {
+  s3ReferenceFileWithRelayMode
+  s3Region
+  s3CustomEndpoint
+  s3Bucket
+  s3AccessKeyId
+  s3SecretAccessKey
+  onChangeS3ReferenceFileWithRelayMode: (val: boolean) => void
+  onChangeS3Region: (val: string) => void
+  onChangeS3CustomEndpoint: (val: string) => void
+  onChangeS3Bucket: (val: string) => void
+  onChangeS3AccessKeyId: (val: string) => void
+  onChangeS3SecretAccessKey: (val: string) => void
+};
 
 
-function AwsSetting(props) {
+export const AwsSettingMolecule = (props: AwsSettingMoleculeProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { adminAppContainer } = props;
-  const { s3ReferenceFileWithRelayMode } = adminAppContainer.state;
 
 
   return (
   return (
-    <React.Fragment>
+    <>
 
 
       <div className="row form-group my-3">
       <div className="row form-group my-3">
         <label className="text-left text-md-right col-md-3 col-form-label">
         <label className="text-left text-md-right col-md-3 col-form-label">
@@ -31,21 +36,21 @@ function AwsSetting(props) {
               aria-haspopup="true"
               aria-haspopup="true"
               aria-expanded="true"
               aria-expanded="true"
             >
             >
-              {s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_relay')}
-              {!s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_redirect')}
+              {props.s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_relay')}
+              {!props.s3ReferenceFileWithRelayMode && t('admin:app_setting.file_delivery_method_redirect')}
             </button>
             </button>
             <div className="dropdown-menu" aria-labelledby="ddS3ReferenceFileWithRelayMode">
             <div className="dropdown-menu" aria-labelledby="ddS3ReferenceFileWithRelayMode">
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminAppContainer.changeS3ReferenceFileWithRelayMode(true) }}
+                onClick={() => { props?.onChangeS3ReferenceFileWithRelayMode(true) }}
               >
               >
                 {t('admin:app_setting.file_delivery_method_relay')}
                 {t('admin:app_setting.file_delivery_method_relay')}
               </button>
               </button>
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminAppContainer.changeS3ReferenceFileWithRelayMode(false) }}
+                onClick={() => { props?.onChangeS3ReferenceFileWithRelayMode(false) }}
               >
               >
                 { t('admin:app_setting.file_delivery_method_redirect')}
                 { t('admin:app_setting.file_delivery_method_redirect')}
               </button>
               </button>
@@ -68,9 +73,9 @@ function AwsSetting(props) {
           <input
           <input
             className="form-control"
             className="form-control"
             placeholder={`${t('eg')} ap-northeast-1`}
             placeholder={`${t('eg')} ap-northeast-1`}
-            defaultValue={adminAppContainer.state.s3Region || ''}
+            defaultValue={props.s3Region || ''}
             onChange={(e) => {
             onChange={(e) => {
-              adminAppContainer.changeS3Region(e.target.value);
+              props?.onChangeS3Region(e.target.value);
             }}
             }}
           />
           />
         </div>
         </div>
@@ -85,9 +90,9 @@ function AwsSetting(props) {
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} http://localhost:9000`}
             placeholder={`${t('eg')} http://localhost:9000`}
-            defaultValue={adminAppContainer.state.s3CustomEndpoint || ''}
+            defaultValue={props.s3CustomEndpoint || ''}
             onChange={(e) => {
             onChange={(e) => {
-              adminAppContainer.changeS3CustomEndpoint(e.target.value);
+              props?.onChangeS3CustomEndpoint(e.target.value);
             }}
             }}
           />
           />
           <p className="form-text text-muted">{t('admin:app_setting.custom_endpoint_change')}</p>
           <p className="form-text text-muted">{t('admin:app_setting.custom_endpoint_change')}</p>
@@ -103,9 +108,9 @@ function AwsSetting(props) {
             className="form-control"
             className="form-control"
             type="text"
             type="text"
             placeholder={`${t('eg')} crowi`}
             placeholder={`${t('eg')} crowi`}
-            defaultValue={adminAppContainer.state.s3Bucket || ''}
+            defaultValue={props.s3Bucket || ''}
             onChange={(e) => {
             onChange={(e) => {
-              adminAppContainer.changeS3Bucket(e.target.value);
+              props.onChangeS3Bucket(e.target.value);
             }}
             }}
           />
           />
         </div>
         </div>
@@ -119,9 +124,9 @@ function AwsSetting(props) {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={adminAppContainer.state.s3AccessKeyId || ''}
+            defaultValue={props.s3AccessKeyId || ''}
             onChange={(e) => {
             onChange={(e) => {
-              adminAppContainer.changeS3AccessKeyId(e.target.value);
+              props?.onChangeS3AccessKeyId(e.target.value);
             }}
             }}
           />
           />
         </div>
         </div>
@@ -135,27 +140,15 @@ function AwsSetting(props) {
           <input
           <input
             className="form-control"
             className="form-control"
             type="text"
             type="text"
-            defaultValue={adminAppContainer.state.s3SecretAccessKey || ''}
+            defaultValue={props.s3SecretAccessKey || ''}
             onChange={(e) => {
             onChange={(e) => {
-              adminAppContainer.changeS3SecretAccessKey(e.target.value);
+              props?.onChangeS3SecretAccessKey(e.target.value);
             }}
             }}
           />
           />
         </div>
         </div>
       </div>
       </div>
 
 
 
 
-    </React.Fragment>
+    </>
   );
   );
-}
-
-
-/**
- * Wrapper component for using unstated
- */
-const AwsSettingWrapper = withUnstatedContainers(AwsSetting, [AdminAppContainer]);
-
-AwsSetting.propTypes = {
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
 };
 };
-
-export default AwsSettingWrapper;

+ 158 - 29
packages/app/src/components/Admin/App/FileUploadSetting.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback } from 'react';
+import React, { ChangeEvent, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -8,30 +8,22 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-import AwsSetting from './AwsSetting';
-import GcsSettings from './GcsSettings';
+import { AwsSettingMolecule } from './AwsSetting';
+import type { AwsSettingMoleculeProps } from './AwsSetting';
+import { GcsSettingMolecule } from './GcsSetting';
+import type { GcsSettingMoleculeProps } from './GcsSetting';
 
 
+const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'] as const;
 
 
-type Props = {
-  adminAppContainer: AdminAppContainer,
-}
-
+type FileUploadSettingMoleculeProps = {
+  fileUploadType: string
+  isFixedFileUploadByEnvVar: boolean
+  envFileUploadType?: string
+  onChangeFileUploadType: (e: ChangeEvent, type: string) => void
+} & AwsSettingMoleculeProps & GcsSettingMoleculeProps;
 
 
-const FileUploadSetting = (props: Props) => {
+export const FileUploadSettingMolecule = React.memo((props: FileUploadSettingMoleculeProps): JSX.Element => {
   const { t } = useTranslation(['admin', 'commons']);
   const { t } = useTranslation(['admin', 'commons']);
-  const { adminAppContainer } = props;
-  const { fileUploadType } = adminAppContainer.state;
-  const fileUploadTypes = ['aws', 'gcs', 'gridfs', 'local'];
-
-  const submitHandler = useCallback(async() => {
-    try {
-      await adminAppContainer.updateFileUploadSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.file_upload_settings'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }, [adminAppContainer, t]);
 
 
   return (
   return (
     <>
     <>
@@ -59,29 +51,166 @@ const FileUploadSetting = (props: Props) => {
                   className="custom-control-input"
                   className="custom-control-input"
                   name="file-upload-type"
                   name="file-upload-type"
                   id={`file-upload-type-radio-${type}`}
                   id={`file-upload-type-radio-${type}`}
-                  checked={adminAppContainer.state.fileUploadType === type}
-                  disabled={adminAppContainer.state.isFixedFileUploadByEnvVar}
-                  onChange={() => { adminAppContainer.changeFileUploadType(type) }}
+                  checked={props.fileUploadType === type}
+                  disabled={props.isFixedFileUploadByEnvVar}
+                  onChange={(e) => { props?.onChangeFileUploadType(e, type) }}
                 />
                 />
                 <label className="custom-control-label" htmlFor={`file-upload-type-radio-${type}`}>{t(`admin:app_setting.${type}_label`)}</label>
                 <label className="custom-control-label" htmlFor={`file-upload-type-radio-${type}`}>{t(`admin:app_setting.${type}_label`)}</label>
               </div>
               </div>
             );
             );
           })}
           })}
         </div>
         </div>
-        {adminAppContainer.state.isFixedFileUploadByEnvVar && (
+        {props.isFixedFileUploadByEnvVar && (
           <p className="alert alert-warning mt-2 text-left offset-3 col-6">
           <p className="alert alert-warning mt-2 text-left offset-3 col-6">
             <i className="icon-exclamation icon-fw">
             <i className="icon-exclamation icon-fw">
             </i><b>FIXED</b><br />
             </i><b>FIXED</b><br />
             {/* eslint-disable-next-line react/no-danger */}
             {/* eslint-disable-next-line react/no-danger */}
-            <b dangerouslySetInnerHTML={{ __html: t('admin:app_setting.fixed_by_env_var', { fileUploadType: adminAppContainer.state.envFileUploadType }) }} />
+            <b dangerouslySetInnerHTML={{ __html: t('admin:app_setting.fixed_by_env_var', { fileUploadType: props.envFileUploadType }) }} />
           </p>
           </p>
         )}
         )}
       </div>
       </div>
 
 
-      {fileUploadType === 'aws' && <AwsSetting />}
-      {fileUploadType === 'gcs' && <GcsSettings />}
+      {props.fileUploadType === 'aws' && <AwsSettingMolecule
+        s3ReferenceFileWithRelayMode={props.s3ReferenceFileWithRelayMode}
+        s3Region={props.s3Region}
+        s3CustomEndpoint={props.s3CustomEndpoint}
+        s3Bucket={props.s3Bucket}
+        s3AccessKeyId={props.s3AccessKeyId}
+        s3SecretAccessKey={props.s3SecretAccessKey}
+        onChangeS3ReferenceFileWithRelayMode={props.onChangeS3ReferenceFileWithRelayMode}
+        onChangeS3Region={props.onChangeS3Region}
+        onChangeS3CustomEndpoint={props.onChangeS3CustomEndpoint}
+        onChangeS3Bucket={props.onChangeS3Bucket}
+        onChangeS3AccessKeyId={props.onChangeS3AccessKeyId}
+        onChangeS3SecretAccessKey={props.onChangeS3SecretAccessKey}
+      />}
+      {props.fileUploadType === 'gcs' && <GcsSettingMolecule
+        gcsReferenceFileWithRelayMode={props.gcsReferenceFileWithRelayMode}
+        gcsUseOnlyEnvVars={props.gcsUseOnlyEnvVars}
+        gcsApiKeyJsonPath={props.gcsApiKeyJsonPath}
+        gcsBucket={props.gcsBucket}
+        gcsUploadNamespace={props.gcsUploadNamespace}
+        envGcsApiKeyJsonPath={props.envGcsApiKeyJsonPath}
+        envGcsBucket={props.envGcsBucket}
+        envGcsUploadNamespace={props.envGcsUploadNamespace}
+        onChangeGcsReferenceFileWithRelayMode={props.onChangeGcsReferenceFileWithRelayMode}
+        onChangeGcsApiKeyJsonPath={props.onChangeGcsApiKeyJsonPath}
+        onChangeGcsBucket={props.onChangeGcsBucket}
+        onChangeGcsUploadNamespace={props.onChangeGcsUploadNamespace}
+      />}
+    </>
+  );
+});
+FileUploadSettingMolecule.displayName = 'FileUploadSettingMolecule';
+
+
+type FileUploadSettingProps = {
+  adminAppContainer: AdminAppContainer
+}
 
 
-      <AdminUpdateButtonRow onClick={submitHandler} disabled={adminAppContainer.state.retrieveError != null} />
+const FileUploadSetting = (props: FileUploadSettingProps): JSX.Element => {
+  const { t } = useTranslation(['admin', 'commons']);
+  const { adminAppContainer } = props;
+
+  const {
+    fileUploadType, isFixedFileUploadByEnvVar, envFileUploadType, retrieveError,
+    s3ReferenceFileWithRelayMode,
+    s3Region, s3CustomEndpoint, s3Bucket,
+    s3AccessKeyId, s3SecretAccessKey,
+    gcsReferenceFileWithRelayMode, gcsUseOnlyEnvVars,
+    gcsApiKeyJsonPath, envGcsApiKeyJsonPath, gcsBucket,
+    envGcsBucket, gcsUploadNamespace, envGcsUploadNamespace,
+  } = adminAppContainer.state;
+
+  const submitHandler = useCallback(async() => {
+    try {
+      await adminAppContainer.updateFileUploadSettingHandler();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:app_setting.file_upload_settings'), ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [adminAppContainer, t]);
+
+  const onChangeFileUploadTypeHandler = useCallback((e: ChangeEvent, type: string) => {
+    adminAppContainer.changeFileUploadType(type);
+  }, [adminAppContainer]);
+
+  // S3
+  const onChangeS3ReferenceFileWithRelayModeHandler = useCallback((val: boolean) => {
+    adminAppContainer.changeS3ReferenceFileWithRelayMode(val);
+  }, [adminAppContainer]);
+
+  const onChangeS3RegionHandler = useCallback((val: string) => {
+    adminAppContainer.changeS3Region(val);
+  }, [adminAppContainer]);
+
+  const onChangeS3CustomEndpointHandler = useCallback((val: string) => {
+    adminAppContainer.changeS3CustomEndpoint(val);
+  }, [adminAppContainer]);
+
+  const onChangeS3BucketHandler = useCallback((val: string) => {
+    adminAppContainer.changeS3Bucket(val);
+  }, [adminAppContainer]);
+
+  const onChangeS3AccessKeyIdHandler = useCallback((val: string) => {
+    adminAppContainer.changeS3AccessKeyId(val);
+  }, [adminAppContainer]);
+
+  const onChangeS3SecretAccessKeyHandler = useCallback((val: string) => {
+    adminAppContainer.changeS3SecretAccessKey(val);
+  }, [adminAppContainer]);
+
+  // GCS
+  const onChangeGcsReferenceFileWithRelayModeHandler = useCallback((val: boolean) => {
+    adminAppContainer.changeGcsReferenceFileWithRelayMode(val);
+  }, [adminAppContainer]);
+
+  const onChangeGcsApiKeyJsonPathHandler = useCallback((val: string) => {
+    adminAppContainer.changeGcsApiKeyJsonPath(val);
+  }, [adminAppContainer]);
+
+  const onChangeGcsBucketHandler = useCallback((val: string) => {
+    adminAppContainer.changeGcsBucket(val);
+  }, [adminAppContainer]);
+
+  const onChangeGcsUploadNamespaceHandler = useCallback((val: string) => {
+    adminAppContainer.changeGcsUploadNamespace(val);
+  }, [adminAppContainer]);
+
+  return (
+    <>
+      <FileUploadSettingMolecule
+        fileUploadType={fileUploadType}
+        isFixedFileUploadByEnvVar={isFixedFileUploadByEnvVar}
+        envFileUploadType={envFileUploadType}
+        onChangeFileUploadType={onChangeFileUploadTypeHandler}
+        s3ReferenceFileWithRelayMode={s3ReferenceFileWithRelayMode}
+        s3Region={s3Region}
+        s3CustomEndpoint={s3CustomEndpoint}
+        s3Bucket={s3Bucket}
+        s3AccessKeyId={s3AccessKeyId}
+        s3SecretAccessKey={s3SecretAccessKey}
+        onChangeS3ReferenceFileWithRelayMode={onChangeS3ReferenceFileWithRelayModeHandler}
+        onChangeS3Region={onChangeS3RegionHandler}
+        onChangeS3CustomEndpoint={onChangeS3CustomEndpointHandler}
+        onChangeS3Bucket={onChangeS3BucketHandler}
+        onChangeS3AccessKeyId={onChangeS3AccessKeyIdHandler}
+        onChangeS3SecretAccessKey={onChangeS3SecretAccessKeyHandler}
+        gcsReferenceFileWithRelayMode={gcsReferenceFileWithRelayMode}
+        gcsUseOnlyEnvVars={gcsUseOnlyEnvVars}
+        gcsApiKeyJsonPath={gcsApiKeyJsonPath}
+        gcsBucket={gcsBucket}
+        gcsUploadNamespace={gcsUploadNamespace}
+        envGcsApiKeyJsonPath={envGcsApiKeyJsonPath}
+        envGcsBucket={envGcsBucket}
+        envGcsUploadNamespace={envGcsUploadNamespace}
+        onChangeGcsReferenceFileWithRelayMode={onChangeGcsReferenceFileWithRelayModeHandler}
+        onChangeGcsApiKeyJsonPath={onChangeGcsApiKeyJsonPathHandler}
+        onChangeGcsBucket={onChangeGcsBucketHandler}
+        onChangeGcsUploadNamespace={onChangeGcsUploadNamespaceHandler}
+      />
+      <AdminUpdateButtonRow onClick={submitHandler} disabled={retrieveError != null} />
     </>
     </>
   );
   );
 };
 };

+ 37 - 34
packages/app/src/components/Admin/App/GcsSettings.jsx → packages/app/src/components/Admin/App/GcsSetting.tsx

@@ -1,18 +1,33 @@
-
-import React from 'react';
-
-import PropTypes from 'prop-types';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import AdminAppContainer from '~/client/services/AdminAppContainer';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-
+export type GcsSettingMoleculeProps = {
+  gcsReferenceFileWithRelayMode
+  gcsUseOnlyEnvVars
+  gcsApiKeyJsonPath
+  gcsBucket
+  gcsUploadNamespace
+  envGcsApiKeyJsonPath?
+  envGcsBucket?
+  envGcsUploadNamespace?
+  onChangeGcsReferenceFileWithRelayMode: (val: boolean) => void
+  onChangeGcsApiKeyJsonPath: (val: string) => void
+  onChangeGcsBucket: (val: string) => void
+  onChangeGcsUploadNamespace: (val: string) => void
+};
 
 
-const GcsSetting = (props) => {
+export const GcsSettingMolecule = (props: GcsSettingMoleculeProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { adminAppContainer } = props;
-  const { gcsReferenceFileWithRelayMode, gcsUseOnlyEnvVars } = adminAppContainer.state;
+
+  const {
+    gcsReferenceFileWithRelayMode,
+    gcsUseOnlyEnvVars,
+    gcsApiKeyJsonPath,
+    envGcsApiKeyJsonPath,
+    gcsBucket,
+    envGcsBucket,
+    gcsUploadNamespace,
+    envGcsUploadNamespace,
+  } = props;
 
 
   return (
   return (
     <>
     <>
@@ -39,14 +54,14 @@ const GcsSetting = (props) => {
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminAppContainer.changeGcsReferenceFileWithRelayMode(true) }}
+                onClick={() => { props?.onChangeGcsReferenceFileWithRelayMode(true) }}
               >
               >
                 {t('admin:app_setting.file_delivery_method_relay')}
                 {t('admin:app_setting.file_delivery_method_relay')}
               </button>
               </button>
               <button
               <button
                 className="dropdown-item"
                 className="dropdown-item"
                 type="button"
                 type="button"
-                onClick={() => { adminAppContainer.changeGcsReferenceFileWithRelayMode(false) }}
+                onClick={() => { props?.onChangeGcsReferenceFileWithRelayMode(false) }}
               >
               >
                 { t('admin:app_setting.file_delivery_method_redirect')}
                 { t('admin:app_setting.file_delivery_method_redirect')}
               </button>
               </button>
@@ -90,12 +105,12 @@ const GcsSetting = (props) => {
                 type="text"
                 type="text"
                 name="gcsApiKeyJsonPath"
                 name="gcsApiKeyJsonPath"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={adminAppContainer.state.gcsApiKeyJsonPath}
-                onChange={e => adminAppContainer.changeGcsApiKeyJsonPath(e.target.value)}
+                defaultValue={gcsApiKeyJsonPath}
+                onChange={e => props?.onChangeGcsApiKeyJsonPath(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <input className="form-control" type="text" value={adminAppContainer.state.envGcsApiKeyJsonPath || ''} readOnly tabIndex="-1" />
+              <input className="form-control" type="text" value={envGcsApiKeyJsonPath || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_API_KEY_JSON_PATH' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_API_KEY_JSON_PATH' }) }} />
@@ -110,12 +125,12 @@ const GcsSetting = (props) => {
                 type="text"
                 type="text"
                 name="gcsBucket"
                 name="gcsBucket"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={adminAppContainer.state.gcsBucket}
-                onChange={e => adminAppContainer.changeGcsBucket(e.target.value)}
+                defaultValue={gcsBucket}
+                onChange={e => props?.onChangeGcsBucket(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <input className="form-control" type="text" value={adminAppContainer.state.envGcsBucket || ''} readOnly tabIndex="-1" />
+              <input className="form-control" type="text" value={envGcsBucket || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_BUCKET' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_BUCKET' }) }} />
@@ -130,12 +145,12 @@ const GcsSetting = (props) => {
                 type="text"
                 type="text"
                 name="gcsUploadNamespace"
                 name="gcsUploadNamespace"
                 readOnly={gcsUseOnlyEnvVars}
                 readOnly={gcsUseOnlyEnvVars}
-                defaultValue={adminAppContainer.state.gcsUploadNamespace}
-                onChange={e => adminAppContainer.changeGcsUploadNamespace(e.target.value)}
+                defaultValue={gcsUploadNamespace}
+                onChange={e => props?.onChangeGcsUploadNamespace(e.target.value)}
               />
               />
             </td>
             </td>
             <td>
             <td>
-              <input className="form-control" type="text" value={adminAppContainer.state.envGcsUploadNamespace || ''} readOnly tabIndex="-1" />
+              <input className="form-control" type="text" value={envGcsUploadNamespace || ''} readOnly tabIndex={-1} />
               <p className="form-text text-muted">
               <p className="form-text text-muted">
                 {/* eslint-disable-next-line react/no-danger */}
                 {/* eslint-disable-next-line react/no-danger */}
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_UPLOAD_NAMESPACE' }) }} />
                 <small dangerouslySetInnerHTML={{ __html: t('admin:app_setting.use_env_var_if_empty', { variable: 'GCS_UPLOAD_NAMESPACE' }) }} />
@@ -147,16 +162,4 @@ const GcsSetting = (props) => {
 
 
     </>
     </>
   );
   );
-
 };
 };
-
-/**
- * Wrapper component for using unstated
- */
-const GcsSettingWrapper = withUnstatedContainers(GcsSetting, [AdminAppContainer]);
-
-GcsSetting.propTypes = {
-  adminAppContainer: PropTypes.instanceOf(AdminAppContainer).isRequired,
-};
-
-export default GcsSettingWrapper;

+ 94 - 2
packages/app/src/components/Admin/G2GDataTransfer.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+  ChangeEvent, useCallback, useEffect, useState,
+} from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -10,6 +12,7 @@ import { useAdminSocket } from '~/stores/socket-io';
 
 
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 import CustomCopyToClipBoard from '../Common/CustomCopyToClipBoard';
 
 
+import { FileUploadSettingMolecule } from './App/FileUploadSetting';
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 import G2GDataTransferExportForm from './G2GDataTransferExportForm';
 import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 import G2GDataTransferStatusIcon from './G2GDataTransferStatusIcon';
 
 
@@ -32,6 +35,19 @@ const G2GDataTransfer = (): JSX.Element => {
     attachments: G2G_PROGRESS_STATUS.PENDING,
     attachments: G2G_PROGRESS_STATUS.PENDING,
   });
   });
 
 
+  // File upload settings
+  const [fileUploadType, setFileUploadType] = useState('aws');
+  const [s3ReferenceFileWithRelayMode, setS3ReferenceFileWithRelayMode] = useState(false);
+  const [s3Region, setS3Region] = useState('');
+  const [s3CustomEndpoint, setS3CustomEndpoint] = useState('');
+  const [s3Bucket, setS3Bucket] = useState('');
+  const [s3AccessKeyId, setS3AccessKeyId] = useState('');
+  const [s3SecretAccessKey, setS3SecretAccessKey] = useState('');
+  const [gcsReferenceFileWithRelayMode, setGcsReferenceFileWithRelayMode] = useState(false);
+  const [gcsApiKeyJsonPath, setGcsApiKeyJsonPath] = useState('');
+  const [gcsBucket, setGcsBucket] = useState('');
+  const [gcsUploadNamespace, setGcsUploadNamespace] = useState('');
+
   const updateSelectedCollections = (newSelectedCollections: Set<string>) => {
   const updateSelectedCollections = (newSelectedCollections: Set<string>) => {
     setSelectedCollections(newSelectedCollections);
     setSelectedCollections(newSelectedCollections);
   };
   };
@@ -107,6 +123,54 @@ const G2GDataTransfer = (): JSX.Element => {
     }
     }
   }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
   }, [setTransferring, startTransferKey, selectedCollections, optionsMap]);
 
 
+  // File upload
+  const onChangeFileUploadTypeHandler = useCallback((e: ChangeEvent, type: string) => {
+    setFileUploadType(type);
+  }, []);
+
+  // S3
+  const onChangeS3ReferenceFileWithRelayModeHandler = useCallback((val: boolean) => {
+    setS3ReferenceFileWithRelayMode(val);
+  }, []);
+
+  const onChangeS3RegionHandler = useCallback((val: string) => {
+    setS3Region(val);
+  }, []);
+
+  const onChangeS3CustomEndpointHandler = useCallback((val: string) => {
+    setS3CustomEndpoint(val);
+  }, []);
+
+  const onChangeS3BucketHandler = useCallback((val: string) => {
+    setS3Bucket(val);
+  }, []);
+
+  const onChangeS3AccessKeyIdHandler = useCallback((val: string) => {
+    setS3AccessKeyId(val);
+  }, []);
+
+  const onChangeS3SecretAccessKeyHandler = useCallback((val: string) => {
+    setS3SecretAccessKey(val);
+  }, []);
+
+  // GCS
+  const onChangeGcsReferenceFileWithRelayModeHandler = useCallback((val: boolean) => {
+    setGcsReferenceFileWithRelayMode(val);
+  }, []);
+
+  const onChangeGcsApiKeyJsonPathHandler = useCallback((val: string) => {
+    setGcsApiKeyJsonPath(val);
+  }, []);
+
+  const onChangeGcsBucketHandler = useCallback((val: string) => {
+    setGcsBucket(val);
+  }, []);
+
+  const onChangeGcsUploadNamespaceHandler = useCallback((val: string) => {
+    setGcsUploadNamespace(val);
+  }, []);
+
+
   useEffect(() => {
   useEffect(() => {
     setCollectionsAndSelectedCollections();
     setCollectionsAndSelectedCollections();
     setupWebsocketEventHandler();
     setupWebsocketEventHandler();
@@ -125,7 +189,35 @@ const G2GDataTransfer = (): JSX.Element => {
       </button>
       </button>
 
 
       {collections.length !== 0 && (
       {collections.length !== 0 && (
-        <div className={isShowExportForm ? '' : 'd-none'}>
+        <div className={`${isShowExportForm ? '' : 'd-none'} px-3 pt-3`}>
+          <h3 className='mb-1'>{t('admin:app_setting.file_upload')}</h3>
+          <FileUploadSettingMolecule
+            fileUploadType={fileUploadType}
+            isFixedFileUploadByEnvVar={false}
+            onChangeFileUploadType={onChangeFileUploadTypeHandler}
+            s3ReferenceFileWithRelayMode={s3ReferenceFileWithRelayMode}
+            s3Region={s3Region}
+            s3CustomEndpoint={s3CustomEndpoint}
+            s3Bucket={s3Bucket}
+            s3AccessKeyId={s3AccessKeyId}
+            s3SecretAccessKey={s3SecretAccessKey}
+            onChangeS3ReferenceFileWithRelayMode={onChangeS3ReferenceFileWithRelayModeHandler}
+            onChangeS3Region={onChangeS3RegionHandler}
+            onChangeS3CustomEndpoint={onChangeS3CustomEndpointHandler}
+            onChangeS3Bucket={onChangeS3BucketHandler}
+            onChangeS3AccessKeyId={onChangeS3AccessKeyIdHandler}
+            onChangeS3SecretAccessKey={onChangeS3SecretAccessKeyHandler}
+            gcsReferenceFileWithRelayMode={gcsReferenceFileWithRelayMode}
+            gcsUseOnlyEnvVars={false}
+            gcsApiKeyJsonPath={gcsApiKeyJsonPath}
+            gcsBucket={gcsBucket}
+            gcsUploadNamespace={gcsUploadNamespace}
+            onChangeGcsReferenceFileWithRelayMode={onChangeGcsReferenceFileWithRelayModeHandler}
+            onChangeGcsApiKeyJsonPath={onChangeGcsApiKeyJsonPathHandler}
+            onChangeGcsBucket={onChangeGcsBucketHandler}
+            onChangeGcsUploadNamespace={onChangeGcsUploadNamespaceHandler}
+          />
+          <h3 className='mb-1'>{t('export_management.export_archive_data')}</h3>
           <G2GDataTransferExportForm
           <G2GDataTransferExportForm
             allCollectionNames={collections}
             allCollectionNames={collections}
             selectedCollections={selectedCollections}
             selectedCollections={selectedCollections}

+ 6 - 70
packages/app/src/components/Admin/G2GDataTransferExportForm.tsx

@@ -7,7 +7,6 @@ import { useTranslation } from 'next-i18next';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import GrowiArchiveImportOption from '~/models/admin/growi-archive-import-option';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForPages from '~/models/admin/import-option-for-pages';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
 import ImportOptionForRevisions from '~/models/admin/import-option-for-revisions';
-// import { useAdminSocket } from '~/stores/socket-io';
 
 
 import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
 import ImportCollectionConfigurationModal from './ImportData/GrowiArchive/ImportCollectionConfigurationModal';
 import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
 import ImportCollectionItem, { DEFAULT_MODE, MODE_RESTRICTED_COLLECTION } from './ImportData/GrowiArchive/ImportCollectionItem';
@@ -37,17 +36,12 @@ type Props = {
 };
 };
 
 
 const G2GDataTransferExportForm = (props: Props): JSX.Element => {
 const G2GDataTransferExportForm = (props: Props): JSX.Element => {
-  // const { data: socket } = useAdminSocket();
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   const {
   const {
     allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
     allCollectionNames, selectedCollections, updateSelectedCollections, optionsMap, updateOptionsMap,
   } = props;
   } = 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 [isConfigurationModalOpen, setConfigurationModalOpen] = useState(false);
   const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
   const [collectionNameForConfiguration, setCollectionNameForConfiguration] = useState<any>();
 
 
@@ -59,7 +53,7 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     updateSelectedCollections(new Set());
     updateSelectedCollections(new Set());
   }, [updateSelectedCollections]);
   }, [updateSelectedCollections]);
 
 
-  const updateOption = (collectionName, data) => {
+  const updateOption = useCallback((collectionName, data) => {
     const options = optionsMap[collectionName];
     const options = optionsMap[collectionName];
 
 
     // merge
     // merge
@@ -70,7 +64,7 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     updateOptionsMap((prev) => {
     updateOptionsMap((prev) => {
       return { ...prev, updatedOptionsMap };
       return { ...prev, updatedOptionsMap };
     });
     });
-  };
+  }, [optionsMap, updateOptionsMap]);
 
 
   const ImportItems = ({ collectionNames }): JSX.Element => {
   const ImportItems = ({ collectionNames }): JSX.Element => {
     const toggleCheckbox = (collectionName, bool) => {
     const toggleCheckbox = (collectionName, bool) => {
@@ -96,8 +90,6 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
     return (
     return (
       <div className="row">
       <div className="row">
         {collectionNames.map((collectionName) => {
         {collectionNames.map((collectionName) => {
-          // const collectionProgress = progressMap[collectionName];
-          // const errors = errorsMap[collectionName];
           const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
           const isConfigButtonAvailable = Object.keys(IMPORT_OPTION_CLASS_MAPPING).includes(collectionName);
 
 
           if (optionsMap[collectionName] == null) {
           if (optionsMap[collectionName] == null) {
@@ -107,11 +99,6 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
           return (
           return (
             <div className="col-md-6 my-1" key={collectionName}>
             <div className="col-md-6 my-1" key={collectionName}>
               <ImportCollectionItem
               <ImportCollectionItem
-                // isImporting={isImporting}
-                // isImported={collectionProgress ? isImported : false}
-                // insertedCount={collectionProgress ? collectionProgress.insertedCount : 0}
-                // modifiedCount={collectionProgress ? collectionProgress.modifiedCount : 0}
-                // errorsCount={errors ? errors.length : 0}
                 isImporting={false}
                 isImporting={false}
                 isImported={false}
                 isImported={false}
                 insertedCount={0}
                 insertedCount={0}
@@ -192,9 +179,9 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
         option={optionsMap[collectionNameForConfiguration]}
         option={optionsMap[collectionNameForConfiguration]}
       />
       />
     );
     );
-  }, [collectionNameForConfiguration, isConfigurationModalOpen]);
+  }, [collectionNameForConfiguration, isConfigurationModalOpen, optionsMap, updateOption]);
 
 
-  const setInitialOptionsMap = () => {
+  const setInitialOptionsMap = useCallback(() => {
     const initialOptionsMap = {};
     const initialOptionsMap = {};
     allCollectionNames.forEach((collectionName) => {
     allCollectionNames.forEach((collectionName) => {
       const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
       const initialMode = (MODE_RESTRICTED_COLLECTION[collectionName] != null)
@@ -204,66 +191,15 @@ const G2GDataTransferExportForm = (props: Props): JSX.Element => {
       initialOptionsMap[collectionName] = new ImportOption(initialMode);
       initialOptionsMap[collectionName] = new ImportOption(initialMode);
     });
     });
     updateOptionsMap(initialOptionsMap);
     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');
-  // }
+  }, [allCollectionNames, updateOptionsMap]);
 
 
   useEffect(() => {
   useEffect(() => {
     setInitialOptionsMap();
     setInitialOptionsMap();
-    // setupWebsocketEventHandler();
-    // teardownWebsocketEventHandler();
   }, []);
   }, []);
 
 
   return (
   return (
     <>
     <>
-      <form className="form-inline mt-4">
+      <form className="form-inline mt-3">
         <div className="form-group">
         <div className="form-group">
           <button type="button" className="btn btn-sm btn-outline-secondary mr-2" onClick={checkAll}>
           <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')}
             <i className="fa fa-check-square-o"></i> {t('admin:export_management.check_all')}

+ 17 - 6
packages/app/src/pages/admin/data-transfer.page.tsx

@@ -1,10 +1,13 @@
+import { isClient } from '@growi/core';
 import {
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
+import { Container, Provider } from 'unstated';
 
 
+import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { CommonProps } from '~/pages/utils/commons';
 import { CommonProps } from '~/pages/utils/commons';
 import { useCurrentUser } from '~/stores/context';
 import { useCurrentUser } from '~/stores/context';
 
 
@@ -23,13 +26,21 @@ const DataTransferPage: NextPage<Props> = (props) => {
 
 
   const title = t('g2g_data_transfer.data_transfer');
   const title = t('g2g_data_transfer.data_transfer');
 
 
+  const injectableContainers: Container<any>[] = [];
+  if (isClient()) {
+    const adminAppContainer = new AdminAppContainer();
+    injectableContainers.push(adminAppContainer);
+  }
+
   return (
   return (
-    <AdminLayout componentTitle={title}>
-      <Head>
-        <title>{title}</title>
-      </Head>
-      <G2GDataTransferPage />
-    </AdminLayout>
+    <Provider inject={[...injectableContainers]}>
+      <AdminLayout componentTitle={title}>
+        <Head>
+          <title>{title}</title>
+        </Head>
+        <G2GDataTransferPage />
+      </AdminLayout>
+    </Provider>
   );
   );
 };
 };