瀏覽代碼

添加数据源同步面板

herj 2 年之前
父節點
當前提交
2f4fa537b7

+ 12 - 1
packages/jldbq-extenison/src/DataViewWidget.tsx

@@ -24,6 +24,10 @@ class DataViewWidget extends ReactWidget {
     return this._tableOpened;
   }
 
+  public get wizardOpened(): ISignal<DataViewWidget, void> {
+    return this._wizardOpened;
+  }
+
   render(): JSX.Element {
     const datasourceView = (
       <DatasourceView
@@ -34,7 +38,13 @@ class DataViewWidget extends ReactWidget {
       />
     );
 
-    const datasyncView = <DatasyncView />;
+    const datasyncView = (
+      <DatasyncView
+        onOpenWizard={() => {
+          this._wizardOpened.emit();
+        }}
+      />
+    );
 
     const datalogView = <DatalogView />;
 
@@ -57,6 +67,7 @@ class DataViewWidget extends ReactWidget {
 
   private _manager: string;
   private _tableOpened = new Signal<DataViewWidget, ITableOpenSignalData>(this);
+  private _wizardOpened = new Signal<DataViewWidget, void>(this);
 }
 
 export default DataViewWidget;

+ 64 - 65
packages/jldbq-extenison/src/DatasyncView.tsx

@@ -1,10 +1,8 @@
 import React from 'react';
-import { Dialog, ReactWidget } from '@jupyterlab/apputils';
-import { Widget } from '@lumino/widgets';
-import { Signal } from '@lumino/signaling';
-import DatasyncForm, { ISyncData } from './DatasyncForm';
 
-interface IProps {}
+interface IProps {
+  onOpenWizard: () => void;
+}
 
 const DatasyncView: React.FunctionComponent<IProps> = props => {
   return (
@@ -14,22 +12,23 @@ const DatasyncView: React.FunctionComponent<IProps> = props => {
       >
         <button
           className="jldbq-btn-add"
-          onClick={async () => {
-            const body = new DatasyncFormDialogBody();
-            const dialog = new Dialog({
-              title: '添加同步任务',
-              body,
-              renderer: new DatasyncFormDialogRenderer()
-            });
-            body.confirm.connect((_sender, data) => {
-              console.log(data);
-              dialog.reject();
-            });
-            body.cancel.connect(() => {
-              dialog.reject();
-            });
-            await dialog.launch();
-          }}
+          // onClick={async () => {
+          //   const body = new DatasyncFormDialogBody();
+          //   const dialog = new Dialog({
+          //     title: '添加同步任务',
+          //     body,
+          //     renderer: new DatasyncFormDialogRenderer()
+          //   });
+          //   body.confirm.connect((_sender, data) => {
+          //     console.log(data);
+          //     dialog.reject();
+          //   });
+          //   body.cancel.connect(() => {
+          //     dialog.reject();
+          //   });
+          //   await dialog.launch();
+          // }}
+          onClick={props.onOpenWizard}
         >
           <svg
             xmlns="http://www.w3.org/2000/svg"
@@ -49,52 +48,52 @@ const DatasyncView: React.FunctionComponent<IProps> = props => {
 
 export default DatasyncView;
 
-class DatasyncFormDialogBody extends ReactWidget {
-  constructor(options?: Widget.IOptions) {
-    super(options);
-  }
+// class DatasyncFormDialogBody extends ReactWidget {
+//   constructor(options?: Widget.IOptions) {
+//     super(options);
+//   }
 
-  render(): JSX.Element {
-    Signal;
-    return (
-      <DatasyncForm
-        datasourceList={[
-          {
-            host: '1.1.1.1',
-            user: 'user',
-            port: 3306,
-            password: '',
-            charset: 'utf8',
-            manager: 'manager',
-            source: 'mysql',
-            databasename: 'default',
-            datasourcename: 'default',
-            sourcename: 'default'
-          }
-        ]}
-        tableListFetcher={async () => {
-          return ['1', '2', '3'];
-        }}
-        onConfirm={v => this._confirm.emit(v)}
-        onCancel={() => this._cancel.emit()}
-      />
-    );
-  }
+//   render(): JSX.Element {
+//     Signal;
+//     return (
+//       <DatasyncForm
+//         datasourceList={[
+//           {
+//             host: '1.1.1.1',
+//             user: 'user',
+//             port: 3306,
+//             password: '',
+//             charset: 'utf8',
+//             manager: 'manager',
+//             source: 'mysql',
+//             databasename: 'default',
+//             datasourcename: 'default',
+//             sourcename: 'default'
+//           }
+//         ]}
+//         tableListFetcher={async () => {
+//           return ['1', '2', '3'];
+//         }}
+//         onConfirm={v => this._confirm.emit(v)}
+//         onCancel={() => this._cancel.emit()}
+//       />
+//     );
+//   }
 
-  get confirm() {
-    return this._confirm;
-  }
+//   get confirm() {
+//     return this._confirm;
+//   }
 
-  get cancel() {
-    return this._cancel;
-  }
+//   get cancel() {
+//     return this._cancel;
+//   }
 
-  private _confirm = new Signal<DatasyncFormDialogBody, ISyncData[]>(this);
-  private _cancel = new Signal<DatasyncFormDialogBody, void>(this);
-}
+//   private _confirm = new Signal<DatasyncFormDialogBody, ISyncData[]>(this);
+//   private _cancel = new Signal<DatasyncFormDialogBody, void>(this);
+// }
 
-class DatasyncFormDialogRenderer extends Dialog.Renderer {
-  createFooter(_buttons: ReadonlyArray<HTMLElement>): Widget {
-    return new Widget();
-  }
-}
+// class DatasyncFormDialogRenderer extends Dialog.Renderer {
+//   createFooter(_buttons: ReadonlyArray<HTMLElement>): Widget {
+//     return new Widget();
+//   }
+// }

+ 17 - 1
packages/jldbq-extenison/src/index.ts → packages/jldbq-extenison/src/index.tsx

@@ -7,14 +7,16 @@ import {
   JupyterFrontEnd,
   JupyterFrontEndPlugin
 } from '@jupyterlab/application';
-import { MainAreaWidget } from '@jupyterlab/apputils';
+import { MainAreaWidget, ReactWidget } from '@jupyterlab/apputils';
 import { ISettingRegistry } from '@jupyterlab/settingregistry';
+import React from 'react';
 import DataViewWidget from './DataViewWidget';
 import DataTableWidget from './DataTableWidget';
 import TaskViewWidget from './TaskViewWidget';
 import MonitorViewWidget from './MonitorViewWidget';
 import { cliIcon, monitorIcon, timerIcon } from './icons';
 import config from './api/config';
+import { SyncWizard } from './wizard';
 
 const PLUGIN_ID = '@jupyterlab/jldbq-extension:plugin';
 
@@ -46,6 +48,20 @@ const plugin: JupyterFrontEndPlugin<void> = {
         app.shell.add(widget, 'main');
       }
     );
+    dataViewWidget.wizardOpened.connect(() => {
+      const widget = new MainAreaWidget({
+        content: ReactWidget.create(
+          <SyncWizard
+            onFinish={data => {
+              console.log(data);
+            }}
+          />
+        )
+      });
+      widget.title.label = '数据源同步';
+      widget.title.icon = cliIcon;
+      app.shell.add(widget, 'main');
+    });
     app.shell.add(dataViewWidget, 'left', { rank: 100 });
 
     const taskViewWidget = new TaskViewWidget();

+ 9 - 0
packages/jldbq-extenison/src/wizard/StepFourForm.tsx

@@ -0,0 +1,9 @@
+import React, { forwardRef } from 'react';
+
+const StepFourForm: React.ForwardRefRenderFunction<{
+  getData: () => any;
+}> = (props, ref) => {
+  return <div />;
+};
+
+export default forwardRef(StepFourForm);

+ 204 - 0
packages/jldbq-extenison/src/wizard/StepOneForm.tsx

@@ -0,0 +1,204 @@
+import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import { useTableNames } from './service';
+
+const MysqlDetail: React.FunctionComponent<{
+  value: any;
+  onChange: (value: any) => void;
+}> = ({ value, onChange }) => {
+  return (
+    <>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-fieldname">
+          查询字段
+        </label>
+        <textarea
+          className="form-input"
+          id="step-1-fieldname"
+          placeholder="SQL查询,一般用于多表关联查询时才用"
+          value={value.field}
+          onChange={evt => onChange({ field: evt.target.value })}
+        />
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-pkey">
+          切分主键
+        </label>
+        <input
+          className="form-input"
+          type="text"
+          id="step-1-pkey"
+          value={value.pkey}
+          onChange={evt => onChange({ pkey: evt.target.value })}
+        />
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-stmt">
+          条件语句
+        </label>
+        <textarea
+          className="form-input"
+          id="step-1-stmt"
+          placeholder="where条件"
+          value={value.stmt}
+          onChange={evt => onChange({ stmt: evt.target.value })}
+        />
+      </div>
+    </>
+  );
+};
+
+const HiveDetail: React.FunctionComponent<{
+  value: any;
+  onChange: (value: any) => void;
+}> = ({ value, onChange }) => {
+  return (
+    <>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-path">
+          <span className="field-required" />
+          选择路径
+        </label>
+        <textarea
+          className="form-input"
+          id="step-1-path"
+          placeholder="要读取的文件路径,如果要读取多个⽂件,可使用正确的表达式*"
+          value={value.path}
+          onChange={evt => onChange({ path: evt.target.value })}
+        />
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-hdfs">
+          <span className="field-required" />
+          HDFS
+        </label>
+        <input
+          className="form-input"
+          type="text"
+          id="step-1-hdfs"
+          value={value.hdfs}
+          onChange={evt => onChange({ hdfs: evt.target.value })}
+        />
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-filetype">
+          <span className="field-required" />
+          文件类型
+        </label>
+        <select
+          className="form-input"
+          id="step-1-filetype"
+          value={value.filetype}
+          onChange={evt => onChange({ filetype: evt.target.value })}
+        >
+          <option value="" />
+          <option value="text">text</option>
+          <option value="orc">orc</option>
+        </select>
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-delim">
+          分隔符
+        </label>
+        <input
+          className="form-input"
+          type="text"
+          id="step-1-delim"
+          value={value.delim}
+          onChange={evt => onChange({ delim: evt.target.value })}
+        />
+      </div>
+    </>
+  );
+};
+
+const checkForm = (formData: any): any => {
+  return null;
+};
+
+const StepOneForm: React.ForwardRefRenderFunction<{
+  getData: () => any;
+}> = (props, ref) => {
+  const [formData, setFormData] = useState<any>({});
+  const [, setFormError] = useState<any>({});
+  const dataSource = formData.dataSource || '';
+  const { data: tableNames } = useTableNames(dataSource);
+  // const loading = !tableNames && !fetchError;
+
+  useImperativeHandle(ref, () => {
+    return {
+      getData: () => {
+        const err = checkForm(formData);
+        if (err) {
+          setFormError(err);
+          return null;
+        }
+        return formData;
+      }
+    };
+  });
+
+  let detail;
+  if (dataSource === 'mysql') {
+    detail = (
+      <MysqlDetail
+        value={formData}
+        onChange={value => setFormData({ ...formData, ...value })}
+      />
+    );
+  } else if (dataSource === 'hive') {
+    detail = (
+      <HiveDetail
+        value={formData}
+        onChange={value => setFormData({ ...formData, ...value })}
+      />
+    );
+  }
+
+  return (
+    <form>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-datasource">
+          <span className="field-required" />
+          选择数据源
+        </label>
+        <select
+          className="form-input"
+          id="step-1-datasource"
+          value={dataSource}
+          onChange={evt =>
+            setFormData({ ...formData, dataSource: evt.target.value })
+          }
+        >
+          <option value="" />
+          <option value="mysql">MySQL</option>
+          <option value="hive">Hive</option>
+        </select>
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-1-datatable">
+          <span className="field-required" />
+          选择表
+        </label>
+        <select
+          className="form-input"
+          id="step-1-datatable"
+          value={formData.tableName}
+          onChange={evt =>
+            setFormData({ ...formData, tableName: evt.target.value })
+          }
+        >
+          <option value="" />
+          {tableNames &&
+            tableNames.map(tn => (
+              <option key={tn} value={tn}>
+                {tn}
+              </option>
+            ))}
+        </select>
+      </div>
+      {detail}
+    </form>
+  );
+};
+
+export default forwardRef(StepOneForm);

+ 90 - 0
packages/jldbq-extenison/src/wizard/StepThreeForm.tsx

@@ -0,0 +1,90 @@
+import React, { forwardRef } from 'react';
+
+// const checkForm = (formData: any): any => {
+//   return null;
+// };
+
+const StepThreeForm: React.ForwardRefRenderFunction<{
+  getData: () => any;
+}> = (props, ref) => {
+  return (
+    <form>
+      <div className="form-group">
+        <p className="form-hint">拖拽提取源数据表中的字段,与加载源数据表</p>
+      </div>
+      <div className="form-group">
+        <div className="form-list">
+          <div className="form-list-header">
+            <button type="button" className="form-list-check">
+              全选
+            </button>
+            <div className="form-list-item">
+              <span className="form-list-cell">提取源:表1</span>
+              <span className="form-link-separator" />
+              <span className="form-list-cell">加载源:表2</span>
+            </div>
+          </div>
+          <div className="form-list-body">
+            <div className="form-list-row">
+              <label className="form-list-check">
+                <input type="checkbox" />
+              </label>
+              <div className="form-list-item">
+                <span className="form-list-cell">表1字段1.string</span>
+                <span className="form-link-separator" />
+                <span className="form-list-cell">表2字段1.string</span>
+              </div>
+            </div>
+            <div className="form-list-row">
+              <label className="form-list-check">
+                <input type="checkbox" disabled />
+              </label>
+              <div className="form-list-item form-list-item-diabled">
+                <span className="form-list-cell">表1字段2.string</span>
+                <span className="form-link-separator" />
+                <span className="form-list-cell">表2字段2.string</span>
+              </div>
+            </div>
+            <div className="form-list-row">
+              <div className="form-list-check" />
+            </div>
+          </div>
+        </div>
+        <div className="form-arrow-separator">
+          <button type="button" className="form-list-add-btn" />
+        </div>
+        <div className="form-list">
+          <div className="form-list-header">
+            <div className="form-list-item">
+              <span className="form-list-cell">映射关系:</span>
+            </div>
+            <button type="button" className="form-list-clear">
+              清空
+            </button>
+          </div>
+          <div className="form-list-body">
+            <div className="form-list-row">
+              <div className="form-list-item">
+                <span className="form-list-cell">表1字段2.string</span>
+                <span className="form-link-separator" />
+                <span className="form-list-cell">表2字段2.string</span>
+              </div>
+              <button type="button" className="form-list-clear" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-3-mode">
+          <span className="field-required" />
+          写入模式
+        </label>
+        <select className="form-input" id="step-3-mode">
+          <option value="append">追加</option>
+        </select>
+      </div>
+    </form>
+  );
+};
+
+export default forwardRef(StepThreeForm);

+ 47 - 0
packages/jldbq-extenison/src/wizard/StepTwoForm.tsx

@@ -0,0 +1,47 @@
+import React, { forwardRef } from 'react';
+
+// const checkForm = (formData: any): any => {
+//   return null;
+// };
+
+const StepTwoForm: React.ForwardRefRenderFunction<{
+  getData: () => any;
+}> = (props, ref) => {
+  return (
+    <form>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-2-datasource">
+          <span className="field-required" />
+          选择数据源
+        </label>
+        <select className="form-input" id="step-2-datasource">
+          <option value="" />
+          <option value="mysql">MySQL</option>
+          <option value="hive">Hive</option>
+        </select>
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-2-datatable">
+          <span className="field-required" />
+          选择表
+        </label>
+        <select className="form-input" id="step-2-datatable">
+          <option value="" />
+          <option value="grade">grade</option>
+        </select>
+      </div>
+      <div className="form-group">
+        <label className="form-label" htmlFor="step-2-presql">
+          preSQL
+        </label>
+        <textarea
+          className="form-input"
+          id="step-2-fieldname"
+          placeholder="preSQL"
+        />
+      </div>
+    </form>
+  );
+};
+
+export default forwardRef(StepTwoForm);

+ 179 - 0
packages/jldbq-extenison/src/wizard/SyncWizard.tsx

@@ -0,0 +1,179 @@
+import React, { useRef, useState } from 'react';
+import StepOneForm from './StepOneForm';
+import StepTwoForm from './StepTwoForm';
+import StepThreeForm from './StepThreeForm';
+import StepFourForm from './StepThreeForm';
+
+const StepIndicator: React.FunctionComponent<{ currentStep: number }> = ({
+  currentStep
+}) => {
+  const desc = [
+    '步骤1: 配置加载源',
+    '步骤2: 配置加载源',
+    '步骤3: 配置转换规则',
+    '步骤4: 设置同步参数'
+  ];
+  return (
+    <ul className="step-index-list">
+      {desc.map((d, idx) => {
+        return (
+          <li key={idx} className={currentStep === idx ? 'step-active' : ''}>
+            <div className="step-index-item">
+              <div className="step-index-label">{idx + 1}</div>
+            </div>
+            <div className="step-index-desc">{d}</div>
+          </li>
+        );
+      })}
+    </ul>
+  );
+};
+
+const StepController: React.FunctionComponent<{
+  currentStep: number;
+  onNext: () => void;
+  onPrev: () => void;
+}> = ({ currentStep, onNext, onPrev }) => {
+  return (
+    <div className="step-control-container">
+      <button
+        disabled={currentStep === 0}
+        className="step-control"
+        onClick={onPrev}
+      >
+        上一步
+      </button>
+      <button className="step-control" onClick={onNext}>
+        {currentStep === 3 ? '完成' : '下一步'}
+      </button>
+    </div>
+  );
+};
+
+export const SyncWizard: React.FunctionComponent<{
+  onFinish: (data: any) => void;
+}> = ({ onFinish }) => {
+  const [showSideBar, setShowSideBar] = useState(false);
+  const [currentStep, setCurrentStep] = useState(0);
+  const stepOneRef = useRef<{ getData: () => any }>(null);
+  const stepTwoRef = useRef<{ getData: () => any }>(null);
+  const stepThreeRef = useRef<{ getData: () => any }>(null);
+  const stepFourRef = useRef<{ getData: () => any }>(null);
+
+  return (
+    <div className="sync-wizard">
+      <StepIndicator currentStep={currentStep} />
+      <div className="step-content-container">
+        <div
+          className={
+            'step-content-form ' + (currentStep === 0 ? 'step-active' : '')
+          }
+        >
+          <StepOneForm ref={stepOneRef} />
+        </div>
+        <div
+          className={
+            'step-content-form ' + (currentStep === 1 ? 'step-active' : '')
+          }
+        >
+          <StepTwoForm ref={stepTwoRef} />
+        </div>
+        <div
+          className={
+            'step-content-form ' + (currentStep === 2 ? 'step-active' : '')
+          }
+        >
+          <StepThreeForm ref={stepThreeRef} />
+        </div>
+        <div
+          className={
+            'step-content-form ' + (currentStep === 3 ? 'step-active' : '')
+          }
+        >
+          <StepFourForm ref={stepFourRef} />
+        </div>
+      </div>
+      <StepController
+        currentStep={currentStep}
+        onPrev={() => setCurrentStep(currentStep - 1)}
+        onNext={() => {
+          if (
+            [stepOneRef, stepTwoRef, stepThreeRef, stepFourRef][
+              currentStep
+            ].current?.getData() === null
+          ) {
+            return;
+          }
+          if (currentStep === 3) {
+            const res = [];
+            res.push(stepOneRef.current!.getData());
+            res.push(stepTwoRef.current!.getData());
+            res.push(stepThreeRef.current!.getData());
+            res.push(stepFourRef.current!.getData());
+            onFinish(res);
+          } else {
+            setCurrentStep(currentStep + 1);
+          }
+        }}
+      />
+      <button className="drawer-button" onClick={() => setShowSideBar(true)}>
+        表结构预览
+      </button>
+      <div
+        className={'drawer-container ' + (showSideBar ? '' : 'drawer-hidden')}
+      >
+        <div className="drawer-titlebar">
+          <div className="drawer-title">表结构预览</div>
+          <button
+            className="drawer-close-btn"
+            onClick={() => setShowSideBar(false)}
+          />
+        </div>
+        <div className="drawer-table-header">
+          <span className="drawer-table-cell" style={{ width: '20%' }}>
+            序号
+          </span>
+          <span className="drawer-table-cell" style={{ width: '35%' }}>
+            字段
+          </span>
+          <span className="drawer-table-cell" style={{ width: '35%' }}>
+            类型
+          </span>
+          <span className="drawer-table-cell" style={{ width: '10%' }}>
+            长度
+          </span>
+        </div>
+        <div className="drawer-table-body">
+          <div className="drawer-table-row">
+            <span className="drawer-table-cell" style={{ width: '20%' }}>
+              1
+            </span>
+            <span className="drawer-table-cell" style={{ width: '35%' }}>
+              SCHED_NAME
+            </span>
+            <span className="drawer-table-cell" style={{ width: '35%' }}>
+              VARCHAR(120)
+            </span>
+            <span className="drawer-table-cell" style={{ width: '10%' }}>
+              120
+            </span>
+          </div>
+          <div className="drawer-table-row">
+            <span className="drawer-table-cell" style={{ width: '20%' }}>
+              2
+            </span>
+            <span className="drawer-table-cell" style={{ width: '35%' }}>
+              SCHED_NAME
+            </span>
+            <span className="drawer-table-cell" style={{ width: '35%' }}>
+              VARCHAR(120)
+            </span>
+            <span className="drawer-table-cell" style={{ width: '10%' }}>
+              120
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 1 - 0
packages/jldbq-extenison/src/wizard/index.ts

@@ -0,0 +1 @@
+export * from './SyncWizard';

+ 43 - 0
packages/jldbq-extenison/src/wizard/mockData.ts

@@ -0,0 +1,43 @@
+export function tableSchema(): any {
+  return [
+    {
+      field: '字段1',
+      type: 'string',
+      length: 120
+    },
+    {
+      field: '字段2',
+      type: 'string',
+      length: 120
+    },
+    {
+      field: '字段3',
+      type: 'string',
+      length: 120
+    },
+    {
+      field: '字段4',
+      type: 'string',
+      length: 120
+    },
+    {
+      field: '字段5',
+      type: 'string',
+      length: 120
+    },
+    {
+      field: '字段6',
+      type: 'int',
+      length: 120
+    },
+    {
+      field: '字段7',
+      type: 'string',
+      length: 120
+    }
+  ];
+}
+
+export function tableNames(): string[] {
+  return ['表1', '表2', '表3', '表4', '表5'];
+}

+ 53 - 0
packages/jldbq-extenison/src/wizard/service.ts

@@ -0,0 +1,53 @@
+import useSWR from 'swr';
+import * as mockData from './mockData';
+
+export interface ITableSchemaResponse {
+  field: string;
+  type: string;
+  length: number;
+}
+
+export interface IService<T> {
+  data: T | undefined;
+  error: any;
+}
+
+// mock
+const tableSchemaFetcher = async (
+  key: string
+): Promise<ITableSchemaResponse[]> => {
+  const [dataSource, , tableName] = key.split('/');
+  if (!dataSource || !tableName) {
+    return [];
+  }
+  // sleep 1
+  await new Promise(resolve => setTimeout(resolve, 1));
+  return mockData.tableSchema();
+};
+
+// mock
+const tableNamesFetcher = async (key: string): Promise<string[]> => {
+  const dataSource = key.split('/')[0];
+  if (!dataSource) {
+    return [];
+  }
+  // sleep 1
+  await new Promise(resolve => setTimeout(resolve, 1));
+  return mockData.tableNames();
+};
+
+export const useTableSchema = (
+  dataSource: string,
+  tableName: string
+): IService<ITableSchemaResponse[]> => {
+  const { data, error } = useSWR(
+    `${dataSource}/tables/${tableName}`,
+    tableSchemaFetcher
+  );
+  return { data, error };
+};
+
+export const useTableNames = (dataSource: string): IService<string[]> => {
+  const { data, error } = useSWR(`${dataSource}/tables`, tableNamesFetcher);
+  return { data, error };
+};

+ 1 - 0
packages/jldbq-extenison/style/base.css

@@ -7,3 +7,4 @@
 @import './syncform.css';
 @import './button.css';
 @import './dsform.css';
+@import './wizard.css';

二進制
packages/jldbq-extenison/style/img/caret.png


二進制
packages/jldbq-extenison/style/img/close.png


二進制
packages/jldbq-extenison/style/img/collapse.png


二進制
packages/jldbq-extenison/style/img/expand.png


二進制
packages/jldbq-extenison/style/img/link.png


二進制
packages/jldbq-extenison/style/img/play.png


二進制
packages/jldbq-extenison/style/img/target.png


二進制
packages/jldbq-extenison/style/img/tick.png


二進制
packages/jldbq-extenison/style/img/warn.png


+ 488 - 0
packages/jldbq-extenison/style/wizard.css

@@ -0,0 +1,488 @@
+.sync-wizard {
+  min-width: 900px;
+  min-height: 100%;
+  overflow-x: hidden;
+  display: flex;
+  flex-direction: column;
+  padding: 30px;
+  position: relative;
+  font-size: 14px;
+}
+
+.sync-wizard,
+.sync-wizard * {
+  box-sizing: border-box;
+}
+
+/* header */
+
+.sync-wizard .step-index-list {
+  display: flex;
+  margin: 0;
+  padding: 0;
+  width: 800px;
+  color: #7d7d7d;
+  margin-bottom: 30px;
+}
+
+.sync-wizard .step-index-list li {
+  list-style: none;
+  flex-grow: 1;
+}
+
+.sync-wizard .step-index-list li.step-active {
+  color: #4883fb;
+}
+
+.sync-wizard .step-index-list li.step-active .step-index-label {
+  border-color: #4883fb;
+  outline: 1px solid #4883fb;
+}
+
+.sync-wizard .step-index-item {
+  display: flex;
+  align-items: center;
+}
+
+.sync-wizard .step-index-list li:not(:last-child) .step-index-item::after {
+  content: '';
+  background-color: #7d7d7d;
+  height: 1px;
+  flex-grow: 1;
+}
+
+.sync-wizard .step-index-desc {
+  margin-top: 10px;
+}
+
+.sync-wizard .step-index-label {
+  width: 34px;
+  height: 34px;
+  border: 1px solid #7d7d7d;
+  border-radius: 17px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* drawer-button */
+
+.sync-wizard .drawer-button {
+  position: absolute;
+  right: 0;
+  top: 130px;
+  z-index: 100;
+  border: none;
+  border-radius: 0;
+  background-color: #488feb;
+  color: white;
+  height: 44px;
+  cursor: pointer;
+  padding: 0 20px 0 38px;
+  display: flex;
+  align-items: center;
+}
+
+.sync-wizard .drawer-button::before {
+  content: '';
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 38px;
+  height: 100%;
+  background-image: url('./img/expand.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 20px;
+}
+
+/* form */
+
+.sync-wizard .form-input {
+  font-size: 12px;
+  border-radius: 2px;
+  border: 1px solid #c8d3e9;
+  color: #4a4a4a;
+  outline: 1px solid transparent;
+  transition: all 0.2s ease;
+}
+
+.sync-wizard .form-input::placeholder {
+  color: #bbbbbb;
+}
+
+.sync-wizard .form-input:focus {
+  border-color: #4883fb;
+  outline: 1px solid #4883fb;
+}
+
+.sync-wizard select.form-input {
+  width: 200px;
+  height: 32px;
+  padding: 0 30px 0 10px;
+  -moz-appearance: none;
+  -webkit-appearance: none;
+  appearance: none;
+  background-image: url('./img/caret.png');
+  background-repeat: no-repeat;
+  background-position: right 0.7em top 50%;
+  background-size: 15px auto;
+}
+
+.sync-wizard input.form-input {
+  width: 200px;
+  height: 32px;
+  padding: 0 10px;
+}
+
+.sync-wizard textarea.form-input {
+  width: 410px;
+  height: 100px;
+  resize: none;
+  padding: 10px;
+}
+
+.sync-wizard .form-label {
+  color: #4a4a4a;
+  height: 32px;
+  width: 100px;
+  display: flex;
+  align-items: center;
+  margin-right: 15px;
+}
+
+.sync-wizard .form-group {
+  display: flex;
+  margin-bottom: 25px;
+}
+
+.sync-wizard .form-group:last-child {
+  margin-bottom: 0;
+}
+
+.sync-wizard .field-required::after {
+  content: '*';
+  color: #ed5555;
+  margin-right: 6px;
+}
+
+.sync-wizard p.form-hint {
+  color: #bbbbbb;
+  margin: 0;
+  font-size: 12px;
+}
+
+/* form-list */
+
+.sync-wizard .form-arrow-separator {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 56px;
+  min-width: 56px;
+  background-image: url('./img/play.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 18px auto;
+}
+
+.sync-wizard .form-arrow-separator .form-list-add-btn {
+  width: 32px;
+  height: 32px;
+  background-color: transparent;
+  border: none;
+  cursor: pointer;
+}
+
+.sync-wizard .form-list {
+  border: 1px solid #c8d3e9;
+  border-radius: 2px;
+  color: #4a4a4a;
+  counter-reset: form-list;
+}
+
+.sync-wizard .form-list-header {
+  border-bottom: 1px solid #c8d3e9;
+}
+
+.sync-wizard .form-list:last-child .form-list-header .form-list-item {
+  flex-grow: 1;
+}
+
+.sync-wizard .form-list:last-child .form-list-row {
+  border-bottom: 1px solid #c8d3e9;
+}
+
+.sync-wizard .form-list:last-child .form-list-row::before {
+  counter-increment: form-list;
+  content: counter(form-list);
+  font-size: 12px;
+  display: flex;
+  justify-content: center;
+  width: 46px;
+  margin-right: -19px;
+}
+
+.sync-wizard .form-list-header,
+.sync-wizard .form-list-row {
+  height: 50px;
+  display: flex;
+  align-items: center;
+}
+
+.sync-wizard .form-list-check {
+  width: 50px;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border-right: 1px solid #c8d3e9 !important;
+}
+
+.sync-wizard button.form-list-check {
+  border: none;
+  padding: 0;
+  margin: 0;
+  background-color: white;
+  cursor: pointer;
+}
+
+.sync-wizard .form-list-check input[type='checkbox'] {
+  -moz-appearance: none;
+  -webkit-appearance: none;
+  appearance: none;
+  margin: 0;
+  padding: 0;
+  width: 19px;
+  height: 19px;
+  border: 1px solid #c8d3e9;
+  border-radius: 2px;
+}
+
+.sync-wizard .form-list-check input[type='checkbox']:checked {
+  background-color: #147bd1;
+  background-image: url('./img/tick.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 16px auto;
+}
+
+.sync-wizard .form-list-check input[type='checkbox']:disabled {
+  background-color: #d8d8d8;
+}
+
+.sync-wizard .form-list-item {
+  padding: 0 19px;
+  display: flex;
+}
+
+.sync-wizard .form-list-cell {
+  width: 130px;
+  height: 33px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+}
+
+.sync-wizard .form-list-body .form-list-cell {
+  padding: 0 5px 0 15px;
+  font-size: 12px;
+  background-color: #ebeff3;
+  border-radius: 2px;
+}
+
+.sync-wizard
+  .form-list:first-child
+  .form-list-body
+  .form-list-cell:first-child {
+  padding-left: 32px;
+  background-color: #eef5fe;
+  position: relative;
+}
+
+.sync-wizard
+  .form-list:first-child
+  .form-list-body
+  .form-list-cell:first-child::before {
+  content: '';
+  background-image: url('./img/target.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 16px auto;
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%;
+  width: 32px;
+  cursor: grab;
+}
+
+.sync-wizard
+  .form-list:first-child
+  .form-list-body
+  .form-list-item-diabled
+  .form-list-cell:first-child::before {
+  cursor: not-allowed;
+}
+
+.sync-wizard .form-link-separator {
+  width: 36px;
+}
+
+.sync-wizard .form-list-body .form-link-separator {
+  background-image: url('./img/link.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 16px auto;
+}
+
+.sync-wizard .form-list-body .form-list-item-diabled .form-link-separator {
+  background-image: url('./img/warn.png');
+}
+
+.sync-wizard .form-list-clear {
+  width: 50px;
+  height: 100%;
+  padding: 0;
+  margin: 0;
+  margin-left: -19px;
+  border: none;
+  background-color: white;
+  cursor: pointer;
+}
+
+.sync-wizard .form-list-header .form-list-clear {
+  font-size: 12px;
+  color: #7d7d7d;
+}
+
+.sync-wizard .form-list-body .form-list-clear {
+  background-image: url('./img/close.png');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 14px auto;
+}
+
+/* content */
+
+.sync-wizard .step-content-container {
+  flex-grow: 1;
+}
+
+.sync-wizard .step-content-form {
+  display: none;
+  animation-name: fade;
+  animation-duration: 0.3s;
+}
+
+@keyframes fade {
+  from {
+    opacity: 0.4;
+  }
+  to {
+    opacity: 1;
+  }
+}
+
+.sync-wizard .step-content-form.step-active {
+  display: block;
+}
+
+/* control */
+
+.sync-wizard .step-control-container {
+  margin: 60px 0;
+}
+
+.sync-wizard .step-control {
+  margin-right: 20px;
+  color: #4a4a4a;
+  border: 1px solid #c8d3e9;
+  border-radius: 2px;
+  height: 41px;
+  width: 96px;
+  background: none;
+  cursor: pointer;
+}
+
+.sync-wizard .step-control:disabled {
+  border-color: #d6d6d6;
+  color: #d6d6d6;
+  cursor: not-allowed;
+}
+
+/* drawer */
+
+.sync-wizard .drawer-container {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 560px;
+  height: 100%;
+  z-index: 110;
+  background-color: white;
+  padding: 10px;
+  box-shadow: -4px 0px 10px 0px rgba(0, 0, 0, 0.12);
+  color: #4a4a4a;
+  transition: all 0.2s ease-out;
+  display: flex;
+  flex-direction: column;
+}
+
+.sync-wizard .drawer-container.drawer-hidden {
+  right: -560px;
+  box-shadow: -4px 0px 10px 0px rgba(0, 0, 0, 0);
+}
+
+.sync-wizard .drawer-titlebar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 10px;
+  margin-bottom: 10px;
+}
+
+.sync-wizard .drawer-close-btn {
+  border: none;
+  background-color: white;
+  padding: 0;
+  width: 24px;
+  height: 24px;
+  background-image: url('./img/collapse.png');
+  background-repeat: no-repeat;
+  background-position: left 0 top 50%;
+  background-size: 24px auto;
+  cursor: pointer;
+}
+
+.sync-wizard .drawer-table-body {
+  flex-grow: 1;
+  min-height: 0;
+  overflow: auto;
+}
+
+.sync-wizard .drawer-table-header,
+.sync-wizard .drawer-table-row {
+  padding: 0 35px;
+  height: 41px;
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+}
+
+.sync-wizard .drawer-table-header {
+  background-color: #f0f5ff;
+  font-weight: bold;
+}
+
+.sync-wizard .drawer-table-row {
+  border-bottom: 1px solid #c8d3e9;
+}
+
+.sync-wizard .drawer-table-cell {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  padding: 0 5px;
+}