wzyyy 3 лет назад
Родитель
Сommit
7c4a254ebf

BIN
public/images/login.png


BIN
public/images/login2.png


+ 5 - 8
src/components/DashBoard/echarts.tsx

@@ -1,24 +1,21 @@
 import { useEffect, useRef } from 'react';
 import * as echarts from 'echarts/core';
-import type { ECharts } from 'echarts';
+import type { ECharts, EChartsOption } from 'echarts';
 import {
-  TitleComponent,
-  ToolboxComponent,
-  TooltipComponent,
   GridComponent,
   LegendComponent,
   MarkLineComponent,
+  TitleComponent,
+  ToolboxComponent,
+  TooltipComponent,
 } from 'echarts/components';
 
-import { LineChart } from 'echarts/charts';
-import { PieChart } from 'echarts/charts';
-import { BarChart } from 'echarts/charts';
+import { BarChart, LineChart, PieChart } from 'echarts/charts';
 
 import { UniversalTransition } from 'echarts/features';
 import { CanvasRenderer } from 'echarts/renderers';
 
 import Style from './index.less';
-import type { EChartsOption } from 'echarts';
 import classNames from 'classnames';
 
 export interface EchartsProps {

+ 1 - 0
src/components/DashBoard/index.less

@@ -5,6 +5,7 @@
   padding: 24px;
   background-color: #fff;
 }
