Selaa lähdekoodia

feat(device): update device table option

Lind 4 vuotta sitten
vanhempi
commit
19ad1900e5

+ 2 - 2
config/proxy.ts

@@ -9,8 +9,8 @@
 export default {
 export default {
   dev: {
   dev: {
     '/jetlinks': {
     '/jetlinks': {
-      // target: 'http://192.168.22.222:8844/',
-      // ws: 'ws://192.168.22.222:8844/',
+      // target: 'http://192.168.23.223:8800/',
+      // ws: 'ws://192.168.23.223:8800/',
       ws: 'ws://demo.jetlinks.cn/jetlinks',
       ws: 'ws://demo.jetlinks.cn/jetlinks',
       target: 'http://demo.jetlinks.cn/jetlinks',
       target: 'http://demo.jetlinks.cn/jetlinks',
       changeOrigin: true,
       changeOrigin: true,

+ 7 - 0
config/routes.ts

@@ -100,6 +100,13 @@
         component: './device/Instance',
         component: './device/Instance',
       },
       },
       {
       {
+        hideInMenu: true,
+        path: '/device/instance/detail/:id',
+        name: 'instance-detail',
+        icon: 'smile',
+        component: './device/Instance/Detail',
+      },
+      {
         path: '/device/command',
         path: '/device/command',
         name: 'command',
         name: 'command',
         icon: 'smile',
         icon: 'smile',

+ 2 - 0
package.json

@@ -71,6 +71,7 @@
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-list": "^1.10.8",
     "@jetlinks/pro-table": "^2.43.7",
     "@jetlinks/pro-table": "^2.43.7",
     "@umijs/route-utils": "^1.0.36",
     "@umijs/route-utils": "^1.0.36",
+    "ahooks": "^2.10.9",
     "antd": "^4.14.0",
     "antd": "^4.14.0",
     "classnames": "^2.2.6",
     "classnames": "^2.2.6",
     "isomorphic-form-data": "^2.0.0",
     "isomorphic-form-data": "^2.0.0",
@@ -83,6 +84,7 @@
     "react-dom": "^17.0.0",
     "react-dom": "^17.0.0",
     "react-helmet-async": "^1.0.4",
     "react-helmet-async": "^1.0.4",
     "rxjs": "^7.2.0",
     "rxjs": "^7.2.0",
+    "rxjs-websockets": "8",
     "umi": "^3.5.0",
     "umi": "^3.5.0",
     "umi-serve": "^1.9.10"
     "umi-serve": "^1.9.10"
   },
   },

+ 19 - 15
src/components/BaseCrud/index.tsx

@@ -33,6 +33,7 @@ export type Props<T> = {
   actionRef: React.MutableRefObject<ActionType | undefined>;
   actionRef: React.MutableRefObject<ActionType | undefined>;
   modelConfig?: ModalProps;
   modelConfig?: ModalProps;
   request?: (params: any) => Promise<Partial<RequestData<T>>>;
   request?: (params: any) => Promise<Partial<RequestData<T>>>;
+  toolBar?: React.ReactNode[];
 };
 };
 
 
 const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
 const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
@@ -49,6 +50,7 @@ const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
     schemaConfig,
     schemaConfig,
     modelConfig,
     modelConfig,
     request,
     request,
+    toolBar,
   } = props;
   } = props;
 
 
   return (
   return (
@@ -74,21 +76,23 @@ const BaseCrud = <T extends Record<string, any>>(props: Props<T>) => {
         dateFormatter="string"
         dateFormatter="string"
         headerTitle={title}
         headerTitle={title}
         defaultParams={defaultParams}
         defaultParams={defaultParams}
-        toolBarRender={() => [
-          <Button onClick={CurdModel.add} key="button" icon={<PlusOutlined />} type="primary">
-            {intl.formatMessage({
-              id: 'pages.data.option.add',
-              defaultMessage: '新增',
-            })}
-          </Button>,
-          menu && (
-            <Dropdown key="menu" overlay={menu}>
-              <Button>
-                <EllipsisOutlined />
-              </Button>
-            </Dropdown>
-          ),
-        ]}
+        toolBarRender={() =>
+          toolBar || [
+            <Button onClick={CurdModel.add} key="button" icon={<PlusOutlined />} type="primary">
+              {intl.formatMessage({
+                id: 'pages.data.option.add',
+                defaultMessage: '新增',
+              })}
+            </Button>,
+            menu && (
+              <Dropdown key="menu" overlay={menu}>
+                <Button>
+                  <EllipsisOutlined />
+                </Button>
+              </Dropdown>
+            ),
+          ]
+        }
       />
       />
       <Save
       <Save
         reload={() => actionRef.current?.reload()}
         reload={() => actionRef.current?.reload()}

+ 8 - 1
src/components/RightContent/index.tsx

@@ -1,16 +1,23 @@
 import { Space } from 'antd';
 import { Space } from 'antd';
 import { QuestionCircleOutlined } from '@ant-design/icons';
 import { QuestionCircleOutlined } from '@ant-design/icons';
-import React from 'react';
+import React, { useEffect } from 'react';
 import { useModel, SelectLang } from 'umi';
 import { useModel, SelectLang } from 'umi';
 import Avatar from './AvatarDropdown';
 import Avatar from './AvatarDropdown';
 import HeaderSearch from '../HeaderSearch';
 import HeaderSearch from '../HeaderSearch';
 import styles from './index.less';
 import styles from './index.less';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import { Store } from 'jetlinks-store';
 
 
 export type SiderTheme = 'light' | 'dark';
 export type SiderTheme = 'light' | 'dark';
 
 
 const GlobalHeaderRight: React.FC = () => {
 const GlobalHeaderRight: React.FC = () => {
   const { initialState } = useModel('@@initialState');
   const { initialState } = useModel('@@initialState');
 
 
+  const [subscribeTopic] = useSendWebsocketMessage();
+
+  useEffect(() => {
+    Store.set('sendMessage', subscribeTopic);
+  }, []);
   if (!initialState || !initialState.settings) {
   if (!initialState || !initialState.settings) {
     return null;
     return null;
   }
   }

+ 17 - 0
src/container.tsx

@@ -0,0 +1,17 @@
+import { createContainer } from 'unstated-next';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+
+function useContainer() {
+  const [subscribeTopic, sendMessage] = useSendWebsocketMessage();
+
+  return {
+    subscribeTopic,
+    sendMessage,
+  };
+}
+
+const Container = createContainer<ReturnType<typeof useContainer>, any>(useContainer);
+
+export { useContainer };
+
+export default Container;

+ 11 - 0
src/hooks/websocket/typings.d.ts

@@ -0,0 +1,11 @@
+export type WebsocketPayload = {
+  payload: {
+    timeString: string;
+    timestamp: number;
+    value: number | Record<string, any>;
+  };
+  requestId: string;
+  topic: string;
+  type: 'complete' | 'error' | 'result';
+  message?: string;
+};

+ 83 - 0
src/hooks/websocket/useSendWebsocketMessage.ts

@@ -0,0 +1,83 @@
+import { useMemo, useRef } from 'react';
+import useWebSocket from './useWebSocket';
+import { Observable } from 'rxjs';
+import Token from '@/utils/token';
+import type { WebsocketPayload } from '@/hooks/websocket/typings';
+import { notification } from 'antd';
+
+const url = `${document.location.protocol.replace('http', 'ws')}//${
+  document.location.host
+}/jetlinks/messaging/${Token.get()}?:X_Access_Token=${Token.get()}`;
+
+enum MsgType {
+  sub = 'sub',
+  unsub = 'unsub',
+}
+
+const subscribeList: Record<string, { next: any; complete: any }[]> = {};
+
+export const useSendWebsocketMessage = () => {
+  const messageHistory = useRef<any>([]);
+
+  /**
+   * 分发消息
+   * @param message
+   */
+  const dispenseMessage = (message: MessageEvent) => {
+    const data = JSON.parse(message.data) as WebsocketPayload;
+    if (data.type === 'error') {
+      notification.error({ key: 'websocket-error', message: data.message });
+    }
+    if (subscribeList[data.requestId]) {
+      if (data.type === 'complete') {
+        subscribeList[data.requestId].forEach((element: any) => {
+          element.complete();
+        });
+      } else if (data.type === 'result') {
+        subscribeList[data.requestId].forEach((element: any) => {
+          element.next(data);
+        });
+      }
+    }
+  };
+  const { sendMessage, latestMessage } = useWebSocket(url, {
+    reconnectInterval: 1000,
+    reconnectLimit: 1,
+    onClose: () => notification.error({ key: 'websocket-error', message: '网络错误,请刷新重试' }),
+    onOpen: (event) => console.log('打开链接', event),
+    onError: (event) => console.log('报错了', event),
+    onMessage: dispenseMessage,
+  });
+
+  messageHistory.current = useMemo(
+    () => messageHistory.current.concat(latestMessage),
+    [latestMessage],
+  );
+
+  const subscribeTopic = (
+    id: string,
+    topic: string,
+    parameter: Record<string, any>,
+  ): Observable<any> => {
+    return new Observable((subscriber) => {
+      if (!subscribeList[id]) {
+        subscribeList[id] = [];
+      }
+      subscribeList[id].push({
+        next: (value: any) => subscriber.next(value),
+        complete: () => subscriber.complete(),
+      });
+      const message = JSON.stringify({ id, topic, parameter, type: MsgType.sub });
+      sendMessage?.(message);
+      return () => {
+        const unsub = JSON.stringify({ id, type: MsgType.unsub });
+        delete subscribeList[id];
+        sendMessage?.(unsub);
+      };
+    });
+  };
+
+  return [subscribeTopic, sendMessage];
+};
+
+export default useSendWebsocketMessage;

+ 163 - 0
src/hooks/websocket/useWebSocket.ts

@@ -0,0 +1,163 @@
+import { useEffect, useRef, useState } from 'react';
+import { usePersistFn, useUnmount } from 'ahooks';
+import { Store } from 'jetlinks-store';
+import SystemConst from '@/utils/const';
+
+export enum ReadyState {
+  Connecting = 0,
+  Open = 1,
+  Closing = 2,
+  Closed = 3,
+}
+
+export interface Options {
+  reconnectLimit?: number;
+  reconnectInterval?: number;
+  manual?: boolean;
+  onOpen?: (event: WebSocketEventMap['open']) => void;
+  onClose?: (event: WebSocketEventMap['close']) => void;
+  onMessage?: (message: WebSocketEventMap['message']) => void;
+  onError?: (event: WebSocketEventMap['error']) => void;
+}
+
+export interface Result {
+  latestMessage?: WebSocketEventMap['message'];
+  sendMessage?: WebSocket['send'];
+  disconnect?: () => void;
+  connect?: () => void;
+  readyState: ReadyState;
+  webSocketIns?: WebSocket;
+}
+
+export default function useWebSocket(socketUrl: string, options: Options = {}): Result {
+  const {
+    reconnectLimit = 3,
+    reconnectInterval = 3 * 1000,
+    manual = false,
+    onOpen,
+    onClose,
+    onMessage,
+    onError,
+  } = options;
+
+  const reconnectTimesRef = useRef(0);
+  const reconnectTimerRef = useRef<NodeJS.Timeout>();
+  const websocketRef = useRef<WebSocket>();
+
+  const [latestMessage, setLatestMessage] = useState<WebSocketEventMap['message']>();
+  const [readyState, setReadyState] = useState<ReadyState>(ReadyState.Closed);
+
+  const connectWs = usePersistFn(() => {
+    const ws = Store.get(SystemConst.GLOBAL_WEBSOCKET) as WebSocket;
+    if (ws) {
+      setReadyState(ws?.readyState);
+    } else {
+      if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
+
+      if (websocketRef.current) {
+        websocketRef.current.close();
+      }
+
+      try {
+        websocketRef.current = new WebSocket(socketUrl);
+
+        websocketRef.current.onerror = (event) => {
+          // eslint-disable-next-line @typescript-eslint/no-use-before-define
+          reconnect();
+          onError?.(event);
+          setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
+        };
+        websocketRef.current.onopen = (event) => {
+          onOpen?.(event);
+          reconnectTimesRef.current = 0;
+          setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
+        };
+        websocketRef.current.onmessage = (message: WebSocketEventMap['message']) => {
+          onMessage?.(message);
+          setLatestMessage(message);
+        };
+        websocketRef.current.onclose = (event) => {
+          // eslint-disable-next-line @typescript-eslint/no-use-before-define
+          reconnect();
+          onClose?.(event);
+          setReadyState(websocketRef.current?.readyState || ReadyState.Closed);
+        };
+        Store.set(SystemConst.GLOBAL_WEBSOCKET, websocketRef.current);
+      } catch (error) {
+        throw new Error(error);
+      }
+    }
+  });
+
+  /**
+   * 重连
+   */
+  const reconnect = usePersistFn(() => {
+    if (
+      reconnectTimesRef.current < reconnectLimit &&
+      websocketRef.current?.readyState !== ReadyState.Open
+    ) {
+      if (reconnectTimerRef.current) {
+        clearTimeout(reconnectTimerRef.current);
+      }
+      reconnectTimerRef.current = setTimeout(() => {
+        connectWs();
+        reconnectTimesRef.current += 1;
+      }, reconnectInterval);
+    }
+  });
+
+  /**
+   * 发送消息
+   * @param message
+   */
+  const sendMessage: WebSocket['send'] = usePersistFn((message) => {
+    const ws = Store.get(SystemConst.GLOBAL_WEBSOCKET) as WebSocket;
+    setReadyState(ws?.readyState);
+    if (readyState === ReadyState.Open) {
+      ws.send(message);
+    } else {
+      connectWs();
+      ws.send(message);
+      // throw new Error('WebSocket disconnected');
+    }
+  });
+
+  /**
+   * 手动 connect
+   */
+  const connect = usePersistFn(() => {
+    reconnectTimesRef.current = 0;
+    connectWs();
+  });
+
+  /**
+   * disconnect websocket
+   */
+  const disconnect = usePersistFn(() => {
+    if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
+
+    reconnectTimesRef.current = reconnectLimit;
+    websocketRef.current?.close();
+  });
+
+  useEffect(() => {
+    // 初始连接
+    if (!manual) {
+      connect();
+    }
+  }, [socketUrl, manual]);
+
+  useUnmount(() => {
+    disconnect();
+  });
+
+  return {
+    latestMessage,
+    sendMessage,
+    connect,
+    disconnect,
+    readyState,
+    webSocketIns: websocketRef.current,
+  };
+}

+ 19 - 2
src/pages/Analysis/CPU/index.tsx

@@ -1,10 +1,27 @@
 import { Gauge } from '@ant-design/charts';
 import { Gauge } from '@ant-design/charts';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import WebsocketTopic from '@/utils/topic';
+import { useEffect, useState } from 'react';
+import type { WebsocketPayload } from '@/hooks/websocket/typings';
 
 
 const CPU = () => {
 const CPU = () => {
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [value, setValue] = useState<number>(0);
+  useEffect(() => {
+    const cpuRealTime = subscribeTopic?.(
+      WebsocketTopic.CPURealTime.id,
+      WebsocketTopic.CPURealTime.topic,
+      { params: { history: 1 } },
+    )?.subscribe((data: WebsocketPayload) => {
+      // +0.01为了解决0.00图表异常
+      setValue(((data.payload.value as number) + 0.01) / 100);
+    });
+    return () => cpuRealTime?.unsubscribe();
+  }, []);
   const config = {
   const config = {
     width: 200,
     width: 200,
     height: 200,
     height: 200,
-    percent: 0.75,
+
     range: {
     range: {
       ticks: [0, 1 / 3, 2 / 3, 1],
       ticks: [0, 1 / 3, 2 / 3, 1],
       color: ['#F4664A', '#FAAD14', '#30BF78'],
       color: ['#F4664A', '#FAAD14', '#30BF78'],
@@ -22,7 +39,7 @@ const CPU = () => {
       },
       },
     },
     },
   };
   };
-  return <Gauge {...config} />;
+  return <Gauge {...config} percent={value} />;
 };
 };
 
 
 export default CPU;
 export default CPU;

+ 45 - 0
src/pages/Analysis/Jvm/index.tsx

@@ -0,0 +1,45 @@
+import { Gauge } from '@ant-design/charts';
+import useSendWebsocketMessage from '@/hooks/websocket/useSendWebsocketMessage';
+import WebsocketTopic from '@/utils/topic';
+import { useEffect, useState } from 'react';
+import type { WebsocketPayload } from '@/hooks/websocket/typings';
+
+const Jvm = () => {
+  const [subscribeTopic] = useSendWebsocketMessage();
+  const [value, setValue] = useState<number>(0);
+  useEffect(() => {
+    const JvmRealTime = subscribeTopic?.(
+      WebsocketTopic.JVMRealTime.id,
+      WebsocketTopic.JVMRealTime.topic,
+      { params: { history: 1 } },
+    )?.subscribe((data: WebsocketPayload) => {
+      // +0.01为了解决0.00图表异常
+      setValue((((data.payload.value as Record<string, any>).usage as number) + 0.01) / 100);
+    });
+    return () => JvmRealTime?.unsubscribe();
+  }, []);
+  const config = {
+    width: 200,
+    height: 200,
+
+    range: {
+      ticks: [0, 1 / 3, 2 / 3, 1],
+      color: ['#F4664A', '#FAAD14', '#30BF78'],
+    },
+    indicator: {
+      pointer: { style: { stroke: '#D0D0D0' } },
+      pin: { style: { stroke: '#D0D0D0' } },
+    },
+    statistic: {
+      content: {
+        style: {
+          fontSize: '36px',
+          lineHeight: '36px',
+        },
+      },
+    },
+  };
+  return <Gauge {...config} percent={value} />;
+};
+
+export default Jvm;

+ 3 - 12
src/pages/Analysis/index.tsx

@@ -3,6 +3,7 @@ import { StatisticCard } from '@ant-design/pro-card';
 import RcResizeObserver from 'rc-resize-observer';
 import RcResizeObserver from 'rc-resize-observer';
 import { useState } from 'react';
 import { useState } from 'react';
 import CPU from '@/pages/Analysis/CPU';
 import CPU from '@/pages/Analysis/CPU';
+import Jvm from '@/pages/Analysis/Jvm';
 
 
 const { Divider } = StatisticCard;
 const { Divider } = StatisticCard;
 
 
@@ -21,25 +22,15 @@ const Analysis = () => {
           <StatisticCard
           <StatisticCard
             statistic={{
             statistic={{
               title: 'CPU使用率',
               title: 'CPU使用率',
-              // value: 20190102,
-              // precision: 2,
-              // suffix: '元',
             }}
             }}
             chart={<CPU />}
             chart={<CPU />}
           />
           />
           <Divider type={responsive ? 'horizontal' : 'vertical'} />
           <Divider type={responsive ? 'horizontal' : 'vertical'} />
           <StatisticCard
           <StatisticCard
             statistic={{
             statistic={{
-              title: '今日设备消息量',
-              value: 234,
+              title: 'JVM内存',
             }}
             }}
-            chart={
-              <img
-                src="https://gw.alipayobjects.com/zos/alicdn/RLeBTRNWv/bianzu%25252043x.png"
-                alt="直方图"
-                width="100%"
-              />
-            }
+            chart={<Jvm />}
           />
           />
           <Divider type={responsive ? 'horizontal' : 'vertical'} />
           <Divider type={responsive ? 'horizontal' : 'vertical'} />
           <StatisticCard
           <StatisticCard

+ 55 - 38
src/pages/device/Alarm/index.tsx

@@ -3,12 +3,15 @@ import BaseService from '@/utils/BaseService';
 import { useRef } from 'react';
 import { useRef } from 'react';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import moment from 'moment';
 import moment from 'moment';
-import { Divider, Modal, Tag } from 'antd';
+import { Modal, Tag, Tooltip } from 'antd';
 import BaseCrud from '@/components/BaseCrud';
 import BaseCrud from '@/components/BaseCrud';
+import { CheckOutlined, EyeOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 
 const service = new BaseService<AlarmItem>('device/alarm/history');
 const service = new BaseService<AlarmItem>('device/alarm/history');
 const Alarm = () => {
 const Alarm = () => {
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
+  const intl = useIntl();
 
 
   const columns: ProColumns<AlarmItem>[] = [
   const columns: ProColumns<AlarmItem>[] = [
     {
     {
@@ -43,44 +46,58 @@ const Alarm = () => {
       title: '操作',
       title: '操作',
       width: '120px',
       width: '120px',
       align: 'center',
       align: 'center',
-      render: (record: any) => (
-        <>
-          <a
-            onClick={() => {
-              let content: string;
-              try {
-                content = JSON.stringify(record.alarmData, null, 2);
-              } catch (error) {
-                content = record.alarmData;
-              }
-              Modal.confirm({
-                width: '40VW',
-                title: '告警数据',
-                content: (
-                  <pre>
-                    {content}
-                    {record.state === 'solve' && (
-                      <>
-                        <br />
-                        <br />
-                        <span style={{ fontSize: 16 }}>处理结果:</span>
-                        <br />
-                        <p>{record.description}</p>
-                      </>
-                    )}
-                  </pre>
-                ),
-                okText: '确定',
-                cancelText: '关闭',
-              });
-            }}
+      valueType: 'option',
+      render: (record: any) => [
+        <a
+          onClick={() => {
+            let content: string;
+            try {
+              content = JSON.stringify(record.alarmData, null, 2);
+            } catch (error) {
+              content = record.alarmData;
+            }
+            Modal.confirm({
+              width: '40VW',
+              title: '告警数据',
+              content: (
+                <pre>
+                  {content}
+                  {record.state === 'solve' && (
+                    <>
+                      <br />
+                      <br />
+                      <span style={{ fontSize: 16 }}>处理结果:</span>
+                      <br />
+                      <p>{record.description}</p>
+                    </>
+                  )}
+                </pre>
+              ),
+              okText: '确定',
+              cancelText: '关闭',
+            });
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
           >
           >
-            详情
-          </a>
-          {record.state !== 'solve' ? <Divider type="vertical" /> : ''}
-          {record.state !== 'solve' && <a onClick={() => {}}>处理</a>}
-        </>
-      ),
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+        <>
+          {record.state !== 'solve' && (
+            <a onClick={() => {}}>
+              <Tooltip title={'处理'}>
+                <CheckOutlined />
+              </Tooltip>
+            </a>
+          )}
+        </>,
+      ],
     },
     },
   ];
   ];
 
 

+ 45 - 29
src/pages/device/Command/index.tsx

@@ -3,13 +3,16 @@ import BaseService from '@/utils/BaseService';
 import { useRef } from 'react';
 import { useRef } from 'react';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { CommandItem } from '@/pages/device/Command/typings';
 import type { CommandItem } from '@/pages/device/Command/typings';
-import { Divider } from 'antd';
+import { Tooltip } from 'antd';
 import moment from 'moment';
 import moment from 'moment';
 import BaseCrud from '@/components/BaseCrud';
 import BaseCrud from '@/components/BaseCrud';
+import { EyeOutlined, SyncOutlined } from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
 
 
 const service = new BaseService('device/message/task');
 const service = new BaseService('device/message/task');
 const Command = () => {
 const Command = () => {
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
+  const intl = useIntl();
 
 
   const columns: ProColumns<CommandItem>[] = [
   const columns: ProColumns<CommandItem>[] = [
     {
     {
@@ -51,37 +54,50 @@ const Command = () => {
       defaultSortOrder: 'descend',
       defaultSortOrder: 'descend',
     },
     },
     {
     {
-      title: '操作',
-      render: (text, record) => (
-        <>
-          <a
-            onClick={() => {
-              // setVisible(true);
-              // setCurrent(record);
-            }}
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
+      align: 'center',
+      width: 200,
+      render: (text, record) => [
+        <a
+          onClick={() => {
+            // setVisible(true);
+            // setCurrent(record);
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
           >
           >
-            查看指令
-          </a>
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+        <a>
           {record.state.value !== 'wait' && (
           {record.state.value !== 'wait' && (
-            <>
-              <Divider type="vertical" />
-              <a
-                onClick={() => {
-                  // service.resend(encodeQueryParam({ terms: { id: record.id } })).subscribe(
-                  //   data => {
-                  //     message.success('操作成功');
-                  //   },
-                  //   () => {},
-                  //   () => handleSearch(searchParam),
-                  // );
-                }}
-              >
-                重新发送
-              </a>
-            </>
+            <a
+              onClick={() => {
+                // service.resend(encodeQueryParam({ terms: { id: record.id } })).subscribe(
+                //   data => {
+                //     message.success('操作成功');
+                //   },
+                //   () => {},
+                //   () => handleSearch(searchParam),
+                // );
+              }}
+            >
+              <Tooltip title="重新发送">
+                <SyncOutlined />
+              </Tooltip>
+            </a>
           )}
           )}
-        </>
-      ),
+        </a>,
+      ],
     },
     },
   ];
   ];
 
 

+ 63 - 17
src/pages/device/Firmware/index.tsx

@@ -1,15 +1,19 @@
 import { PageContainer } from '@ant-design/pro-layout';
 import { PageContainer } from '@ant-design/pro-layout';
 import BaseService from '@/utils/BaseService';
 import BaseService from '@/utils/BaseService';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
-import { Divider, Popconfirm } from 'antd';
+import { message, Popconfirm, Tooltip } from 'antd';
 import moment from 'moment';
 import moment from 'moment';
 import { useRef } from 'react';
 import { useRef } from 'react';
 import BaseCrud from '@/components/BaseCrud';
 import BaseCrud from '@/components/BaseCrud';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { EditOutlined, EyeOutlined, MinusOutlined } from '@ant-design/icons';
+import { CurdModel } from '@/components/BaseCrud/model';
 
 
 const service = new BaseService('firmware');
 const service = new BaseService('firmware');
 
 
 const Firmware = () => {
 const Firmware = () => {
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
+  const intl = useIntl();
 
 
   const columns: ProColumns<FirmwareItem>[] = [
   const columns: ProColumns<FirmwareItem>[] = [
     {
     {
@@ -38,26 +42,68 @@ const Firmware = () => {
       defaultSortOrder: 'descend',
       defaultSortOrder: 'descend',
     },
     },
     {
     {
-      title: '操作',
-      width: '300px',
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
       align: 'center',
       align: 'center',
-      renderText: () => (
-        <>
-          <a
-            onClick={() => {
-              // router.push(`/device/firmware/save/${record.id}`);
+      width: 200,
+
+      render: (text, record) => [
+        <a
+          onClick={() => {
+            // router.push(`/device/firmware/save/${record.id}`);
+          }}
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
+          >
+            <EyeOutlined />
+          </Tooltip>
+        </a>,
+        <a key="editable" onClick={() => CurdModel.update(record)}>
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            })}
+          >
+            <EditOutlined />
+          </Tooltip>
+        </a>,
+        <a>
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.data.option.remove.tips',
+              defaultMessage: '确认删除?',
+            })}
+            onConfirm={async () => {
+              await service.remove(record.id);
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
             }}
             }}
           >
           >
-            查看
-          </a>
-          <Divider type="vertical" />
-          <a onClick={() => {}}>编辑</a>
-          <Divider type="vertical" />
-          <Popconfirm title="确定删除?" onConfirm={() => {}}>
-            <a>删除</a>
+            <Tooltip
+              title={intl.formatMessage({
+                id: 'pages.data.option.remove',
+                defaultMessage: '删除',
+              })}
+            >
+              <MinusOutlined />
+            </Tooltip>
           </Popconfirm>
           </Popconfirm>
-        </>
-      ),
+        </a>,
+      ],
     },
     },
   ];
   ];
 
 

+ 81 - 0
src/pages/device/Instance/Detail/index.tsx

@@ -0,0 +1,81 @@
+import { PageContainer } from '@ant-design/pro-layout';
+import { InstanceModel } from '@/pages/device/Instance';
+import { history } from 'umi';
+import { Button, Descriptions } from 'antd';
+import { useEffect, useState } from 'react';
+import { statusMap } from '@/pages/device/Product';
+
+const InstanceDetail = () => {
+  const [tab, setTab] = useState<string>('detail');
+  useEffect(() => {
+    if (!InstanceModel.current) history.goBack();
+  }, []);
+  return (
+    <PageContainer
+      onBack={() => history.goBack()}
+      onTabChange={setTab}
+      tabList={[
+        {
+          key: 'detail',
+          tab: '实例信息',
+        },
+        {
+          key: 'metadata',
+          tab: '物模型',
+        },
+        {
+          key: 'log',
+          tab: '日志管理',
+        },
+        {
+          key: 'alarm',
+          tab: '告警设置',
+        },
+        {
+          key: 'visualization',
+          tab: '可视化',
+        },
+        {
+          key: 'shadow',
+          tab: '设备影子',
+        },
+      ]}
+      content={
+        <Descriptions size="small" column={3}>
+          <Descriptions.Item label="设备ID">{InstanceModel.current?.id}</Descriptions.Item>
+          <Descriptions.Item label="产品名称">{InstanceModel.current?.name}</Descriptions.Item>
+          <Descriptions.Item label="设备类型">
+            {InstanceModel.current?.deviceType?.text}
+          </Descriptions.Item>
+          <Descriptions.Item label="链接协议">
+            {InstanceModel.current?.protocolName}
+          </Descriptions.Item>
+          <Descriptions.Item label="消息协议">
+            {InstanceModel.current?.transportProtocol}
+          </Descriptions.Item>
+          <Descriptions.Item label="创建时间">
+            {InstanceModel.current?.createTime}
+          </Descriptions.Item>
+          <Descriptions.Item label="注册时间">
+            {InstanceModel.current?.createTime}
+          </Descriptions.Item>
+          <Descriptions.Item label="最后上线时间">
+            {InstanceModel.current?.createTime}
+          </Descriptions.Item>
+          <Descriptions.Item label="说明">{InstanceModel.current?.createTime}</Descriptions.Item>
+        </Descriptions>
+      }
+      extra={[
+        statusMap[0],
+        <Button key="2">停用</Button>,
+        <Button key="1" type="primary">
+          应用配置
+        </Button>,
+      ]}
+    >
+      设备实例{tab}
+      {JSON.stringify(InstanceModel.current)}
+    </PageContainer>
+  );
+};
+export default InstanceDetail;

+ 79 - 51
src/pages/device/Instance/index.tsx

@@ -2,10 +2,20 @@ import { PageContainer } from '@ant-design/pro-layout';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { ProColumns, ActionType } from '@jetlinks/pro-table';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import type { DeviceInstance } from '@/pages/device/Instance/typings';
 import moment from 'moment';
 import moment from 'moment';
-import { Badge, Divider, Popconfirm } from 'antd';
+import { Badge, message, Popconfirm, Tooltip } from 'antd';
 import { useRef } from 'react';
 import { useRef } from 'react';
 import BaseCrud from '@/components/BaseCrud';
 import BaseCrud from '@/components/BaseCrud';
 import BaseService from '@/utils/BaseService';
 import BaseService from '@/utils/BaseService';
+import { Link } from 'umi';
+import {
+  CloseCircleOutlined,
+  EditOutlined,
+  EyeOutlined,
+  PlayCircleOutlined,
+} from '@ant-design/icons';
+import { useIntl } from '@@/plugin-locale/localeExports';
+import { CurdModel } from '@/components/BaseCrud/model';
+import { model } from '@formily/reactive';
 
 
 const statusMap = new Map();
 const statusMap = new Map();
 statusMap.set('在线', 'success');
 statusMap.set('在线', 'success');
@@ -15,10 +25,15 @@ statusMap.set('online', 'success');
 statusMap.set('offline', 'error');
 statusMap.set('offline', 'error');
 statusMap.set('notActive', 'processing');
 statusMap.set('notActive', 'processing');
 
 
+export const InstanceModel = model<{
+  current: DeviceInstance | undefined;
+}>({
+  current: undefined,
+});
 const service = new BaseService<DeviceInstance>('device/instance');
 const service = new BaseService<DeviceInstance>('device/instance');
 const Instance = () => {
 const Instance = () => {
   const actionRef = useRef<ActionType>();
   const actionRef = useRef<ActionType>();
-
+  const intl = useIntl();
   const columns: ProColumns<DeviceInstance>[] = [
   const columns: ProColumns<DeviceInstance>[] = [
     {
     {
       title: 'ID',
       title: 'ID',
@@ -70,60 +85,73 @@ const Instance = () => {
       ellipsis: true,
       ellipsis: true,
     },
     },
     {
     {
-      title: '操作',
-      width: '200px',
+      title: intl.formatMessage({
+        id: 'pages.data.option',
+        defaultMessage: '操作',
+      }),
+      valueType: 'option',
       align: 'center',
       align: 'center',
-      render: (record: any) => (
-        <>
-          <a
-            onClick={() => {
-              // router.push(`/device/instance/save/${record.id}`);
-            }}
+      width: 200,
+      render: (text, record) => [
+        <Link
+          onClick={() => {
+            InstanceModel.current = record;
+          }}
+          to={`/device/instance/detail/${record.id}`}
+          key="link"
+        >
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.detail',
+              defaultMessage: '查看',
+            })}
+            key={'detail'}
           >
           >
-            查看
-          </a>
-          <Divider type="vertical" />
-          <a
-            onClick={() => {
-              // setCurrentItem(record);
-              // setAddVisible(true);
+            <EyeOutlined />
+          </Tooltip>
+        </Link>,
+        <a key="editable" onClick={() => CurdModel.update(record)}>
+          <Tooltip
+            title={intl.formatMessage({
+              id: 'pages.data.option.edit',
+              defaultMessage: '编辑',
+            })}
+          >
+            <EditOutlined />
+          </Tooltip>
+        </a>,
+
+        <a href={record.id} target="_blank" rel="noopener noreferrer" key="view">
+          <Popconfirm
+            title={intl.formatMessage({
+              id: 'pages.data.option.disable.tips',
+              defaultMessage: '确认禁用?',
+            })}
+            onConfirm={async () => {
+              await service.update({
+                id: record.id,
+                // status: record.state?.value ? 0 : 1,
+              });
+              message.success(
+                intl.formatMessage({
+                  id: 'pages.data.option.success',
+                  defaultMessage: '操作成功!',
+                }),
+              );
+              actionRef.current?.reload();
             }}
             }}
           >
           >
-            编辑
-          </a>
-          <Divider type="vertical" />
-          {record.state?.value === 'notActive' ? (
-            <span>
-              <Popconfirm
-                title="确认启用?"
-                onConfirm={() => {
-                  // changeDeploy(record);
-                }}
-              >
-                <a>启用</a>
-              </Popconfirm>
-              <Divider type="vertical" />
-              <Popconfirm
-                title="确认删除?"
-                onConfirm={() => {
-                  // deleteInstance(record);
-                }}
-              >
-                <a>删除</a>
-              </Popconfirm>
-            </span>
-          ) : (
-            <Popconfirm
-              title="确认禁用设备?"
-              onConfirm={() => {
-                // unDeploy(record);
-              }}
+            <Tooltip
+              title={intl.formatMessage({
+                id: `pages.data.option.${record.state.value ? 'disable' : 'enable'}`,
+                defaultMessage: record.state.value ? '禁用' : '启用',
+              })}
             >
             >
-              <a>禁用</a>
-            </Popconfirm>
-          )}
-        </>
-      ),
+              {record.state.value ? <CloseCircleOutlined /> : <PlayCircleOutlined />}
+            </Tooltip>
+          </Popconfirm>
+        </a>,
+      ],
     },
     },
   ];
   ];
 
 

+ 1 - 1
src/pages/device/Product/Detail/index.tsx

@@ -13,7 +13,7 @@ const ProductDetail = observer(() => {
   return (
   return (
     <PageContainer
     <PageContainer
       onBack={() => history.goBack()}
       onBack={() => history.goBack()}
-      extraContent={<Space size={24}></Space>}
+      extraContent={<Space size={24} />}
       content={
       content={
         <Descriptions size="small" column={2}>
         <Descriptions size="small" column={2}>
           <Descriptions.Item label="产品ID">{productModel.current?.id}</Descriptions.Item>
           <Descriptions.Item label="产品ID">{productModel.current?.id}</Descriptions.Item>

+ 1 - 0
src/pages/log/Access/index.tsx

@@ -87,6 +87,7 @@ const Access: React.FC = () => {
         service={service}
         service={service}
         title="访问日志"
         title="访问日志"
         schema={{}}
         schema={{}}
+        toolBar={[]}
         actionRef={actionRef}
         actionRef={actionRef}
       />
       />
     </PageContainer>
     </PageContainer>

+ 2 - 0
src/utils/const.ts

@@ -12,6 +12,8 @@ class SystemConst {
   static BASE_CURD_CURRENT = 'BASE_CURD_CURRENT';
   static BASE_CURD_CURRENT = 'BASE_CURD_CURRENT';
 
 
   static BASE_CURD_MODEL = 'BASE_CURD_MODEL';
   static BASE_CURD_MODEL = 'BASE_CURD_MODEL';
+
+  static GLOBAL_WEBSOCKET = 'GLOBAL-WEBSOCKET';
 }
 }
 
 
 export default SystemConst;
 export default SystemConst;

+ 11 - 0
src/utils/topic.ts

@@ -0,0 +1,11 @@
+export const WebsocketTopic = {
+  CPURealTime: {
+    id: 'analysis-cpu-realtime',
+    topic: '/dashboard/systemMonitor/cpu/usage/realTime',
+  },
+  JVMRealTime: {
+    id: 'analysis-jvm-realTime',
+    topic: '/dashboard/jvmMonitor/memory/info/realTime',
+  },
+};
+export default WebsocketTopic;