Procházet zdrojové kódy

feat(merge): merge sc

feat: 规则编排和日志管理
Lind před 3 roky
rodič
revize
3c3c4dd6c3

+ 43 - 0
src/components/ProTableCard/CardItems/ruleInstance.tsx

@@ -0,0 +1,43 @@
+import { Avatar, Card } from 'antd';
+import React from 'react';
+import { BadgeStatus } from '@/components';
+import { StatusColorEnum } from '@/components/BadgeStatus';
+import '@/style/common.less';
+import type { InstanceItem } from '@/pages/rule-engine/Instance/typings';
+
+export interface RuleInstanceCardProps extends InstanceItem {
+  actions?: React.ReactNode[];
+  avatarSize?: number;
+}
+
+export default (props: RuleInstanceCardProps) => {
+  return (
+    <Card style={{ width: '100%' }} cover={null} actions={props.actions}>
+      <div className={'pro-table-card-item'}>
+        <div className={'card-item-avatar'}>
+          <Avatar
+            size={props.avatarSize || 64}
+            src={
+              'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/staticfiles/passport/OKR.png'
+            }
+          />
+        </div>
+        <div className={'card-item-body'}>
+          <div className={'card-item-header'}>
+            <span className={'card-item-header-name ellipsis'}>{props.name}</span>
+            <BadgeStatus
+              status={props.state.value}
+              text={props.state.text}
+              statusNames={{
+                started: StatusColorEnum.success,
+                stopped: StatusColorEnum.error,
+                disable: StatusColorEnum.processing,
+              }}
+            />
+          </div>
+          {props.description}
+        </div>
+      </div>
+    </Card>
+  );
+};

+ 52 - 0
src/pages/Log/Access/Detail/index.tsx

@@ -0,0 +1,52 @@
+import { Descriptions, Modal } from 'antd';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<AccessLogItem>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<Partial<AccessLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Descriptions bordered>
+        <Descriptions.Item label="URL">{data?.url}</Descriptions.Item>
+        <Descriptions.Item label="请求方法" span={2}>
+          {data?.httpMethod}
+        </Descriptions.Item>
+        <Descriptions.Item label="动作">{data?.action}</Descriptions.Item>
+        <Descriptions.Item label="类名" span={2}>
+          {data?.target}
+        </Descriptions.Item>
+        <Descriptions.Item label="方法名">{data?.method}</Descriptions.Item>
+        <Descriptions.Item label="IP" span={2}>
+          {data?.ip}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求时间">
+          {moment(data?.requestTime).format('YYYY-MM-DD HH:mm:ss')}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求耗时" span={2}>
+          {(data?.responseTime || 0) - (data?.requestTime || 0)}ms
+        </Descriptions.Item>
+        <Descriptions.Item label="请求头" span={3}>
+          {JSON.stringify(data?.httpHeaders)}
+        </Descriptions.Item>
+        <Descriptions.Item label="请求参数" span={3}>
+          {JSON.stringify(data?.parameters)}
+        </Descriptions.Item>
+        <Descriptions.Item label="异常信息" span={3}>
+          {data?.exception}
+        </Descriptions.Item>
+      </Descriptions>
+    </Modal>
+  );
+};
+export default Detail;

+ 58 - 36
src/pages/log/Access/index.tsx

@@ -1,29 +1,29 @@
-import { PageContainer } from '@ant-design/pro-layout';
 import BaseService from '@/utils/BaseService';