+
 .header {
   display: flex;
   gap: 20px;

+ 15 - 9
src/components/DashBoard/timePicker.tsx

@@ -12,9 +12,11 @@ export enum TimeKey {
 
 export type TimeType = keyof typeof TimeKey;
 
+type ValueType = { start: number; end: number; type: TimeType };
+
 interface ExtraTimePickerProps extends Omit<DatePickerProps, 'onChange' | 'value'> {
-  onChange?: (value: number[]) => void;
-  value?: number[];
+  onChange?: (data: ValueType) => void;
+  value?: ValueType;
   defaultTime?: TimeType;
 }
 
@@ -23,7 +25,7 @@ export const getTimeByType = (type: TimeType) => {
     case TimeKey.week:
       return moment().subtract(6, 'days').valueOf();
     case TimeKey.month:
-      return moment().startOf('month').valueOf();
+      return moment().subtract(29, 'days').valueOf();
     case TimeKey.year:
       return moment().subtract(365, 'days').valueOf();
     default:
@@ -36,9 +38,13 @@ export default (props: ExtraTimePickerProps) => {
 
   const { value, onChange, ...extraProps } = props;
 
-  const change = (startTime: number, endTime: number) => {
+  const change = (startTime: number, endTime: number, type: TimeType) => {
     if (onChange) {
-      onChange([startTime, endTime]);
+      onChange({
+        start: startTime,
+        end: endTime,
+        type,
+      });
     }
   };
 
@@ -46,7 +52,7 @@ export default (props: ExtraTimePickerProps) => {
     const endTime = moment(new Date()).valueOf();
     const startTime: number = getTimeByType(type);
     setRadioValue(type);
-    change(startTime, endTime);
+    change(startTime, endTime, type);
   };
 
   useEffect(() => {
@@ -61,14 +67,14 @@ export default (props: ExtraTimePickerProps) => {
           {...extraProps}
           value={
             value && [
-              moment(value && value[0] ? value[0] : new Date()),
-              moment(value && value[1] ? value[1] : new Date()),
+              moment(value && value.start ? value.start : new Date()),
+              moment(value && value.end ? value.end : new Date()),
             ]
           }
           onChange={(rangeValue) => {
             setRadioValue(undefined);
             if (rangeValue && rangeValue.length === 2) {
-              change(rangeValue[0]!.valueOf(), rangeValue[1]!.valueOf());
+              change(rangeValue[0]!.valueOf(), rangeValue[1]!.valueOf(), radioValue!);
             }
           }}
           renderExtraFooter={() => (

+ 29 - 28
src/pages/link/DashBoard/index.tsx

@@ -5,7 +5,6 @@ import { useEffect, useRef, useState } from 'react';
 import type { EChartsOption } from 'echarts';
 import { useRequest } from 'umi';
 import Service from './service';
-import moment from 'moment';
 
 type RefType = {
   getValues: Function;
@@ -14,7 +13,9 @@ type RefType = {
 const service = new Service('dashboard');
 
 export default () => {
-  const [options, setOptions] = useState<EChartsOption>({});
+  const [networkOptions] = useState<EChartsOption | undefined>(undefined);
+  const [cpuOptions] = useState<EChartsOption | undefined>(undefined);
+  const [jvmOptions] = useState<EChartsOption | undefined>(undefined);
   const [serverId, setServerId] = useState(undefined);
 
   const NETWORKRef = useRef<RefType>(); // 网络流量
@@ -34,11 +35,11 @@ export default () => {
           object: 'network',
           measurement: 'traffic',
           dimension: 'agg',
+          group: 'network',
           params: {
             type: data.type,
-            interval: '1h',
-            from: moment(data.time[0]).format('YYYY-MM-DD HH:mm:ss'),
-            to: moment(data.time[1]).format('YYYY-MM-DD HH:mm:ss'),
+            from: data.time.start,
+            to: data.time.end,
           },
         },
       ]);
@@ -51,36 +52,36 @@ export default () => {
       service.queryMulti([
         {
           dashboard: 'systemMonitor',
-          object: 'cpu',
+          object: 'stats',
           measurement: 'traffic',
           dimension: 'agg',
+          group: 'cpu',
           params: {
-            type: data.type,
-            interval: '1h',
-            from: moment(data.time[0]).format('YYYY-MM-DD HH:mm:ss'),
-            to: moment(data.time[1]).format('YYYY-MM-DD HH:mm:ss'),
+            from: data.time.start,
+            to: data.time.end,
           },
         },
       ]);
     }
   };
 
-  const getEcharts = async () => {
-    setOptions({
-      xAxis: {
-        type: 'category',
-        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
-      },
-      yAxis: {
-        type: 'value',
-      },
-      series: [
+  const getJVMEcharts = () => {
+    const data = CPURef.current!.getValues();
+    if (data) {
+      service.queryMulti([
         {
-          data: [150, 230, 224, 218, 135, 147, 260],
-          type: 'line',
+          dashboard: 'systemMonitor',
+          object: 'stats',
+          measurement: 'traffic',
+          dimension: 'agg',
+          group: 'jvm',
+          params: {
+            from: data.time.start,
+            to: data.time.end,
+          },
         },
-      ],
-    });
+      ]);
+    }
   };
 
   useEffect(() => {
@@ -125,7 +126,7 @@ export default () => {
               ),
             }}
             defaultTime={'week'}
-            options={options}
+            options={networkOptions}
             onParamsChange={getNetworkEcharts}
           />
         </div>
@@ -136,7 +137,7 @@ export default () => {
             ref={CPURef}
             height={400}
             defaultTime={'week'}
-            options={options}
+            options={cpuOptions}
             onParamsChange={getCPUEcharts}
           />
           <DashBoard
@@ -145,8 +146,8 @@ export default () => {
             ref={JVMRef}
             height={400}
             defaultTime={'week'}
-            options={options}
-            onParamsChange={getEcharts}
+            options={jvmOptions}
+            onParamsChange={getJVMEcharts}
           />
         </div>
       </div>

+ 38 - 0
src/pages/rule-engine/DashBoard/index.less

@@ -0,0 +1,38 @@
+.media-dash-board {
+  .top-card-items {
+    margin-bottom: 12px;
+
+    .top-card-item {
+      width: 25%;
+      padding: 6px 24px;
+      border: 1px solid #e3e3e3;
+
+      .top-card-top {
+        display: flex;
+        padding: 12px 0;
+
+        .top-card-top-left {
+          width: 80px;
+        }
+
+        .top-card-top-right {
+          .top-card-total {
+            font-weight: bold;
+            font-size: 20px;
+          }
+        }
+      }
+
+      .top-card-bottom {
+        display: flex;
+        justify-content: space-between;
+        padding: 12px 0;
+        border-top: 1px solid #e3e3e3;
+      }
+    }
+  }
+
+  .media-dash-board-body {
+    border: 1px solid #f0f0f0;
+  }
+}

+ 160 - 0
src/pages/rule-engine/DashBoard/index.tsx

@@ -0,0 +1,160 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { EChartsOption } from 'echarts';
+import { useState } from 'react';
+import { Statistic, StatisticCard } from '@ant-design/pro-card';
+import { Card, Select } from 'antd';
+import './index.less';
+import Header from '@/components/DashBoard/header';
+import Echarts from '@/components/DashBoard/echarts';
+
+const imgStyle = {
+  display: 'block',
+  width: 42,
+  height: 42,
+};
+
+const Dashboard = () => {
+  const [options, setOptions] = useState<EChartsOption>({});
+
+  const getEcharts = async (params: any) => {
+    // 请求数据
+    console.log(params);
+
+    setOptions({
+      xAxis: {
+        type: 'category',
+        data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
+      },
+      yAxis: {
+        type: 'value',
+      },
+      series: [
+        {
+          data: [150, 230, 224, 218, 135, 147, 260],
+          type: 'line',
+        },
+      ],
+    });
+  };
+
+  return (
+    <PageContainer>
+      <div style={{ display: 'flex' }}>
+        <StatisticCard
+          title="今日告警"
+          statistic={{
+            value: 75,
+            suffix: '次',
+          }}
+          chart={
+            <img
+              src="https://gw.alipayobjects.com/zos/alicdn/PmKfn4qvD/mubiaowancheng-lan.svg"
+              width="100%"
+              alt="进度条"
+            />
+          }
+          footer={
+            <>
+              <Statistic value={15.1} title="当月告警" suffix="次" layout="horizontal" />
+            </>
+          }
+          style={{ width: '24%', marginRight: '5px' }}
+        />
+        <StatisticCard
+          statistic={{
+            title: '告警配置',
+            value: 2176,
+            icon: (
+              <img
+                style={imgStyle}
+                src="https://gw.alipayobjects.com/mdn/rms_7bc6d8/afts/img/A*dr_0RKvVzVwAAAAAAAAAAABkARQnAQ"
+                alt="icon"
+              />
+            ),
+          }}
+          style={{ width: '25%', marginRight: '5px' }}
+          footer={
+            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+              <Statistic value={76} title="正常" suffix="次" layout="horizontal" />
+              <Statistic value={76} title="禁用" suffix="次" layout="horizontal" />
+            </div>
+          }
+        />
+        <div style={{ width: '50%' }}>
+          <StatisticCard
+            title="最新告警"
+            statistic={{
+              // title: '最新告警'
+              value: undefined,
+            }}
+            chart={
+              <ul>
+                {[
+                  {
+                    dateTime: '2022-01-01 00:00:00',
+                    name: '一楼烟感告警',
+                    product: '产品',
+                    level: '1极告警',
+                  },
+                  {
+                    dateTime: '2022-01-01 00:00:00',
+                    name: '一楼烟感告警',
+                    product: '产品',
+                    level: '1极告警',
+                  },
+                  {
+                    dateTime: '2022-01-01 00:00:00',
+                    name: '一楼烟感告警',
+                    product: '产品',
+                    level: '1极告警',
+                  },
+                  {
+                    dateTime: '2022-01-01 00:00:00',
+                    name: '一楼烟感告警',
+                    product: '产品',
+                    level: '1极告警',
+                  },
+                ].map((item) => (
+                  <li>
+                    <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                      <div>{item.dateTime}</div>
+                      <div>{item.name}</div>
+                      <div>{item.product}</div>
+                      <div>{item.level}</div>
+                    </div>
+                  </li>
+                ))}
+              </ul>
+            }
+          />
+        </div>
+      </div>
+      <Card style={{ marginTop: 10 }}>
+        <div
+          // className={classNames(Style['dash-board-echarts'], className)}
+          style={{
+            height: 200,
+          }}
+        >
+          <Header
+            title={'告警统计'}
+            extraParams={{
+              key: 'test',
+              Children: (
+                <Select
+                  options={[
+                    { label: '设备', value: 'device' },
+                    { label: '产品', value: 'product' },
+                  ]}
+                ></Select>
+              ),
+            }}
+            onParamsChange={getEcharts}
+          />
+          <Echarts options={options} />
+        </div>
+      </Card>
+    </PageContainer>
+  );
+};
+export default Dashboard;

+ 0 - 37
src/pages/rule-engine/Dashboard/index.tsx

@@ -1,37 +0,0 @@
-import { PageContainer } from '@ant-design/pro-layout';
-// import {EChartsOption} from "echarts";
-// import {useState} from "react";
-
-const Dashboard = () => {
-  // const [options, setOptions] = useState<EChartsOption>({});
-  //
-  // const getEcharts = async (data: any) => {
-  //
-  // };
-  return (
-    <PageContainer>
-      123
-      {/*<StatisticCard*/}
-      {/*  title="今日告警"*/}
-      {/*  statistic={{*/}
-      {/*    value: 75,*/}
-      {/*    suffix: "次"*/}
-      {/*  }}*/}
-      {/*  chart={*/}
-      {/*    <img*/}
-      {/*      src="https://gw.alipayobjects.com/zos/alicdn/PmKfn4qvD/mubiaowancheng-lan.svg"*/}
-      {/*      width="100%"*/}
-      {/*      alt="进度条"*/}
-      {/*    />*/}
-      {/*  }*/}
-      {/*  footer={*/}
-      {/*    <>*/}
-      {/*      <Statistic value={15.1} title="当月告警" suffix="次" layout="horizontal"/>*/}
-      {/*    </>*/}
-      {/*  }*/}
-      {/*  style={{width: 250}}*/}
-      {/*/>*/}
-    </PageContainer>
-  );
-};
-export default Dashboard;

+ 211 - 0
src/pages/system/DataSource/Management/DataRow.tsx

@@ -0,0 +1,211 @@
+import { Form, FormGrid, FormItem, Input, NumberPicker, Password, Radio } from '@formily/antd';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/react';
+import { createSchemaField } from '@formily/react';
+import { Modal } from 'antd';
+
+interface Props {
+  close: () => void;
+  reload: (data: any) => void;
+  data: Partial<DataSourceItem>;
+}
+
+const DataRow = (props: Props) => {
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      FormGrid,
+      NumberPicker,
+      Radio,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '列名',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+            required: true,
+          },
+          type: {
+            title: '类型',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入类型',
+            },
+            name: 'typeId',
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入类型',
+              },
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+            ],
+            required: true,
+          },
+          length: {
+            title: '长度',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'NumberPicker',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入长度',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入长度',
+              },
+              {
+                maximum: 99999,
+                minimum: 1,
+              },
+            ],
+            required: true,
+          },
+          scale: {
+            title: '精度',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'NumberPicker',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入精度',
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请输入精度',
+              },
+              {
+                maximum: 99999,
+                minimum: 1,
+              },
+            ],
+            required: true,
+          },
+          notnull: {
+            title: '不能为空',
+            type: 'boolean',
+            default: false,
+            'x-decorator': 'FormItem',
+            'x-component': 'Radio.Group',
+            'x-component-props': {
+              placeholder: '请选择是否不能为空',
+              optionType: 'button',
+              buttonStyle: 'solid',
+            },
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择是否不能为空',
+              },
+            ],
+            required: true,
+            enum: [
+              {
+                label: '是',
+                value: true,
+              },
+              {
+                label: '否',
+                value: false,
+              },
+            ],
+          },
+          description: {
+            title: '说明',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    props.reload(data);
+  };
+
+  return (
+    <Modal
+      width={'55vw'}
+      title={`${props.data?.id ? '编辑' : '新增'}列`}
+      visible
+      onCancel={() => {
+        props.close();
+      }}
+      onOk={() => {
+        handleSave();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default DataRow;

+ 82 - 0
src/pages/system/DataSource/Management/DataTable.tsx

@@ -0,0 +1,82 @@
+import { Form, FormGrid, FormItem, Input, Password, Select } from '@formily/antd';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/react';
+import { createSchemaField } from '@formily/react';
+import { Modal } from 'antd';
+
+interface Props {
+  close: () => void;
+  save: (data: any) => void;
+  data: any;
+}
+
+const DataTable = (props: Props) => {
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      Select,
+      FormGrid,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        title: '名称',
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        'x-decorator-props': {
+          gridSpan: 1,
+        },
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        name: 'name',
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
+        required: true,
+      },
+    },
+  };
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    props.save(data);
+  };
+
+  return (
+    <Modal
+      title={`${props.data?.name ? '编辑' : '新增'}`}
+      visible
+      onCancel={() => {
+        props.close();
+      }}
+      onOk={() => {
+        handleSave();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default DataTable;

+ 267 - 0
src/pages/system/DataSource/Management/EditTable.tsx

@@ -0,0 +1,267 @@
+import { ArrayTable, Editable, Form, FormItem, Input, NumberPicker, Radio } from '@formily/antd';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import { Button } from 'antd';
+import { useEffect } from 'react';
+import RemoveData from './RemoveData';
+
+interface Props {
+  onChange: (data: any) => void;
+  data: Partial<DataSourceItem>[];
+  table: {
+    id: string;
+    table: string;
+  };
+}
+
+const EditTable = (props: Props) => {
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Editable,
+      Input,
+      ArrayTable,
+      NumberPicker,
+      Radio,
+      RemoveData,
+    },
+  });
+
+  const form = createForm({
+    initialValues: {
+      array: props.data,
+    },
+  });
+
+  const schema = {
+    type: 'object',
+    properties: {
+      array: {
+        type: 'array',
+        'x-decorator': 'FormItem',
+        'x-component': 'ArrayTable',
+        'x-component-props': {
+          pagination: { pageSize: 10 },
+          scroll: { x: '100%' },
+        },
+        items: {
+          type: 'object',
+          properties: {
+            column1: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '列名' },
+              properties: {
+                name: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                  'x-component-props': {
+                    placeholder: '请输入名称',
+                  },
+                  name: 'name',
+                  'x-validator': [
+                    {
+                      max: 64,
+                      message: '最多可输入64个字符',
+                    },
+                    {
+                      required: true,
+                      message: '请输入名称',
+                    },
+                  ],
+                  required: true,
+                },
+              },
+            },
+            column2: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '类型' },
+              properties: {
+                type: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Input',
+                  'x-component-props': {
+                    placeholder: '请输入类型',
+                  },
+                  name: 'typeId',
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请输入类型',
+                    },
+                    {
+                      max: 64,
+                      message: '最多可输入64个字符',
+                    },
+                  ],
+                  required: true,
+                },
+              },
+            },
+            column3: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '长度' },
+              properties: {
+                length: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'NumberPicker',
+                  'x-component-props': {
+                    placeholder: '请输入长度',
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请输入长度',
+                    },
+                    {
+                      maximum: 99999,
+                      minimum: 1,
+                    },
+                  ],
+                  required: true,
+                },
+              },
+            },
+            column4: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '精度' },
+              properties: {
+                scale: {
+                  type: 'string',
+                  'x-decorator': 'FormItem',
+                  'x-component': 'NumberPicker',
+                  'x-component-props': {
+                    placeholder: '请输入精度',
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请输入精度',
+                    },
+                    {
+                      maximum: 99999,
+                      minimum: 0,
+                    },
+                  ],
+                  required: true,
+                },
+              },
+            },
+            column5: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { width: 120, title: '不能为空' },
+              properties: {
+                notnull: {
+                  type: 'boolean',
+                  default: false,
+                  'x-decorator': 'FormItem',
+                  'x-component': 'Radio.Group',
+                  'x-component-props': {
+                    placeholder: '请选择是否不能为空',
+                    optionType: 'button',
+                    buttonStyle: 'solid',
+                  },
+                  'x-validator': [
+                    {
+                      required: true,
+                      message: '请选择是否不能为空',
+                    },
+                  ],
+                  required: true,
+                  enum: [
+                    {
+                      label: '是',
+                      value: true,
+                    },
+                    {
+                      label: '否',
+                      value: false,
+                    },
+                  ],
+                },
+              },
+            },
+            column6: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': { title: '说明' },
+              properties: {
+                description: {
+                  'x-component': 'Input',
+                  'x-decorator': 'FormItem',
+                  'x-component-props': {
+                    placeholder: '请输入说明',
+                  },
+                },
+              },
+            },
+            column7: {
+              type: 'void',
+              'x-component': 'ArrayTable.Column',
+              'x-component-props': {
+                title: '操作',
+                dataIndex: 'operations',
+                width: 50,
+              },
+              properties: {
+                item: {
+                  type: 'void',
+                  'x-component': 'FormItem',
+                  properties: {
+                    remove: {
+                      type: 'number',
+                      'x-component': 'RemoveData',
+                      'x-component-props': {
+                        type: props.table,
+                      },
+                    },
+                  },
+                },
+              },
+            },
+          },
+        },
+        properties: {
+          add: {
+            type: 'void',
+            'x-component': 'ArrayTable.Addition',
+            title: '新增行',
+          },
+        },
+      },
+    },
+  };
+
+  useEffect(() => {
+    form.setValues({ array: props?.data || [] });
+  }, [props.data]);
+
+  return (
+    <div>
+      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
+        <Button
+          type="primary"
+          style={{ marginBottom: 20 }}
+          onClick={async () => {
+            const data: any = await form.submit();
+            props.onChange(data);
+          }}
+        >
+          保存
+        </Button>
+      </div>
+      <Form form={form}>
+        <SchemaField schema={schema} />
+      </Form>
+    </div>
+  );
+};
+
+export default EditTable;

