Browse Source

完善添加数据源功能

herj 2 years ago
parent
commit
965630eec7

+ 2 - 2
packages/apputils/src/dialog.tsx

@@ -415,8 +415,6 @@ export class Dialog<T> extends Widget {
       this.dispose();
       return;
     }
-    this._promise = null;
-    ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
     const body = this._body;
     let value: T | null = null;
     if (
@@ -426,6 +424,8 @@ export class Dialog<T> extends Widget {
     ) {
       value = body.getValue();
     }
+    this._promise = null;
+    ArrayExt.removeFirstOf(Private.launchQueue, promise.promise);
     this.dispose();
     promise.resolve({ button, value });
   }

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

@@ -39,7 +39,9 @@
     "@mui/lab": "^5.0.0-alpha.83",
     "@mui/material": "^5.8.1",
     "@silevis/reactgrid": "^4.0.3",
-    "react": "^17.0.1"
+    "react": "^17.0.1",
+    "react-select": "^5.4.0",
+    "swr": "^1.3.0"
   },
   "devDependencies": {
     "rimraf": "~3.0.0",

+ 1 - 1
packages/jldbq-extenison/src/DataTable.tsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import { Cell, Column, Id, ReactGrid, Row } from '@silevis/reactgrid';
 import { useMemo, useState } from 'react';
-import { ITableData } from './model';
+import { ITableData } from './api/datasource';
 
 interface IProps {
   data: ITableData;

+ 11 - 17
packages/jldbq-extenison/src/DataTableWidget.tsx

@@ -2,32 +2,26 @@ import React from 'react';
 import { ReactWidget } from '@jupyterlab/apputils';
 import { Widget } from '@lumino/widgets';
 import DataTable from './DataTable';
-import { ITableData } from './model';
+import { fetchTableContent, IDatasource, ITableData } from './api/datasource';
 
 class DataTableWidget extends ReactWidget {
   constructor(
-    backend: string,
-    dbType: string,
+    datasource: IDatasource,
     tableName: string,
     options?: Widget.IOptions
   ) {
     super(options);
-    const baseUrl = `${backend}/show/${dbType}/results`;
-    void this._requestData(baseUrl, tableName);
+    void this._requestData(datasource, tableName);
   }
 
-  private _requestData = async (baseUrl: string, tableName: string) => {
-    const tableNameUri = encodeURIComponent(tableName);
-    void fetch(`${baseUrl}/item=${tableNameUri}`)
-      .then(res => res.json())
-      .then(data => {
-        this._data = data;
-      })
-      .catch(err => {
-        console.error(err);
-        this._err = err;
-      })
-      .then(() => this.update());
+  private _requestData = async (datasource: IDatasource, tableName: string) => {
+    const res = await fetchTableContent(datasource, tableName);
+    if (res === 'error') {
+      this._err = res;
+    } else {
+      this._data = res;
+    }
+    this.update();
   };
 
   render(): JSX.Element {

+ 11 - 14
packages/jldbq-extenison/src/DataViewWidget.tsx

@@ -1,25 +1,21 @@
 import React from 'react';
 import { ReactWidget } from '@jupyterlab/apputils';
 import { ISignal, Signal } from '@lumino/signaling';
-import { Widget } from '@lumino/widgets';
 import DatasourceView from './DatasourceView';
 import DatasyncView from './DatasyncView';
 import DataView from './DataView';
+import { IDatasource } from './api/datasource';
+import { Widget } from '@lumino/widgets';
 
 interface ITableOpenSignalData {
-  dbType: string;
+  datasource: IDatasource;
   tableName: string;
 }
 
 class DataViewWidget extends ReactWidget {
-  private _dataId = (+new Date()).toString();
-  private _dbType = 'hive';
-  private _tableOpened = new Signal<DataViewWidget, ITableOpenSignalData>(this);
-  private _backend: string;
-
-  constructor(backend: string, options?: Widget.IOptions) {
+  constructor(manager: string, options?: Widget.IOptions) {
     super(options);
-    this._backend = backend;
+    this._manager = manager;
   }
 
   public get tableOpened(): ISignal<DataViewWidget, ITableOpenSignalData> {
@@ -29,11 +25,9 @@ class DataViewWidget extends ReactWidget {
   render(): JSX.Element {
     const datasourceView = (
       <DatasourceView
-        baseUrl={`${this._backend}/show/${this._dbType}`}
-        dataId={this._dataId}
-        dbType={this._dbType}
-        onOpenTable={tableName =>
-          this._tableOpened.emit({ dbType: this._dbType, tableName })
+        manager={this._manager}
+        onOpenTable={(datasource, tableName) =>
+          this._tableOpened.emit({ datasource, tableName })
         }
       />
     );
@@ -55,6 +49,9 @@ class DataViewWidget extends ReactWidget {
       </div>
     );
   }
+
+  private _manager: string;
+  private _tableOpened = new Signal<DataViewWidget, ITableOpenSignalData>(this);
 }
 
 export default DataViewWidget;

+ 63 - 74
packages/jldbq-extenison/src/DatasourceView.tsx

@@ -1,98 +1,76 @@
 import React from 'react';
-import { useEffect, useState } from 'react';
-import { showDialog } from '@jupyterlab/apputils';
+import { useState } from 'react';
+import { Dialog, showDialog } from '@jupyterlab/apputils';
 import TreeView from '@mui/lab/TreeView';
 import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
 import ChevronRightIcon from '@mui/icons-material/ChevronRight';
 import TreeItem from '@mui/lab/TreeItem';
 import databaseSvgStr from '../style/icons/database-solid-ghost.svg';
+import {
+  fetchTables,
+  IDatasource,
+  setDatasource,
+  useDatasourceList
+} from './api/datasource';
+import { DatasourceFormDialogBody } from './components/DatasourceForm';
 
 const dragImg = new Image();
 dragImg.src = 'data:image/svg+xml;base64,' + btoa(databaseSvgStr);
 
 interface IProps {
-  baseUrl: string;
-  dataId: string;
-  dbType: string;
-  onOpenTable: (tableName: string) => void;
+  manager: string;
+  onOpenTable: (datasource: IDatasource, tableName: string) => void;
 }
 
-const DatasourceView = ({
-  baseUrl,
-  dataId,
-  dbType,
+const DatasourceView: React.FunctionComponent<IProps> = ({
+  manager,
   onOpenTable
-}: IProps): JSX.Element => {
-  const [dbs, setDbs] = useState<string[] | null>(null);
-  const [tables, setTables] = useState<Record<string, string[]>>({});
-  const [error, setError] = useState<any>(null);
+}) => {
+  const { datasources, loading, error, refresh } = useDatasourceList(manager);
+  const [tables, setTables] = useState<
+    Partial<{
+      [key: string]: string[] | 'error';
+    }>
+  >({});
 
-  useEffect(() => {
-    setDbs(null);
-    setTables({});
-    setError(null);
-
-    let abort = false;
-    fetch(`${baseUrl}/databases`)
-      .then(res => res.json())
-      .then(data => {
-        if (!abort) {
-          setDbs(data);
-        }
-      })
-      .catch(err => {
-        if (!abort) {
-          console.error(err);
-          setError(err);
-        }
+  const handleNodeToggle = async (ds: IDatasource) => {
+    if (!tables[ds.sourcename]) {
+      const tables = await fetchTables(ds);
+      setTables(prev => {
+        return { [ds.sourcename]: tables, ...prev };
       });
-
-    return () => {
-      abort = true;
-    };
-  }, [baseUrl, dataId]);
-
-  const handleNodeToggle = (db: string) => {
-    if (!tables[db]) {
-      const dbUri = encodeURIComponent(db);
-      fetch(`${baseUrl}/tables/item=${dbUri}`)
-        .then(res => res.json())
-        .then(data => {
-          setTables(prev => {
-            return { [db]: data, ...prev };
-          });
-        })
-        .catch(err => console.error(err));
     }
   };
 
   let tree: JSX.Element[] | JSX.Element;
   if (error) {
     tree = <div style={{ padding: '0px 12px' }}>Error</div>;
-  } else if (!dbs) {
+  } else if (loading) {
     tree = <div style={{ padding: '0px 12px' }}>Loading ...</div>;
   } else {
-    tree = dbs.map(db => {
+    tree = datasources!.map(ds => {
+      const table = tables[ds.sourcename];
       let tableItems;
-      if (!tables[db]) {
+      if (!table) {
+        tableItems = (
+          <TreeItem nodeId={`${ds.sourcename}-loading`} label="Loading ..." />
+        );
+      } else if (table === 'error') {
         tableItems = (
-          <TreeItem
-            nodeId={`${dataId}-${dbType}-${db}-loading`}
-            label="Loading ..."
-          />
+          <TreeItem nodeId={`${ds.sourcename}-error`} label="Error" />
         );
-      } else if (tables[db].length === 0) {
+      } else if (table.length === 0) {
         tableItems = (
-          <TreeItem nodeId={`${dataId}-${dbType}-${db}-empty`} label="Empty" />
+          <TreeItem nodeId={`${ds.sourcename}-error`} label="Empty" />
         );
       } else {
-        tableItems = tables[db].map(t => {
+        tableItems = table.map(t => {
           return (
             <TreeItem
-              key={`${dataId}-${dbType}-${db}-${t}`}
-              nodeId={`${dataId}-${dbType}-${db}-${t}`}
+              key={`${ds.sourcename}-${t}`}
+              nodeId={`${ds.sourcename}-${t}`}
               label={t}
-              onDoubleClick={() => onOpenTable(`${db}-${t}`)}
+              onDoubleClick={() => onOpenTable(ds, t)}
               draggable="true"
               onDragStart={evt => {
                 const data = {
@@ -101,8 +79,8 @@ const DatasourceView = ({
                     editType: 'createNode',
                     finalized: true,
                     nodeTemplate: {
-                      id: `${dataId}-${dbType}-${db}-${t}`,
-                      op: `database-${dbType}-input`,
+                      id: `${ds.manager}-${ds.sourcename}-${t}`,
+                      op: `database-${ds.source}-input`,
                       description: t,
                       type: 'execution_node',
                       label: t,
@@ -142,10 +120,10 @@ const DatasourceView = ({
       }
       return (
         <TreeItem
-          key={`${dataId}-${dbType}-${db}`}
-          nodeId={`${dataId}-${dbType}-${db}`}
-          label={db}
-          onClick={() => handleNodeToggle(db)}
+          key={ds.sourcename}
+          nodeId={ds.sourcename}
+          label={ds.databasename}
+          onClick={() => handleNodeToggle(ds)}
         >
           {tableItems}
         </TreeItem>
@@ -153,17 +131,28 @@ const DatasourceView = ({
     });
   }
 
+  const handleAddDatasource = async () => {
+    const { value } = await showDialog({
+      title: '添加数据源',
+      body: new DatasourceFormDialogBody(manager),
+      buttons: [
+        Dialog.okButton({ label: '确定' }),
+        Dialog.cancelButton({ label: '取消' })
+      ]
+    });
+    if (!value) {
+      return;
+    }
+    await setDatasource(value);
+    refresh();
+  };
+
   return (
     <div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
       <div
         style={{ display: 'flex', justifyContent: 'flex-end', padding: '10px' }}
       >
-        <button
-          className="jldbq-btn-add"
-          onClick={async () => {
-            await showDialog();
-          }}
-        >
+        <button className="jldbq-btn-add" onClick={handleAddDatasource}>
           <svg
             xmlns="http://www.w3.org/2000/svg"
             width="20"

+ 30 - 1
packages/jldbq-extenison/src/DatasyncView.tsx

@@ -1,7 +1,36 @@
 import React from 'react';
+import { showDialog } from '@jupyterlab/apputils';
+import DatasyncForm from './components/DatasyncForm';
 
 const DatasyncView: React.FunctionComponent = () => {
-  return <div />;
+  return (
+    <div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column' }}>
+      <div
+        style={{ display: 'flex', justifyContent: 'flex-end', padding: '10px' }}
+      >
+        <button
+          className="jldbq-btn-add"
+          onClick={async () => {
+            await showDialog({
+              title: '添加同步任务',
+              body: <DatasyncForm />
+            });
+          }}
+        >
+          <svg
+            xmlns="http://www.w3.org/2000/svg"
+            width="20"
+            height="20"
+            fill="currentColor"
+            viewBox="0 0 16 16"
+          >
+            <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
+          </svg>
+          添加同步任务
+        </button>
+      </div>
+    </div>
+  );
 };
 
 export default DatasyncView;

+ 5 - 0
packages/jldbq-extenison/src/api/config.ts

@@ -0,0 +1,5 @@
+const config = {
+  endpoint: 'http://192.168.51.19:5000'
+};
+
+export default config;

+ 103 - 0
packages/jldbq-extenison/src/api/datasource.ts

@@ -0,0 +1,103 @@
+import useSWR from 'swr';
+import config from './config';
+
+export interface IDatasource {
+  host: string;
+  user: string;
+  port: number;
+  password: string;
+  charset: 'utf8';
+  manager: string;
+  source: 'mysql' | 'hive';
+  databasename: string;
+  sourcename: string;
+}
+
+interface IDatasourceResponse {
+  datasources?: IDatasource[];
+  loading: boolean;
+  error?: any;
+  refresh: () => void;
+}
+
+export type ITableData = Record<string, Record<string, any>>;
+
+export const useDatasourceList = (manager: string): IDatasourceResponse => {
+  const endpoint = config.endpoint;
+  const item = encodeURIComponent(manager);
+  const url = `${endpoint}/managelist/item=${item}`;
+  const { data, error, mutate } = useSWR(url, (...args) =>
+    fetch(...args).then(res => res.json())
+  );
+  return {
+    datasources: data,
+    loading: !error && !data,
+    error,
+    refresh: mutate
+  };
+};
+
+export const setDatasource = async (
+  datasource: IDatasource
+): Promise<string> => {
+  const endpoint = config.endpoint;
+  const url = `${endpoint}/manage`;
+  try {
+    const resp = await fetch(url, {
+      method: 'post',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(datasource)
+    });
+    if (!resp.ok) {
+      return 'error';
+    }
+    const data = await resp.text();
+    return data;
+  } catch (e) {
+    console.error(e);
+    return 'error';
+  }
+};
+
+export const fetchTables = async (
+  ds: IDatasource
+): Promise<'error' | string[]> => {
+  const endpoint = config.endpoint;
+  const item = encodeURIComponent(
+    `${ds.manager}-${ds.sourcename}-${ds.databasename}`
+  );
+  const url = `${endpoint}/show/${ds.source}/tables/item=${item}`;
+  try {
+    const resp = await fetch(url);
+    if (!resp.ok) {
+      return 'error';
+    }
+    const data = (await resp.json()) as string[];
+    return data;
+  } catch (e) {
+    console.error(e);
+    return 'error';
+  }
+};
+
+export const fetchTableContent = async (
+  ds: IDatasource,
+  t: string
+): Promise<ITableData | 'error'> => {
+  const endpoint = config.endpoint;
+  const item = encodeURIComponent(
+    `${ds.manager}-${ds.sourcename}-${ds.databasename}-${t}`
+  );
+  const url = `${endpoint}/show/${ds.source}/results/item=${item}`;
+  try {
+    const resp = await fetch(url);
+    if (!resp.ok) {
+      return 'error';
+    }
+    const data = (await resp.json()) as ITableData;
+    return data;
+  } catch (e) {
+    console.error(e);
+    return 'error';
+  }
+};

+ 0 - 0
packages/jldbq-extenison/src/api/index.ts


+ 177 - 13
packages/jldbq-extenison/src/components/DatasourceForm.tsx

@@ -1,24 +1,188 @@
 import React from 'react';
+import { Dialog, ReactWidget } from '@jupyterlab/apputils';
+import { IDatasource } from '../api/datasource';
+import { Widget } from '@lumino/widgets';
 
-export const DatasourceForm: React.FunctionComponent = () => {
+interface IDatasourceItem {
+  address: string;
+  username: string;
+  password: string;
+  databasename: string;
+  type: 'mysql' | 'hive';
+}
+
+interface IProps {
+  value: Partial<IDatasourceItem>;
+  error: keyof IDatasourceItem | null;
+  onChange: (v: Partial<IDatasourceItem>) => void;
+}
+
+const DatasourceForm: React.FunctionComponent<IProps> = ({
+  value,
+  error,
+  onChange
+}) => {
   return (
     <form className="jldbq-form">
-      <label>
-        <span>数据源:</span>
-        <input placeholder="请输入数据源地址" />
+      <label className={error === 'type' ? 'jldbq-form-error' : ''}>
+        <span className="jldbq-form-label">选择数据库类型:</span>
+        <div
+          style={{
+            marginLeft: '15px',
+            display: 'flex',
+            justifyContent: 'space-around',
+            alignItems: 'center'
+          }}
+        >
+          <button
+            type="button"
+            className={
+              'jldbq-btn ' + (value.type === 'mysql' ? 'jldbq-btn-current' : '')
+            }
+            onClick={() => onChange({ type: 'mysql' })}
+          >
+            MySQL
+          </button>
+          <button
+            type="button"
+            className={
+              'jldbq-btn ' + (value.type === 'hive' ? 'jldbq-btn-current' : '')
+            }
+            onClick={() => onChange({ type: 'hive' })}
+          >
+            Hive
+          </button>
+        </div>
+      </label>
+      <label className={error === 'address' ? 'jldbq-form-error' : ''}>
+        <span className="jldbq-form-label">数据源地址:</span>
+        <input
+          placeholder="请输入数据源地址"
+          value={value.address || ''}
+          onChange={evt => onChange({ address: evt.target.value })}
+        />
       </label>
-      <label>
-        <span>用户名:</span>
-        <input placeholder="请输入用户名" />
+      <label className={error === 'username' ? 'jldbq-form-error' : ''}>
+        <span className="jldbq-form-label">用户名:</span>
+        <input
+          placeholder="请输入用户名"
+          value={value.username || ''}
+          onChange={evt => onChange({ username: evt.target.value })}
+        />
       </label>
-      <label>
-        <span>密码:</span>
-        <input type="password" placeholder="请输入密码" />
+      <label className={error === 'password' ? 'jldbq-form-error' : ''}>
+        <span className="jldbq-form-label">密码:</span>
+        <input
+          type="password"
+          placeholder="请输入密码"
+          value={value.password || ''}
+          onChange={evt => onChange({ password: evt.target.value })}
+        />
       </label>
-      <label>
-        <span>Kerberos:</span>
-        <input placeholder="请输入Kerberos验证" />
+      <label className={error === 'databasename' ? 'jldbq-form-error' : ''}>
+        <span className="jldbq-form-label">数据库:</span>
+        <input
+          placeholder="请输入数据库名称"
+          value={value.databasename || ''}
+          onChange={evt => onChange({ databasename: evt.target.value })}
+        />
       </label>
     </form>
   );
 };
+
+export class DatasourceFormDialogBody
+  extends ReactWidget
+  implements Dialog.IBodyWidget<IDatasource> {
+  constructor(manager: string, options?: Widget.IOptions) {
+    super(options);
+    this._manager = manager;
+  }
+  render(): JSX.Element {
+    return (
+      <DatasourceForm
+        value={this._datasourceItem}
+        error={this._error}
+        onChange={v => {
+          this._datasourceItem = { ...this._datasourceItem, ...v };
+          this._error = null;
+          this.update();
+        }}
+      />
+    );
+  }
+
+  getValue(): IDatasource {
+    const {
+      address,
+      username: user,
+      password,
+      databasename,
+      type: source
+    } = this._datasourceItem;
+    if (!source) {
+      this._setError('type');
+    }
+    if (!address || !validateIpAndPort(address)) {
+      this._setError('address');
+    }
+    const [host, portStr] = address.split(':');
+    const port = portStr ? +portStr : 3306;
+    if (!user) {
+      this._setError('username');
+    }
+    if (!password) {
+      this._setError('password');
+    }
+    if (!databasename) {
+      this._setError('databasename');
+    }
+    return {
+      host,
+      user,
+      port,
+      password,
+      charset: 'utf8',
+      source,
+      databasename,
+      sourcename: databasename,
+      manager: this._manager
+    };
+  }
+
+  private _setError(error: keyof IDatasourceItem): never {
+    this._error = error;
+    this.update();
+    throw new Error(error);
+  }
+
+  private _datasourceItem: Partial<IDatasourceItem> = {};
+  private _error: IProps['error'] = null;
+  private _manager: string;
+}
+
+function validateIpAndPort(input: string): boolean {
+  const parts = input.split(':');
+  if (parts.length > 2) {
+    return false;
+  }
+  const ip = parts[0].split('.');
+  if (
+    ip.length !== 4 ||
+    !ip.every(function (segment) {
+      return validateNum(segment, 0, 255);
+    })
+  ) {
+    return false;
+  }
+  const port = parts[1];
+  if (port && !validateNum(port, 1, 65535)) {
+    return false;
+  }
+  return true;
+}
+
+function validateNum(input: string, min: number, max: number): boolean {
+  const num = +input;
+  return num >= min && num <= max && input === num.toString();
+}

+ 67 - 0
packages/jldbq-extenison/src/components/DatasyncForm.tsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import Select, {
+  components,
+  DropdownIndicatorProps,
+  NoticeProps
+} from 'react-select';
+
+const DropdownIndicator: React.FunctionComponent<DropdownIndicatorProps> = props => {
+  return (
+    <components.DropdownIndicator {...props}>
+      <svg
+        xmlns="http://www.w3.org/2000/svg"
+        width="16"
+        height="16"
+        fill="currentColor"
+        className="bi bi-caret-down-fill"
+        viewBox="0 0 16 16"
+      >
+        <path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" />
+      </svg>
+    </components.DropdownIndicator>
+  );
+};
+
+const NoOptionsMessage: React.FunctionComponent<NoticeProps> = props => {
+  return (
+    <components.NoOptionsMessage {...props}>
+      没有数据
+    </components.NoOptionsMessage>
+  );
+};
+
+const DatasyncForm: React.FunctionComponent = () => {
+  return (
+    <form className="jldbq-form">
+      <label>
+        <span className="jldbq-form-label">数据源:</span>
+        <Select
+          className="react-select-container"
+          classNamePrefix="react-select"
+          placeholder="选择数据源"
+          components={{ DropdownIndicator, NoOptionsMessage }}
+          options={[{ value: 'a', label: 'a' }]}
+        />
+      </label>
+      <label>
+        <span className="jldbq-form-label">同步对象:</span>
+        <Select
+          className="react-select-container"
+          classNamePrefix="react-select"
+          placeholder="选择同步对象"
+          components={{ DropdownIndicator, NoOptionsMessage }}
+        />
+      </label>
+      <label>
+        <span className="jldbq-form-label">并发数:</span>
+        <input
+          className="jldbq-form-input"
+          type="number"
+          placeholder="请输入并发数"
+        />
+      </label>
+    </form>
+  );
+};
+
+export default DatasyncForm;

+ 18 - 16
packages/jldbq-extenison/src/index.ts

@@ -40,24 +40,26 @@ const plugin: JupyterFrontEndPlugin<void> = {
   autoStart: true,
   requires: [ISettingRegistry],
   activate: async (app: JupyterFrontEnd, registry: ISettingRegistry) => {
-    let backend: string;
-    try {
-      const settings = await registry.load(
-        '@jupyterlab/jldbq-extension:plugin'
-      );
-      backend = settings.composite['flaskBackend'] as string;
-    } catch (err) {
-      console.log(err);
-      backend = 'http://localhost:5000';
-    }
+    // let backend: string;
+    // try {
+    //   const settings = await registry.load(
+    //     '@jupyterlab/jldbq-extension:plugin'
+    //   );
+    //   backend = settings.composite['flaskBackend'] as string;
+    // } catch (err) {
+    //   console.log(err);
+    //   backend = 'http://localhost:5000';
+    // }
 
-    const dataViewWidget = new DataViewWidget(backend);
+    const manager = 'testlocal';
+
+    const dataViewWidget = new DataViewWidget(manager);
     dataViewWidget.id = 'DatabaseView';
     dataViewWidget.title.icon = cliIcon;
-    dataViewWidget.title.caption = 'DatabaseView';
+    dataViewWidget.title.caption = '数据开发';
     dataViewWidget.title.label = '数据开发';
-    dataViewWidget.tableOpened.connect((_sender, { dbType, tableName }) => {
-      const content = new DataTableWidget(backend, dbType, tableName);
+    dataViewWidget.tableOpened.connect((_sender, { datasource, tableName }) => {
+      const content = new DataTableWidget(datasource, tableName);
       const widget = new MainAreaWidget({ content });
       widget.title.label = tableName;
       widget.title.icon = cliIcon;
@@ -68,14 +70,14 @@ const plugin: JupyterFrontEndPlugin<void> = {
     const taskViewWidget = new TaskViewWidget();
     taskViewWidget.id = 'jldbq-sync';
     taskViewWidget.title.icon = timerIcon;
-    taskViewWidget.title.caption = 'Sync';
+    taskViewWidget.title.caption = '定时任务';
     taskViewWidget.title.label = '定时任务';
     app.shell.add(taskViewWidget, 'left', { rank: 1000 });
 
     const monitorViewWidget = new MonitorViewWidget();
     monitorViewWidget.id = 'jldbq-monitor';
     monitorViewWidget.title.icon = monitorIcon;
-    monitorViewWidget.title.caption = 'Monitor';
+    monitorViewWidget.title.caption = '调度监控';
     monitorViewWidget.title.label = '调度监控';
     app.shell.add(monitorViewWidget, 'left', { rank: 1100 });
   }

+ 0 - 1
packages/jldbq-extenison/src/model.ts

@@ -1 +0,0 @@
-export type ITableData = Record<string, Record<string, any>>;

+ 82 - 17
packages/jldbq-extenison/style/base.css

@@ -22,11 +22,24 @@
   opacity: 0.8;
 }
 
+.jldbq-btn-add {
+  display: flex;
+  align-items: center;
+  padding: 0;
+  background: none;
+  border: none;
+  color: #147bd1;
+  cursor: pointer;
+}
+
+.jldbq-btn-add:hover {
+  opacity: 0.8;
+}
+
 .jldbq-form {
   display: flex;
   flex-direction: column;
   padding: 40px;
-  align-items: flex-end;
 }
 
 .jldbq-form label {
@@ -35,37 +48,89 @@
   align-items: center;
 }
 
-.jldbq-form span {
-  color: #4a4a4a;
-}
-
 .jldbq-form label:last-child {
   margin-bottom: 0;
 }
 
-.jldbq-form input {
+.jldbq-form label.jldbq-form-error:last-child {
+  border: 1px solid #ed5555;
+}
+
+.jldbq-form .jldbq-form-label {
+  color: #4a4a4a;
+  width: 75px;
+  text-align: end;
+}
+
+.jldbq-form .jldbq-form-input {
+  font-family: inherit;
+  box-sizing: border-box;
   width: 320px;
   height: 32px;
   border-radius: 2px;
   border: 1px solid #c8d3e9;
   margin-left: 15px;
   padding: 0 15px;
+  font-size: 12px;
+  transition: all 100ms;
+  outline: 1px solid transparent;
 }
 
-.jldbq-form input:focus {
-  outline: 2px solid #147bd1;
+.jldbq-form .jldbq-form-input:focus {
+  outline-color: #147bd1;
 }
 
-.jldbq-btn-add {
-  display: flex;
-  align-items: center;
+.jldbq-form .react-select-container {
+  width: 320px;
+  margin-left: 15px;
+}
+
+.jldbq-form .react-select__control {
+  border-radius: 2px;
+  border: 1px solid #c8d3e9;
+  min-height: 32px;
+}
+
+.jldbq-form .react-select__control:hover {
+  border-color: #c8d3e9;
+}
+
+.jldbq-form .react-select__indicator-separator {
+  background-color: #c8d3e9;
+  margin: 4px 0;
+}
+
+.jldbq-form .react-select__indicator {
+  color: #4883fb;
+  padding: 0 8px;
+}
+
+.jldbq-form .react-select__value-container {
+  padding: 0 15px;
+  font-size: 12px;
+}
+
+.jldbq-form .react-select__single-value {
   padding: 0;
-  background: none;
-  border: none;
-  color: #147bd1;
-  cursor: pointer;
+  margin: 0;
 }
 
-.jldbq-btn-add:hover {
-  opacity: 0.8;
+.jldbq-form .react-select__placeholder {
+  padding: 0;
+  margin: 0;
+}
+
+.jldbq-form .react-select__input-container {
+  padding: 0;
+  margin: 0;
+}
+
+.jldbq-form .react-select__menu-notice {
+  font-size: 12px;
+}
+
+.jldbq-form .react-select__option {
+  font-size: 12px;
+  padding-left: 16px;
+  padding-right: 16px;
 }

+ 1 - 1
packages/jldbq-extenison/tsconfig.json

@@ -4,7 +4,7 @@
     "outDir": "lib",
     "rootDir": "src"
   },
-  "include": ["src/*"],
+  "include": ["src/**/*"],
   "references": [
     {
       "path": "../application"

+ 5 - 0
yarn.lock

@@ -15915,6 +15915,11 @@ svgo@^1.2.2, svgo@^1.3.2:
     unquote "~1.1.1"
     util.promisify "~1.0.0"
 
+swr@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.npmmirror.com/swr/-/swr-1.3.0.tgz#c6531866a35b4db37b38b72c45a63171faf9f4e8"
+  integrity sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==
+
 symbol-tree@^3.2.2, symbol-tree@^3.2.4:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"