-import React, { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import type { AccessLogItem } from '@/pages/log/Access/typings';
+import type { AccessLogItem } from '@/pages/Log/Access/typings';
 import moment from 'moment';
 import { Tag, Tooltip } from 'antd';
-import { CurdModel } from '@/components/BaseCrud/model';
-import { EditOutlined } from '@ant-design/icons';
+import { EyeOutlined } from '@ant-design/icons';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import BaseCrud from '@/components/BaseCrud';
+import ProTable from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import Detail from '@/pages/Log/Access/Detail';
 
 const service = new BaseService('logger/access');
-const Access: React.FC = () => {
+
+const Access = () => {
   const actionRef = useRef<ActionType>();
   const intl = useIntl();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<AccessLogItem>>({});
+
   const columns: ProColumns<AccessLogItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
       title: 'IP',
       dataIndex: 'ip',
-      // ellipsis: true
+      ellipsis: true,
     },
     {
       title: intl.formatMessage({
@@ -31,7 +31,12 @@ const Access: React.FC = () => {
         defaultMessage: '请求路径',
       }),
       dataIndex: 'url',
-      // ellipsis: true,
+      ellipsis: true,
+    },
+    {
+      title: '请求方法',
+      dataIndex: 'httpMethod',
+      ellipsis: true,
     },
     {
       title: intl.formatMessage({
@@ -39,7 +44,7 @@ const Access: React.FC = () => {
         defaultMessage: '说明',
       }),
       dataIndex: 'description',
-      // ellipsis: true,
+      ellipsis: true,
       render: (text, record) => {
         return `${record.action}-${record.describe}`;
       },
@@ -51,8 +56,10 @@ const Access: React.FC = () => {
       }),
       dataIndex: 'requestTime',
       sorter: true,
+      valueType: 'dateTime',
       defaultSortOrder: 'descend',
-      // ellipsis: true,
+      ellipsis: true,
+      width: 200,
       renderText: (text: string) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
     },
     {
@@ -60,7 +67,6 @@ const Access: React.FC = () => {
         id: 'pages.log.access.requestTimeConsuming',
         defaultMessage: '请求耗时',
       }),
-      // width: 100,
       renderText: (record: AccessLogItem) => (
         <Tag color="purple">{record.responseTime - record.requestTime}ms</Tag>
       ),
@@ -80,35 +86,51 @@ const Access: React.FC = () => {
       }),
       valueType: 'option',
       align: 'center',
-      width: 200,
       render: (text, record) => [
-        <a key="editable" onClick={() => CurdModel.update(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
+        <a
+          key="editable"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+        >
+          <Tooltip title={'查看'}>
+            <EyeOutlined />
           </Tooltip>
         </a>,
       ],
     },
   ];
   return (
-    <PageContainer>
-      <BaseCrud<AccessLogItem>
+    <>
+      <SearchComponent<AccessLogItem>
+        field={columns}
+        target="access-log"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<AccessLogItem>
         columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.log.access',
-          defaultMessage: '访问日志',
-        })}
-        schema={{}}
-        toolBar={[]}
+        params={param}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'responseTime', order: 'desc' }] })
+        }
+        defaultParams={{ sorts: [{ responseTime: 'desc' }] }}
+        search={false}
         actionRef={actionRef}
       />
-    </PageContainer>
+      {visible && (
+        <Detail
+          data={current}
+          close={() => {
+            setVisible(false);
+            setCurrent({});
+          }}
+        />
+      )}
+    </>
   );
 };
 export default Access;

src/pages/log/Access/typings.d.ts → src/pages/Log/Access/typings.d.ts


+ 35 - 0
src/pages/Log/System/Detail/index.tsx

@@ -0,0 +1,35 @@
+import { Input, Modal, Space, Tag } from 'antd';
+import type { SystemLogItem } from '@/pages/Log/System/typings';
+import { useEffect, useState } from 'react';
+import moment from 'moment';
+
+interface Props {
+  data: Partial<SystemLogItem>;
+  close: () => void;
+}
+
+const Detail = (props: Props) => {
+  const [data, setDada] = useState<Partial<SystemLogItem>>(props.data || {});
+
+  useEffect(() => {
+    setDada(props.data);
+  }, [props.data]);
+
+  return (
+    <Modal title={'详情'} visible onCancel={props.close} onOk={props.close} width={1000}>
+      <Space>
+        <span>[{data?.threadName}]</span>
+        <span>{moment(data?.createTime).format('YYYY-MM-DD HH:mm:ss')}</span>
+        <span>{data?.className}</span>
+      </Space>
+      <p>
+        <Tag color={data?.level === 'ERROR' ? 'red' : 'orange'}>{data?.level}</Tag>
+        {data?.message}
+      </p>
+      <div>
+        <Input.TextArea rows={20} value={data?.exceptionStack} />
+      </div>
+    </Modal>
+  );
+};
+export default Detail;

+ 65 - 40
src/pages/log/System/index.tsx

@@ -1,35 +1,25 @@
-import { PageContainer } from '@ant-design/pro-layout';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import type { SystemLogItem } from '@/pages/log/System/typings';
+import type { SystemLogItem } from '@/pages/Log/System/typings';
 import { Tag, Tooltip } from 'antd';
 import moment from 'moment';
