diff --git a/dinky-web/src/pages/DataStudioNew/CenterTabContent/SqlTask.tsx b/dinky-web/src/pages/DataStudioNew/CenterTabContent/SqlTask.tsx index d990f71484..443e6cdca9 100644 --- a/dinky-web/src/pages/DataStudioNew/CenterTabContent/SqlTask.tsx +++ b/dinky-web/src/pages/DataStudioNew/CenterTabContent/SqlTask.tsx @@ -158,7 +158,6 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { updateCenterTab({ ...props.tabData, params: newParams }); setOriginStatementValue(statement); - if (params?.statement && params?.statement !== taskDetail.statement) { setDiff([{ key: 'statement', server: taskDetail.statement, cache: params.statement }]); setOpenDiffModal(true); @@ -198,13 +197,7 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { editorInstance.current = editor; // @ts-ignore editor['id'] = currentState.taskId; - editor.onDidChangeCursorPosition((e) => { - // props.footContainerCacher.cache.codePosition = [e.position.lineNumber, e.position.column]; - // dispatch({ - // type: STUDIO_MODEL.saveFooterValue, - // payload: { ...props.footContainerCacher.cache } - // }); - }); + editor.onDidChangeCursorPosition((e) => {}); registerEditorKeyBindingAndAction(editor); }; @@ -422,15 +415,13 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { updateAction({ actionType: DataStudioActionType.TASK_RUN_DEBUG, params: { - taskId: params.taskId, - columns: res.data?.result?.columns ?? [], - rowData: res.data?.result?.rowData ?? [], + taskId: params.taskId } }); setCurrentState((prevState) => { return { ...prevState, - status: res.data.status === 'SUCCESS' ? (res.data.pipeline?'RUNNING':'SUCCESS') : res.data.status + status: res.data.status === 'SUCCESS' ? 'RUNNING' : res.data.status }; }); } @@ -438,12 +429,14 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { const handleStop = useCallback(async () => { const result = await cancelTask('', currentState.taskId, false); - setCurrentState((prevState) => { - return { - ...prevState, - status: 'CANCEL' - }; - }); + if (result.success) { + setCurrentState((prevState) => { + return { + ...prevState, + status: 'CANCEL' + }; + }); + } }, [currentState.taskId]); const handleGotoDevOps = useCallback(async () => { @@ -753,10 +746,10 @@ export const SqlTask = memo((props: FlinkSqlProps & any) => { ) => void; + children: React.ReactNode | JSX.Element | string; +} + +export const ContextMenuSpace = (props: ContextMenuSpaceProps) => { + const { onContextMenu, children } = props; + return ( + <> + + {children} + + + ); +}; diff --git a/dinky-web/src/pages/DataStudioNew/DockLayoutFunction.tsx b/dinky-web/src/pages/DataStudioNew/DockLayoutFunction.tsx index be821e37f7..a7bef13701 100644 --- a/dinky-web/src/pages/DataStudioNew/DockLayoutFunction.tsx +++ b/dinky-web/src/pages/DataStudioNew/DockLayoutFunction.tsx @@ -67,7 +67,7 @@ export const createNewPanel = ( { id: route.key, content: <>, - title: route.title, + title: route.title(), group: route.position } ] diff --git a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalConfig.ts b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalConfig.ts new file mode 100644 index 0000000000..e6460f1180 --- /dev/null +++ b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalConfig.ts @@ -0,0 +1,300 @@ +/* + * + * 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. + * + */ + +export type TermProps = { + mode: string; + wsUrl: string; + fontSize: number; + backspaceAsCtrlH: boolean; + initSql?: string; + sessionId?: string; + connectAddress: string; +}; +export const setTermConfig = (config: TermProps) => { + localStorage.setItem('terminal-config', JSON.stringify(config)); +}; + +export const getTermConfig = () => { + const initializeConfig = () => { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return { + mode: 'MODE_EMBEDDED', + wsUrl: `${protocol}//${window.location.host}/api/ws/sql-gateway`, + fontSize: 14, + backspaceAsCtrlH: true + }; + }; + + const configKey = 'terminal-config'; + const storedConfig = localStorage.getItem(configKey); + + if (storedConfig) { + try { + return JSON.parse(storedConfig); + } catch (error) { + console.error('Failed to parse stored config:', error); + return initializeConfig(); + } + } else { + return initializeConfig(); + } +}; + +export const HEART_TIMEOUT = 30 * 1000; +export const HEART_TNERVAL = 30 * 1000; + +export const enum TerminalEvent { + TERM_KEY_EVENT = 'TERM_KEY_EVENT', + TERM_HEART_EVENT = 'TERM_HEART_EVENT', + TERM_CLOSE_EVENT = 'TERM_CLOSE_EVENT', + TERM_RESIZE = 'TERM_RESIZE' +} + +export const enum KeyCode { + Backspace = 8, + Tab = 9, + Enter = 13, + Shift = 16, + Ctrl = 17, + Alt = 18, + PauseBreak = 19, + CapsLock = 20, + Escape = 27, + Space = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + LeftArrow = 37, + UpArrow = 38, + RightArrow = 39, + DownArrow = 40, + Insert = 45, + Delete = 46, + Key0 = 48, + Key1 = 49, + Key2 = 50, + Key3 = 51, + Key4 = 52, + Key5 = 53, + Key6 = 54, + Key7 = 55, + Key8 = 56, + Key9 = 57, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftWindowKey = 91, + RightWindowKey = 92, + SelectKey = 93, + Numpad0 = 96, + Numpad1 = 97, + Numpad2 = 98, + Numpad3 = 99, + Numpad4 = 100, + Numpad5 = 101, + Numpad6 = 102, + Numpad7 = 103, + Numpad8 = 104, + Numpad9 = 105, + Multiply = 106, + Add = 107, + Subtract = 109, + DecimalPoint = 110, + Divide = 111, + F1 = 112, + F2 = 113, + F3 = 114, + F4 = 115, + F5 = 116, + F6 = 117, + F7 = 118, + F8 = 119, + F9 = 120, + F10 = 121, + F11 = 122, + F12 = 123, + NumLock = 144, + ScrollLock = 145, + SemiColon = 186, + EqualSign = 187, + Comma = 188, + Dash = 189, + Period = 190, + ForwardSlash = 191, + GraveAccent = 192, + OpenBracket = 219, + BackSlash = 220, + CloseBracket = 221, + SingleQuote = 222 +} + +export const enum AsciiCode { + Null = 0, + StartOfHeader = 1, + StartOfText = 2, + EndOfText = 3, + EndOfTransmission = 4, + Enquiry = 5, + Acknowledge = 6, + Bell = 7, + Backspace = 8, + HorizontalTab = 9, + LineFeed = 10, + VerticalTab = 11, + FormFeed = 12, + CarriageReturn = 13, + ShiftOut = 14, + ShiftIn = 15, + DataLinkEscape = 16, + DeviceControl1 = 17, + DeviceControl2 = 18, + DeviceControl3 = 19, + DeviceControl4 = 20, + NegativeAcknowledge = 21, + SynchronousIdle = 22, + EndOfTransmissionBlock = 23, + Cancel = 24, + EndOfMedium = 25, + Substitute = 26, + Escape = 27, + FileSeparator = 28, + GroupSeparator = 29, + RecordSeparator = 30, + UnitSeparator = 31, + Space = 32, + ExclamationMark = 33, + DoubleQuote = 34, + Hash = 35, + Dollar = 36, + Percent = 37, + Ampersand = 38, + SingleQuote = 39, + LeftParenthesis = 40, + RightParenthesis = 41, + Asterisk = 42, + Plus = 43, + Comma = 44, + Minus = 45, + Period = 46, + Slash = 47, + Zero = 48, + One = 49, + Two = 50, + Three = 51, + Four = 52, + Five = 53, + Six = 54, + Seven = 55, + Eight = 56, + Nine = 57, + Colon = 58, + Semicolon = 59, + LessThan = 60, + Equals = 61, + GreaterThan = 62, + QuestionMark = 63, + At = 64, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LeftSquareBracket = 91, + Backslash = 92, + RightSquareBracket = 93, + Caret = 94, + Underscore = 95, + Backtick = 96, + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + LeftCurlyBracket = 123, + Pipe = 124, + RightCurlyBracket = 125, + Tilde = 126, + Delete = 127 +} diff --git a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalContent.tsx b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalContent.tsx new file mode 100644 index 0000000000..b5a440169e --- /dev/null +++ b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/TerminalContent.tsx @@ -0,0 +1,180 @@ +/* + * + * 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 React, { useEffect } from 'react'; +import { AsciiCode, HEART_TIMEOUT, HEART_TNERVAL, KeyCode, TerminalEvent } from './TerminalConfig'; +import { FitAddon } from '@xterm/addon-fit'; +import { Terminal } from '@xterm/xterm'; +import { TermProps } from '@/pages/DataStudio/MiddleContainer/Terminal/TerminalConfig'; +import './xterm.css'; + +const TerminalContent: React.FC = (props) => { + const { mode, wsUrl, backspaceAsCtrlH, fontSize, initSql, sessionId, connectAddress } = props; + // const currentData = tabsItem.params.taskData; + + let term: Terminal; + let ws: WebSocket; + let lastHeartTime: Date = new Date(); + let heartInterval: NodeJS.Timeout; + + const sendWs = (data: any, type: string) => { + ws.send(JSON.stringify({ type: type, data: data })); + }; + + const CheckWebSocket = (ws: WebSocket) => { + if (ws) { + return !(ws.readyState === WebSocket.CLOSING || ws.readyState === WebSocket.CLOSED); + } + return false; + }; + + const preprocessInput = (data: any) => { + if (backspaceAsCtrlH) { + if (data.charCodeAt(0) === AsciiCode.Delete) { + data = String.fromCharCode(KeyCode.Backspace); + } + } + if (data.charCodeAt(0) === AsciiCode.EndOfText) { + data = String.fromCharCode(AsciiCode.Substitute); + } + if (data === '\r') { + data = '\n'; + } + return data; + }; + + const connectWs = (term: Terminal) => { + const termCols = term.cols; + const termRows = term.rows; + + const params = `mode=${mode}&cols=${termCols}&rows=${termRows}&sessionId=${sessionId}&initSql=${encodeURIComponent(initSql ?? '')}&connectAddress=${connectAddress}`; + + ws = new WebSocket(`${wsUrl}/?${params}`); + + ws.binaryType = 'arraybuffer'; + ws.onopen = function () { + if (heartInterval) { + clearInterval(heartInterval); + } + heartInterval = setInterval(() => { + if (!CheckWebSocket(ws)) { + clearInterval(heartInterval); + } else { + let currentDate = new Date(); + if (lastHeartTime.getTime() - currentDate.getTime() > HEART_TIMEOUT) { + console.error('ws heart timeout'); + } + sendWs('PING', TerminalEvent.TERM_HEART_EVENT); + } + }, HEART_TNERVAL); + }; + + ws.onclose = function (evt: CloseEvent) { + term.writeln('\nThe connection closed: ' + evt.code + ' ' + evt.reason); + }; + + ws.onerror = function (evt: Event) { + term.write('\nThe connection error:' + evt); + }; + + ws.onmessage = function (e: MessageEvent) { + if (typeof e.data === 'object') { + const data = new Uint8Array(e.data); + if (data.length == 1 && data[0] === KeyCode.Backspace) { + term.write('\b \b'); + } else { + term.write(data); + } + } else { + //预留接口 + term.write(e.data.replace('\r', '\n\n')); + } + }; + }; + + const createTerminal = () => { + if (term) { + return term; + } else { + const terminal = new Terminal({ + // rendererType: "canvas", //渲染类型 + // cols: this.cols,// 设置之后会输入多行之后覆盖现象 + // convertEol: true, //启用时,光标将设置为下一行的开头 + scrollback: 1000, //终端中的回滚量 + fontSize: fontSize, //字体大小 + // disableStdin: false, //是否应禁用输入。 + // cursorStyle: "block", //光标样式 + // cursorBlink: true, //光标闪烁 + // tabStopWidth: 4, + theme: { + // foreground: "black", //字体 + // background: "#000000", //背景色 + // cursor: "help" //设置光标 + } + }); + const fitAddon = new FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(document.getElementById('terminal-container') as HTMLElement); + fitAddon.fit(); + terminal.focus(); + window.addEventListener('resize', () => fitAddon.fit()); + return terminal; + } + }; + + const initAll = () => { + if (!term) { + term = createTerminal(); + } + term.onData((data) => { + if (!CheckWebSocket(ws)) { + console.error('ws not activated'); + return; + } + // this.lastSendTime = new Date(); + data = preprocessInput(data); + sendWs(data, TerminalEvent.TERM_KEY_EVENT); + }); + + term.onResize(({ cols, rows }) => { + if (!CheckWebSocket(ws)) { + return; + } + sendWs(JSON.stringify({ columns: cols, rows }), TerminalEvent.TERM_RESIZE); + }); + connectWs(term); + }; + + useEffect(() => { + initAll(); + return () => { + if (CheckWebSocket(ws)) { + sendWs('', TerminalEvent.TERM_CLOSE_EVENT); + //延迟关闭,给与后端注销时间 + setTimeout(() => ws.close(), 2000); + } + clearInterval(heartInterval); + term?.dispose(); + }; + }, []); + + return
; +}; + +export default TerminalContent; diff --git a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/index.tsx b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/index.tsx new file mode 100644 index 0000000000..a9b759fa71 --- /dev/null +++ b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/index.tsx @@ -0,0 +1,223 @@ +/* + * + * 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 { API_CONSTANTS } from '@/services/endpoints'; +import React, { useState } from 'react'; +import { + Button, + Col, + Divider, + Form, + Input, + InputNumber, + Radio, + Row, + Select, + Space, + Switch, + Tag, + Typography +} from 'antd'; +import { EditTwoTone } from '@ant-design/icons'; +import useHookRequest from '@/hooks/useHookRequest'; +import { getData } from '@/services/api'; +import { Cluster } from '@/types/RegCenter/data'; +import { CLUSTER_TYPE_OPTIONS, ClusterType } from '@/pages/RegCenter/Cluster/constants'; +import { + getTermConfig, + setTermConfig, + TermProps +} from '@/pages/DataStudio/MiddleContainer/Terminal/TerminalConfig'; +import TerminalContent from '@/pages/DataStudio/MiddleContainer/Terminal/TerminalContent'; +import { l } from '@/utils/intl'; + +const TerminalTab = () => { + // const [form] = Form.useForm(); + + const EMBBED_FILTER = [ + ClusterType.STANDALONE, + ClusterType.YARN_SESSION, + ClusterType.KUBERNETES_SESSION + ]; + const GATEWAY_FILTER = [ClusterType.SQL_GATEWAY]; + + const [disableUrlEditable, setUrlEditable] = useState(true); + const [openTerm, setOpenTerm] = useState(false); + const [connectCfg, setConnectCfg] = useState(getTermConfig()); + + const [currentMode, setCurrentMode] = useState(connectCfg.mode); + + const dealClusterData = (data: Cluster.Instance[]) => { + const renderClusterItem = (item: Cluster.Instance) => { + return ( + }> + + {CLUSTER_TYPE_OPTIONS().find((record) => item.type === record.value)?.label} + + {item.name} + + {item.alias} + + {item.hosts} + + ); + }; + + return data + .filter((item: Cluster.Instance) => { + if (currentMode === 'MODE_EMBEDDED') { + return EMBBED_FILTER.includes(item.type as ClusterType); + } else { + return GATEWAY_FILTER.includes(item.type as ClusterType); + } + }) + .map((item: Cluster.Instance) => { + return { + label: renderClusterItem(item), + value: item.hosts + }; + }); + }; + + const clusterData = useHookRequest(getData, { + refreshDeps: [currentMode], + defaultParams: [API_CONSTANTS.CLUSTER_INSTANCE_LIST, { isAutoCreate: false }], + onSuccess: (data: Cluster.Instance[]) => dealClusterData(data) + }); + + const envData = useHookRequest(getData, { + defaultParams: [API_CONSTANTS.LIST_FLINK_SQL_ENV], + onSuccess: (data: any[]) => + data.map((item: any) => ({ label: item.name, value: item.statement })) + }); + + const onFinish = (values: TermProps) => { + setTermConfig(values); + setConnectCfg(values); + setOpenTerm(true); + }; + + const formItemLayout = { + labelCol: { + xs: { span: 24 }, + sm: { span: 6 } + }, + wrapperCol: { + xs: { span: 24 }, + sm: { span: 14 } + } + }; + + return ( + <> + {openTerm ? ( + + ) : ( +
+
+ + setCurrentMode(e.target.value)}> + Embedded + + SQL Gateway + + + + + + + + + + + + {l('datastudio.middle.terminal.websocket.tip')} + + + + setUrlEditable(!disableUrlEditable)} /> + + + + + + + + + + {l('datastudio.middle.terminal.cluster.tip')} + + + + + + + + + + + + + + + + + + + + + {l('datastudio.middle.terminal.backspaceAsCtrlH.tip')} + + + + + + +
+
+ )} + + ); +}; + +export default TerminalTab; diff --git a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/xterm.css b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/xterm.css new file mode 100644 index 0000000000..f26f72b608 --- /dev/null +++ b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal/xterm.css @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * @license MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ + +/** + * Default styles for xterm.js + */ + +.xterm { + cursor: text; + position: relative; + user-select: none; + -ms-user-select: none; + -webkit-user-select: none; +} + +.xterm.focus, +.xterm:focus { + outline: none; +} + +.xterm .xterm-helpers { + position: absolute; + top: 0; + /** + * The z-index of the helpers must be higher than the canvases in order for + * IMEs to appear on top. + */ + z-index: 5; +} + +.xterm .xterm-helper-textarea { + padding: 0; + border: 0; + margin: 0; + /* Move textarea out of the screen to the far left, so that the cursor is not visible */ + position: absolute; + opacity: 0; + left: -9999em; + top: 0; + width: 0; + height: 0; + z-index: -5; + /** Prevent wrapping so the IME appears against the textarea at the correct position */ + white-space: nowrap; + overflow: hidden; + resize: none; +} + +.xterm .composition-view { + /* TODO: Composition position got messed up somewhere */ + background: #000; + color: #FFF; + display: none; + position: absolute; + white-space: nowrap; + z-index: 1; +} + +.xterm .composition-view.active { + display: block; +} + +.xterm .xterm-viewport { + /* On OS X this is required in order for the scroll bar to appear fully opaque */ + background-color: #000; + overflow-y: scroll; + cursor: default; + position: absolute; + right: 0; + left: 0; + top: 0; + bottom: 0; +} + +.xterm .xterm-screen { + position: relative; +} + +.xterm .xterm-screen canvas { + position: absolute; + left: 0; + top: 0; +} + +.xterm .xterm-scroll-area { + visibility: hidden; +} + +.xterm-char-measure-element { + display: inline-block; + visibility: hidden; + position: absolute; + top: 0; + left: -9999em; + line-height: normal; +} + +.xterm.enable-mouse-events { + /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */ + cursor: default; +} + +.xterm.xterm-cursor-pointer, +.xterm .xterm-cursor-pointer { + cursor: pointer; +} + +.xterm.column-select.focus { + /* Column selection mode */ + cursor: crosshair; +} + +.xterm .xterm-accessibility, +.xterm .xterm-message { + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 10; + color: transparent; + pointer-events: none; +} + +.xterm .live-region { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.xterm-dim { + /* Dim should not apply to background, so the opacity of the foreground color is applied + * explicitly in the generated class and reset to 1 here */ + opacity: 1 !important; +} + +.xterm-underline-1 { text-decoration: underline; } +.xterm-underline-2 { text-decoration: double underline; } +.xterm-underline-3 { text-decoration: wavy underline; } +.xterm-underline-4 { text-decoration: dotted underline; } +.xterm-underline-5 { text-decoration: dashed underline; } + +.xterm-overline { + text-decoration: overline; +} + +.xterm-overline.xterm-underline-1 { text-decoration: overline underline; } +.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; } +.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; } +.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; } +.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; } + +.xterm-strikethrough { + text-decoration: line-through; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration { + z-index: 6; + position: absolute; +} + +.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer { + z-index: 7; +} + +.xterm-decoration-overview-ruler { + z-index: 8; + position: absolute; + top: 0; + right: 0; + pointer-events: none; +} + +.xterm-decoration-top { + z-index: 2; + position: relative; +} diff --git a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/index.tsx b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/index.tsx index 75b72660d1..04f895652b 100644 --- a/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/index.tsx +++ b/dinky-web/src/pages/DataStudioNew/Toolbar/FlinkSqlClient/index.tsx @@ -17,10 +17,10 @@ * */ -import TerminalTab from '@/pages/DataStudio/MiddleContainer/Terminal'; import React, { useRef, useState } from 'react'; import { Tabs } from 'antd'; import './index.less'; +import TerminalTab from '@/pages/DataStudioNew/Toolbar/FlinkSqlClient/Terminal'; type TargetKey = React.MouseEvent | React.KeyboardEvent | string; type TabItem = { label: string; diff --git a/dinky-web/src/pages/DataStudioNew/constants.tsx b/dinky-web/src/pages/DataStudioNew/constants.tsx new file mode 100644 index 0000000000..3254b29620 --- /dev/null +++ b/dinky-web/src/pages/DataStudioNew/constants.tsx @@ -0,0 +1,45 @@ +/* + * + * 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 { l } from '@/utils/intl'; +import { CloseCircleTwoTone, IssuesCloseOutlined } from '@ant-design/icons'; +import { Space } from 'antd'; +import { DefaultOptionType } from 'antd/es/select'; +import { MenuItemType } from 'rc-menu/lib/interface'; + +export const TAG_RIGHT_CONTEXT_MENU: MenuItemType[] = [ + { + key: 'closeAll', + label: ( + + + {l('right.menu.closeAll')} + + ) + }, + { + key: 'closeOther', + label: ( + + + {l('right.menu.closeOther')} + + ) + } +]; diff --git a/dinky-web/src/pages/DataStudioNew/data.d.tsx b/dinky-web/src/pages/DataStudioNew/data.d.tsx index 6f7d6ceedc..0ae7022987 100644 --- a/dinky-web/src/pages/DataStudioNew/data.d.tsx +++ b/dinky-web/src/pages/DataStudioNew/data.d.tsx @@ -52,6 +52,8 @@ export enum DataStudioActionType { CATALOG_REFRESH = 'catalog-refresh', TASK_RUN_CHECK = 'task-run-check', TASK_DELETE = 'task-delete', + TASK_CLOSE_ALL = 'task-close-all', + TASK_CLOSE_OTHER = 'task-close-other', TASK_RUN_DAG = 'task-run-dag', TASK_RUN_LINEAGE = 'task-run-lineage', TASK_RUN_SUBMIT = 'task-run-submit', diff --git a/dinky-web/src/pages/DataStudioNew/function.tsx b/dinky-web/src/pages/DataStudioNew/function.tsx index 1ab64a712b..9c96b4ad5b 100644 --- a/dinky-web/src/pages/DataStudioNew/function.tsx +++ b/dinky-web/src/pages/DataStudioNew/function.tsx @@ -88,6 +88,7 @@ export const handleRightClick = ( e: any, stateAction: Dispatch> ) => { + e.preventDefault(); // 阻止浏览器默认的右键行为 let x = e.clientX; let y = e.clientY; // 判断右键的位置是否超出屏幕 , 如果超出屏幕则设置为屏幕的最大值 diff --git a/dinky-web/src/pages/DataStudioNew/index.tsx b/dinky-web/src/pages/DataStudioNew/index.tsx index dfaf6247be..9522cd7ed4 100644 --- a/dinky-web/src/pages/DataStudioNew/index.tsx +++ b/dinky-web/src/pages/DataStudioNew/index.tsx @@ -20,7 +20,7 @@ import { DockLayout, TabData } from 'rc-dock'; import React, { lazy, useEffect, useMemo, useRef, useState } from 'react'; import { PageContainer } from '@ant-design/pro-layout'; -import { Col, ConfigProvider, Row, Spin, theme, theme as antdTheme } from 'antd'; +import { Col, ConfigProvider, Row, Space, Spin, theme as antdTheme } from 'antd'; import Toolbar from '@/pages/DataStudioNew/Toolbar'; import { DataStudioActionType, RightContextMenuState } from '@/pages/DataStudioNew/data.d'; import { @@ -51,6 +51,10 @@ import './css/index.less'; import { getTenantByLocalStorage } from '@/utils/function'; import FooterContainer from '@/pages/DataStudioNew/FooterContainer'; import { useToken } from 'antd/es/theme/internal'; +import { TAG_RIGHT_CONTEXT_MENU } from '@/pages/DataStudioNew/constants'; +import { ContextMenuSpace } from '@/pages/DataStudioNew/ContextMenuSpace'; +import { sleep } from '@antfu/utils'; + const SqlTask = lazy(() => import('@/pages/DataStudioNew/CenterTabContent/SqlTask')); const DataSourceDetail = lazy( () => import('@/pages/DataStudioNew/CenterTabContent/DataSourceDetail') @@ -82,12 +86,21 @@ const DataStudioNew: React.FC = (props: any) => { const { drop } = useAliveController(); const menuItem = useRightMenuItem({ dataStudioState }); - // 右键弹出框状态 - const [rightContextMenuState, setRightContextMenuState] = useState({ + // 作业树右键弹出框状态 + const [edgeAreaRightMenuState, setEdgeAreaRightMenuState] = useState({ show: false, position: InitContextMenuPosition }); + // 标签右键弹出框状态 + const [tagRightMenuState, setTagRightMenuState] = useState< + RightContextMenuState & { id?: string } + >({ + show: false, + position: InitContextMenuPosition, + id: undefined + }); + const [loading, setLoading] = useState(true); const theme = useTheme() as 'realDark' | 'light'; const themeAlgorithm = useMemo(() => { @@ -208,11 +221,17 @@ const DataStudioNew: React.FC = (props: any) => { // 工具栏宽度 const toolbarSize = dataStudioState.toolbar.showDesc ? 60 : 40; - // 右键菜单handle - const rightContextMenuHandle = (e: any) => handleRightClick(e, setRightContextMenuState); - - const handleMenuClick = (values: MenuInfo) => { - setRightContextMenuState((prevState) => ({ ...prevState, show: false })); + /** + * 边缘区域调整布局右键点击事件 | edge area adjustment layout right-click events + * @param e + */ + const edgeAreaRightMenuHandle = (e: any) => handleRightClick(e, setEdgeAreaRightMenuState); + /** + * 右键菜单的点击事件 | right-click menu click event of the right-click menu + * @param values + */ + const handleEdgeAreaRightMenuClick = (values: MenuInfo) => { + setEdgeAreaRightMenuState((prevState) => ({ ...prevState, show: false })); switch (values.key) { case 'showToolbarDesc': @@ -258,6 +277,52 @@ const DataStudioNew: React.FC = (props: any) => { } }; + /** + * 标签右键菜单handle | the right-click menu handle of the tag + * @param e + */ + const tagRightMenuHandle = (e: any) => handleRightClick(e, setTagRightMenuState); + /** + * 右键菜单的点击事件 | right-click menu click event of the right-click menu + * @param {MenuInfo} node + */ + const handleTagRightMenuClick = (node: MenuInfo) => { + setTagRightMenuState((prevState) => ({ ...prevState, show: false })); + const { key } = node; + const current = dockLayoutRef.current; + const handleCloseOther = () => { + if (current) { + dataStudioState.centerContent.tabs.forEach((tab: CenterTab) => { + if (tab.id === tagRightMenuState.id) return; + const currentLayoutData = current.getLayout(); + const source = Algorithm.find(currentLayoutData, tab.id) as TabData; + const layoutData = Algorithm.removeFromLayout(currentLayoutData, source); + current.changeLayout(layoutData, tab.id, 'remove', false); + }); + } + }; + switch (key) { + case 'closeAll': + if (current) { + // 先关闭其他,再睡眠50ms 关闭当前页,否则会导致布局混乱 + handleCloseOther(); + sleep(50).then(() => { + const currentLayoutData = current.getLayout(); + + const source = Algorithm.find(currentLayoutData, tagRightMenuState.id!!) as TabData; + const layoutData = Algorithm.removeFromLayout(currentLayoutData, source); + current.changeLayout(layoutData, tagRightMenuState.id!!, 'remove', false); + }); + } + break; + case 'closeOther': + handleCloseOther(); + break; + default: + break; + } + }; + const saveTab = (tabData: TabData & any) => { let { id, group, title } = tabData; return { id, group, title }; @@ -297,31 +362,41 @@ const DataStudioNew: React.FC = (props: any) => { } const getTitle = () => { + const rightMenuHandle = (e: React.MouseEvent) => { + setTagRightMenuState((prevState) => ({ ...prevState, id: id })); + tagRightMenuHandle(e); + }; switch (tabData.tabType) { case 'task': const titleContent = ( - <> + {getTabIcon(tabData.params.dialect, 19)} {tabData.title} - + ); if (tabData.isUpdate) { return ( - - {titleContent} - {' *'} - + + + {titleContent} + {' *'} + + ); } - return {titleContent}; + return ( + {titleContent} + ); case 'dataSource': const dialect = tabData.params.type; return ( - <> + {getTabIcon(dialect, 19)} {tabData.title} - + ); default: - return <>{tabData.title}; + return ( + {tabData.title} + ); } }; @@ -454,7 +529,7 @@ const DataStudioNew: React.FC = (props: any) => { height: 'inherit' }} flex='none' - onContextMenu={rightContextMenuHandle} + onContextMenu={edgeAreaRightMenuHandle} > {/*左上工具栏*/} @@ -553,7 +628,7 @@ const DataStudioNew: React.FC = (props: any) => { { - {/*右键菜单*/} + {/* 边缘区域布局右键菜单*/} - setRightContextMenuState((prevState) => ({ ...prevState, show: false })) + setEdgeAreaRightMenuState((prevState) => ({ ...prevState, show: false })) } items={menuItem} - onClick={handleMenuClick} + onClick={handleEdgeAreaRightMenuClick} + /> + {/*标签的右键菜单*/} + + setTagRightMenuState((prevState) => ({ ...prevState, show: false })) + } /> diff --git a/dinky-web/src/pages/RegCenter/Resource/components/ResourceOverView/index.tsx b/dinky-web/src/pages/RegCenter/Resource/components/ResourceOverView/index.tsx index 9046a60cd9..a1d32e7e7f 100644 --- a/dinky-web/src/pages/RegCenter/Resource/components/ResourceOverView/index.tsx +++ b/dinky-web/src/pages/RegCenter/Resource/components/ResourceOverView/index.tsx @@ -73,17 +73,17 @@ const ResourceOverView: React.FC = (props) => { ); }; - useAsyncEffect(() => { + useEffect(() => { dispatch({ type: CONFIG_MODEL_ASYNC.queryResourceConfig, payload: SettingConfigKeyEnum.RESOURCE.toLowerCase() }); }, []); - useAsyncEffect(async () => { + useEffect(() => { // if enableResource is true, then refresh the tree, otherwise do nothing if (enableResource) { - await refreshTree(); + refreshTree(); } }, [enableResource]);