Przeglądaj źródła

整理wizard表单组件

herj 2 lat temu
rodzic
commit
056b899b02

+ 1 - 0
packages/jldbq-extenison/package.json

@@ -40,6 +40,7 @@
     "@mui/material": "^5.8.1",
     "@silevis/reactgrid": "^4.0.3",
     "antd": "^4.22.3",
+    "lodash": "^4.17.21",
     "moment": "^2.29.4",
     "react": "^17.0.1",
     "react-beautiful-dnd": "^13.1.0",

+ 94 - 96
packages/jldbq-extenison/src/wizard/StepFourForm.tsx

@@ -1,103 +1,101 @@
-import React, { forwardRef, useState } from 'react';
-import { FormNumberInput, Input, IStepForm, Select, useWizard } from './form';
+import React, { forwardRef } from 'react';
+import {
+  FormContainer,
+  FormNumberInput,
+  Input,
+  IStepForm,
+  Select,
+  useWizard
+} from './form';
 import { JsonSchemaDrawer } from './drawer';
 
-interface IForm {
-  route: string;
-  block: string;
-  timeout: number;
-  retry: number;
-  startTime: string;
-  timeField: string;
-  shardField: string;
-  timeRule: string;
-  taskName: string;
-}
+export namespace Step4 {
+  export const KEY = 'step4';
+  export const TITLE = '设置同步参数';
 
-const StepFourForm: IStepForm<IForm> = (props, ref) => {
-  const [showSideBar, setShowSideBar] = useState(false);
-  const { wizardData, formProps } = useWizard(ref);
-  const { watch } = formProps;
-  const formData = watch();
+  export interface IForm {
+    route: string;
+    block: string;
+    timeout: number;
+    retry: number;
+    startTime: string;
+    timeField: string;
+    shardField: string;
+    timeRule: string;
+    taskName: string;
+  }
 
-  return (
-    <form onSubmit={evt => evt.preventDefault()}>
-      <Select<IForm>
-        label="路由策略"
-        name="route"
-        options={[{ label: '第一个', value: 'first' }]}
-        required
-        formProps={formProps}
-      />
-      <Select<IForm>
-        label="阻塞处理方式"
-        name="block"
-        options={[{ label: '单机串行', value: 'single' }]}
-        required
-        formProps={formProps}
-      />
-      <FormNumberInput<IForm>
-        label="超时时间"
-        name="timeout"
-        positive
-        required
-        formProps={formProps}
-      />
-      <FormNumberInput<IForm>
-        label="失败重试次数"
-        name="retry"
-        positive
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="增量开始时间"
-        name="startTime"
-        placeholder="2022.8.6 17:22:14"
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="增量时间字段"
-        name="timeField"
-        placeholder="-DlastTime='%s' -DcurrentTime='%s'"
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="分区字段"
-        name="shardField"
-        placeholder="分区字段"
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="定时规则"
-        name="timeRule"
-        placeholder="0 0/2***?"
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="同步任务名称"
-        name="taskName"
-        placeholder="同步任务名称"
-        required
+  const StepForm: IStepForm<IForm> = (props, ref) => {
+    const { wizardData, formProps } = useWizard(ref);
+    const { watch } = formProps;
+    const formData = watch();
+
+    const drawer = (
+      <JsonSchemaDrawer jsonSchema={{ ...wizardData, [KEY]: formData }} />
+    );
+
+    return (
+      <FormContainer
         formProps={formProps}
-      />
-      <button
-        type="button"
-        tabIndex={-1}
-        className="drawer-button"
-        onClick={() => setShowSideBar(true)}
+        drawerLabel="构建预览"
+        drawer={drawer}
+        darkDrawer
       >
-        构建预览
-      </button>
-      <JsonSchemaDrawer
-        jsonSchema={{ ...wizardData, step4: formData }}
-        show={showSideBar}
-        onClose={() => setShowSideBar(false)}
-      />
-    </form>
-  );
-};
+        <Select<IForm>
+          label="路由策略"
+          name="route"
+          options={[{ label: '第一个', value: 'first' }]}
+          required
+        />
+        <Select<IForm>
+          label="阻塞处理方式"
+          name="block"
+          options={[{ label: '单机串行', value: 'single' }]}
+          required
+        />
+        <FormNumberInput<IForm>
+          label="超时时间"
+          name="timeout"
+          positive
+          required
+        />
+        <FormNumberInput<IForm>
+          label="失败重试次数"
+          name="retry"
+          positive
+          required
+        />
+        <Input<IForm>
+          label="增量开始时间"
+          name="startTime"
+          placeholder="2022.8.6 17:22:14"
+          required
+        />
+        <Input<IForm>
+          label="增量时间字段"
+          name="timeField"
+          placeholder="-DlastTime='%s' -DcurrentTime='%s'"
+        />
+        <Input<IForm>
+          label="分区字段"
+          name="shardField"
+          placeholder="分区字段"
+        />
+        <Input<IForm>
+          label="定时规则"
+          name="timeRule"
+          placeholder="0 0/2***?"
+          required
+        />
+        <Input<IForm>
+          label="同步任务名称"
+          name="taskName"
+          placeholder="同步任务名称"
+          required
+        />
+      </FormContainer>
+    );
+  };
 
-export default forwardRef(StepFourForm);
+  export const Form = forwardRef(StepForm);
+}