+ 51 - 0
src/pages/system/DataSource/Management/RemoveData.tsx

@@ -0,0 +1,51 @@
+import { DeleteOutlined } from '@ant-design/icons';
+import { ArrayItems } from '@formily/antd';
+import { Popconfirm } from 'antd';
+import { service } from '@/pages/system/DataSource';
+import _ from 'lodash';
+import { useField } from '@formily/react';
+
+interface Props {
+  type: any;
+}
+
+const RemoveData = (props: Props) => {
+  const { type } = props;
+  const row = ArrayItems.useRecord!();
+
+  const index = ArrayItems.useIndex!();
+  const self = useField();
+  const array = ArrayItems.useArray!();
+  if (!array) return null;
+  if (array.field?.pattern !== 'editable') return null;
+
+  return (
+    <div>
+      <Popconfirm
+        title={'确认删除'}
+        onConfirm={() => {
+          if (self?.disabled) return;
+          service.rdbTables(type.id, type.table).then((resp) => {
+            if (resp.status === 200) {
+              if ([..._.map(resp.result.columns, 'name')].includes(row?.name)) {
+                service.delRdbTablesColumn(type.id, type.table, [row?.name]).then((response) => {
+                  if (response.status === 200) {
+                    array.field?.remove?.(index);
+                    array.props?.onRemove?.(index);
+                  }
+                });
+              } else {
+                array.field?.remove?.(index);
+                array.props?.onRemove?.(index);
+              }
+            }
+          });
+        }}
+      >
+        <DeleteOutlined />
+      </Popconfirm>
+    </div>
+  );
+};
+
+export default RemoveData;

