From d47403d0fc1f9a02803819886025c650e028cd2a Mon Sep 17 00:00:00 2001 From: gaoyan Date: Tue, 22 Oct 2024 11:09:40 +0800 Subject: [PATCH] [Bug][Web] Fix ws bug (#3881) --- .../Modal/WsErrorShow/WsErrorShow.tsx | 33 +++++++++++ dinky-web/src/locales/en-US/global.ts | 5 +- dinky-web/src/locales/zh-CN/global.ts | 5 +- dinky-web/src/models/UseWebSocketModel.tsx | 55 +++++++++++++------ .../Console/ConsoleContent.tsx | 22 +++++++- 5 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 dinky-web/src/components/Modal/WsErrorShow/WsErrorShow.tsx diff --git a/dinky-web/src/components/Modal/WsErrorShow/WsErrorShow.tsx b/dinky-web/src/components/Modal/WsErrorShow/WsErrorShow.tsx new file mode 100644 index 0000000000..1cb2fd44ea --- /dev/null +++ b/dinky-web/src/components/Modal/WsErrorShow/WsErrorShow.tsx @@ -0,0 +1,33 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import { Button, Result } from 'antd'; +import { WsState } from '@/models/UseWebSocketModel'; +import * as React from 'react'; +import { l } from '@/utils/intl'; + +const WsErrorShow = (props: { state: WsState; extra?: React.ReactNode }) => { + const { state, extra } = props; + + return ( + + ); +}; + +export default WsErrorShow; diff --git a/dinky-web/src/locales/en-US/global.ts b/dinky-web/src/locales/en-US/global.ts index 09c8b1a00b..ce78f16d68 100644 --- a/dinky-web/src/locales/en-US/global.ts +++ b/dinky-web/src/locales/en-US/global.ts @@ -236,5 +236,8 @@ export default { 'global.job.status.failed-tip': 'Failed to submit to the cluster, unable to get the task name', 'global.operation.unable': 'Unable to operate', - 'global.operation.able': 'Can operate' + 'global.operation.able': 'Can operate', + + //WS + 'global.ws.failed': 'Failed to connect to WebSocket' }; diff --git a/dinky-web/src/locales/zh-CN/global.ts b/dinky-web/src/locales/zh-CN/global.ts index d3c55ef835..88bde72050 100644 --- a/dinky-web/src/locales/zh-CN/global.ts +++ b/dinky-web/src/locales/zh-CN/global.ts @@ -232,5 +232,8 @@ export default { 'global.job.status.failed-tip': '未成功提交到集群,无法获取任务名称/作业ID', 'global.operation.unable': '无法操作', - 'global.operation.able': '可以操作' + 'global.operation.able': '可以操作', + + //WS + 'global.ws.failed': '连接WebSocket失败' }; diff --git a/dinky-web/src/models/UseWebSocketModel.tsx b/dinky-web/src/models/UseWebSocketModel.tsx index 3da3ebd774..6894ac5ad4 100644 --- a/dinky-web/src/models/UseWebSocketModel.tsx +++ b/dinky-web/src/models/UseWebSocketModel.tsx @@ -21,6 +21,7 @@ import { useEffect, useRef, useState } from 'react'; import { ErrorMessage } from '@/utils/messages'; import { v4 as uuidv4 } from 'uuid'; import { TOKEN_KEY } from '@/services/constants'; + export type SseData = { topic: string; data: Record; @@ -39,21 +40,33 @@ export type SubscriberData = { params: string[]; call: (data: SseData) => void; }; + +export type WsState = { + wsOnReady: boolean; + wsUrl: string; +}; + export default () => { const subscriberRef = useRef([]); - const wsUrl = `ws://${window.location.hostname}:${window.location.port}/api/ws/global`; - const ws = useRef(new WebSocket(wsUrl)); + const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; + const wsUrl = `${protocol}://${window.location.hostname}:${window.location.port}/api/ws/global`; + const [wsState, setWsState] = useState({ wsOnReady: true, wsUrl }); + + const ws = useRef(); const reconnect = () => { - if (ws.current.readyState === WebSocket.OPEN) { + if (ws.current && ws.current.readyState === WebSocket.CLOSED) { ws.current.close(); } ws.current = new WebSocket(wsUrl); ws.current.onopen = () => { + setWsState({ wsOnReady: true, wsUrl }); receiveMessage(); subscribe(); }; + ws.current.onerror = () => setWsState({ wsOnReady: false, wsUrl }); + ws.current.onclose = () => setWsState({ wsOnReady: false, wsUrl }); }; const subscribe = () => { @@ -68,31 +81,36 @@ export default () => { topics[sub.topic] = [...topics[sub.topic]]; } }); - if (ws.current.readyState === WebSocket.CLOSED) { + if (!ws.current || ws.current.readyState === WebSocket.CLOSED) { reconnect(); - } else { + } else if (ws.current.readyState === WebSocket.OPEN) { const token = JSON.parse(localStorage.getItem(TOKEN_KEY) ?? '{}')?.tokenValue; ws.current.send(JSON.stringify({ token, topics })); + } else { + //TODO 这里要做些什么 } }; const receiveMessage = () => { - ws.current.onmessage = (e) => { - try { - const data: SseData = JSON.parse(e.data); - subscriberRef.current - .filter((sub) => sub.topic === data.topic) - .filter((sub) => !sub.params || sub.params.find((x) => data.data[x])) - .forEach((sub) => sub.call(data)); - } catch (e: any) { - ErrorMessage(e); - } - }; + if (ws.current) { + ws.current.onmessage = (e) => { + try { + const data: SseData = JSON.parse(e.data); + subscriberRef.current + .filter((sub) => sub.topic === data.topic) + .filter((sub) => !sub.params || sub.params.find((x) => data.data[x])) + .forEach((sub) => sub.call(data)); + } catch (e: any) { + ErrorMessage(e); + } + }; + } }; + useEffect(() => { receiveMessage(); setInterval(() => { - if (ws.current.readyState === WebSocket.CLOSED) { + if (!ws.current || ws.current.readyState != WebSocket.OPEN) { reconnect(); } }, 2000); @@ -111,6 +129,7 @@ export default () => { return { subscribeTopic, - reconnect + reconnect, + wsState }; }; diff --git a/dinky-web/src/pages/DataStudio/BottomContainer/Console/ConsoleContent.tsx b/dinky-web/src/pages/DataStudio/BottomContainer/Console/ConsoleContent.tsx index 35a5e08e4e..9d9d793fea 100644 --- a/dinky-web/src/pages/DataStudio/BottomContainer/Console/ConsoleContent.tsx +++ b/dinky-web/src/pages/DataStudio/BottomContainer/Console/ConsoleContent.tsx @@ -29,11 +29,12 @@ import { SplitPane } from '@andrewray/react-multi-split-pane'; import { Pane } from '@andrewray/react-multi-split-pane/dist/lib/Pane'; import { CheckOutlined, CloseCircleFilled, LoadingOutlined, XFilled } from '@ant-design/icons'; import { connect, useModel, useRequest } from '@umijs/max'; -import { Empty, Space, Typography } from 'antd'; +import { Button, Empty, Space, Typography } from 'antd'; import { DataNode } from 'antd/es/tree'; import DirectoryTree from 'antd/es/tree/DirectoryTree'; import { Key, useEffect, useRef, useState } from 'react'; import { SseData, Topic } from '@/models/UseWebSocketModel'; +import WsErrorShow from '@/components/Modal/WsErrorShow/WsErrorShow'; const { Text } = Typography; @@ -72,11 +73,17 @@ const ConsoleContent = (props: ConsoleProps) => { const [processNode, setProcessNode] = useState(); const [expandedKeys, setExpandedKeys] = useState([]); + const [showCacheData, setShowCacheData] = useState(false); + const process = `FlinkSubmit/${tab.params.taskId}`; const { subscribeTopic } = useModel('UseWebSocketModel', (model: any) => ({ subscribeTopic: model?.subscribeTopic })); + const { wsState } = useModel('UseWebSocketModel', (model: any) => ({ + wsState: model?.wsState + })); + const onUpdate = (data: ProcessStep) => { setProcessNode((prevState: any) => { //如果key不一致代表重新提交了任务,清空旧状态 @@ -147,6 +154,19 @@ const ConsoleContent = (props: ConsoleProps) => { setExpandedKeys(expandedKeys); }; + if (!wsState?.wsOnReady && !showCacheData) { + return ( + setShowCacheData(true)}> + {l('devops.jobinfo.recently.job.status')} + + } + /> + ); + } + return (