Forráskód Böngészése

feat: 数据源管理

leo 2 éve
szülő
commit
f125a4b53d

+ 1 - 1
packages/jldbq-extenison/schema/plugin.json

@@ -7,7 +7,7 @@
       "title": "Flask Backend address.",
       "description": "Flask Backend address.",
       "type": "string",
-      "default": "http://127.0.0.1:5000"
+      "default": "http://192.168.199.80:18082"
     }
   },
   "additionalProperties": false

+ 18 - 5
packages/jldbq-extenison/src/DataTableWidget.tsx

@@ -2,20 +2,33 @@ import React from 'react';
 import { ReactWidget } from '@jupyterlab/apputils';
 import { Widget } from '@lumino/widgets';
 import DataTable from './DataTable';
-import { fetchTableContent, IDatasource, ITableData } from './api/datasource';
+import {
+  fetchTableContent,
+  fetchTableSchema,
+  IDatasourceItem,
+  ITableData
+} from './api/datasource';
 
 class DataTableWidget extends ReactWidget {
   constructor(
-    datasource: IDatasource,
+    datasource: IDatasourceItem,
     tableName: string,
+    action: 'schema' | 'content',
     options?: Widget.IOptions
   ) {
     super(options);
-    void this._requestData(datasource, tableName);
+    void this._requestData(datasource, tableName, action);
   }
 
-  private _requestData = async (datasource: IDatasource, tableName: string) => {
-    const res = await fetchTableContent(datasource, tableName);
+  private _requestData = async (
+    datasource: IDatasourceItem,
+    tableName: string,
+    action: 'schema' | 'content'
+  ) => {
+    const res =
+      action === 'content'
+        ? await fetchTableContent(datasource, tableName)
+        : await fetchTableSchema(datasource, tableName);
     if (res === 'error') {
       this._err = res;
     } else {

+ 5 - 4
packages/jldbq-extenison/src/DataViewWidget.tsx

@@ -5,12 +5,13 @@ import DatasourceView from './DatasourceView';
 import DatasyncView from './DatasyncView';
 import DatalogView from './DatalogView';
 import DataView from './DataView';
-import { IDatasource } from './api/datasource';
+import { IDatasourceItem } from './api/datasource';
 import { Widget } from '@lumino/widgets';
 
 interface ITableOpenSignalData {
-  datasource: IDatasource;
+  datasource: IDatasourceItem;
   tableName: string;
+  action: 'schema' | 'content';
 }
 
 class DataViewWidget extends ReactWidget {
@@ -27,8 +28,8 @@ class DataViewWidget extends ReactWidget {
     const datasourceView = (
       <DatasourceView
         manager={this._manager}
-        onOpenTable={(datasource, tableName) =>
-          this._tableOpened.emit({ datasource, tableName })
+        onOpenTable={(datasource, tableName, action) =>
+          this._tableOpened.emit({ datasource, tableName, action })
         }
       />
     );

+ 37 - 32
packages/jldbq-extenison/src/DatasourceForm.tsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import { Dialog, ReactWidget } from '@jupyterlab/apputils';
-import { IDatasource } from './api/datasource';
+import { IDatasourceForm, textDatasource } from './api/datasource';
 import { Widget } from '@lumino/widgets';
 import { Input, Select } from 'antd';
 
@@ -40,10 +40,10 @@ const DatasourceForm: React.FunctionComponent<IProps> = ({
         </Select>
       </label>
       <label className={error === 'datasourcename' ? 'jldbq-form-error' : ''}>
-        <span className="jldbq-form-label">名称:</span>
+        <span className="jldbq-form-label">数据源名称:</span>
         <Input
           className="jldbq-form-input"
-          placeholder="请输入名称"
+          placeholder="请输入数据源名称"
           value={value.datasourcename || ''}
           onChange={evt => onChange({ datasourcename: evt.target.value })}
         />
@@ -94,10 +94,10 @@ const DatasourceForm: React.FunctionComponent<IProps> = ({
 
 export class DatasourceFormDialogBody
   extends ReactWidget
-  implements Dialog.IBodyWidget<IDatasource> {
+  implements Dialog.IBodyWidget<IDatasourceForm> {
   constructor(manager: string, options?: Widget.IOptions) {
     super(options);
-    this._manager = manager;
+    // this._manager = manager;
   }
   render(): JSX.Element {
     return (
@@ -109,51 +109,56 @@ export class DatasourceFormDialogBody
           this._error = null;
           this.update();
         }}
-        onConnection={v => console.log('val', v)}
+        onConnection={async v => {
+          const params = {
+            jdbc_url: v.address,
+            jdbc_username: v.username,
+            jdbc_password: v.password,
+            database_name: v.databasename,
+            datasource_name: v.datasourcename,
+            datasource: v.type
+          } as IDatasourceForm;
+          await textDatasource(params);
+        }}
       />
     );
   }
 
-  getValue(): IDatasource {
+  getValue(): IDatasourceForm {
     const {
-      address,
-      username: user,
-      password,
-      databasename,
-      datasourcename,
-      type: source
+      address: jdbc_url,
+      username: jdbc_username,
+      password: jdbc_password,
+      databasename: database_name,
+      datasourcename: datasource_name,
+      type: datasource
     } = this._datasourceItem;
-    if (!source) {
+    if (!datasource) {
       this._setError('type');
     }
-    if (!address || !validateIpAndPort(address)) {
+    if (!jdbc_url || !validateIpAndPort(jdbc_url)) {
       this._setError('address');
     }
-    const [host, portStr] = address.split(':');
-    const port = portStr ? +portStr : 3306;
-    if (!user) {
+    if (!jdbc_username) {
       this._setError('username');
     }
-    if (!password) {
+    if (!jdbc_password) {
       this._setError('password');
     }
-    if (!databasename && source === 'mysql') {
+    if (!database_name && datasource === 'mysql') {
       this._setError('databasename');
     }
-    if (!datasourcename) {
+    if (!datasource_name) {
       this._setError('datasourcename');
     }
     return {
-      host,
-      user,
-      port,
-      password,
-      charset: 'utf8',
-      source,
-      databasename: databasename as string,
-      datasourcename,
-      sourcename: databasename as string,
-      manager: this._manager
+      jdbc_url,
+      jdbc_username,
+      jdbc_password,
+      database_name: database_name as any,
+      datasource_name,
+      datasource
+      // manager: this._manager
     };
   }
 
@@ -165,7 +170,7 @@ export class DatasourceFormDialogBody
 
   private _datasourceItem: Partial<IDatasourceItem> = {};
   private _error: IProps['error'] = null;
-  private _manager: string;
+  // private _manager: string;
 }
 
 function validateIpAndPort(input: string): boolean {

+ 88 - 23
packages/jldbq-extenison/src/DatasourceView.tsx

@@ -5,10 +5,13 @@ 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 Menu from '@mui/material/Menu';
+import MenuItem from '@mui/material/MenuItem';
 import databaseSvgStr from '../style/icons/database-solid-ghost.svg';
 import {
   fetchTables,
-  IDatasource,
+  // IDatasource,
+  IDatasourceItem,
   setDatasource,
   useDatasourceList
 } from './api/datasource';
@@ -19,7 +22,11 @@ dragImg.src = 'data:image/svg+xml;base64,' + btoa(databaseSvgStr);
 
 interface IProps {
   manager: string;
-  onOpenTable: (datasource: IDatasource, tableName: string) => void;
+  onOpenTable: (
+    datasource: IDatasourceItem,
+    tableName: string,
+    action: 'schema' | 'content'
+  ) => void;
 }
 
 const DatasourceView: React.FunctionComponent<IProps> = ({
@@ -33,11 +40,71 @@ const DatasourceView: React.FunctionComponent<IProps> = ({
     }>
   >({});
 
-  const handleNodeToggle = async (ds: IDatasource) => {
-    if (!tables[ds.sourcename]) {
-      const tables = await fetchTables(ds);
+  const [contextMenu, setContextMenu] = React.useState<{
+    mouseX: number;
+    mouseY: number;
+  } | null>(null);
+
+  const [contextData, setContextData] = React.useState<{
+    datasource: IDatasourceItem;
+    tableName: string;
+  }>({} as any);
+
+  const handleContextMenu = (
+    event: React.MouseEvent,
+    datasource: IDatasourceItem,
+    tableName: string
+  ) => {
+    event.preventDefault();
+    setContextMenu(
+      contextMenu === null
+        ? {
+            mouseX: event.clientX + 2,
+            mouseY: event.clientY - 6
+          }
+        : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
+          // Other native context menus might behave different.
+          // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
+          null
+    );
+    setContextData({ datasource, tableName });
+  };
+
+  const handleClose = () => {
+    setContextMenu(null);
+  };
+
+  const handleContent = () => {
+    setContextMenu(null);
+    onOpenTable(contextData.datasource, contextData.tableName, 'content');
+  };
+
+  const handleSchema = () => {
+    setContextMenu(null);
+    onOpenTable(contextData.datasource, contextData.tableName, 'schema');
+  };
+
+  const contextMenuView = (
+    <Menu
+      open={contextMenu !== null}
+      onClose={handleClose}
+      anchorReference="anchorPosition"
+      anchorPosition={
+        contextMenu !== null
+          ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
+          : undefined
+      }
+    >
+      <MenuItem onClick={handleContent}>预览表内容</MenuItem>
+      <MenuItem onClick={handleSchema}>预览表结构</MenuItem>
+    </Menu>
+  );
+
+  const handleNodeToggle = async (id: number) => {
+    if (!tables[id]) {
+      const tables = await fetchTables(id);
       setTables(prev => {
-        return { [ds.sourcename]: tables, ...prev };
+        return { [id]: tables, ...prev };
       });
     }
   };
@@ -49,29 +116,26 @@ const DatasourceView: React.FunctionComponent<IProps> = ({
     tree = <div style={{ padding: '0px 12px' }}>Loading ...</div>;
   } else {
     tree = datasources!.map(ds => {
-      const table = tables[ds.sourcename];
+      const table = tables[ds.id];
       let tableItems;
       if (!table) {
         tableItems = (
-          <TreeItem nodeId={`${ds.sourcename}-loading`} label="Loading ..." />
+          <TreeItem nodeId={`${ds.id}-loading`} label="Loading ..." />
         );
       } else if (table === 'error') {
-        tableItems = (
-          <TreeItem nodeId={`${ds.sourcename}-error`} label="Error" />
-        );
+        tableItems = <TreeItem nodeId={`${ds.id}-error`} label="Error" />;
       } else if (table.length === 0) {
-        tableItems = (
-          <TreeItem nodeId={`${ds.sourcename}-error`} label="Empty" />
-        );
+        tableItems = <TreeItem nodeId={`${ds.id}-error`} label="Empty" />;
       } else {
         tableItems = table.map(t => {
           return (
             <TreeItem
               title={t}
-              key={`${ds.sourcename}-${t}`}
-              nodeId={`${ds.sourcename}-${t}`}
+              key={`${ds.id}-${t}`}
+              nodeId={`${ds.id}-${t}`}
               label={t}
-              onDoubleClick={() => onOpenTable(ds, t)}
+              onDoubleClick={() => onOpenTable(ds, t, 'content')}
+              onContextMenu={evt => handleContextMenu(evt, ds, t)}
               draggable="true"
               onDragStart={evt => {
                 const data = {
@@ -80,8 +144,8 @@ const DatasourceView: React.FunctionComponent<IProps> = ({
                     editType: 'createNode',
                     finalized: true,
                     nodeTemplate: {
-                      id: `${ds.manager}-${ds.sourcename}-${t}`,
-                      op: `database-${ds.source}-input`,
+                      id: `${ds.manager}-${ds.id}-${t}`,
+                      op: `database-${ds.datasource}-input`,
                       description: t,
                       type: 'execution_node',
                       label: t,
@@ -121,12 +185,13 @@ const DatasourceView: React.FunctionComponent<IProps> = ({
       }
       return (
         <TreeItem
-          key={ds.sourcename}
-          nodeId={ds.sourcename}
-          label={ds.databasename}
-          onClick={() => handleNodeToggle(ds)}
+          key={ds.id}
+          nodeId={ds.id.toString()}
+          label={ds.database_name}
+          onClick={() => handleNodeToggle(ds.id)}
         >
           {tableItems}
+          {contextMenuView}
         </TreeItem>
       );
     });

+ 1 - 1
packages/jldbq-extenison/src/api/config.ts

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

+ 97 - 23
packages/jldbq-extenison/src/api/datasource.ts

@@ -17,8 +17,28 @@ export interface IDatasource {
   sourcename: string;
 }
 
+export interface IDatasourceItem {
+  jdbc_url: string;
+  jdbc_username: string;
+  jdbc_password: string;
+  manager: string;
+  datasource: DbType;
+  database_name: string;
+  datasource_name: string;
+  id: number;
+}
+
+export interface IDatasourceForm {
+  jdbc_url: string;
+  jdbc_username: string;
+  jdbc_password: string;
+  database_name: string;
+  datasource_name: string;
+  datasource: 'mysql' | 'hive';
+}
+
 interface IDatasourceResponse {
-  datasources?: IDatasource[];
+  datasources?: IDatasourceItem[];
   loading: boolean;
   error?: any;
   refresh: () => void;
@@ -28,24 +48,25 @@ 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 item = encodeURIComponent(manager);
+  // const url = `${endpoint}/jpt/item=${item}`;
+  const url = `${endpoint}/jpt/datasource/`;
   const { data, error, mutate } = useSWR(url, (...args) =>
     fetch(...args).then(res => res.json())
   );
   return {
-    datasources: data,
+    datasources: data?.data.items,
     loading: !error && !data,
-    error,
+    error: error || data?.code !== 200,
     refresh: mutate
   };
 };
 
 export const setDatasource = async (
-  datasource: IDatasource
+  datasource: IDatasourceForm
 ): Promise<string> => {
   const endpoint = config.endpoint;
-  const url = `${endpoint}/manage`;
+  const url = `${endpoint}/jpt/datasource`;
   try {
     const resp = await fetch(url, {
       method: 'post',
@@ -56,6 +77,30 @@ export const setDatasource = async (
       return 'error';
     }
     const data = await resp.text();
+    console.log(data);
+    return data;
+  } catch (e) {
+    console.error(e);
+    return 'error';
+  }
+};
+
+export const textDatasource = async (
+  datasource: IDatasourceForm
+): Promise<string> => {
+  const endpoint = config.endpoint;
+  const url = `${endpoint}/jpt/datasource/test`;
+  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.json();
+    console.log(data);
     return data;
   } catch (e) {
     console.error(e);
@@ -64,20 +109,20 @@ export const setDatasource = async (
 };
 
 export const fetchTables = async (
-  ds: IDatasource
+  ds_id: number
 ): 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}`;
+  const url = `${endpoint}/jpt/datasource/table_names?ds_id=${ds_id}`;
   try {
-    const resp = await fetch(url);
+    const resp = await fetch(url, { method: 'post' });
     if (!resp.ok) {
       return 'error';
     }
-    const data = (await resp.json()) as string[];
-    return data;
+    const data = await resp.json();
+    if (data.code !== 200) {
+      return 'error';
+    }
+    return data.data;
   } catch (e) {
     console.error(e);
     return 'error';
@@ -85,21 +130,50 @@ export const fetchTables = async (
 };
 
 export const fetchTableContent = async (
-  ds: IDatasource,
+  ds: IDatasourceItem,
   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}`;
+  const url = `${endpoint}/jpt/datasource/preview?ds_id=${ds.id}&table_name=${t}`;
   try {
-    const resp = await fetch(url);
+    const resp = await fetch(url, { method: 'post' });
     if (!resp.ok) {
       return 'error';
     }
-    const data = (await resp.json()) as ITableData;
-    return data;
+    const data = await resp.json();
+    if (data.code !== 200) {
+      return 'error';
+    }
+    const formatData = {} as ITableData;
+    data.data.header.map((col: string | number, index: number) => {
+      formatData[col] = data.data.content.map((row: any[]) => row[index]);
+    });
+    return formatData;
+  } catch (e) {
+    console.error(e);
+    return 'error';
+  }
+};
+
+export const fetchTableSchema = async (
+  ds: IDatasourceItem,
+  t: string
+): Promise<ITableData | 'error'> => {
+  const endpoint = config.endpoint;
+  const url = `${endpoint}/jpt/datasource/table_schema?ds_id=${ds.id}&table_name=${t}`;
+  try {
+    const resp = await fetch(url, { method: 'post' });
+    if (!resp.ok) {
+      return 'error';
+    }
+    const data = await resp.json();
+    if (data.code !== 200) {
+      return 'error';
+    }
+    const formatData = {} as ITableData;
+    formatData['序号'] = data.data.map((item: any, index: number) => index + 1);
+    formatData['字段'] = data.data.map((item: any) => item);
+    return formatData;
   } catch (e) {
     console.error(e);
     return 'error';

+ 9 - 7
packages/jldbq-extenison/src/index.ts

@@ -37,13 +37,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
     dataViewWidget.title.icon = cliIcon;
     dataViewWidget.title.caption = '数据开发';
     dataViewWidget.title.label = '数据开发';
-    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;
-      app.shell.add(widget, 'main');
-    });
+    dataViewWidget.tableOpened.connect(
+      (_sender, { datasource, tableName, action }) => {
+        const content = new DataTableWidget(datasource, tableName, action);
+        const widget = new MainAreaWidget({ content });
+        widget.title.label = tableName;
+        widget.title.icon = cliIcon;
+        app.shell.add(widget, 'main');
+      }
+    );
     app.shell.add(dataViewWidget, 'left', { rank: 100 });
 
     const taskViewWidget = new TaskViewWidget();