-import BaseCrud from '@/components/BaseCrud';
 import BaseService from '@/utils/BaseService';
-import { CurdModel } from '@/components/BaseCrud/model';
-import { EditOutlined } from '@ant-design/icons';
+import { EyeOutlined } from '@ant-design/icons';
+import ProTable from '@jetlinks/pro-table';
+import SearchComponent from '@/components/SearchComponent';
+import Detail from '@/pages/Log/System/Detail';
 
 const service = new BaseService<SystemLogItem>('logger/system');
 const System = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const [param, setParam] = useState({});
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<SystemLogItem>>({});
 
   const columns: ProColumns<SystemLogItem>[] = [
     {
-      dataIndex: 'index',
-      valueType: 'indexBorder',
-      width: 48,
-    },
-    {
-      title: intl.formatMessage({
-        id: 'pages.log.system.threadName',
-        defaultMessage: '线程',
-      }),
-      dataIndex: 'threadName',
-      ellipsis: true,
-    },
-    {
       title: intl.formatMessage({
         id: 'pages.table.name',
         defaultMessage: '名称',
@@ -38,13 +28,29 @@ const System = () => {
       ellipsis: true,
     },
     {
-      title: intl.formatMessage({
-        id: 'pages.log.system.level',
-        defaultMessage: '级别',
-      }),
+      title: '日志级别',
       dataIndex: 'level',
       width: 80,
       render: (text) => <Tag color={text === 'ERROR' ? 'red' : 'orange'}>{text}</Tag>,
+      valueType: 'select',
+      valueEnum: {
+        ERROR: {
+          text: 'ERROR',
+          status: 'ERROR',
+        },
+        INFO: {
+          text: 'INFO',
+          status: 'INFO',
+        },
+        DEBUG: {
+          text: 'DEBUG',
+          status: 'DEBUG',
+        },
+        WARN: {
+          text: 'WARN',
+          status: 'WARN',
+        },
+      },
     },
     {
       title: intl.formatMessage({
@@ -72,6 +78,7 @@ const System = () => {
       width: 200,
       sorter: true,
       ellipsis: true,
+      valueType: 'dateTime',
       defaultSortOrder: 'descend',
       renderText: (text) => moment(text).format('YYYY-MM-DD HH:mm:ss'),
     },
@@ -84,32 +91,50 @@ const System = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a key="editable" onClick={() => CurdModel.update(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.data.option.edit',
-              defaultMessage: '编辑',
-            })}
-          >
-            <EditOutlined />
+        <a
+          key="editable"
+          onClick={() => {
+            setVisible(true);
+            setCurrent(record);
+          }}
+        >
+          <Tooltip title="查看">
+            <EyeOutlined />
           </Tooltip>
         </a>,
       ],
     },
   ];
   return (
-    <PageContainer>
-      <BaseCrud<SystemLogItem>
+    <>
+      <SearchComponent<SystemLogItem>
+        field={columns}
+        target="system-log"
+        onSearch={(data) => {
+          actionRef.current?.reset?.();
+          setParam(data);
+        }}
+      />
+      <ProTable<SystemLogItem>
         columns={columns}
-        service={service}
-        title={intl.formatMessage({
-          id: 'pages.log.system',
-          defaultMessage: '系统日志',
-        })}
-        schema={{}}
+        params={param}
+        request={async (params) =>
+          service.query({ ...params, sorts: [{ name: 'createTime', order: 'desc' }] })
+        }
+        defaultParams={{ sorts: [{ createTime: 'desc' }] }}
+        search={false}
         actionRef={actionRef}
       />
-    </PageContainer>
+      {visible && (
+        <Detail
+          data={current}
+          close={() => {
+            setVisible(false);
+            setCurrent({});
+          }}
+        />
+      )}
+    </>
   );
 };
 export default System;

src/pages/log/System/typings.d.ts → src/pages/Log/System/typings.d.ts


+ 28 - 0
src/pages/Log/index.tsx

@@ -0,0 +1,28 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { useState } from 'react';
+import Access from '@/pages/Log/Access';
+import System from '@/pages/Log/System';
+
+const Log = () => {
+  const [tab, setTab] = useState<string>('access');
+  const list = [
+    {
+      key: 'access',
+      tab: '访问日志',
+      component: <Access />,
+    },
+    {
+      key: 'system',
+      tab: '系统日志',
+      component: <System />,
+    },
+  ];
+
+  return (
+    <PageContainer onTabChange={setTab} tabList={list} tabActiveKey={tab}>
+      {list.find((k) => k.key === tab)?.component}
+    </PageContainer>
+  );
+};
+
+export default Log;

+ 122 - 0
src/pages/rule-engine/Instance/Save/index.tsx

@@ -0,0 +1,122 @@
+import { message, Modal } from 'antd';
+import { useIntl } from 'umi';
+import { createForm } from '@formily/core';
+import { createSchemaField } from '@formily/react';
+import React, { useEffect, useState } from 'react';
+import * as ICONS from '@ant-design/icons';
+import { Form, FormItem, Input } from '@formily/antd';
+import type { ISchema } from '@formily/json-schema';
+import { service } from '@/pages/rule-engine/Instance';
+import type { InstanceItem } from '../typings';
+
+interface Props {
+  data: Partial<InstanceItem>;
+  close: () => void;
+}
+
+const Save = (props: Props) => {
+  const intl = useIntl();
+
+  const [data, setData] = useState<Partial<InstanceItem>>(props.data);
+
+  useEffect(() => {
+    setData(props.data);
+  }, [props.data]);
+
+  const form = createForm({
+    validateFirst: true,
+    initialValues: data,
+  });
+
+  const SchemaField = createSchemaField({
+    components: {
+      FormItem,
+      Input,
+    },
+    scope: {
+      icon(name: any) {
+        return React.createElement(ICONS[name]);
+      },
+    },
+  });
+
+  const schema: ISchema = {
+    type: 'object',
+    properties: {
+      name: {
+        title: intl.formatMessage({
+          id: 'pages.table.name',
+          defaultMessage: '名称',
+        }),
+        type: 'string',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input',
+        name: 'name',
+        'x-component-props': {
+          placeholder: '请输入名称',
+        },
+        'x-validator': [
+          {
+            max: 64,
+            message: '最多可输入64个字符',
+          },
+          {
+            required: true,
+            message: '请输入名称',
+          },
+        ],
+      },
+      description: {
+        title: '说明',
+        'x-decorator': 'FormItem',
+        'x-component': 'Input.TextArea',
+        'x-component-props': {
+          rows: 5,
+          placeholder: '请输入说明',
+        },
+        'x-validator': [
+          {
+            max: 200,
+            message: '最多可输入200个字符',
+          },
+        ],
+      },
+    },
+  };
+
+  const save = async () => {
+    const value = await form.submit<InstanceItem>();
+    let response = undefined;
+    if (!props.data?.id) {
+      response = await service.saveRule(value);
+    } else {
+      response = await service.modify(props.data.id, { ...props.data, ...value });
+    }
+    if (response.status === 200) {
+      message.success(
+        intl.formatMessage({
+          id: 'pages.data.option.success',
+          defaultMessage: '操作成功',
+        }),
+      );
+      props.close();
+    } else {
+      message.error('操作失败!');
+    }
+  };
+
+  return (
+    <Modal
+      title={props.data?.id ? '编辑' : '新增'}
+      visible
+      onCancel={props.close}
+      onOk={save}
+      width="35vw"
+    >
+      <Form form={form} layout="vertical">
+        <SchemaField schema={schema} />
+      </Form>
+    </Modal>
+  );
+};
+export default Save;

+ 259 - 78
src/pages/rule-engine/Instance/index.tsx

@@ -1,24 +1,136 @@
 import { PageContainer } from '@ant-design/pro-layout';
-import BaseService from '@/utils/BaseService';
+import Service from '@/pages/rule-engine/Instance/serivce';
 import type { InstanceItem } from '@/pages/rule-engine/Instance/typings';
-import { useRef } from 'react';
+import { useRef, useState } from 'react';
 import type { ActionType, ProColumns } from '@jetlinks/pro-table';
 import {
-  CaretRightOutlined,
+  CheckCircleOutlined,
+  DeleteOutlined,
   EditOutlined,
   EyeOutlined,
-  MinusOutlined,
-  ReloadOutlined,
+  PlusOutlined,
+  StopOutlined,
 } from '@ant-design/icons';
-import { Badge, Tooltip } from 'antd';
-import BaseCrud from '@/components/BaseCrud';
+import { Badge, Button, message, Popconfirm, Tooltip } from 'antd';
 import { useIntl } from '@@/plugin-locale/localeExports';
-import type { ISchema } from '@formily/json-schema';
+import SearchComponent from '@/components/SearchComponent';
+import { ProTableCard } from '@/components';
+import RuleInstanceCard from '@/components/ProTableCard/CardItems/ruleInstance';
+import Save from '@/pages/rule-engine/Instance/Save';
+import SystemConst from '@/utils/const';
+
+export const service = new Service('rule-engine/instance');
 
-export const service = new BaseService<InstanceItem>('rule-engine/instance');
 const Instance = () => {
   const intl = useIntl();
   const actionRef = useRef<ActionType>();
+  const [visible, setVisible] = useState<boolean>(false);
+  const [current, setCurrent] = useState<Partial<InstanceItem>>({});
+  const [searchParams, setSearchParams] = useState<any>({});
+
+  const tools = (record: InstanceItem) => [
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.edit',
+        defaultMessage: '编辑',
+      })}
+      key={'edit'}
+    >
+      <Button
+        type={'link'}
+        style={{ padding: 0 }}
+        onClick={() => {
+          setCurrent(record);
+          setVisible(true);
+        }}
+      >
+        <EditOutlined />
+      </Button>
+    </Tooltip>,
+    <Tooltip
+      title={intl.formatMessage({
+        id: 'pages.data.option.detail',
+        defaultMessage: '查看',
+      })}
+      key={'detail'}
+    >
+      <Button
+        type={'link'}
+        style={{ padding: 0 }}
+        onClick={() => {
+          window.open(`/${SystemConst.API_BASE}/rule-editor/index.html#flow/${record.id}`);
+        }}
+      >
+        <EyeOutlined />
+      </Button>
+    </Tooltip>,
+    <Popconfirm
+      key={'state'}
+      title={intl.formatMessage({
+        id: `pages.data.option.${record.state.value !== 'stopped' ? 'disabled' : 'enabled'}.tips`,
+        defaultMessage: '确认禁用?',
+      })}
+      onConfirm={async () => {
+        if (record.state.value !== 'stopped') {
+          await service.stopRule(record.id);
+        } else {
+          await service.startRule(record.id);
+        }
+        message.success(
+          intl.formatMessage({
+            id: 'pages.data.option.success',
+            defaultMessage: '操作成功!',
+          }),
+        );
+        actionRef.current?.reload();
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: `pages.data.option.${record.state.value !== 'stopped' ? 'disabled' : 'enabled'}`,
+          defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
+        })}
+      >
+        <Button type={'link'} style={{ padding: 0 }}>
+          {record.state.value !== 'stopped' ? <StopOutlined /> : <CheckCircleOutlined />}
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+    <Popconfirm
+      title={intl.formatMessage({
+        id:
+          record.state.value === 'notActive'
+            ? 'pages.data.option.remove.tips'
+            : 'pages.device.instance.deleteTip',
+      })}
+      key={'delete'}
+      onConfirm={async () => {
+        if (record.state.value === 'notActive') {
+          await service.remove(record.id);
+          message.success(
+            intl.formatMessage({
+              id: 'pages.data.option.success',
+              defaultMessage: '操作成功!',
+            }),
+          );
+          actionRef.current?.reload();
+        } else {
+          message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+        }
+      }}
+    >
+      <Tooltip
+        title={intl.formatMessage({
+          id: 'pages.data.option.remove',
+          defaultMessage: '删除',
+        })}
+      >
+        <Button type={'link'} style={{ padding: 0 }}>
+          <DeleteOutlined />
+        </Button>
+      </Tooltip>
+    </Popconfirm>,
+  ];
 
   const columns: ProColumns<InstanceItem>[] = [
     {
@@ -35,6 +147,21 @@ const Instance = () => {
       render: (text: any) => (
         <Badge color={text?.value === 'stopped' ? 'red' : 'green'} text={text?.text} />
       ),
+      valueType: 'select',
+      valueEnum: {
+        started: {
+          text: '已启动',
+          status: 'started',
+        },
+        disable: {
+          text: '已禁用',
+          status: 'disable',
+        },
+        stopped: {
+          text: '已停止',
+          status: 'stopped',
+        },
+      },
     },
     {
       dataIndex: 'description',
@@ -50,7 +177,13 @@ const Instance = () => {
       align: 'center',
       width: 200,
       render: (text, record) => [
-        <a key={'edit'} onClick={() => console.log(record)}>
+        <a
+          key={'edit'}
+          onClick={() => {
+            setCurrent(record);
+            setVisible(true);
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.edit',
@@ -60,7 +193,12 @@ const Instance = () => {
             <EditOutlined />
           </Tooltip>
         </a>,
-        <a key={'see'}>
+        <a
+          key={'see'}
+          onClick={() => {
+            window.open(`/${SystemConst.API_BASE}/rule-editor/index.html#flow/${record.id}`);
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.ruleEngine.option.detail',
@@ -70,94 +208,137 @@ const Instance = () => {
             <EyeOutlined />
           </Tooltip>
         </a>,
-        <a key={'enabled'} onClick={() => console.log(record)}>
-          <Tooltip
-            title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.start',
-              defaultMessage: '启动',
-            })}
-          >
-            <CaretRightOutlined />
-          </Tooltip>
-        </a>,
-        <a key={'reload'} onClick={() => console.log(record)}>
+        <Popconfirm
+          key={'state'}
+          title={intl.formatMessage({
+            id: `pages.data.option.${
+              record.state.value !== 'stopped' ? 'disabled' : 'enabled'
+            }.tips`,
+            defaultMessage: '确认禁用?',
+          })}
+          onConfirm={async () => {
+            if (record.state.value !== 'stopped') {
+              await service.stopRule(record.id);
+            } else {
+              await service.startRule(record.id);
+            }
+            message.success(
+              intl.formatMessage({
+                id: 'pages.data.option.success',
+                defaultMessage: '操作成功!',
+              }),
+            );
+            actionRef.current?.reload();
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
-              id: 'pages.ruleEngine.option.restart',
-              defaultMessage: '重启',
+              id: `pages.data.option.${record.state.value !== 'stopped' ? 'disabled' : 'enabled'}`,
+              defaultMessage: record.state.value !== 'notActive' ? '禁用' : '启用',
             })}
           >
-            <ReloadOutlined />
+            <Button type={'link'} style={{ padding: 0 }}>
+              {record.state.value !== 'stopped' ? <StopOutlined /> : <CheckCircleOutlined />}
+            </Button>
           </Tooltip>
-        </a>,
-
-        <a key={'delete'}>
+        </Popconfirm>,
+        <Popconfirm
+          title={intl.formatMessage({
+            id:
+              record.state.value === 'notActive'
+                ? 'pages.data.option.remove.tips'
+                : 'pages.device.instance.deleteTip',
+          })}
+          key={'delete'}
+          onConfirm={async () => {
+            if (record.state.value === 'notActive') {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
+            } else {
+              message.error(intl.formatMessage({ id: 'pages.device.instance.deleteTip' }));
+            }
+          }}
+        >
           <Tooltip
             title={intl.formatMessage({
               id: 'pages.data.option.remove',
               defaultMessage: '删除',
             })}
           >
-            <MinusOutlined />
+            <Button type={'link'} style={{ padding: 0 }}>
+              <DeleteOutlined />
+            </Button>
           </Tooltip>
-        </a>,
+        </Popconfirm>,
       ],
     },
   ];
 
-  const schema: ISchema = {
-    type: 'object',
-    properties: {
-      name: {
-        title: intl.formatMessage({
-          id: 'pages.table.name',
-          defaultMessage: '名称',
-        }),
-        type: 'string',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input',
-        name: 'name',
-        'x-validator': [
-          {
-            max: 64,
-            message: '最多可输入64个字符',
-          },
-          {
-            required: true,
-            message: '请输入名称',
-          },
-        ],
-      },
-      description: {
-        title: '说明',
-        'x-decorator': 'FormItem',
-        'x-component': 'Input.TextArea',
-        'x-component-props': {
-          rows: 5,
-        },
-        'x-validator': [
-          {
-            max: 200,
-            message: '最多可输入200个字符',
-          },
-        ],
-      },
-    },
-  };
-
   return (
     <PageContainer>
-      <BaseCrud
+      <SearchComponent<InstanceItem>
+        field={columns}
+        target="device-instance"
+        onSearch={(data) => {
+          console.log(data);
+          // 重置分页数据
+          actionRef.current?.reset?.();
+          setSearchParams(data);
+        }}
+      />
+      <ProTableCard<InstanceItem>
         columns={columns}
-        service={service}
-        search={false}
-        title={intl.formatMessage({
-          id: 'pages.ruleEngine.instance',
-          defaultMessage: '规则实例',
-        })}
-        schema={schema}
         actionRef={actionRef}
+        params={searchParams}
+        options={{ fullScreen: true }}
+        request={(params) =>
+          service.query({
+            ...params,
+            sorts: [
+              {
+                name: 'createTime',
+                order: 'desc',
+              },
+            ],
+          })
+        }
+        rowKey="id"
+        search={false}
+        pagination={{ pageSize: 10 }}
+        headerTitle={[
+          <Button
+            onClick={() => {
+              setVisible(true);
+              setCurrent({});
+            }}
+            style={{ marginRight: 12 }}
+            key="button"
+            icon={<PlusOutlined />}
+            type="primary"
+          >
+            {intl.formatMessage({
+              id: 'pages.data.option.add',
+              defaultMessage: '新增',
+            })}
+          </Button>,
+        ]}
+        cardRender={(record) => <RuleInstanceCard {...record} actions={tools(record)} />}
       />
+      {visible && (
+        <Save
+          data={current}
+          close={() => {
+            setVisible(false);
+            actionRef.current?.reload();
+          }}
+        />
+      )}
     </PageContainer>
   );
 };

+ 24 - 0
src/pages/rule-engine/Instance/serivce.ts

@@ -0,0 +1,24 @@
+import BaseService from '@/utils/BaseService';
+import { request } from 'umi';
+import SystemConst from '@/utils/const';
+import type { InstanceItem } from './typings';
+
+class Service extends BaseService<InstanceItem> {
+  saveRule = (data: InstanceItem) =>
+    request(`/${SystemConst.API_BASE}/rule-editor/flows/_create`, {
+      method: 'POST',
+      data,
+    });
+
+  startRule = (id: string) =>
+    request(`/${SystemConst.API_BASE}/rule-engine/instance/${id}/_start`, {
+      method: 'POST',
+    });
+
+  stopRule = (id: string) =>
+    request(`/${SystemConst.API_BASE}/rule-engine/instance/${id}/_stop`, {
+      method: 'POST',
+    });
+}
+
+export default Service;

+ 1 - 0
src/pages/rule-engine/Instance/typings.d.ts

@@ -6,6 +6,7 @@ type InstanceItem = {
   modelMeta: string;
   modelType: string;
   modelVersion: number;
+  description?: string;
   state: {
     text: string;
     value: string;

+ 0 - 2
src/pages/system/Menu/index.tsx

@@ -100,7 +100,6 @@ export default observer(() => {
         defaultMessage: '排序',
       }),
       width: 80,
-      hideInSearch: true,
       dataIndex: 'sortIndex',
     },
     {
@@ -117,7 +116,6 @@ export default observer(() => {
         defaultMessage: '创建时间',
       }),
       width: 180,
-      valueType: 'dateTime',
       dataIndex: 'createTime',
       render: (_, record) => {
         return record.createTime ? moment(record.createTime).format('YYYY-MM-DD HH:mm:ss') : '-';

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

@@ -39,17 +39,13 @@ export const MENUS_CODE = {
   'link/Protocol': 'link/Protocol',
   'link/Type': 'link/Type',
   'link/AccessConfig': 'link/AccessConfig',
-  'log/Access': 'log/Access',
-  'log/System': 'log/System',
+  Log: 'Log',
   'media/Cascade': 'media/Cascade',
   'media/Config': 'media/Config',
   'media/Device': 'media/Device',
   'media/Reveal': 'media/Reveal',
-
-  'notice/Type': 'notice/Type',
   'notice/Config': 'notice/Config',
   'notice/Template': 'notice/Template',
-
   'rule-engine/Instance': 'rule-engine/Instance',
   'rule-engine/SQLRule': 'rule-engine/SQLRule',
   'rule-engine/Scene': 'rule-engine/Scene',