+ 292 - 0
src/pages/system/DataSource/Management/index copy.tsx

@@ -0,0 +1,292 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Input, message, Popconfirm, Space, Tooltip, Tree } from 'antd';
+import { useEffect, useRef, useState } from 'react';
+import { service } from '@/pages/system/DataSource';
+import { useIntl, useLocation } from 'umi';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import PermissionButton from '@/components/PermissionButton';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+import usePermissions from '@/hooks/permission';
+import SearchComponent from '@/components/SearchComponent';
+import DataTable from './DataTable';
+import styles from './index.less';
+import DataRow from './DataRow';
+
+const Management = () => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+  const intl = useIntl();
+  const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const { permission: userPermission } = usePermissions('system/User');
+
+  const [rdbList, setRdbList] = useState<any[]>([]);
+  const [defaultSelectedKeys, setDefaultSelectedKeys] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
+  const [rowVisible, setRowVisible] = useState<boolean>(false);
+  const [rowCurrent, setRowCurrent] = useState<any>({});
+
+  useEffect(() => {
+    service.rdbTree(id).then((resp) => {
+      if (resp.status === 200) {
+        setRdbList(resp.result);
+        setDefaultSelectedKeys([resp.result[0]?.name]);
+      }
+    });
+  }, []);
+
+  useEffect(() => {
+    actionRef.current?.reload();
+  }, [defaultSelectedKeys]);
+
+  const columns: ProColumns<DataSourceType>[] = [
+    {
+      title: '列名',
+      dataIndex: 'name',
+      ellipsis: true,
+    },
+    {
+      title: '类型',
+      dataIndex: 'type',
+      ellipsis: true,
+    },
+    {
+      title: '长度',
+      dataIndex: 'length',
+      ellipsis: true,
+    },
+    {
+      title: '精度',
+      dataIndex: 'scale',
+      ellipsis: true,
+    },
+    {
+      title: '不能为空',
+      dataIndex: 'notnull',
+      ellipsis: true,
+      render: (text, record) => {
+        console.log(record.notnull);
+        return <span>{text ? '是' : '否'}</span>;
+      },
+      valueType: 'select',
+      valueEnum: {
+        true: {
+          text: '是',
+          status: true,
+        },
+        false: {
+          text: '否',
+          status: false,
+        },
+      },
+    },
+    {
+      dataIndex: 'description',
+      title: '说明',
+      ellipsis: true,
+    },
+    {
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      render: (_, record) => [
+        <a key="edit">
+          <EditOutlined
+            onClick={() => {
+              setRowCurrent(record);
+              setRowVisible(true);
+            }}
+          />
+        </a>,
+        <PermissionButton
+          type="link"
+          key="delete"
+          style={{ padding: 0 }}
+          isPermission={userPermission.delete}
+          tooltip={{ title: '删除' }}
+        >
+          <Popconfirm
+            onConfirm={async () => {
+              // await service.remove(record.id);
+              // actionRef.current?.reload();
+            }}
+            title="确认删除?"
+          >
+            <DeleteOutlined />
+          </Popconfirm>
+        </PermissionButton>,
+      ],
+    },
+  ];
+
+  return (
+    <PageContainer>
+      <Card>
+        <div className={styles.datasourceBox}>
+          <div className={styles.left}>
+            <Input.Search
+              placeholder="请输入"
+              onSearch={() => {}}
+              style={{ width: '100%', marginBottom: 20 }}
+            />
+            <div className={styles.tables}>
+              <Tree
+                showLine
+                showIcon
+                defaultExpandAll
+                height={500}
+                selectedKeys={[...defaultSelectedKeys]}
+                onSelect={(selectedKeys) => {
+                  if (!selectedKeys.includes('tables')) {
+                    setDefaultSelectedKeys([...selectedKeys]);
+                  }
+                }}
+              >
+                <Tree.TreeNode
+                  title={() => {
+                    return (
+                      <div style={{ display: 'flex', justifyContent: 'space-between', width: 230 }}>
+                        <div>数据源名称</div>
+                        <div>
+                          <a>
+                            <PlusOutlined
+                              onClick={() => {
+                                setCurrent({});
+                                setVisible(true);
+                              }}
+                            />
+                          </a>
+                        </div>
+                      </div>
+                    );
+                  }}
+                  key={'tables'}
+                >
+                  {rdbList.map((item) => (
+                    <Tree.TreeNode
+                      key={item.name}
+                      title={() => {
+                        return (
+                          <div className={styles.treeTitle}>
+                            <div className={styles.title}>
+                              <span
+                                className={
+                                  defaultSelectedKeys[0] === item?.name ? styles.active : ''
+                                }
+                              >
+                                <Tooltip title={item.name}>{item.name}</Tooltip>
+                              </span>
+                            </div>
+                            <div className={styles.options}>
+                              <Space>
+                                <a>
+                                  <EditOutlined
+                                    onClick={() => {
+                                      setCurrent(item);
+                                      setVisible(true);
+                                    }}
+                                  />
+                                </a>
+                                <a>
+                                  <Popconfirm title="确认删除!" onConfirm={() => {}}>
+                                    <DeleteOutlined />
+                                  </Popconfirm>
+                                </a>
+                              </Space>
+                            </div>
+                          </div>
+                        );
+                      }}
+                    />
+                  ))}
+                </Tree.TreeNode>
+              </Tree>
+            </div>
+          </div>
+          <div className={styles.right}>
+            <SearchComponent<DataSourceType>
+              field={columns}
+              target="datasource-manage"
+              onSearch={(data) => {
+                // 重置分页数据
+                actionRef.current?.reset?.();
+                setParam(data);
+              }}
+            />
+            <ProTable<DataSourceType>
+              actionRef={actionRef}
+              params={param}
+              columns={columns}
+              search={false}
+              rowKey="name"
+              headerTitle={
+                <PermissionButton
+                  onClick={() => {
+                    setRowCurrent({});
+                    setRowVisible(true);
+                  }}
+                  isPermission={userPermission.add}
+                  key="add"
+                  icon={<PlusOutlined />}
+                  type="primary"
+                >
+                  新增列
+                </PermissionButton>
+              }
+              request={async (params) => {
+                if (defaultSelectedKeys.length > 0) {
+                  const response = await service.rdbTables(id, defaultSelectedKeys[0], {
+                    ...params,
+                    sorts: [{ name: 'createTime', order: 'desc' }],
+                  });
+                  return {
+                    result: { data: response.result?.columns || [] },
+                    success: true,
+                    status: 200,
+                  } as any;
+                } else {
+                  return {
+                    result: { data: [] },
+                    success: true,
+                    status: 200,
+                  } as any;
+                }
+              }}
+            />
+          </div>
+        </div>
+      </Card>
+      {visible && (
+        <DataTable
+          data={current}
+          save={(data) => {
+            rdbList.push(data);
+            setRdbList([...rdbList]);
+            message.success('操作成功!');
+            setVisible(false);
+          }}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
+      {rowVisible && (
+        <DataRow
+          data={rowCurrent}
+          reload={() => {
+            // setRowVisible(false);
+          }}
+          close={() => {
+            setRowVisible(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default Management;

+ 52 - 0
src/pages/system/DataSource/Management/index.less

@@ -0,0 +1,52 @@
+.datasourceBox {
+  display: flex;
+  width: 100%;
+  min-height: 500px;
+  overflow: hidden;
+
+  .left {
+    width: 280px;
+
+    .tables {
+      :global {
+        .ant-tree-treenode .ant-tree-node-selected {
+          background-color: white;
+        }
+      }
+
+      .treeTitle {
+        display: flex;
+        justify-content: space-between;
+        width: 200px;
+
+        .title {
+          width: 150px;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+
+        .options {
+          display: none;
+        }
+      }
+
+      .treeTitle:hover {
+        .options {
+          display: block;
+        }
+      }
+
+      .active {
+        background-color: #c3d3f7;
+      }
+    }
+  }
+
+  .right {
+    width: calc(100% - 310px);
+    margin-left: 15px;
+    padding-left: 15px;
+    border-left: 1px solid #f0f0f0;
+  }
+}

+ 205 - 0
src/pages/system/DataSource/Management/index.tsx

@@ -0,0 +1,205 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { Card, Input, message, Popconfirm, Space, Tooltip, Tree } from 'antd';
+import { useEffect, useState } from 'react';
+import { service } from '@/pages/system/DataSource';
+import { useLocation } from 'umi';
+import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
+// import usePermissions from '@/hooks/permission';
+import DataTable from './DataTable';
+import styles from './index.less';
+import EditTable from './EditTable';
+import _ from 'lodash';
+
+const Management = () => {
+  const location = useLocation<{ id: string }>();
+  const id = (location as any).query?.id;
+
+  const [rdbList, setRdbList] = useState<any[]>([]);
+  const [allList, setAllList] = useState<any[]>([]);
+  const [defaultSelectedKeys, setDefaultSelectedKeys] = useState<any[]>([]);
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<any>({});
+  const [model, setModel] = useState<'edit' | 'add' | 'list'>('list');
+  const [tableList, setTableList] = useState<any[]>([]);
+  const [param, setParam] = useState<string | undefined>(undefined);
+
+  const handleSearch = () => {
+    service.rdbTree(id).then((resp) => {
+      if (resp.status === 200) {
+        setAllList(resp.result);
+        setDefaultSelectedKeys([resp.result[0]?.name]);
+      }
+    });
+  };
+
+  useEffect(() => {
+    handleSearch();
+  }, []);
+
+  useEffect(() => {
+    if (defaultSelectedKeys.length > 0) {
+      service.rdbTables(id, defaultSelectedKeys[0]).then((resp) => {
+        if (resp.status === 200) {
+          setTableList(resp.result?.columns || []);
+        }
+      });
+    } else {
+      setTableList([]);
+    }
+  }, [defaultSelectedKeys]);
+
+  useEffect(() => {
+    if (!!param) {
+      const list = allList.filter((item) => {
+        return item.name.includes(param);
+      });
+      setRdbList([...list]);
+      if (!_.map(list, 'name').includes(defaultSelectedKeys[0])) {
+        setDefaultSelectedKeys([list[0]?.name]);
+      }
+    } else {
+      setRdbList([...allList]);
+    }
+  }, [allList, param]);
+
+  return (
+    <PageContainer>
+      <Card>
+        <div className={styles.datasourceBox}>
+          <div className={styles.left}>
+            <Input.Search
+              placeholder="请输入"
+              onSearch={(val: string) => {
+                setParam(val);
+              }}
+              allowClear
+              style={{ width: '100%', marginBottom: 20 }}
+            />
+            <div className={styles.tables}>
+              <Tree showLine showIcon defaultExpandAll height={500}>
+                <Tree.TreeNode
+                  title={() => {
+                    return (
+                      <div style={{ display: 'flex', justifyContent: 'space-between', width: 230 }}>
+                        <div>数据源名称</div>
+                        <div>
+                          <a>
+                            <PlusOutlined
+                              onClick={() => {
+                                setCurrent({});
+                                setModel('add');
+                                setVisible(true);
+                              }}
+                            />
+                          </a>
+                        </div>
+                      </div>
+                    );
+                  }}
+                  key={'tables'}
+                >
+                  {rdbList.map((item, index) => (
+                    <Tree.TreeNode
+                      key={item.name}
+                      title={() => {
+                        return (
+                          <div className={styles.treeTitle}>
+                            <div
+                              className={styles.title}
+                              onClick={() => {
+                                setDefaultSelectedKeys([item.name]);
+                              }}
+                            >
+                              <span
+                                className={
+                                  defaultSelectedKeys[0] === item?.name ? styles.active : ''
+                                }
+                              >
+                                <Tooltip title={item.name}>{item.name}</Tooltip>
+                              </span>
+                            </div>
+                            <div className={styles.options}>
+                              <Space>
+                                <a>
+                                  <EditOutlined
+                                    onClick={() => {
+                                      setCurrent(item);
+                                      setVisible(true);
+                                      setModel('edit');
+                                    }}
+                                  />
+                                </a>
+                                <a>
+                                  <Popconfirm
+                                    title="确认删除!"
+                                    onConfirm={() => {
+                                      const list = [...rdbList];
+                                      list.splice(index, 1);
+                                      setAllList([...list]);
+                                      if (item.name === defaultSelectedKeys[0]) {
+                                        setDefaultSelectedKeys([list[0]?.name]);
+                                      }
+                                    }}
+                                  >
+                                    <DeleteOutlined />
+                                  </Popconfirm>
+                                </a>
+                              </Space>
+                            </div>
+                          </div>
+                        );
+                      }}
+                    />
+                  ))}
+                </Tree.TreeNode>
+              </Tree>
+            </div>
+          </div>
+          <div className={styles.right}>
+            <EditTable
+              table={{ id, table: defaultSelectedKeys[0] }}
+              data={tableList}
+              onChange={async (data: any) => {
+                const resp = await service.saveRdbTables(id, {
+                  name: defaultSelectedKeys[0],
+                  columns: [...data.array],
+                });
+                if (resp.status === 200) {
+                  message.success('保存成功');
+                  handleSearch();
+                }
+              }}
+            />
+          </div>
+        </div>
+      </Card>
+      {visible && (
+        <DataTable
+          data={current}
+          save={(data) => {
+            if (model === 'edit') {
+              const list = [...rdbList].map((item) => {
+                return {
+                  name: item?.name === current?.name ? data.name : item.name,
+                };
+              });
+              setAllList(list);
+            } else {
+              const list = [...rdbList];
+              list.unshift(data);
+              setAllList([...list]);
+            }
+            setModel('list');
+            message.success('操作成功!');
+            setVisible(false);
+          }}
+          close={() => {
+            setVisible(false);
+          }}
+        />
+      )}
+    </PageContainer>
+  );
+};
+
+export default Management;

+ 360 - 0
src/pages/system/DataSource/Save/index.tsx

@@ -0,0 +1,360 @@
+import { Form, FormGrid, FormItem, Input, Password, Select } from '@formily/antd';
+import { createForm } from '@formily/core';
+import type { ISchema } from '@formily/react';
+import { createSchemaField } from '@formily/react';
+import { message, Modal } from 'antd';
+import { Store } from 'jetlinks-store';
+import { service } from '@/pages/system/DataSource';
+
+interface Props {
+  close: () => void;
+  reload: () => void;
+  data: Partial<DataSourceItem>;
+}
+
+const Save = (props: Props) => {
+  const form = createForm({
+    validateFirst: true,
+    initialValues: props.data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+      Password,
+      Select,
+      FormGrid,
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      layout: {
+        type: 'void',
+        'x-decorator': 'FormGrid',
+        'x-decorator-props': {
+          maxColumns: 2,
+          minColumns: 2,
+          columnGap: 24,
+        },
+        properties: {
+          name: {
+            title: '名称',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入名称',
+            },
+            name: 'name',
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入名称',
+              },
+            ],
+            required: true,
+          },
+          typeId: {
+            title: '类型',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Select',
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请选择类型',
+            },
+            name: 'typeId',
+            'x-validator': [
+              {
+                required: true,
+                message: '请选择类型',
+              },
+            ],
+            required: true,
+            enum: Store.get('datasource-type'),
+          },
+          'shareConfig.url': {
+            title: 'URL',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入r2bdc或者jdbc连接地址,示例:r2dbc:mysql://127.0.0.1:3306/test',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入URL',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rdb"}}',
+                },
+              },
+            },
+          },
+          'shareConfig.adminUrl': {
+            title: '管理地址',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入管理地址,示例:http://localhost:15672',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入管理地址',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rabbitmq"}}',
+                },
+              },
+            },
+          },
+          'shareConfig.addresses': {
+            title: '链接地址',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入连接地址,示例:localhost:5672',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入链接地址',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rabbitmq"}}',
+                },
+              },
+            },
+          },
+          'shareConfig.username': {
+            title: '用户名',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入用户名',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入用户名',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{["rdb","rabbitmq"].includes($deps[0])}}',
+                },
+              },
+            },
+          },
+          'shareConfig.password': {
+            title: '密码',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Password',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 1,
+            },
+            'x-component-props': {
+              placeholder: '请输入密码',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入密码',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{["rdb","rabbitmq"].includes($deps[0])}}',
+                },
+              },
+            },
+          },
+          'shareConfig.virtualHost': {
+            title: '虚拟域',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入虚拟域',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入虚拟域',
+              },
+            ],
+            required: true,
+            default: '/',
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rabbitmq"}}',
+                },
+              },
+            },
+          },
+          'shareConfig.schema': {
+            title: 'schema',
+            type: 'string',
+            'x-decorator': 'FormItem',
+            'x-component': 'Input',
+            'x-visible': false,
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              placeholder: '请输入schema',
+            },
+            'x-validator': [
+              {
+                max: 64,
+                message: '最多可输入64个字符',
+              },
+              {
+                required: true,
+                message: '请输入schema',
+              },
+            ],
+            required: true,
+            'x-reactions': {
+              dependencies: ['typeId'],
+              fulfill: {
+                state: {
+                  visible: '{{$deps[0]==="rdb"}}',
+                },
+              },
+            },
+          },
+          description: {
+            title: '说明',
+            'x-component': 'Input.TextArea',
+            'x-decorator': 'FormItem',
+            'x-decorator-props': {
+              gridSpan: 2,
+            },
+            'x-component-props': {
+              rows: 3,
+              showCount: true,
+              maxLength: 200,
+              placeholder: '请输入说明',
+            },
+          },
+        },
+      },
+    },
+  };
+
+  const handleSave = async () => {
+    const data: any = await form.submit();
+    const response: any = props.data?.id ? await service.update(data) : await service.save(data);
+    if (response.status === 200) {
+      message.success('保存成功');
+      props.reload();
+    }
+  };
+
+  return (
+    <Modal
+      width={'55vw'}
+      title={`${props.data?.id ? '编辑' : '新增'}数据源`}
+      visible
+      onCancel={() => {
+        props.close();
+      }}
+      onOk={() => {
+        handleSave();
+      }}
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+
+export default Save;

+ 202 - 274
src/pages/system/DataSource/index.tsx

@@ -1,75 +1,75 @@
-import { PageContainer } from '@ant-design/pro-layout';
-import BaseCrud from '@/components/BaseCrud';
-import type { ProColumns } from '@jetlinks/pro-table';
-import { CurdModel } from '@/components/BaseCrud/model';
-import { message, Popconfirm, Tooltip } from 'antd';
-import { CloseCircleOutlined, EditOutlined, PlayCircleOutlined } from '@ant-design/icons';
-import type { ActionType } from '@jetlinks/pro-table';
-import { useEffect, useRef, useState } from 'react';
-import type { ISchema } from '@formily/json-schema';
 import Service from '@/pages/system/DataSource/service';
-import { from, mergeMap, toArray } from 'rxjs';
-import { map } from 'rxjs/operators';
+import { PageContainer } from '@ant-design/pro-layout';
+import SearchComponent from '@/components/SearchComponent';
+import type { ActionType, ProColumns } from '@jetlinks/pro-table';
+import ProTable from '@jetlinks/pro-table';
+import { Badge, message, Popconfirm } from 'antd';
+import {
+  CloseCircleOutlined,
+  DatabaseOutlined,
+  DeleteOutlined,
+  EditOutlined,
+  PlayCircleOutlined,
+  PlusOutlined,
+} from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
+import { useEffect, useRef, useState } from 'react';
+import { observer } from '@formily/react';
+import { PermissionButton } from '@/components';
+import usePermissions from '@/hooks/permission';
+import Save from './Save';
+import { Store } from 'jetlinks-store';
+import { getMenuPathByCode, MENUS_CODE } from '@/utils/menu';
+import { useHistory } from 'umi';
 
 export const service = new Service('datasource/config');
 
-const stateIconMap = {
-  enabled: <CloseCircleOutlined />,
-  disabled: <PlayCircleOutlined />,
-};
-
-const DataSource = () => {
+const DataSource = observer(() => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const history = useHistory<Record<string, string>>();
+
+  const { permission: userPermission } = usePermissions('system/DataSource');
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<DataSourceItem>>({});
 
-  const [type, setType] = useState<DataSourceType[]>([]);
   useEffect(() => {
-    service
-      .getType()
-      .pipe(
-        mergeMap((data: DataSourceType[]) => from(data)),
-        map((i: DataSourceType) => ({ label: i.name, value: i.id })),
-        toArray(),
-      )
-      .subscribe((data: Partial<DataSourceType>[]) => {
-        setType(data as DataSourceType[]);
-      });
+    service.getType().then((res) => {
+      if (res.status === 200) {
+        const list = res?.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+        Store.set('datasource-type', list);
+      }
+    });
   }, []);
 
   const columns: ProColumns<DataSourceItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
-      title: 'ID',
-      dataIndex: 'id',
-      width: 240,
-      sorter: true,
-      defaultSortOrder: 'ascend',
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.table.name',
-        defaultMessage: '名称',
-      }),
+      title: '名称',
       dataIndex: 'name',
+      ellipsis: true,
     },
     {
-      title: intl.formatMessage({
-        id: 'pages.table.type',
-        defaultMessage: '类型',
-      }),
+      title: '类型',
       dataIndex: 'typeId',
+      ellipsis: true,
+      valueType: 'select',
+      request: async () => {
+        const res = await service.getType();
+        if (res.status === 200) {
+          const list = res.result.map((pItem: any) => ({ label: pItem.name, value: pItem.id }));
+          return list;
+        }
+        return [];
+      },
+      render: (_, row) =>
+        (Store.get('datasource-type') || []).find((item: any) => row.typeId === item.value)
+          ?.label || row.typeId,
+      filterMultiple: true,
     },
     {
-      title: intl.formatMessage({
-        id: 'pages.table.description',
-        defaultMessage: '说明',
-      }),
       dataIndex: 'description',
+      title: '说明',
+      ellipsis: true,
     },
     {
       title: intl.formatMessage({
@@ -77,7 +77,29 @@ const DataSource = () => {
         defaultMessage: '状态',
       }),
       dataIndex: 'state',
-      render: (value: any) => value.text,
+      valueType: 'select',
+      valueEnum: {
+        enabled: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.normal',
+            defaultMessage: '正常',
+          }),
+          status: 'enabled',
+        },
+        disabled: {
+          text: intl.formatMessage({
+            id: 'pages.searchTable.titleStatus.disable',
+            defaultMessage: '已禁用',
+          }),
+          status: 'disabled',
+        },
+      },
+      render: (_, record) => (
+        <Badge
+          status={record.state?.value === 'enabled' ? 'success' : 'error'}
+          text={record.state?.text}
+        />
+      ),
     },
     {
       title: intl.formatMessage({
@@ -85,247 +107,153 @@ const DataSource = () => {
         defaultMessage: '操作',
       }),
       valueType: 'option',
-      align: 'center',
       width: 200,
-      render: (text, record) => [
-        <a key="editable" onClick={() => CurdModel.update(record)}>
-          <Tooltip
-            title={intl.formatMessage({
+      render: (_, record) => [
+        <PermissionButton
+          style={{ padding: 0 }}
+          type="link"
+          isPermission={userPermission.update}
+          key="editable"
+          onClick={() => {
+            setCurrent(record);
+            setVisible(true);
+          }}
+          tooltip={{
+            title: intl.formatMessage({
               id: 'pages.data.option.edit',
               defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
-          </Tooltip>
-        </a>,
-        <a key="status">
-          <Popconfirm
-            title={intl.formatMessage({
+            }),
+          }}
+        >
+          <EditOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          style={{ padding: 0 }}
+          type="link"
+          isPermission={userPermission.update}
+          key="manage"
+          onClick={() => {
+            const url = getMenuPathByCode(MENUS_CODE[`system/DataSource/Management`]);
+            history.push(`${url}?id=${record.id}`);
+          }}
+          tooltip={{
+            title: '管理',
+          }}
+        >
+          <DatabaseOutlined />
+        </PermissionButton>,
+        <PermissionButton
+          style={{ padding: 0 }}
+          isPermission={userPermission.action}
+          type="link"
+          key="changeState"
+          popConfirm={{
+            title: intl.formatMessage({
               id: `pages.data.option.${
-                record.state.value === 'disabled' ? 'enabled' : 'disabled'
+                record.state?.value === 'enabled' ? 'disabled' : 'enabled'
               }.tips`,
-              defaultMessage: `确认${record.state.value === 'disabled' ? '启' : '禁'}用?`,
-            })}
-            onConfirm={async () => {
-              const state = record.state.value === 'disabled' ? 'enable' : 'disable';
-              await service.changeStatus(record.id, state);
-              message.success(
-                intl.formatMessage({
-                  id: 'pages.data.option.success',
-                  defaultMessage: '操作成功!',
-                }),
+              defaultMessage: `确认${record.state?.value === 'enabled' ? '禁用' : '启用'}?`,
+            }),
+            onConfirm: async () => {
+              const resp = await service.changeStatus(
+                record.id,
+                record.state?.value === 'enabled' ? 'disable' : 'enable',
               );
+              if (resp.status === 200) {
+                message.success(
+                  intl.formatMessage({
+                    id: 'pages.data.option.success',
+                    defaultMessage: '操作成功!',
+                  }),
+                );
+                actionRef.current?.reload();
+              }
+            },
+          }}
+          tooltip={{
+            title: intl.formatMessage({
+              id: `pages.data.option.${record.state?.value === 'enabled' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state?.value === 'enabled' ? '禁用' : '启用',
+            }),
+          }}
+        >
+          {record.state?.value === 'enabled' ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
+        </PermissionButton>,
+        <PermissionButton
+          type="link"
+          key="delete"
+          style={{ padding: 0 }}
+          isPermission={userPermission.delete}
+          disabled={record.state?.value === 'enabled'}
+          tooltip={{ title: record.state?.value === 'disabled' ? '删除' : '请先禁用,再删除。' }}
+        >
+          <Popconfirm
+            onConfirm={async () => {
+              await service.remove(record.id);
               actionRef.current?.reload();
             }}
+            title="确认删除?"
           >
-            <Tooltip
-              title={intl.formatMessage({
-                id: `pages.data.option.${
-                  record.state.value === 'enabled' ? 'disabled' : 'enabled'
-                }`,
-                defaultMessage: record.state.text,
-              })}
-            >
-              {stateIconMap[record.state.value]}
-            </Tooltip>
+            <DeleteOutlined />
           </Popconfirm>
-        </a>,
+        </PermissionButton>,
       ],
     },
   ];
 
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      grid: {
-        type: 'void',
-        'x-component': 'FormGrid',
-        'x-component-props': {
-          minColumns: [1],
-          maxColumns: [2],
-        },
-        properties: {
-          name: {
-            title: intl.formatMessage({
-              id: 'pages.table.name',
-              defaultMessage: '名称',
-            }),
-            'x-component': 'Input',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-          },
-          typeId: {
-            title: intl.formatMessage({
-              id: 'pages.table.type',
-              defaultMessage: '类型',
-            }),
-            'x-component': 'Select',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            enum: type,
-          },
-          'shareConfig.adminUrl': {
-            title: '管理地址',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            required: true,
-            default: 'http://localhost:15672',
-            'x-visible': false,
-            'x-component': 'Input',
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="rabbitmq"?"visible":"none"}}',
-                },
-              },
-            },
-          },
-          'shareConfig.addresses': {
-            title: '链接地址',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            required: true,
-            default: 'localhost:5672',
-            'x-component': 'Input',
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="rabbitmq"?"visible":"none"}}',
-                },
-              },
-            },
-          },
-          'shareConfig.virtualHost': {
-            title: '虚拟域',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            required: true,
-            default: '/',
-            'x-component': 'Input',
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="rabbitmq"?"visible":"none"}}',
-                },
-              },
-            },
-          },
-          'shareConfig.username': {
-            title: '用户名',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            'x-component': 'Input',
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="rabbitmq"?"visible":"none"}}',
-                },
-              },
-            },
-          },
-          'shareConfig.password': {
-            title: '密码',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 1,
-              labelCol: 6,
-            },
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="rabbitmq"?"visible":"none"}}',
-                },
-              },
-            },
-            'x-component': 'Input',
-          },
-          'shareConfig.bootstrapServers': {
-            title: '地址',
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 2,
-              labelCol: 3,
-              wrapperCol: 20,
-            },
-            'x-component': 'Select',
-            'x-component-props': {
-              mode: 'tags',
-            },
-            'x-display': 'none',
-            'x-reactions': {
-              dependencies: ['typeId'],
-              fulfill: {
-                state: {
-                  display: '{{$deps[0]==="kafka"?"visible":"none"}}',
-                },
-              },
-            },
-          },
-          description: {
-            title: intl.formatMessage({
-              id: 'pages.table.description',
-              defaultMessage: '说明',
-            }),
-            'x-decorator': 'FormItem',
-            'x-decorator-props': {
-              gridSpan: 2,
-              labelCol: 3,
-              wrapperCol: 20,
-            },
-            'x-component': 'Input.TextArea',
-            'x-component-props': {
-              rows: 4,
-            },
-          },
-        },
-      },
-    },
-  };
+  const [param, setParam] = useState({});
 
   return (
     <PageContainer>
-      <BaseCrud<DataSourceItem>
-        modelConfig={{
-          width: 1000,
+      <SearchComponent<DataSourceItem>
+        field={columns}
+        target="datasource"
+        onSearch={(data) => {
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setParam(data);
         }}
-        columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.system.datasource.',
-          defaultMessage: '数据源管理',
-        })}
-        schema={schema}
+      />
+      <ProTable<DataSourceItem>
         actionRef={actionRef}
+        params={param}
+        columns={columns}
+        search={false}
+        rowKey="id"
+        headerTitle={
+          <PermissionButton
+            onClick={() => {
+              setCurrent({});
+              setVisible(true);
+            }}
+            isPermission={userPermission.add}
+            key="add"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </PermissionButton>
+        }
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
       />
+      {visible && (
+        <Save
+          close={() => {
+            setVisible(false);
+          }}
+          data={current}
+          reload={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
     </PageContainer>
   );
-};
+});
 export default DataSource;

+ 25 - 9
src/pages/system/DataSource/service.ts

@@ -1,19 +1,35 @@
 import BaseService from '@/utils/BaseService';
+import SystemConst from '@/utils/const';
 import { request } from '@@/plugin-request/request';
-import { defer, from } from 'rxjs';
-import { filter, map } from 'rxjs/operators';
 
 class Service extends BaseService<DataSourceItem> {
   changeStatus = (id: string, status: 'disable' | 'enable') =>
     request(`${this.uri}/${id}/_${status}`, { method: 'PUT' });
 
-  getType = () =>
-    defer(() =>
-      from(request(`${this.uri}/types`, { method: 'GET' })).pipe(
-        filter((resp) => resp.status === 200),
-        map((resp) => resp.result),
-      ),
-    );
+  getType = () => request(`${this.uri}/types`, { method: 'GET' });
+
+  rdbTree = (datasourceId: string) =>
+    request(`${SystemConst.API_BASE}/datasource/rdb/${datasourceId}/tables?includeColumns=false`, {
+      method: 'GET',
+    });
+
+  rdbTables = (datasourceId: string, table: string, data?: any) =>
+    request(`${SystemConst.API_BASE}/datasource/rdb/${datasourceId}/table/${table}`, {
+      method: 'GET',
+      params: data,
+    });
+
+  saveRdbTables = (datasourceId: string, data: any) =>
+    request(`${SystemConst.API_BASE}/datasource/rdb/${datasourceId}/table`, {
+      method: 'PATCH',
+      data,
+    });
+
+  delRdbTablesColumn = (datasourceId: string, table: string, data: any) =>
+    request(`${SystemConst.API_BASE}/datasource/rdb/${datasourceId}/table/${table}/drop-column`, {
+      method: 'POST',
+      data,
+    });
 }
 
 export default Service;

+ 8 - 4
src/pages/system/DataSource/typings.d.ts

@@ -3,7 +3,7 @@ type DataSourceItem = {
   name: string;
   shareCluster: true;
   shareConfig: Record<string, any>;
-  state: {
+  state?: {
     text: string;
     value: string;
   };
@@ -15,8 +15,12 @@ type DataSourceItem = {
 };
 
 type DataSourceType = {
-  label: string;
-  value: string;
-  id: string;
   name: string;
+  comment: string;
+  length: number;
+  notnull: boolean;
+  precision: number;
+  primaryKey: boolean;
+  scale: number;
+  type: string;
 };

+ 3 - 0
src/utils/menu/index.ts

@@ -69,6 +69,9 @@ const extraRouteObj = {
       { code: 'View', name: 'Api详情' },
     ],
   },
+  'system/DataSource': {
+    children: [{ code: 'Management', name: '管理' }],
+  },
 };
 //额外路由
 export const extraRouteArr = [

+ 1 - 0
src/utils/menu/router.ts

@@ -77,6 +77,7 @@ export enum MENUS_CODE {
   'rule-engine/Alarm/Configuration' = 'rule-engine/Alarm/Configuration',
   'simulator/Device' = 'simulator/Device',
   'system/DataSource' = 'system/DataSource',
+  'system/DataSource/Management' = 'system/DataSource/Management',
   'system/Department/Assets' = 'system/Department/Assets',
   'system/Department/Member' = 'system/Department/Member',
   'system/Department' = 'system/Department',