+ 125 - 142
packages/jldbq-extenison/src/wizard/StepOneForm.tsx

@@ -1,158 +1,141 @@
-import React, { forwardRef, useState } from 'react';
-import { UseFormReturn } from 'react-hook-form';
-import { Input, IStepForm, Select, Textarea, useWizard } from './form';
+import React, { forwardRef } from 'react';
+import {
+  FormContainer,
+  Input,
+  IStepForm,
+  Select,
+  Textarea,
+  useWizard
+} from './form';
 import { useTableNames } from './service';
 import { TableShemaDrawer } from './drawer';
 
-interface IMysqlDetail {
-  tableName: string;
-  field: string;
-  pkey: string;
-  stmt: string;
-}
-
-interface IHiveDetail {
-  tableName: string;
-  path: string;
-  hdfs: string;
-  filetype: string;
-  delim: string;
-}
+export namespace Step1 {
+  export const KEY = 'step1';
+  export const TITLE = '配置提取源';
 
-interface IForm {
-  dataSource1: string;
-  detail1: IMysqlDetail | IHiveDetail;
-}
+  interface IMysqlDetail {
+    tableName: string;
+    field: string;
+    pkey: string;
+    stmt: string;
+  }
 
-const MysqlDetail: React.FunctionComponent<{
-  formProps: UseFormReturn<IForm>;
-}> = ({ formProps }) => {
-  const { data: tableNames } = useTableNames('mysql');
+  interface IHiveDetail {
+    tableName: string;
+    path: string;
+    hdfs: string;
+    filetype: string;
+    delim: string;
+  }
 
-  return (
-    <>
-      <Select<IForm>
-        label="选择表"
-        name="detail1.tableName"
-        options={tableNames ? tableNames.map(value => ({ value })) : []}
-        required
-        formProps={formProps}
-      />
-      <Textarea<IForm>
-        label="查询字段"
-        name="detail1.field"
-        placeholder="SQL查询, 一般用于多表关联查询时才用"
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="切分主键"
-        name="detail1.pkey"
-        formProps={formProps}
-      />
-      <Textarea<IForm>
-        label="条件语句"
-        name="detail1.stmt"
-        placeholder="where条件"
-        formProps={formProps}
-      />
-    </>
-  );
-};
+  export interface IForm {
+    dataSource: string;
+    detail: IMysqlDetail | IHiveDetail;
+  }
 
-const HiveDetail: React.FunctionComponent<{
-  formProps: UseFormReturn<IForm>;
-}> = ({ formProps }) => {
-  const { data: tableNames } = useTableNames('hive');
+  const MysqlDetail: React.FunctionComponent = () => {
+    const { data: tableNames } = useTableNames('mysql');
 
-  return (
-    <>
-      <Select<IForm>
-        label="选择表"
-        name="detail1.tableName"
-        options={tableNames ? tableNames.map(value => ({ value })) : []}
-        required
-        formProps={formProps}
-      />
-      <Textarea<IForm>
-        label="选择路径"
-        name="detail1.path"
-        placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="HDFS"
-        name="detail1.hdfs"
-        placeholder="hdfs namenode节点地址"
-        required
-        formProps={formProps}
-      />
-      <Select<IForm>
-        label="文件类型"
-        name="detail1.filetype"
-        options={[{ value: 'text' }, { value: 'orc' }]}
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="分隔符"
-        name="detail1.delim"
-        required
-        formProps={formProps}
-      />
-    </>
-  );
-};
+    return (
+      <>
+        <Select<IForm>
+          label="选择表"
+          name="detail.tableName"
+          options={tableNames ? tableNames.map(value => ({ value })) : []}
+          required
+        />
+        <Textarea<IForm>
+          label="查询字段"
+          name="detail.field"
+          placeholder="SQL查询, 一般用于多表关联查询时才用"
+        />
+        <Input<IForm> label="切分主键" name="detail.pkey" />
+        <Textarea<IForm>
+          label="条件语句"
+          name="detail.stmt"
+          placeholder="where条件"
+        />
+      </>
+    );
+  };
 
-const StepOneForm: IStepForm<IForm> = (props, ref) => {
-  const [showSideBar, setShowSideBar] = useState(false);
-  const { formProps } = useWizard(ref);
-  const { watch, resetField, setValue } = formProps;
-  const dataSource = watch('dataSource1');
-  const tableName = watch('detail1.tableName');
+  const HiveDetail: React.FunctionComponent = () => {
+    const { data: tableNames } = useTableNames('hive');
 
-  const resetDetail = () => {
-    resetField('detail1');
-    setValue('detail1.tableName', '');
+    return (
+      <>
+        <Select<IForm>
+          label="选择表"
+          name="detail.tableName"
+          options={tableNames ? tableNames.map(value => ({ value })) : []}
+          required
+        />
+        <Textarea<IForm>
+          label="选择路径"
+          name="detail.path"
+          placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
+          required
+        />
+        <Input<IForm>
+          label="HDFS"
+          name="detail.hdfs"
+          placeholder="hdfs namenode节点地址"
+          required
+        />
+        <Select<IForm>
+          label="文件类型"
+          name="detail.filetype"
+          options={[{ value: 'text' }, { value: 'orc' }]}
+          required
+        />
+        <Input<IForm> label="分隔符" name="detail.delim" required />
+      </>
+    );
   };
 
-  let detail: JSX.Element | null = null;
-  if (dataSource === 'mysql') {
-    detail = <MysqlDetail formProps={formProps} />;
-  } else if (dataSource === 'hive') {
-    detail = <HiveDetail formProps={formProps} />;
-  }
+  const StepForm: IStepForm<IForm> = (props, ref) => {
+    const { formProps } = useWizard(ref);
+    const { watch, resetField, setValue } = formProps;
+    const dataSource = watch('dataSource');
+    const tableName = watch('detail.tableName');
 
-  return (
-    <form onSubmit={evt => evt.preventDefault()}>
-      <Select<IForm>
-        label="选择数据源"
-        name="dataSource1"
-        options={[
-          { label: 'MySQL', value: 'mysql' },
-          { label: 'Hive', value: 'hive' }
-        ]}
-        required
+    const resetDetail = () => {
+      resetField('detail');
+      setValue('detail.tableName', '');
+    };
+
+    let detail: JSX.Element | null = null;
+    if (dataSource === 'mysql') {
+      detail = <MysqlDetail />;
+    } else if (dataSource === 'hive') {
+      detail = <HiveDetail />;
+    }
+
+    const drawer = (
+      <TableShemaDrawer dataSource={dataSource} tableName={tableName} />
+    );
+
+    return (
+      <FormContainer
         formProps={formProps}
-        changeCallback={resetDetail}
-      />
-      {detail}
-      <button
-        type="button"
-        tabIndex={-1}
-        className="drawer-button"
-        onClick={() => setShowSideBar(true)}
+        drawerLabel="表结构预览"
+        drawer={drawer}
       >
-        表结构预览
-      </button>
-      <TableShemaDrawer
-        dataSource={dataSource}
-        tableName={tableName}
-        show={showSideBar}
-        onClose={() => setShowSideBar(false)}
-      />
-    </form>
-  );
-};
+        <Select<IForm>
+          label="选择数据源"
+          name="dataSource"
+          options={[
+            { label: 'MySQL', value: 'mysql' },
+            { label: 'Hive', value: 'hive' }
+          ]}
+          required
+          changeCallback={resetDetail}
+        />
+        {detail}
+      </FormContainer>
+    );
+  };
 
-export default forwardRef(StepOneForm);
+  export const Form = forwardRef(StepForm);
+}

+ 94 - 80
packages/jldbq-extenison/src/wizard/StepThreeForm.tsx

@@ -1,5 +1,6 @@
 import React, { forwardRef } from 'react';
 import {
+  FormContainer,
   FormDndSortList,
   IMapping,
   IStepForm,
@@ -7,95 +8,108 @@ import {
   Select,
   useWizard
 } from './form';
+import { Step1 } from './StepOneForm';
+import { Step2 } from './StepTwoForm';
 import { ITableSchemaResponse, useTableSchema } from './service';
 
-interface IForm {
-  mapping: IMapping<ITableSchemaResponse>[];
-  mode: string;
-}
+export namespace Step3 {
+  export const KEY = 'step3';
+  export const TITLE = '配置转换规则';
 
-const StepThreeForm: IStepForm<IForm> = (props, ref) => {
-  const { wizardData, formProps } = useWizard(ref, { mapping: [] });
+  export interface IForm {
+    mapping: IMapping<ITableSchemaResponse>[];
+    mode: string;
+  }
 
-  const ds1: string | undefined = wizardData['step1']?.dataSource1;
-  const tn1: string | undefined = wizardData['step1']?.detail1?.tableName;
-  const ds2: string | undefined = wizardData['step2']?.dataSource2;
-  const tn2: string | undefined = wizardData['step2']?.detail2?.tableName;
+  interface IWizardData {
+    [Step1.KEY]?: Partial<Step1.IForm>;
+    [Step2.KEY]?: Partial<Step2.IForm>;
+  }
 
-  const { data: sourceSchema } = useTableSchema(ds1, tn1);
-  const { data: destSchema } = useTableSchema(ds2, tn2);
+  const StepForm: IStepForm<IForm> = (props, ref) => {
+    const { wizardData, formProps } = useWizard(ref, { mapping: [] });
 
-  const createRowId = (
-    sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
-    destSchema?: ITableSchemaResponse,
-    idx?: number
-  ): { sourceId: string; destId: string; rowId: string } => {
-    let sourceId: string;
-    if (sourceSchema instanceof PlaceHolderItem) {
-      sourceId = `${ds1}-${tn1}-${sourceSchema.id}`;
-    } else if (sourceSchema) {
-      sourceId = `${ds1}-${tn1}-${sourceSchema.field}`;
-    } else {
-      sourceId = `${ds1}-${tn1}-source${idx}`;
-    }
-    const destId = destSchema
-      ? `${ds2}-${tn2}-${destSchema.field}`
-      : `${ds2}-${tn2}-dest${idx}`;
-    const rowId = `${sourceId}-${destId}`;
-    return { sourceId, destId, rowId };
-  };
+    // cast
+    const data: IWizardData = wizardData;
 
-  const formatItemName = (
-    schema: ITableSchemaResponse,
-    tableName?: string
-  ): string => {
-    return `${tableName || ''}${schema.field}.${schema.type}`;
-  };
+    const ds1 = data[Step1.KEY]?.dataSource;
+    const tn1 = data[Step1.KEY]?.detail?.tableName;
+    const ds2 = data[Step2.KEY]?.dataSource;
+    const tn2 = data[Step2.KEY]?.detail?.tableName;
+
+    const { data: sourceSchema } = useTableSchema(ds1, tn1);
+    const { data: destSchema } = useTableSchema(ds2, tn2);
+
+    const createRowId = (
+      sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
+      destSchema?: ITableSchemaResponse,
+      idx?: number
+    ): { sourceId: string; destId: string; rowId: string } => {
+      let sourceId: string;
+      if (sourceSchema instanceof PlaceHolderItem) {
+        sourceId = `${ds1}-${tn1}-${sourceSchema.id}`;
+      } else if (sourceSchema) {
+        sourceId = `${ds1}-${tn1}-${sourceSchema.field}`;
+      } else {
+        sourceId = `${ds1}-${tn1}-source${idx}`;
+      }
+      const destId = destSchema
+        ? `${ds2}-${tn2}-${destSchema.field}`
+        : `${ds2}-${tn2}-dest${idx}`;
+      const rowId = `${sourceId}-${destId}`;
+      return { sourceId, destId, rowId };
+    };
+
+    const formatItemName = (
+      schema: ITableSchemaResponse,
+      tableName?: string
+    ): string => {
+      return `${tableName || ''}${schema.field}.${schema.type}`;
+    };
+
+    const matchField = (
+      sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
+      destSchema?: ITableSchemaResponse
+    ): boolean => {
+      return (
+        !!sourceSchema &&
+        !!destSchema &&
+        !(sourceSchema instanceof PlaceHolderItem) &&
+        sourceSchema.type === destSchema.type &&
+        sourceSchema.length === destSchema.length
+      );
+    };
 
-  const matchField = (
-    sourceSchema?: ITableSchemaResponse | PlaceHolderItem,
-    destSchema?: ITableSchemaResponse
-  ): boolean => {
     return (
-      !!sourceSchema &&
-      !!destSchema &&
-      !(sourceSchema instanceof PlaceHolderItem) &&
-      sourceSchema.type === destSchema.type &&
-      sourceSchema.length === destSchema.length
+      <FormContainer formProps={formProps}>
+        <div className="form-group">
+          <p className="form-hint">拖拽提取源数据表中的字段,与加载源数据表</p>
+        </div>
+        <FormDndSortList<ITableSchemaResponse, IForm>
+          initSourceList={sourceSchema || []}
+          initDestList={destSchema || []}
+          sourceName={tn1 || ''}
+          destName={tn2 || ''}
+          matchFn={matchField}
+          createRowId={createRowId}
+          formatItemName={(value, isSource) =>
+            formatItemName(value, isSource ? tn1 : tn2)
+          }
+          name="mapping"
+          required
+        />
+        <Select<IForm>
+          label="写入模式"
+          name="mode"
+          options={[
+            { label: '追加', value: 'append' },
+            { label: '覆盖', value: 'overwrite' }
+          ]}
+          required
+        />
+      </FormContainer>
     );
   };
 
-  return (
-    <form onSubmit={evt => evt.preventDefault()}>
-      <div className="form-group">
-        <p className="form-hint">拖拽提取源数据表中的字段,与加载源数据表</p>
-      </div>
-      <FormDndSortList<ITableSchemaResponse, IForm>
-        initSourceList={sourceSchema || []}
-        initDestList={destSchema || []}
-        sourceName={tn1 || ''}
-        destName={tn2 || ''}
-        matchFn={matchField}
-        createRowId={createRowId}
-        formatItemName={(value, isSource) =>
-          formatItemName(value, isSource ? tn1 : tn2)
-        }
-        name="mapping"
-        required
-        formProps={formProps}
-      />
-      <Select<IForm>
-        label="写入模式"
-        name="mode"
-        options={[
-          { label: '追加', value: 'append' },
-          { label: '覆盖', value: 'overwrite' }
-        ]}
-        required
-        formProps={formProps}
-      />
-    </form>
-  );
-};
-
-export default forwardRef(StepThreeForm);
+  export const Form = forwardRef(StepForm);
+}

+ 117 - 129
packages/jldbq-extenison/src/wizard/StepTwoForm.tsx

@@ -1,145 +1,133 @@
-import React, { forwardRef, useState } from 'react';
-import { UseFormReturn } from 'react-hook-form';
-import { Input, IStepForm, Select, Textarea, useWizard } from './form';
+import React, { forwardRef } from 'react';
+import {
+  FormContainer,
+  Input,
+  IStepForm,
+  Select,
+  Textarea,
+  useWizard
+} from './form';
 import { TableShemaDrawer } from './drawer';
 import { useTableNames } from './service';
 
-interface IMysqlDetail {
-  tableName: string;
-  preSql: string;
-}
-
-interface IHiveDetail {
-  tableName: string;
-  path: string;
-  hdfs: string;
-  filetype: string;
-  delim: string;
-}
+export namespace Step2 {
+  export const KEY = 'step2';
+  export const TITLE = '配置加载源';
 
-interface IForm {
-  dataSource2: string;
-  detail2: IMysqlDetail | IHiveDetail;
-}
+  interface IMysqlDetail {
+    tableName: string;
+    preSql: string;
+  }
 
-const MysqlDetail: React.FunctionComponent<{
-  formProps: UseFormReturn<IForm>;
-}> = ({ formProps }) => {
-  const { data: tableNames } = useTableNames('mysql');
+  interface IHiveDetail {
+    tableName: string;
+    path: string;
+    hdfs: string;
+    filetype: string;
+    delim: string;
+  }
 
-  return (
-    <>
-      <Select<IForm>
-        label="选择表"
-        name="detail2.tableName"
-        options={tableNames ? tableNames.map(value => ({ value })) : []}
-        required
-        formProps={formProps}
-      />
-      <Textarea<IForm>
-        label="preSQL"
-        name="detail2.preSql"
-        placeholder="preSQL"
-        formProps={formProps}
-      />
-    </>
-  );
-};
+  export interface IForm {
+    dataSource: string;
+    detail: IMysqlDetail | IHiveDetail;
+  }
 
-const HiveDetail: React.FunctionComponent<{
-  formProps: UseFormReturn<IForm>;
-}> = ({ formProps }) => {
-  const { data: tableNames } = useTableNames('hive');
+  const MysqlDetail: React.FunctionComponent = () => {
+    const { data: tableNames } = useTableNames('mysql');
 
-  return (
-    <>
-      <Select<IForm>
-        label="选择表"
-        name="detail2.tableName"
-        options={tableNames ? tableNames.map(value => ({ value })) : []}
-        required
-        formProps={formProps}
-      />
-      <Textarea<IForm>
-        label="选择路径"
-        name="detail2.path"
-        placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="HDFS"
-        name="detail2.hdfs"
-        placeholder="hdfs namenode节点地址"
-        required
-        formProps={formProps}
-      />
-      <Select<IForm>
-        label="文件类型"
-        name="detail2.filetype"
-        options={[{ value: 'text' }, { value: 'orc' }]}
-        required
-        formProps={formProps}
-      />
-      <Input<IForm>
-        label="分隔符"
-        name="detail2.delim"
-        required
-        formProps={formProps}
-      />
-    </>
-  );
-};
+    return (
+      <>
+        <Select<IForm>
+          label="选择表"
+          name="detail.tableName"
+          options={tableNames ? tableNames.map(value => ({ value })) : []}
+          required
+        />
+        <Textarea<IForm>
+          label="preSQL"
+          name="detail.preSql"
+          placeholder="preSQL"
+        />
+      </>
+    );
+  };
 
-const StepTwoForm: IStepForm<IForm> = (props, ref) => {
-  const [showSideBar, setShowSideBar] = useState(false);
-  const { formProps } = useWizard(ref);
-  const { watch, resetField, setValue } = formProps;
-  const dataSource = watch('dataSource2');
-  const tableName = watch('detail2.tableName');
+  const HiveDetail: React.FunctionComponent = () => {
+    const { data: tableNames } = useTableNames('hive');
 
-  const resetDetail = () => {
-    resetField('detail2');
-    setValue('detail2.tableName', '');
+    return (
+      <>
+        <Select<IForm>
+          label="选择表"
+          name="detail.tableName"
+          options={tableNames ? tableNames.map(value => ({ value })) : []}
+          required
+        />
+        <Textarea<IForm>
+          label="选择路径"
+          name="detail.path"
+          placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
+          required
+        />
+        <Input<IForm>
+          label="HDFS"
+          name="detail.hdfs"
+          placeholder="hdfs namenode节点地址"
+          required
+        />
+        <Select<IForm>
+          label="文件类型"
+          name="detail.filetype"
+          options={[{ value: 'text' }, { value: 'orc' }]}
+          required
+        />
+        <Input<IForm> label="分隔符" name="detail.delim" required />
+      </>
+    );
   };
 
-  let detail: JSX.Element | null = null;
-  if (dataSource === 'mysql') {
-    detail = <MysqlDetail formProps={formProps} />;
-  } else if (dataSource === 'hive') {
-    detail = <HiveDetail formProps={formProps} />;
-  }
+  const StepForm: IStepForm<IForm> = (props, ref) => {
+    const { formProps } = useWizard(ref);
+    const { watch, resetField, setValue } = formProps;
+    const dataSource = watch('dataSource');
+    const tableName = watch('detail.tableName');
 
-  return (
-    <form onSubmit={evt => evt.preventDefault()}>
-      <Select<IForm>
-        label="选择数据源"
-        name="dataSource2"
-        options={[
-          { label: 'MySQL', value: 'mysql' },
-          { label: 'Hive', value: 'hive' }
-        ]}
-        required
+    const resetDetail = () => {
+      resetField('detail');
+      setValue('detail.tableName', '');
+    };
+
+    let detail: JSX.Element | null = null;
+    if (dataSource === 'mysql') {
+      detail = <MysqlDetail />;
+    } else if (dataSource === 'hive') {
+      detail = <HiveDetail />;
+    }
+
+    const drawer = (
+      <TableShemaDrawer dataSource={dataSource} tableName={tableName} />
+    );
+
+    return (
+      <FormContainer
         formProps={formProps}
-        changeCallback={resetDetail}
-      />
-      {detail}
-      <button
-        type="button"
-        tabIndex={-1}
-        className="drawer-button"
-        onClick={() => setShowSideBar(true)}
+        drawerLabel="表结构预览"
+        drawer={drawer}
       >
-        表结构预览
-      </button>
-      <TableShemaDrawer
-        dataSource={dataSource}
-        tableName={tableName}
-        show={showSideBar}
-        onClose={() => setShowSideBar(false)}
-      />
-    </form>
-  );
-};
+        <Select<IForm>
+          label="选择数据源"
+          name="dataSource"
+          options={[
+            { label: 'MySQL', value: 'mysql' },
+            { label: 'Hive', value: 'hive' }
+          ]}
+          required
+          changeCallback={resetDetail}
+        />
+        {detail}
+      </FormContainer>
+    );
+  };
 
-export default forwardRef(StepTwoForm);
+  export const Form = forwardRef(StepForm);
+}

+ 17 - 15
packages/jldbq-extenison/src/wizard/Wizard.tsx

@@ -58,13 +58,28 @@ export const Wizard: React.VoidFunctionComponent<{
   const currentKey = confs[currentStep].key;
   const totalSteps = confs.length;
 
+  const onNext = async () => {
+    const formData = await refs[currentKey]?.getData();
+    if (!formData) {
+      return;
+    }
+    const newData = { ...wizardData, [currentKey]: formData };
+    setWizardData(newData);
+
+    if (currentStep === totalSteps - 1) {
+      onFinish(newData);
+    } else {
+      setCurrentStep(currentStep + 1);
+    }
+  };
+
   const stepHeaderItems = confs.map(({ key, title }, idx) => {
     return (
       <li key={key} className={currentStep === idx ? 'step-active' : ''}>
         <div className="step-index-item">
           <div className="step-index-label">{idx + 1}</div>
         </div>
-        <div className="step-index-desc">{title}</div>
+        <div className="step-index-desc">{`步骤${idx + 1}: ${title}`}</div>
       </li>
     );
   });
@@ -92,20 +107,7 @@ export const Wizard: React.VoidFunctionComponent<{
         current={currentStep}
         total={totalSteps}
         onPrev={() => setCurrentStep(currentStep - 1)}
-        onNext={async () => {
-          const formData = await refs[currentKey]?.getData();
-          if (!formData) {
-            return;
-          }
-          const newData = { ...wizardData, [currentKey]: formData };
-          setWizardData(newData);
-
-          if (currentStep === totalSteps - 1) {
-            onFinish(newData);
-          } else {
-            setCurrentStep(currentStep + 1);
-          }
-        }}
+        onNext={onNext}
       />
     </div>
   );

+ 46 - 29
packages/jldbq-extenison/src/wizard/drawer.tsx

@@ -3,17 +3,39 @@ import SyntaxHighlighter from 'react-syntax-highlighter';
 import { solarizedDark } from 'react-syntax-highlighter/dist/esm/styles/hljs';
 import { useTableSchema } from './service';
 
-export const TableShemaDrawer: React.FunctionComponent<{
-  dataSource?: string;
-  tableName?: string;
+export const DrawerButton: React.FunctionComponent<{
+  label: string;
+  dark?: boolean;
+  onClick: () => void;
+}> = ({ label, dark, onClick }) => {
+  return (
+    <button
+      type="button"
+      tabIndex={-1}
+      className={'drawer-button' + (dark ? ' drawer-button-dark' : '')}
+      onClick={onClick}
+    >
+      {label}
+    </button>
+  );
+};
+
+export const Drawer: React.FunctionComponent<{
   show: boolean;
+  dark?: boolean;
+  title?: string;
   onClose: () => void;
-}> = ({ dataSource, tableName, show, onClose }) => {
-  const { data } = useTableSchema(dataSource, tableName);
+}> = ({ show, dark, title, onClose, children }) => {
   return (
-    <div className={'drawer-container ' + (show ? '' : 'drawer-hidden')}>
+    <div
+      className={
+        'drawer-container' +
+        (dark ? ' drawer-dark' : '') +
+        (show ? '' : ' drawer-hidden')
+      }
+    >
       <div className="drawer-titlebar">
-        <div className="drawer-title">表结构预览</div>
+        <div className="drawer-title">{title}</div>
         <button
           type="button"
           tabIndex={-1}
@@ -21,6 +43,18 @@ export const TableShemaDrawer: React.FunctionComponent<{
           onClick={onClose}
         />
       </div>
+      {children}
+    </div>
+  );
+};
+
+export const TableShemaDrawer: React.FunctionComponent<{
+  dataSource?: string;
+  tableName?: string;
+}> = ({ dataSource, tableName }) => {
+  const { data } = useTableSchema(dataSource, tableName);
+  return (
+    <>
       <div className="drawer-table-header">
         <span className="drawer-table-cell" style={{ width: '20%' }}>
           序号
@@ -54,33 +88,16 @@ export const TableShemaDrawer: React.FunctionComponent<{
             </div>
           ))}
       </div>
-    </div>
+    </>
   );
 };
 
 export const JsonSchemaDrawer: React.FunctionComponent<{
   jsonSchema: any;
-  show: boolean;
-  onClose: () => void;
-}> = ({ jsonSchema, show, onClose }) => {
+}> = ({ jsonSchema }) => {
   return (
-    <div
-      className={
-        'drawer-container drawer-dark ' + (show ? '' : 'drawer-hidden')
-      }
-    >
-      <div className="drawer-titlebar">
-        <div className="drawer-title" />
-        <button
-          type="button"
-          tabIndex={-1}
-          className="drawer-close-btn"
-          onClick={onClose}
-        />
-      </div>
-      <SyntaxHighlighter showLineNumbers language="json" style={solarizedDark}>
-        {JSON.stringify(jsonSchema, null, 2)}
-      </SyntaxHighlighter>
-    </div>
+    <SyntaxHighlighter showLineNumbers language="json" style={solarizedDark}>
+      {JSON.stringify(jsonSchema, null, 2)}
+    </SyntaxHighlighter>
   );
 };

+ 71 - 25
packages/jldbq-extenison/src/wizard/form.tsx

@@ -12,14 +12,18 @@ import {
   Droppable,
   DropResult
 } from 'react-beautiful-dnd';
+import _uniqueId from 'lodash/uniqueId';
 import {
   Controller,
   DeepPartial,
   FieldValues,
+  FormProvider,
   Path,
   useForm,
+  useFormContext,
   UseFormReturn
 } from 'react-hook-form';
+import { Drawer, DrawerButton } from './drawer';
 
 export interface IStepFormHandle<T> {
   getData: () => Promise<T | null>;
@@ -60,22 +64,65 @@ export const useWizard = <T,>(
   return { wizardData, formProps };
 };
 
+interface IFormContainer<T>
+  extends React.PropsWithChildren<{
+    drawerLabel?: string;
+    drawer?: React.ReactNode;
+    darkDrawer?: boolean;
+    formProps: UseFormReturn<T>;
+  }> {}
+
+export const FormContainer = <T,>({
+  drawerLabel,
+  drawer,
+  darkDrawer,
+  formProps,
+  children
+}: IFormContainer<T>): React.ReactElement => {
+  const [showDrawer, setShowDrawer] = useState(false);
+  return (
+    <FormProvider {...formProps}>
+      <form onSubmit={evt => evt.preventDefault()}>
+        {children}
+        {drawerLabel !== undefined && (
+          <>
+            <DrawerButton
+              label={drawerLabel}
+              dark={darkDrawer}
+              onClick={() => setShowDrawer(true)}
+            />
+            <Drawer
+              show={showDrawer}
+              dark={darkDrawer}
+              title={drawerLabel}
+              onClose={() => setShowDrawer(false)}
+            >
+              {drawer}
+            </Drawer>
+          </>
+        )}
+      </form>
+    </FormProvider>
+  );
+};
+
 export const Select = <T extends FieldValues>({
   label,
   name,
   options,
   required,
-  formProps,
   changeCallback
 }: {
   label: string;
   name: Path<T>;
   options: { label?: string; value: string }[];
   required?: boolean;
-  formProps: UseFormReturn<T>;
   changeCallback?: () => void;
 }): React.ReactElement => {
-  const { register, getFieldState, clearErrors, formState } = formProps;
+  const [id] = useState(() => _uniqueId(name));
+  const { register, getFieldState, clearErrors, formState } = useFormContext<
+    T
+  >();
   const { error } = getFieldState(name, formState);
   const fieldProps = register(name, { required });
   const { onChange } = fieldProps;
@@ -92,7 +139,7 @@ export const Select = <T extends FieldValues>({
     <div className="form-group">
       <label
         className={'form-label ' + (required ? 'field-required' : '')}
-        htmlFor={name}
+        htmlFor={id}
       >
         {label}
       </label>
@@ -101,7 +148,7 @@ export const Select = <T extends FieldValues>({
       >
         <select
           className="form-input"
-          id={name}
+          id={id}
           {...fieldProps}
           onChange={handleChange}
         >
@@ -122,16 +169,17 @@ export const Input = <T extends FieldValues>({
   label,
   name,
   placeholder,
-  required,
-  formProps
+  required
 }: {
   label: string;
   name: Path<T>;
   placeholder?: string;
   required?: boolean;
-  formProps: UseFormReturn<T>;
 }): React.ReactElement => {
-  const { register, getFieldState, clearErrors, formState } = formProps;
+  const [id] = useState(() => _uniqueId(name));
+  const { register, getFieldState, clearErrors, formState } = useFormContext<
+    T
+  >();
   const { error } = getFieldState(name, formState);
   const fieldProps = register(name, { required });
   const { onChange } = fieldProps;
@@ -145,7 +193,7 @@ export const Input = <T extends FieldValues>({
     <div className="form-group">
       <label
         className={'form-label ' + (required ? 'field-required' : '')}
-        htmlFor={name}
+        htmlFor={id}
       >
         {label}
       </label>
@@ -156,7 +204,7 @@ export const Input = <T extends FieldValues>({
           className="form-input"
           type="text"
           autoComplete="off"
-          id={name}
+          id={id}
           placeholder={placeholder}
           {...fieldProps}
           onChange={handleChange}
@@ -171,16 +219,17 @@ export const Textarea = <T extends FieldValues>({
   label,
   name,
   placeholder,
-  required,
-  formProps
+  required
 }: {
   label: string;
   name: Path<T>;
   placeholder?: string;
   required?: boolean;
-  formProps: UseFormReturn<T>;
 }): React.ReactElement => {
-  const { register, getFieldState, clearErrors, formState } = formProps;
+  const [id] = useState(() => _uniqueId(name));
+  const { register, getFieldState, clearErrors, formState } = useFormContext<
+    T
+  >();
   const { error } = getFieldState(name, formState);
   const fieldProps = register(name, { required });
   const { onChange } = fieldProps;
@@ -194,7 +243,7 @@ export const Textarea = <T extends FieldValues>({
     <div className="form-group">
       <label
         className={'form-label ' + (required ? 'field-required' : '')}
-        htmlFor={name}
+        htmlFor={id}
       >
         {label}
       </label>
@@ -204,7 +253,7 @@ export const Textarea = <T extends FieldValues>({
         <textarea
           className="form-input"
           autoComplete="off"
-          id={name}
+          id={id}
           placeholder={placeholder}
           {...fieldProps}
           onChange={handleChange}
@@ -262,16 +311,15 @@ export const FormNumberInput = <T extends FieldValues>({
   label,
   name,
   positive,
-  required,
-  formProps
+  required
 }: {
   label: string;
   name: Path<T>;
   positive?: boolean;
   required?: boolean;
-  formProps: UseFormReturn<T>;
 }): React.ReactElement => {
-  const { clearErrors, control } = formProps;
+  const [id] = useState(() => _uniqueId(name));
+  const { clearErrors, control } = useFormContext<T>();
 
   return (
     <Controller
@@ -293,7 +341,7 @@ export const FormNumberInput = <T extends FieldValues>({
               }
             >
               <NumberInput
-                id={name}
+                id={id}
                 onChange={value => {
                   clearErrors(name);
                   onChange(positive ? Math.max(value, 0) : value);
@@ -758,7 +806,6 @@ const DndSortList = <T,>({
 export const FormDndSortList = <T, U extends FieldValues>({
   name,
   required,
-  formProps,
   ...otherProps
 }: {
   initSourceList: T[];
@@ -774,9 +821,8 @@ export const FormDndSortList = <T, U extends FieldValues>({
   formatItemName: (value: T, isSource: boolean) => string;
   name: Path<U>;
   required?: boolean;
-  formProps: UseFormReturn<U>;
 }): React.ReactElement => {
-  const { clearErrors, control } = formProps;
+  const { clearErrors, control } = useFormContext<U>();
 
   return (
     <Controller

+ 8 - 16
packages/jldbq-extenison/src/wizard/index.tsx

@@ -1,9 +1,9 @@
 import React from 'react';
 import { Wizard } from './Wizard';
-import StepOneForm from './StepOneForm';
-import StepTwoForm from './StepTwoForm';
-import StepThreeForm from './StepThreeForm';
-import StepFourForm from './StepFourForm';
+import { Step1 } from './StepOneForm';
+import { Step2 } from './StepTwoForm';
+import { Step3 } from './StepThreeForm';
+import { Step4 } from './StepFourForm';
 
 /* eslint-disable react/jsx-key */
 export const SyncWizard: React.FunctionComponent<{
@@ -13,18 +13,10 @@ export const SyncWizard: React.FunctionComponent<{
     <Wizard onFinish={onFinish}>
       {register => {
         return [
-          <StepOneForm
-            {...register({ key: 'step1', title: '步骤1: 配置提取源' })}
-          />,
-          <StepTwoForm
-            {...register({ key: 'step2', title: '步骤2: 配置加载源' })}
-          />,
-          <StepThreeForm
-            {...register({ key: 'step3', title: '步骤3: 配置转换规则' })}
-          />,
-          <StepFourForm
-            {...register({ key: 'step4', title: '步骤4: 设置同步参数' })}
-          />
+          <Step1.Form {...register({ key: Step1.KEY, title: Step1.TITLE })} />,
+          <Step2.Form {...register({ key: Step2.KEY, title: Step2.TITLE })} />,
+          <Step3.Form {...register({ key: Step3.KEY, title: Step3.TITLE })} />,
+          <Step4.Form {...register({ key: Step4.KEY, title: Step4.TITLE })} />
         ];
       }}
     </Wizard>

+ 8 - 0
packages/jldbq-extenison/style/wizard/drawer.css

@@ -14,6 +14,10 @@
   align-items: center;
 }
 
+.sync-wizard .drawer-button.drawer-button-dark {
+  background-color: #002b36;
+}
+
 .sync-wizard .drawer-button::before {
   content: '';
   position: absolute;
@@ -106,3 +110,7 @@
 .sync-wizard .drawer-dark .drawer-close-btn {
   background-image: url('../img/collapse-white.png');
 }
+
+.sync-wizard .drawer-dark .drawer-title {
+  color: #edeeee;
+}