瀏覽代碼

Merge branch 'yili' of http://gogsb.soaringnova.com/ylproj/jupyterlab into yili

leo 2 年之前
父節點
當前提交
7bb74546ea

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

@@ -42,10 +42,13 @@
     "antd": "^4.22.3",
     "moment": "^2.29.4",
     "react": "^17.0.1",
+    "react-beautiful-dnd": "^13.1.0",
     "react-select": "^5.4.0",
+    "react-syntax-highlighter": "^15.5.0",
     "swr": "^1.3.0"
   },
   "devDependencies": {
+    "@types/react-beautiful-dnd": "^13.1.2",
     "rimraf": "~3.0.0",
     "typedoc": "~0.21.2",
     "typescript": "~4.1.3"

+ 2 - 0
packages/jldbq-extenison/src/index.tsx

@@ -53,6 +53,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
         content: ReactWidget.create(
           <SyncWizard
             onFinish={data => {
+              // TODO:
+              alert(JSON.stringify(data, null, 2));
               console.log(data);
             }}
           />

+ 92 - 28
packages/jldbq-extenison/src/wizard/StepFourForm.tsx

@@ -1,18 +1,79 @@
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
+import React, {
+  forwardRef,
+  useImperativeHandle,
+  useRef,
+  useState
+} from 'react';
 import { JsonSchemaDrawer } from './drawer';
 
+const FormNumberInput: React.FunctionComponent<{
+  id?: string;
+  value?: number;
+  onChange: (value: number) => void;
+}> = ({ id, value, onChange }) => {
+  const valStr = value !== undefined ? value.toString() : '';
+  const [draft, setDraft] = useState(valStr);
+  const [isEditing, setIsEditing] = useState(false);
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  // getDerivedProps
+  if (!isEditing && valStr !== draft) {
+    setDraft(valStr);
+  }
+
+  return (
+    <div className="form-input-number">
+      <button type="button" onClick={() => onChange((value || 0) - 1)} />
+      <input
+        className="form-input"
+        type="text"
+        autoComplete="off"
+        ref={inputRef}
+        id={id}
+        value={draft}
+        onFocus={() => {
+          inputRef.current?.select();
+          setIsEditing(true);
+        }}
+        onBlur={() => {
+          const newVal = parseInt(draft, 10);
+          if (!isNaN(newVal)) {
+            onChange(newVal);
+          }
+          setIsEditing(false);
+        }}
+        onChange={evt => setDraft(evt.target.value)}
+      />
+      <button type="button" onClick={() => onChange((value || 0) + 1)} />
+    </div>
+  );
+};
+
+interface IForm {
+  route: string;
+  block: string;
+  timeout: number;
+  retry: number;
+  startTime: string;
+  timeField: string;
+  shardField: string;
+  timeRule: string;
+  taskName: string;
+}
+
+// TODO:
 const checkForm = (formData: any): any => {
   return null;
 };
 
 const StepFourForm: React.ForwardRefRenderFunction<
   {
-    getData: () => any;
+    getData: () => Partial<IForm> | null;
   },
   { wizardData: any[] }
 > = ({ wizardData }, ref) => {
   const [showSideBar, setShowSideBar] = useState(false);
-  const [formData, setFormData] = useState<any>({});
+  const [formData, setFormData] = useState<Partial<IForm>>({});
   const [, setFormError] = useState<any>({});
 
   useImperativeHandle(ref, () => {
@@ -31,8 +92,10 @@ const StepFourForm: React.ForwardRefRenderFunction<
   return (
     <form>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-2-datasource">
-          <span className="field-required" />
+        <label
+          className="form-label field-required"
+          htmlFor="step-2-datasource"
+        >
           路由策略
         </label>
         <select
@@ -48,8 +111,7 @@ const StepFourForm: React.ForwardRefRenderFunction<
         </select>
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-block">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-block">
           阻塞处理方式
         </label>
         <select
@@ -65,44 +127,39 @@ const StepFourForm: React.ForwardRefRenderFunction<
         </select>
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-timeout">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-timeout">
           超时时间
         </label>
-        <input
-          className="form-input"
-          type="number"
+        <FormNumberInput
           id="step-4-timeout"
           value={formData.timeout}
-          onChange={evt =>
-            setFormData({ ...formData, timeout: evt.target.value })
+          onChange={value =>
+            setFormData({ ...formData, timeout: Math.max(value, 0) })
           }
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-retry">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-retry">
           失败重试次数
         </label>
-        <input
-          className="form-input"
-          type="number"
+        <FormNumberInput
           id="step-4-retry"
           value={formData.retry}
-          onChange={evt =>
-            setFormData({ ...formData, retry: evt.target.value })
+          onChange={value =>
+            setFormData({ ...formData, retry: Math.max(value, 0) })
           }
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-starttime">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-starttime">
           增量开始时间
         </label>
         <input
           className="form-input"
           type="text"
           id="step-4-starttime"
+          autoComplete="off"
+          placeholder="2022.8.6 17:22:14"
           value={formData.startTime}
           onChange={evt =>
             setFormData({ ...formData, startTime: evt.target.value })
@@ -117,6 +174,8 @@ const StepFourForm: React.ForwardRefRenderFunction<
           className="form-input"
           type="text"
           id="step-4-timefield"
+          autoComplete="off"
+          placeholder="-DlastTime='%s' -DcurrentTime='%s'"
           value={formData.timeField}
           onChange={evt =>
             setFormData({ ...formData, timeField: evt.target.value })
@@ -131,6 +190,8 @@ const StepFourForm: React.ForwardRefRenderFunction<
           className="form-input"
           type="text"
           id="step-4-shardfield"
+          autoComplete="off"
+          placeholder="手动输入分区字段"
           value={formData.shardField}
           onChange={evt =>
             setFormData({ ...formData, shardField: evt.target.value })
@@ -138,14 +199,15 @@ const StepFourForm: React.ForwardRefRenderFunction<
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-timerule">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-timerule">
           定时规则
         </label>
         <input
           className="form-input"
           type="text"
           id="step-4-timerule"
+          autoComplete="off"
+          placeholder="0 0/2***?"
           value={formData.timeRule}
           onChange={evt =>
             setFormData({ ...formData, timeRule: evt.target.value })
@@ -153,14 +215,15 @@ const StepFourForm: React.ForwardRefRenderFunction<
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-4-taskname">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-4-taskname">
           同步任务名称
         </label>
         <input
           className="form-input"
           type="text"
           id="step-4-taskname"
+          autoComplete="off"
+          placeholder="同步任务名称"
           value={formData.taskName}
           onChange={evt =>
             setFormData({ ...formData, taskName: evt.target.value })
@@ -169,10 +232,11 @@ const StepFourForm: React.ForwardRefRenderFunction<
       </div>
       <button
         type="button"
+        tabIndex={-1}
         className="drawer-button"
         onClick={() => setShowSideBar(true)}
       >
-        构建
+        构建预览
       </button>
       <JsonSchemaDrawer
         jsonSchema={wizardData.slice(0, 3).concat(formData)}

+ 13 - 10
packages/jldbq-extenison/src/wizard/StepOneForm.tsx

@@ -28,6 +28,7 @@ const MysqlDetail: React.FunctionComponent<{
           className="form-input"
           type="text"
           id="step-1-pkey"
+          autoComplete="off"
           value={value.pkey}
           onChange={evt => onChange({ pkey: evt.target.value })}
         />
@@ -55,8 +56,7 @@ const HiveDetail: React.FunctionComponent<{
   return (
     <>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-path">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-1-path">
           选择路径
         </label>
         <textarea
@@ -68,21 +68,20 @@ const HiveDetail: React.FunctionComponent<{
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-hdfs">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-1-hdfs">
           HDFS
         </label>
         <input
           className="form-input"
           type="text"
           id="step-1-hdfs"
+          autoComplete="off"
           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 className="form-label field-required" htmlFor="step-1-filetype">
           文件类型
         </label>
         <select
@@ -104,6 +103,7 @@ const HiveDetail: React.FunctionComponent<{
           className="form-input"
           type="text"
           id="step-1-delim"
+          autoComplete="off"
           value={value.delim}
           onChange={evt => onChange({ delim: evt.target.value })}
         />
@@ -112,6 +112,7 @@ const HiveDetail: React.FunctionComponent<{
   );
 };
 
+// TODO:
 const checkForm = (formData: any): any => {
   return null;
 };
@@ -162,8 +163,10 @@ const StepOneForm: React.ForwardRefRenderFunction<
   return (
     <form>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-datasource">
-          <span className="field-required" />
+        <label
+          className="form-label field-required"
+          htmlFor="step-1-datasource"
+        >
           选择数据源
         </label>
         <select
@@ -180,8 +183,7 @@ const StepOneForm: React.ForwardRefRenderFunction<
         </select>
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-datatable">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-1-datatable">
           选择表
         </label>
         <select
@@ -204,6 +206,7 @@ const StepOneForm: React.ForwardRefRenderFunction<
       {detail}
       <button
         type="button"
+        tabIndex={-1}
         className="drawer-button"
         onClick={() => setShowSideBar(true)}
       >

+ 365 - 50
packages/jldbq-extenison/src/wizard/StepThreeForm.tsx

@@ -1,24 +1,94 @@
-import React, { forwardRef, useImperativeHandle, useState } from 'react';
-// import { useTableSchema } from './service';
+import React, {
+  forwardRef,
+  useEffect,
+  useImperativeHandle,
+  useState
+} from 'react';
+import {
+  DragDropContext,
+  Draggable,
+  Droppable,
+  DropResult
+} from 'react-beautiful-dnd';
+import { ITableSchemaResponse, useTableSchema } from './service';
 
+interface IMapping {
+  sourceSchema: ITableSchemaResponse;
+  destSchema: ITableSchemaResponse;
+}
+
+interface IForm {
+  mapping: IMapping[];
+  mode: string;
+}
+
+// TODO:
 const checkForm = (formData: any): any => {
   return null;
 };
 
+const useSchemaList = (
+  ds: string,
+  tn: string
+): [
+  ITableSchemaResponse[],
+  {
+    reorder: (startIndex: number, endIndex: number) => void;
+    remove: (indices: number[]) => void;
+    prepend: (value: ITableSchemaResponse[]) => void;
+  },
+  number
+] => {
+  const { data: schema } = useTableSchema(ds, tn);
+  const [data, setData] = useState<ITableSchemaResponse[]>();
+  const [size, setSize] = useState(0);
+
+  useEffect(() => {
+    if (data || !schema) {
+      return;
+    }
+    setData(schema);
+    setSize(schema.length);
+  }, [data, schema]);
+
+  const reorder = (startIndex: number, endIndex: number) => {
+    const result = Array.from(data || []);
+    const [removed] = result.splice(startIndex, 1);
+    result.splice(endIndex, 0, removed);
+    setData(result);
+  };
+
+  const remove = (indices: number[]) => {
+    const idx = new Set(indices);
+    const result = (data || []).filter((v, i) => !idx.has(i));
+    setData(result);
+  };
+
+  const prepend = (value: ITableSchemaResponse[]) => {
+    const result = value.concat(data || []);
+    setData(result);
+  };
+
+  return [data || [], { reorder, remove, prepend }, size];
+};
+
 const StepThreeForm: React.ForwardRefRenderFunction<
   {
-    getData: () => any;
+    getData: () => Partial<IForm> | null;
   },
   { wizardData: any[] }
 > = ({ wizardData }, ref) => {
-  const [formData, setFormData] = useState<any>({});
-  // const [selection, setSelection] = useState<string[]>([]);
+  const { dataSource: ds1, tableName: tn1 } = wizardData[0];
+  const { dataSource: ds2, tableName: tn2 } = wizardData[1];
+
+  const [formData, setFormData] = useState<Partial<IForm>>({});
+  const [selection, setSelection] = useState(new Map<string, IMapping>());
+  const [schema1, updater1, size1] = useSchemaList(ds1, tn1);
+  const [schema2, updater2, size2] = useSchemaList(ds2, tn2);
   const [, setFormError] = useState<any>({});
-  const { dataSource: ds1 } = wizardData[0];
-  const { dataSource: ds2 } = wizardData[1];
-  // const { data: schema1 } = useTableSchema(ds1, tn1);
-  // const { data: schema2 } = useTableSchema(ds2, tn2);
-  // const loading = !tableNames && !fetchError;
+
+  const maxListLength = Math.max(size1, size2) || 5;
+  const listLength = Math.max(schema1.length, schema2.length);
 
   useImperativeHandle(ref, () => {
     return {
@@ -33,6 +103,259 @@ const StepThreeForm: React.ForwardRefRenderFunction<
     };
   });
 
+  const createRowId = (
+    idx: number,
+    sourceSchema?: ITableSchemaResponse,
+    destSchema?: ITableSchemaResponse
+  ): { sourceId: string; destId: string; rowId: string } => {
+    const sourceId = sourceSchema
+      ? `${ds1}-${tn1}-${sourceSchema.field}`
+      : `${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 = (
+    tableName: string,
+    schema: ITableSchemaResponse
+  ): string => {
+    return `${tableName}${schema.field}.${schema.type}`;
+  };
+
+  const onDragEnd = (result: DropResult) => {
+    if (!result.destination) {
+      return;
+    }
+    const idx1 = result.source.index;
+    const idx2 = result.destination.index;
+    const sourceId1 = `${ds1}-${tn1}-${schema1[idx1].field}`;
+    const sourceId2 = `${ds1}-${tn1}-${schema1[idx2].field}`;
+    const destId1 = `${ds2}-${tn2}-${schema2[idx1].field}`;
+    const destId2 = `${ds2}-${tn2}-${schema2[idx2].field}`;
+    const rowId1 = `${sourceId1}-${destId1}`;
+    const rowId2 = `${sourceId2}-${destId2}`;
+    const newSelState = new Map(selection);
+    newSelState.delete(rowId1);
+    newSelState.delete(rowId2);
+    setSelection(newSelState);
+    updater1.reorder(idx1, idx2);
+  };
+
+  const handleAddData = () => {
+    const indices: number[] = [];
+    for (let i = 0; i < listLength; i++) {
+      const sourceSchema = schema1[i];
+      const destSchema = schema2[i];
+      const sourceId = `${ds1}-${tn1}-${sourceSchema.field}`;
+      const destId = `${ds2}-${tn2}-${destSchema.field}`;
+      const rowId = `${sourceId}-${destId}`;
+      if (selection.has(rowId)) {
+        indices.push(i);
+      }
+    }
+    updater1.remove(indices);
+    updater2.remove(indices);
+    const oldMapping = formData.mapping || [];
+    const mapping = [...oldMapping, ...Array.from(selection.values())];
+    setFormData({ ...formData, mapping });
+    setSelection(new Map());
+  };
+
+  const handleCheckbox = (
+    checked: boolean,
+    rowId: string,
+    sourceSchema: ITableSchemaResponse,
+    destSchema: ITableSchemaResponse
+  ) => {
+    const newState = new Map(selection);
+    if (checked) {
+      newState.set(rowId, { sourceSchema, destSchema });
+    } else {
+      newState.delete(rowId);
+    }
+    setSelection(newState);
+  };
+
+  const matchField = (
+    sourceSchema?: ITableSchemaResponse,
+    destSchema?: ITableSchemaResponse
+  ): boolean | undefined => {
+    return (
+      sourceSchema &&
+      destSchema &&
+      sourceSchema.type === destSchema.type &&
+      sourceSchema.length === destSchema.length
+    );
+  };
+
+  const selectable = new Map<string, IMapping>();
+  const handleSelectAll = () => {
+    if (selectable.size === 0) {
+      setSelection(new Map());
+    } else {
+      const newState = new Map(selection);
+      selectable.forEach((v, k) => newState.set(k, v));
+      setSelection(newState);
+    }
+  };
+
+  const handleRemoveMapping = (row: IMapping) => {
+    const mapping = formData.mapping!.filter(mapping => row !== mapping);
+    setFormData({ ...formData, mapping });
+    updater1.prepend([row.sourceSchema]);
+    updater2.prepend([row.destSchema]);
+  };
+
+  const handleRemoveAllMapping = () => {
+    if (!formData.mapping) {
+      return;
+    }
+    const source = formData.mapping.map(row => row.sourceSchema);
+    const dest = formData.mapping.map(row => row.destSchema);
+    updater1.prepend(source);
+    updater2.prepend(dest);
+    setFormData({ ...formData, mapping: [] });
+  };
+
+  const checkList: JSX.Element[] = [];
+  const hintList: JSX.Element[] = [];
+  const sourceList: JSX.Element[] = [];
+  const destList: JSX.Element[] = [];
+
+  for (let i = 0; i < maxListLength; i++) {
+    const sourceSchema = schema1[i];
+    const destSchema = schema2[i];
+    const isSchemaMatch = matchField(sourceSchema, destSchema);
+    const { sourceId, destId, rowId } = createRowId(
+      i,
+      sourceSchema,
+      destSchema
+    );
+
+    if (sourceSchema && destSchema && !selection.has(rowId) && isSchemaMatch) {
+      selectable.set(rowId, {
+        sourceSchema,
+        destSchema
+      });
+    }
+
+    if (sourceSchema && destSchema) {
+      checkList.push(
+        <div key={rowId} className="form-list-row">
+          <label className="form-list-check">
+            <input
+              type="checkbox"
+              disabled={!isSchemaMatch}
+              checked={selection.has(rowId)}
+              onChange={evt =>
+                handleCheckbox(
+                  evt.target.checked,
+                  rowId,
+                  sourceSchema,
+                  destSchema
+                )
+              }
+            />
+          </label>
+        </div>
+      );
+      hintList.push(
+        <div key={rowId} className="form-list-row">
+          <div
+            className={
+              'form-link-separator ' +
+              (isSchemaMatch ? '' : 'form-list-item-diabled')
+            }
+          />
+        </div>
+      );
+    } else {
+      checkList.push(
+        <div key={rowId} className="form-list-row">
+          <label className="form-list-check" />
+        </div>
+      );
+      hintList.push(
+        <div key={rowId} className="form-list-row">
+          <div className="form-link-separator form-link-empty" />
+        </div>
+      );
+    }
+    if (sourceSchema) {
+      sourceList.push(
+        <Draggable key={sourceId} draggableId={sourceId} index={i}>
+          {(provided, snapshot) => (
+            <div
+              className="form-list-row"
+              ref={provided.innerRef}
+              {...provided.draggableProps}
+              {...provided.dragHandleProps}
+              tabIndex={-1}
+            >
+              <div className="form-list-item">
+                <span className="form-list-cell">
+                  {formatItemName(tn1, sourceSchema)}
+                </span>
+              </div>
+            </div>
+          )}
+        </Draggable>
+      );
+    } else {
+      sourceList.push(
+        <div key={sourceId} className="form-list-row">
+          <div className="form-list-item">
+            <span className="form-list-cell form-list-cell-empty" />
+          </div>
+        </div>
+      );
+    }
+    if (destSchema) {
+      destList.push(
+        <div key={destId} className="form-list-row">
+          <div className="form-list-item">
+            <span className="form-list-cell">
+              {formatItemName(tn2, destSchema)}
+            </span>
+          </div>
+        </div>
+      );
+    } else {
+      destList.push(
+        <div key={destId} className="form-list-row">
+          <div className="form-list-item">
+            <span className="form-list-cell form-list-cell-empty" />
+          </div>
+        </div>
+      );
+    }
+  }
+
+  const mappingList = formData.mapping
+    ? formData.mapping.map((row, idx) => {
+        const { rowId } = createRowId(idx, row.sourceSchema, row.destSchema);
+        const desc1 = formatItemName(tn1, row.sourceSchema);
+        const desc2 = formatItemName(tn2, row.destSchema);
+        return (
+          <div key={rowId} className="form-list-row">
+            <div className="form-list-item">
+              <span className="form-list-cell">{desc1}</span>
+              <span className="form-link-separator" />
+              <span className="form-list-cell">{desc2}</span>
+            </div>
+            <button
+              type="button"
+              className="form-list-clear"
+              onClick={() => handleRemoveMapping(row)}
+            />
+          </div>
+        );
+      })
+    : [];
+
   return (
     <form>
       <div className="form-group">
@@ -41,46 +364,44 @@ const StepThreeForm: React.ForwardRefRenderFunction<
       <div className="form-group">
         <div className="form-list">
           <div className="form-list-header">
-            <button type="button" className="form-list-check">
+            <button
+              type="button"
+              className="form-list-check"
+              onClick={handleSelectAll}
+            >
               全选
             </button>
             <div className="form-list-item">
-              <span className="form-list-cell">提取源:{ds1}</span>
+              <span className="form-list-cell">提取源:{tn1}</span>
               <span className="form-link-separator" />
-              <span className="form-list-cell">加载源:{ds2}</span>
+              <span className="form-list-cell">加载源:{tn2}</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 className="form-list-checklist">{checkList}</div>
+            <DragDropContext onDragEnd={onDragEnd}>
+              <Droppable droppableId="form-list-sourcelist">
+                {(provided, snapshot) => (
+                  <div
+                    className="form-list-sourcelist"
+                    ref={provided.innerRef}
+                    {...provided.droppableProps}
+                  >
+                    {sourceList}
+                    {provided.placeholder}
+                  </div>
+                )}
+              </Droppable>
+            </DragDropContext>
+            <div className="form-list-hintlist">{hintList}</div>
+            <div className="form-list-destlist">{destList}</div>
           </div>
         </div>
         <div className="form-arrow-separator">
           <button
             type="button"
             className="form-list-add-btn"
-            onClick={() => setFormData({})}
+            onClick={handleAddData}
           />
         </div>
         <div className="form-list">
@@ -88,25 +409,19 @@ const StepThreeForm: React.ForwardRefRenderFunction<
             <div className="form-list-item">
               <span className="form-list-cell">映射关系:</span>
             </div>
-            <button type="button" className="form-list-clear">
+            <button
+              type="button"
+              className="form-list-clear"
+              onClick={handleRemoveAllMapping}
+            >
               清空
             </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 className="form-list-body">{mappingList}</div>
         </div>
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-3-mode">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-3-mode">
           写入模式
         </label>
         <select

+ 12 - 10
packages/jldbq-extenison/src/wizard/StepTwoForm.tsx

@@ -9,8 +9,7 @@ const HiveDetail: React.FunctionComponent<{
   return (
     <>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-path">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-1-path">
           选择路径
         </label>
         <textarea
@@ -22,21 +21,20 @@ const HiveDetail: React.FunctionComponent<{
         />
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-1-hdfs">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-1-hdfs">
           HDFS
         </label>
         <input
           className="form-input"
           type="text"
           id="step-1-hdfs"
+          autoComplete="off"
           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 className="form-label field-required" htmlFor="step-1-filetype">
           文件类型
         </label>
         <select
@@ -58,6 +56,7 @@ const HiveDetail: React.FunctionComponent<{
           className="form-input"
           type="text"
           id="step-1-delim"
+          autoComplete="off"
           value={value.delim}
           onChange={evt => onChange({ delim: evt.target.value })}
         />
@@ -66,6 +65,7 @@ const HiveDetail: React.FunctionComponent<{
   );
 };
 
+// TODO:
 const checkForm = (formData: any): any => {
   return null;
 };
@@ -125,8 +125,10 @@ const StepTwoForm: React.ForwardRefRenderFunction<
   return (
     <form>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-2-datasource">
-          <span className="field-required" />
+        <label
+          className="form-label field-required"
+          htmlFor="step-2-datasource"
+        >
           选择数据源
         </label>
         <select
@@ -143,8 +145,7 @@ const StepTwoForm: React.ForwardRefRenderFunction<
         </select>
       </div>
       <div className="form-group">
-        <label className="form-label" htmlFor="step-2-datatable">
-          <span className="field-required" />
+        <label className="form-label field-required" htmlFor="step-2-datatable">
           选择表
         </label>
         <select
@@ -167,6 +168,7 @@ const StepTwoForm: React.ForwardRefRenderFunction<
       {detail}
       <button
         type="button"
+        tabIndex={-1}
         className="drawer-button"
         onClick={() => setShowSideBar(true)}
       >

+ 1 - 1
packages/jldbq-extenison/src/wizard/SyncWizard.tsx

@@ -22,7 +22,7 @@ const StepController: React.FunctionComponent<{
         上一步
       </button>
       <button className="step-control" onClick={onNext}>
-        {currentStep === 3 ? '完成' : '下一步'}
+        {currentStep === 3 ? '构建' : '下一步'}
       </button>
     </div>
   );

+ 22 - 4
packages/jldbq-extenison/src/wizard/drawer.tsx

@@ -1,4 +1,6 @@
 import React from 'react';
+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<{
@@ -12,7 +14,12 @@ export const TableShemaDrawer: React.FunctionComponent<{
     <div className={'drawer-container ' + (show ? '' : 'drawer-hidden')}>
       <div className="drawer-titlebar">
         <div className="drawer-title">表结构预览</div>
-        <button type="button" className="drawer-close-btn" onClick={onClose} />
+        <button
+          type="button"
+          tabIndex={-1}
+          className="drawer-close-btn"
+          onClick={onClose}
+        />
       </div>
       <div className="drawer-table-header">
         <span className="drawer-table-cell" style={{ width: '20%' }}>
@@ -57,12 +64,23 @@ export const JsonSchemaDrawer: React.FunctionComponent<{
   onClose: () => void;
 }> = ({ jsonSchema, show, onClose }) => {
   return (
-    <div className={'drawer-container ' + (show ? '' : 'drawer-hidden')}>
+    <div
+      className={
+        'drawer-container drawer-dark ' + (show ? '' : 'drawer-hidden')
+      }
+    >
       <div className="drawer-titlebar">
         <div className="drawer-title" />
-        <button type="button" className="drawer-close-btn" onClick={onClose} />
+        <button
+          type="button"
+          tabIndex={-1}
+          className="drawer-close-btn"
+          onClick={onClose}
+        />
       </div>
-      <pre className="json-schema">{JSON.stringify(jsonSchema, null, 2)}</pre>
+      <SyntaxHighlighter showLineNumbers language="json" style={solarizedDark}>
+        {JSON.stringify(jsonSchema, null, 2)}
+      </SyntaxHighlighter>
     </div>
   );
 };

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

@@ -7,4 +7,9 @@
 @import './syncform.css';
 @import './button.css';
 @import './dsform.css';
-@import './wizard.css';
+@import './wizard/base.css';
+@import './wizard/content.css';
+@import './wizard/drawer.css';
+@import './wizard/form.css';
+@import './wizard/header.css';
+@import './wizard/list.css';

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


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

@@ -1,488 +0,0 @@
-.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;
-}

+ 15 - 0
packages/jldbq-extenison/style/wizard/base.css

@@ -0,0 +1,15 @@
+.sync-wizard {
+  min-width: 900px;
+  min-height: 100%;
+  overflow-x: hidden;
+  display: flex;
+  flex-direction: column;
+  padding: 15px 60px;
+  position: relative;
+  font-size: 14px;
+}
+
+.sync-wizard,
+.sync-wizard * {
+  box-sizing: border-box;
+}

+ 43 - 0
packages/jldbq-extenison/style/wizard/content.css

@@ -0,0 +1,43 @@
+.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;
+}
+
+.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: unset;
+}

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

@@ -0,0 +1,108 @@
+.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;
+}
+
+.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: transparent;
+  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;
+}
+
+.sync-wizard .drawer-container.drawer-dark {
+  background-color: #002b36;
+}
+
+.sync-wizard .drawer-dark .drawer-close-btn {
+  background-image: url('../img/collapse-white.png');
+}

+ 121 - 0
packages/jldbq-extenison/style/wizard/form.css

@@ -0,0 +1,121 @@
+.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-input-number {
+  position: relative;
+}
+
+.sync-wizard .form-input-number button {
+  position: absolute;
+  top: 1px;
+  width: 32px;
+  height: 30px;
+  border: none;
+  border-radius: 0;
+  background-color: #efeff1;
+  padding: 0;
+  margin: 0;
+  cursor: pointer;
+}
+
+.sync-wizard .form-input-number button:first-child {
+  left: 1px;
+  border-right: 1px solid #c8d3e9;
+  border-top-left-radius: 2px;
+  border-bottom-left-radius: 2px;
+  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iIzdmN2M3ZiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNNCA4YS41LjUgMCAwIDEgLjUtLjVoN2EuNS41IDAgMCAxIDAgMWgtN0EuNS41IDAgMCAxIDQgOHoiLz48L3N2Zz4');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 20px auto;
+}
+
+.sync-wizard .form-input-number button:last-child {
+  right: 1px;
+  border-left: 1px solid #c8d3e9;
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0iIzdmN2M3ZiIgdmlld0JveD0iMCAwIDE2IDE2Ij48cGF0aCBkPSJNOCA0YS41LjUgMCAwIDEgLjUuNXYzaDNhLjUuNSAwIDAgMSAwIDFoLTN2M2EuNS41IDAgMCAxLTEgMHYtM2gtM2EuNS41IDAgMCAxIDAtMWgzdi0zQS41LjUgMCAwIDEgOCA0eiIvPjwvc3ZnPg==');
+  background-repeat: no-repeat;
+  background-position: left 50% top 50%;
+  background-size: 20px auto;
+}
+
+.sync-wizard .form-input-number .form-input {
+  text-align: center;
+}
+
+.sync-wizard .form-label {
+  color: #4a4a4a;
+  height: 32px;
+  width: 104px;
+  padding-left: 12px;
+  position: relative;
+  display: flex;
+  align-items: center;
+  margin-right: 15px;
+}
+
+.sync-wizard .form-label.field-required::before {
+  content: '*';
+  color: #ed5555;
+  position: absolute;
+  left: 0;
+  top: 0;
+  line-height: 32px;
+}
+
+.sync-wizard .form-group {
+  display: flex;
+  margin-bottom: 25px;
+}
+
+.sync-wizard .form-group:last-child {
+  margin-bottom: 0;
+}
+
+.sync-wizard p.form-hint {
+  color: #bbbbbb;
+  margin: 0;
+  font-size: 12px;
+}

+ 48 - 0
packages/jldbq-extenison/style/wizard/header.css

@@ -0,0 +1,48 @@
+.sync-wizard .step-index-list {
+  display: flex;
+  margin: 0;
+  padding: 0;
+  min-width: 800px;
+  color: #7d7d7d;
+  margin-bottom: 60px;
+}
+
+.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;
+}

+ 208 - 0
packages/jldbq-extenison/style/wizard/list.css

@@ -0,0 +1,208 @@
+.sync-wizard .form-list:not(:last-child) .form-list-body {
+  display: flex;
+}
+
+.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;
+  min-width: 392px;
+}
+
+.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:not(:last-child) {
+  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;
+}
+
+.sync-wizard .form-list-header .form-list-check {
+  border-right: 1px solid #c8d3e9;
+  width: 51px;
+}
+
+.sync-wizard .form-list-checklist {
+  border-right: 1px solid #c8d3e9;
+}
+
+.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-sourcelist .form-list-cell {
+  padding-left: 32px;
+  background-color: #eef5fe;
+  position: relative;
+}
+
+.sync-wizard .form-list-sourcelist .form-list-cell.form-list-cell-empty,
+.sync-wizard .form-list-destlist .form-list-cell.form-list-cell-empty {
+  background-color: transparent;
+}
+
+.sync-wizard
+  .form-list-sourcelist
+  .form-list-cell.form-list-cell-empty::before {
+  background-image: none;
+}
+
+.sync-wizard .form-list-sourcelist .form-list-cell::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;
+}
+
+.sync-wizard .form-link-separator {
+  width: 36px;
+}
+
+.sync-wizard .form-list-hintlist .form-link-separator {
+  height: 100%;
+  margin: 0 -19px;
+}
+
+.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-link-separator.form-link-empty {
+  background-image: none;
+}
+
+.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;
+}

+ 161 - 4
yarn.lock

@@ -1053,7 +1053,7 @@
   dependencies:
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3":
+"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3":
   version "7.18.9"
   resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a"
   integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==
@@ -3747,6 +3747,21 @@
   dependencies:
     "@types/node" "*"
 
+"@types/hast@^2.0.0":
+  version "2.3.4"
+  resolved "https://registry.npmmirror.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc"
+  integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==
+  dependencies:
+    "@types/unist" "*"
+
+"@types/hoist-non-react-statics@^3.3.0":
+  version "3.3.1"
+  resolved "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
+  integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==
+  dependencies:
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+
 "@types/html-minifier-terser@^5.0.0":
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz#693b316ad323ea97eed6b38ed1a3cc02b1672b57"
@@ -3915,6 +3930,13 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-beautiful-dnd@^13.1.2":
+  version "13.1.2"
+  resolved "https://registry.npmmirror.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130"
+  integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react-color@^3.0.1":
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.5.tgz#b8bdf8df7085bd1577658fb37d9a18d7ba3963bb"
@@ -3958,6 +3980,16 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-redux@^7.1.20":
+  version "7.1.24"
+  resolved "https://registry.npmmirror.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
+  integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ==
+  dependencies:
+    "@types/hoist-non-react-statics" "^3.3.0"
+    "@types/react" "*"
+    hoist-non-react-statics "^3.3.0"
+    redux "^4.0.0"
+
 "@types/react-syntax-highlighter@11.0.4":
   version "11.0.4"
   resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd"
@@ -4051,6 +4083,11 @@
   dependencies:
     source-map "^0.6.1"
 
+"@types/unist@*":
+  version "2.0.6"
+  resolved "https://registry.npmmirror.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
+  integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
+
 "@types/url-parse@^1.4.3":
   version "1.4.3"
   resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329"
@@ -6947,6 +6984,13 @@ crypto@~1.0.1:
   resolved "https://registry.yarnpkg.com/crypto/-/crypto-1.0.1.tgz#2af1b7cad8175d24c8a1b0778255794a21803037"
   integrity sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==
 
+css-box-model@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
+  integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
+  dependencies:
+    tiny-invariant "^1.0.6"
+
 css-loader@^3.5.3:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.6.0.tgz#2e4b2c7e6e2d27f8c8f28f61bffcd2e6c91ef645"
@@ -8593,7 +8637,7 @@ fastq@^1.6.0:
   dependencies:
     reusify "^1.0.4"
 
-fault@^1.0.2:
+fault@^1.0.0, fault@^1.0.2:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
   integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
@@ -9474,11 +9518,27 @@ hastscript@^5.0.0:
     property-information "^5.0.0"
     space-separated-tokens "^1.0.0"
 
+hastscript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.npmmirror.com/hastscript/-/hastscript-6.0.0.tgz#e8768d7eac56c3fdeac8a92830d58e811e5bf640"
+  integrity sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==
+  dependencies:
+    "@types/hast" "^2.0.0"
+    comma-separated-tokens "^1.0.0"
+    hast-util-parse-selector "^2.0.0"
+    property-information "^5.0.0"
+    space-separated-tokens "^1.0.0"
+
 he@1.2.x, he@^1.1.0, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
 
+highlight.js@^10.4.1, highlight.js@~10.7.0:
+  version "10.7.3"
+  resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
+  integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
+
 highlight.js@~9.15.0, highlight.js@~9.15.1:
   version "9.15.10"
   resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2"
@@ -9493,7 +9553,7 @@ hmac-drbg@^1.0.1:
     minimalistic-assert "^1.0.0"
     minimalistic-crypto-utils "^1.0.1"
 
-hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
+hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
   integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -11846,6 +11906,14 @@ lowlight@1.12.1:
     fault "^1.0.2"
     highlight.js "~9.15.0"
 
+lowlight@^1.17.0:
+  version "1.20.0"
+  resolved "https://registry.npmmirror.com/lowlight/-/lowlight-1.20.0.tgz#ddb197d33462ad0d93bf19d17b6c301aa3941888"
+  integrity sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==
+  dependencies:
+    fault "^1.0.0"
+    highlight.js "~10.7.0"
+
 lru-cache@6.0.0, lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -12068,7 +12136,7 @@ media-typer@0.3.0:
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
-memoize-one@^5.0.0:
+memoize-one@^5.0.0, memoize-one@^5.1.1:
   version "5.2.1"
   resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
   integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
@@ -13355,6 +13423,18 @@ parse-entities@^1.1.2:
     is-decimal "^1.0.0"
     is-hexadecimal "^1.0.0"
 
+parse-entities@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.npmmirror.com/parse-entities/-/parse-entities-2.0.0.tgz#53c6eb5b9314a1f4ec99fa0fdf7ce01ecda0cbe8"
+  integrity sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==
+  dependencies:
+    character-entities "^1.0.0"
+    character-entities-legacy "^1.0.0"
+    character-reference-invalid "^1.0.0"
+    is-alphanumerical "^1.0.0"
+    is-decimal "^1.0.0"
+    is-hexadecimal "^1.0.0"
+
 parse-json@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
@@ -13827,6 +13907,11 @@ pretty-ms@^5.0.0:
   dependencies:
     parse-ms "^2.1.0"
 
+prismjs@^1.27.0:
+  version "1.28.0"
+  resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.28.0.tgz#0d8f561fa0f7cf6ebca901747828b149147044b6"
+  integrity sha512-8aaXdYvl1F7iC7Xm1spqSaY/OJBpYW3v+KJ+F17iYxvdc8sfjW194COK5wVhMZX45tGteiBQgdvD/nhxcRwylw==
+
 prismjs@^1.8.4, prismjs@~1.17.0:
   version "1.17.1"
   resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.17.1.tgz#e669fcbd4cdd873c35102881c33b14d0d68519be"
@@ -13834,6 +13919,11 @@ prismjs@^1.8.4, prismjs@~1.17.0:
   optionalDependencies:
     clipboard "^2.0.0"
 
+prismjs@~1.27.0:
+  version "1.27.0"
+  resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
+  integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
+
 private@~0.1.5:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -14082,6 +14172,11 @@ quick-lru@^4.0.1:
   resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
   integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
 
+raf-schd@^4.0.2:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
+  integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
+
 ramda@^0.21.0:
   version "0.21.0"
   resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35"
@@ -14534,6 +14629,19 @@ react-base16-styling@^0.8.0:
     csstype "^3.0.2"
     lodash.curry "^4.1.1"
 
+react-beautiful-dnd@^13.1.0:
+  version "13.1.0"
+  resolved "https://registry.npmmirror.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz#ec97c81093593526454b0de69852ae433783844d"
+  integrity sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+    css-box-model "^1.2.0"
+    memoize-one "^5.1.1"
+    raf-schd "^4.0.2"
+    react-redux "^7.2.0"
+    redux "^4.0.4"
+    use-memo-one "^1.1.1"
+
 react-color@^2.17.0:
   version "2.19.3"
   resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
@@ -14745,6 +14853,18 @@ react-popper@^1.3.7:
     typed-styles "^0.0.7"
     warning "^4.0.2"
 
+react-redux@^7.2.0:
+  version "7.2.8"
+  resolved "https://registry.npmmirror.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"
+  integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw==
+  dependencies:
+    "@babel/runtime" "^7.15.4"
+    "@types/react-redux" "^7.1.20"
+    hoist-non-react-statics "^3.3.2"
+    loose-envify "^1.4.0"
+    prop-types "^15.7.2"
+    react-is "^17.0.2"
+
 react-select@^5.4.0:
   version "5.4.0"
   resolved "https://registry.npmmirror.com/react-select/-/react-select-5.4.0.tgz#81f6ac73906126706f104751ee14437bd16798f4"
@@ -14779,6 +14899,17 @@ react-syntax-highlighter@^12.2.1:
     prismjs "^1.8.4"
     refractor "^2.4.1"
 
+react-syntax-highlighter@^15.5.0:
+  version "15.5.0"
+  resolved "https://registry.npmmirror.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20"
+  integrity sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==
+  dependencies:
+    "@babel/runtime" "^7.3.1"
+    highlight.js "^10.4.1"
+    lowlight "^1.17.0"
+    prismjs "^1.27.0"
+    refractor "^3.6.0"
+
 react-textarea-autosize@^8.1.1:
   version "8.3.3"
   resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8"
@@ -15014,6 +15145,13 @@ redent@^3.0.0:
     indent-string "^4.0.0"
     strip-indent "^3.0.0"
 
+redux@^4.0.0, redux@^4.0.4:
+  version "4.2.0"
+  resolved "https://registry.npmmirror.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13"
+  integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==
+  dependencies:
+    "@babel/runtime" "^7.9.2"
+
 refractor@^2.4.1:
   version "2.10.1"
   resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.10.1.tgz#166c32f114ed16fd96190ad21d5193d3afc7d34e"
@@ -15023,6 +15161,15 @@ refractor@^2.4.1:
     parse-entities "^1.1.2"
     prismjs "~1.17.0"
 
+refractor@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.npmmirror.com/refractor/-/refractor-3.6.0.tgz#ac318f5a0715ead790fcfb0c71f4dd83d977935a"
+  integrity sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==
+  dependencies:
+    hastscript "^6.0.0"
+    parse-entities "^2.0.0"
+    prismjs "~1.27.0"
+
 regenerate-unicode-properties@^8.2.0:
   version "8.2.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
@@ -16676,6 +16823,11 @@ tiny-emitter@^2.0.0:
   resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
   integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
 
+tiny-invariant@^1.0.6:
+  version "1.2.0"
+  resolved "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9"
+  integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==
+
 tinycolor2@^1.4.1:
   version "1.4.2"
   resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
@@ -17279,6 +17431,11 @@ use-latest@^1.0.0:
   dependencies:
     use-isomorphic-layout-effect "^1.0.0"
 
+use-memo-one@^1.1.1:
+  version "1.1.2"
+  resolved "https://registry.npmmirror.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20"
+  integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==
+
 use@^3.1.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"