This is an automated email from the ASF dual-hosted git repository.
chengpan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git
The following commit(s) were added to refs/heads/master by this push:
new 71b099f73 [KYUUBI #5366][UI] Add submit SQL page in Kyuubi Web UI
71b099f73 is described below
commit 71b099f73fc6af0fa06c14ed61962ffb377cd395
Author: He Zhao <[email protected]>
AuthorDate: Mon Nov 13 20:16:40 2023 +0800
[KYUUBI #5366][UI] Add submit SQL page in Kyuubi Web UI
### _Why are the changes needed?_
This PR is to introduce a submit SQL editor page to close #5366. Users
then can submit SQL in SQL editor page to retrieve, update or do other data
manipulations by Spark engine with this feature.
Once users open up a new tab in SQL editor, a new connection(session) is
established with the a Kyuubi server instance. After that, users can input
their SQL statements in a text box provided on the page and select the desired
number of data rows to be shown in result from a dropdown list located at the
right side of the run button. When the statement is executed in one of server
instances, the execution status of the job is displayed to the user. After the
statement is executed suc [...]


### _How was this patch tested?_
- [ ] Add some test cases that check the changes thoroughly including
negative and positive cases if possible
- [ ] Add screenshots for manual tests if appropriate
- [ ] [Run
test](https://kyuubi.readthedocs.io/en/master/contributing/code/testing.html#running-tests)
locally before make a pull request
### _Was this patch authored or co-authored using generative AI tooling?_
No
Closes #5616 from zhaohehuhu/dev-1103.
Closes #5366
078d19edb [William Tong] code optimize (#9)
1d839bc37 [William Tong] code change (#8)
ddffd1917 [William Tong] fix code conflict in layout (#7)
f1fc9a248 [William Tong] UI change (#6)
265f02dad [William Tong] sql UI change (#5)
4bb22f0c2 [William Tong] Merge branch 'master' into dev-1103
065a4977d [William Tong] Merge master into branch (#4)
3438012c6 [William Tong] Merge pull request #3 from zhaohehuhu/dev-1103-3
a3740283f [weitong] add error message for sql page
f04acf29d [William Tong] Merge branch 'master' into dev-1103
93fa6a2f6 [William Tong] Merge pull request #2 from zhaohehuhu/dev-1103-2
f0669ba0e [weitong] fix
ecdbe5c1e [hezhao2] fix
531761de9 [hezhao2] change TabPaneName to TabPanelName
466a5ba1b [hezhao2] remove api2
ab0661f97 [hezhao2] change format for table header
5f4bca57d [hezhao2] move logo picture to images folder
159c35a3d [hezhao2] change layout
7379c4e2f [weitong] code change
0ddc6d54c [weitong] sql lab page
dda47bda4 [hezhao2] add license
9f80e20b1 [weitong] sql page test
Lead-authored-by: He Zhao <[email protected]>
Co-authored-by: William Tong <[email protected]>
Co-authored-by: hezhao2 <[email protected]>
Co-authored-by: weitong <[email protected]>
Signed-off-by: Cheng Pan <[email protected]>
---
kyuubi-server/web-ui/src/api/editor/index.ts | 77 ++++++
.../src/api/{server/index.ts => editor/types.ts} | 29 ++-
kyuubi-server/web-ui/src/api/server/index.ts | 2 +-
.../web-ui/src/api/server/{index.ts => types.ts} | 17 +-
.../web-ui/src/assets/images/document.svg | 22 ++
.../web-ui/src/assets/{ => images}/kyuubi-logo.svg | 0
.../web-ui/src/assets/{ => images}/kyuubi.png | Bin
.../web-ui/src/components/monaco-editor/index.vue | 2 +-
.../web-ui/src/components/monaco-editor/types.ts | 7 +-
.../web-ui/src/layout/components/aside/index.vue | 4 +-
.../web-ui/src/layout/components/aside/types.ts | 4 +-
kyuubi-server/web-ui/src/locales/en_US/index.ts | 14 +-
kyuubi-server/web-ui/src/locales/zh_CN/index.ts | 14 +-
.../web-ui/src/router/{lab => editor}/index.ts | 6 +-
kyuubi-server/web-ui/src/router/index.ts | 4 +-
.../web-ui/src/views/editor/components/Editor.vue | 290 +++++++++++++++++++++
.../web-ui/src/views/editor/components/Log.vue | 56 ++++
.../web-ui/src/views/editor/components/Result.vue | 144 ++++++++++
.../index.ts => views/editor/components/types.ts} | 38 ++-
kyuubi-server/web-ui/src/views/editor/index.vue | 141 ++++++++++
.../editor/styles/shared-styles.scss} | 20 +-
kyuubi-server/web-ui/src/views/lab/index.vue | 64 -----
22 files changed, 842 insertions(+), 113 deletions(-)
diff --git a/kyuubi-server/web-ui/src/api/editor/index.ts
b/kyuubi-server/web-ui/src/api/editor/index.ts
new file mode 100644
index 000000000..daaf0471c
--- /dev/null
+++ b/kyuubi-server/web-ui/src/api/editor/index.ts
@@ -0,0 +1,77 @@
+/*
+ * 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 request from '@/utils/request'
+import type {
+ IOpenSessionRequest,
+ IRunSqlRequest,
+ IGetSqlRowsetRequest,
+ IGetSqlMetadataRequest
+} from './types'
+
+export function openSession(data: IOpenSessionRequest): any {
+ return request({
+ url: 'api/v1/sessions',
+ method: 'post',
+ data
+ })
+}
+
+export function closeSession(identifier: string): any {
+ return request({
+ url: `api/v1/sessions/${identifier}`,
+ method: 'delete'
+ })
+}
+
+export function runSql(data: IRunSqlRequest, identifier: string): any {
+ return request({
+ url: `api/v1/sessions/${identifier}/operations/statement`,
+ method: 'post',
+ data
+ })
+}
+
+export function getSqlRowset(params: IGetSqlRowsetRequest): any {
+ return request({
+ url: `api/v1/operations/${params.operationHandleStr}/rowset`,
+ method: 'get',
+ params
+ })
+}
+
+export function getSqlMetadata(params: IGetSqlMetadataRequest): any {
+ return request({
+ url: `api/v1/operations/${params.operationHandleStr}/resultsetmetadata`,
+ method: 'get',
+ params
+ })
+}
+
+export function getLog(identifier: string): any {
+ return request({
+ url: `api/v1/operations/${identifier}/log`,
+ method: 'get'
+ })
+}
+
+export function closeOperation(identifier: string) {
+ return request({
+ url: `api/v1/admin/operations/${identifier}`,
+ method: 'delete'
+ })
+}
diff --git a/kyuubi-server/web-ui/src/api/server/index.ts
b/kyuubi-server/web-ui/src/api/editor/types.ts
similarity index 65%
copy from kyuubi-server/web-ui/src/api/server/index.ts
copy to kyuubi-server/web-ui/src/api/editor/types.ts
index e2d74d7db..0bc4c2086 100644
--- a/kyuubi-server/web-ui/src/api/server/index.ts
+++ b/kyuubi-server/web-ui/src/api/editor/types.ts
@@ -15,11 +15,28 @@
* limitations under the License.
*/
-import request from '@/utils/request'
+interface IOpenSessionRequest {
+ 'kyuubi.engine.type': string
+}
+
+interface IRunSqlRequest {
+ statement: string
+ runAsync: boolean
+}
+
+interface IGetSqlRowsetRequest {
+ operationHandleStr: string
+ fetchorientation: 'FETCH_NEXT'
+ maxrows: number
+}
+
+interface IGetSqlMetadataRequest {
+ operationHandleStr: string
+}
-export function getAllServer() {
- return request({
- url: 'api/v1/admin/server',
- method: 'get'
- })
+export {
+ IOpenSessionRequest,
+ IRunSqlRequest,
+ IGetSqlRowsetRequest,
+ IGetSqlMetadataRequest
}
diff --git a/kyuubi-server/web-ui/src/api/server/index.ts
b/kyuubi-server/web-ui/src/api/server/index.ts
index e2d74d7db..4dd402b67 100644
--- a/kyuubi-server/web-ui/src/api/server/index.ts
+++ b/kyuubi-server/web-ui/src/api/server/index.ts
@@ -17,7 +17,7 @@
import request from '@/utils/request'
-export function getAllServer() {
+export function getAllServer(): any {
return request({
url: 'api/v1/admin/server',
method: 'get'
diff --git a/kyuubi-server/web-ui/src/api/server/index.ts
b/kyuubi-server/web-ui/src/api/server/types.ts
similarity index 82%
copy from kyuubi-server/web-ui/src/api/server/index.ts
copy to kyuubi-server/web-ui/src/api/server/types.ts
index e2d74d7db..c747f4360 100644
--- a/kyuubi-server/web-ui/src/api/server/index.ts
+++ b/kyuubi-server/web-ui/src/api/server/types.ts
@@ -15,11 +15,14 @@
* limitations under the License.
*/
-import request from '@/utils/request'
-
-export function getAllServer() {
- return request({
- url: 'api/v1/admin/server',
- method: 'get'
- })
+interface IServer {
+ attributes: any | null
+ host: string
+ instance: string
+ namespace: string
+ nodeName: string
+ port: number
+ status: string
}
+
+export { IServer }
diff --git a/kyuubi-server/web-ui/src/assets/images/document.svg
b/kyuubi-server/web-ui/src/assets/images/document.svg
new file mode 100644
index 000000000..e3d1bfe1b
--- /dev/null
+++ b/kyuubi-server/web-ui/src/assets/images/document.svg
@@ -0,0 +1,22 @@
+<!--
+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.
+-->
+<svg width="64" height="80" viewBox="0 0 64 80" fill="none"
xmlns="http://www.w3.org/2000/svg">
+<path d="M53.2326 10.7391L53.2336 10.7402L63.5
21.7963V79.5H0.5V0.5H43.7811L53.2326 10.7391Z" fill="#FAFAFA" stroke="#D9D9D9"/>
+<path d="M44 0V22.4H64" stroke="#D9D9D9"/>
+</svg>
diff --git a/kyuubi-server/web-ui/src/assets/kyuubi-logo.svg
b/kyuubi-server/web-ui/src/assets/images/kyuubi-logo.svg
similarity index 100%
rename from kyuubi-server/web-ui/src/assets/kyuubi-logo.svg
rename to kyuubi-server/web-ui/src/assets/images/kyuubi-logo.svg
diff --git a/kyuubi-server/web-ui/src/assets/kyuubi.png
b/kyuubi-server/web-ui/src/assets/images/kyuubi.png
similarity index 100%
rename from kyuubi-server/web-ui/src/assets/kyuubi.png
rename to kyuubi-server/web-ui/src/assets/images/kyuubi.png
diff --git a/kyuubi-server/web-ui/src/components/monaco-editor/index.vue
b/kyuubi-server/web-ui/src/components/monaco-editor/index.vue
index a98a3f31b..65a2dba34 100644
--- a/kyuubi-server/web-ui/src/components/monaco-editor/index.vue
+++ b/kyuubi-server/web-ui/src/components/monaco-editor/index.vue
@@ -88,7 +88,7 @@
editor = monaco.editor.create(codeEditBox.value, {
value: props.modelValue,
language: props.language,
- theme: monacoEditorThemeRef.value,
+ theme: props.theme || monacoEditorThemeRef.value,
...props.options
})
diff --git a/kyuubi-server/web-ui/src/components/monaco-editor/types.ts
b/kyuubi-server/web-ui/src/components/monaco-editor/types.ts
index 80400565e..aa962d43c 100644
--- a/kyuubi-server/web-ui/src/components/monaco-editor/types.ts
+++ b/kyuubi-server/web-ui/src/components/monaco-editor/types.ts
@@ -53,10 +53,7 @@ export const editorProps = {
default: 'sql'
},
theme: {
- type: String as PropType<Theme>,
- validator(value: string): boolean {
- return ['vs', 'vs-dark'].includes(value)
- },
+ type: String as PropType<any>,
default: 'vs'
},
options: {
@@ -72,7 +69,7 @@ export const editorProps = {
},
readOnly: false,
contextmenu: true,
- fontSize: 16,
+ fontSize: 14,
scrollBeyondLastLine: true,
overviewRulerBorder: false
}
diff --git a/kyuubi-server/web-ui/src/layout/components/aside/index.vue
b/kyuubi-server/web-ui/src/layout/components/aside/index.vue
index 42db213e3..c5d1e41ae 100644
--- a/kyuubi-server/web-ui/src/layout/components/aside/index.vue
+++ b/kyuubi-server/web-ui/src/layout/components/aside/index.vue
@@ -18,8 +18,8 @@
<template>
<header>
- <img v-if="!isCollapse" src="@/assets/kyuubi-logo.svg" />
- <img v-else class="collapsed-logo" src="@/assets/kyuubi.png" />
+ <img v-if="!isCollapse" src="@/assets/images/kyuubi-logo.svg" />
+ <img v-else class="collapsed-logo" src="@/assets/images/kyuubi.png" />
<span v-if="!isCollapse">{{ version }}</span>
</header>
<c-menu :is-collapse="isCollapse" :active-path="activePath" :menus="menus" />
diff --git a/kyuubi-server/web-ui/src/layout/components/aside/types.ts
b/kyuubi-server/web-ui/src/layout/components/aside/types.ts
index 1a6461634..76bb1f387 100644
--- a/kyuubi-server/web-ui/src/layout/components/aside/types.ts
+++ b/kyuubi-server/web-ui/src/layout/components/aside/types.ts
@@ -49,8 +49,8 @@ export const MENUS = [
router: '/swagger'
},
{
- label: 'SQL Lab',
+ label: 'SQL Editor',
icon: 'Cpu',
- router: '/lab'
+ router: '/editor'
}
]
diff --git a/kyuubi-server/web-ui/src/locales/en_US/index.ts
b/kyuubi-server/web-ui/src/locales/en_US/index.ts
index 8606c74da..9bb0144ff 100644
--- a/kyuubi-server/web-ui/src/locales/en_US/index.ts
+++ b/kyuubi-server/web-ui/src/locales/en_US/index.ts
@@ -37,6 +37,11 @@ export default {
engine_ui: 'Engine UI',
failure_reason: 'Failure Reason',
session_properties: 'Session Properties',
+ no_data: 'No data',
+ no_log: 'No log',
+ run_sql_tips: 'Run a SQL to get result',
+ result: 'Result',
+ log: 'Log',
operation: {
text: 'Operation',
delete_confirm: 'Delete Confirm',
@@ -44,7 +49,8 @@ export default {
cancel_confirm: 'Cancel Confirm',
close: 'Close',
cancel: 'Cancel',
- delete: 'Delete'
+ delete: 'Delete',
+ run: 'Run'
},
message: {
delete_succeeded: 'Delete {name} Succeeded',
@@ -52,6 +58,10 @@ export default {
close_succeeded: 'Close {name} Succeeded',
close_failed: 'Close {name} Failed',
cancel_succeeded: 'Cancel {name} Succeeded',
- cancel_failed: 'Cancel {name} Failed'
+ cancel_failed: 'Cancel {name} Failed',
+ run_sql_failed: 'Run SQL Failed',
+ get_sql_log_failed: 'Get SQL Log Failed',
+ get_sql_result_failed: 'Get SQL Result Failed',
+ get_sql_metadata_failed: 'Get SQL Metadata Failed'
}
}
diff --git a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
index 0c4cb66db..198f379ec 100644
--- a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
+++ b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
@@ -37,6 +37,11 @@ export default {
engine_ui: 'Engine UI',
failure_reason: '失败原因',
session_properties: 'Session 参数',
+ no_data: '无数据',
+ no_log: '无日志',
+ run_sql_tips: '请运行SQL获取结果',
+ result: '结果',
+ log: '日志',
operation: {
text: '操作',
delete_confirm: '确认删除',
@@ -44,7 +49,8 @@ export default {
cancel_confirm: '确认取消',
close: '关闭',
cancel: '取消',
- delete: '删除'
+ delete: '删除',
+ run: '运行'
},
message: {
delete_succeeded: '删除 {name} 成功',
@@ -52,6 +58,10 @@ export default {
close_succeeded: '关闭 {name} 成功',
close_failed: '关闭 {name} 失败',
cancel_succeeded: '取消 {name} 成功',
- cancel_failed: '取消 {name} 失败'
+ cancel_failed: '取消 {name} 失败',
+ run_sql_failed: '运行SQL失败',
+ get_sql_log_failed: '获取SQL日志失败',
+ get_sql_result_failed: '获取SQL结果失败',
+ get_sql_metadata_failed: '获取SQL元数据失败'
}
}
diff --git a/kyuubi-server/web-ui/src/router/lab/index.ts
b/kyuubi-server/web-ui/src/router/editor/index.ts
similarity index 89%
copy from kyuubi-server/web-ui/src/router/lab/index.ts
copy to kyuubi-server/web-ui/src/router/editor/index.ts
index d78838079..9d4df889c 100644
--- a/kyuubi-server/web-ui/src/router/lab/index.ts
+++ b/kyuubi-server/web-ui/src/router/editor/index.ts
@@ -17,9 +17,9 @@
const routes = [
{
- path: '/lab',
- name: 'lab',
- component: () => import('@/views/lab/index.vue')
+ path: '/editor',
+ name: 'editor',
+ component: () => import('@/views/editor/index.vue')
}
]
diff --git a/kyuubi-server/web-ui/src/router/index.ts
b/kyuubi-server/web-ui/src/router/index.ts
index 9df39574f..7bbe34446 100644
--- a/kyuubi-server/web-ui/src/router/index.ts
+++ b/kyuubi-server/web-ui/src/router/index.ts
@@ -20,7 +20,7 @@ import overviewRoutes from './overview'
import managementRoutes from './management'
import detailRoutes from './detail'
import swaggerRoutes from './swagger'
-import labRoutes from './lab'
+import editorRoutes from './editor'
const routes = [
{
@@ -40,7 +40,7 @@ const routes = [
...managementRoutes,
...detailRoutes,
...swaggerRoutes,
- ...labRoutes
+ ...editorRoutes
]
}
]
diff --git a/kyuubi-server/web-ui/src/views/editor/components/Editor.vue
b/kyuubi-server/web-ui/src/views/editor/components/Editor.vue
new file mode 100644
index 000000000..21faee5a3
--- /dev/null
+++ b/kyuubi-server/web-ui/src/views/editor/components/Editor.vue
@@ -0,0 +1,290 @@
+<!--
+* 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.
+-->
+
+<template>
+ <div class="editor">
+ <el-space>
+ <el-button
+ :disabled="!param.engineType || !editorVariables.content"
+ :loading="resultLoading"
+ type="success"
+ icon="VideoPlay"
+ @click="handleQuerySql">
+ {{ $t('operation.run') }}
+ </el-button>
+ <el-dropdown @command="handleChangeLimit">
+ <span class="el-dropdown-link">
+ Limit: {{ limit }}
+ <el-icon class="el-icon--right">
+ <arrow-down />
+ </el-icon>
+ </span>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-for="l in [10, 50, 100]" :key="l" :command="l">
+ Limit: {{ l }}
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ <el-select
+ v-model="param.engineType"
+ disabled
+ :placeholder="$t('engine_type')">
+ <el-option
+ v-for="item in getEngineType()"
+ :key="item"
+ :label="item"
+ :value="item" />
+ </el-select>
+ </el-space>
+ <section>
+ <MonacoEditor
+ v-model="editorVariables.content"
+ :language="editorVariables.language"
+ :theme="theme"
+ @editor-mounted="editorMounted"
+ @change="handleContentChange"
+ @editor-save="editorSave" />
+ </section>
+ <el-tabs v-model="activeTab" type="card" class="result-el-tabs">
+ <el-tab-pane
+ v-loading="resultLoading"
+ :label="`${$t('result')}${
+ sqlResult?.length ? ` (${sqlResult?.length})` : ''
+ }`"
+ name="result">
+ <Result :data="sqlResult" :error-messages="errorMessages" />
+ </el-tab-pane>
+ <el-tab-pane v-loading="logLoading" :label="$t('log')" name="log">
+ <Log :data="sqlLog" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script lang="ts" setup>
+ import MonacoEditor from '@/components/monaco-editor/index.vue'
+ import Result from './Result.vue'
+ import Log from './Log.vue'
+ import { ref, reactive, onUnmounted, toRaw } from 'vue'
+ import type { Ref } from 'vue'
+ import * as monaco from 'monaco-editor'
+ import { format } from 'sql-formatter'
+ import { ElMessage } from 'element-plus'
+ import { useI18n } from 'vue-i18n'
+ import { getEngineType } from '@/utils/engine'
+ import {
+ openSession,
+ closeSession,
+ runSql,
+ getSqlRowset,
+ getSqlMetadata,
+ getLog,
+ closeOperation
+ } from '@/api/editor'
+ import type {
+ IResponse,
+ ISqlResult,
+ IFields,
+ ILog,
+ IErrorMessage,
+ IError
+ } from './types'
+
+ const { t } = useI18n()
+ const param = reactive({
+ engineType: 'SPARK_SQL'
+ })
+ const limit = ref(10)
+ const sqlResult: Ref<any[] | null> = ref(null)
+ const sqlLog = ref('')
+ const activeTab = ref('result')
+ const resultLoading = ref(false)
+ const logLoading = ref(false)
+ const sessionIdentifier = ref('')
+ const theme = ref('customTheme')
+ const errorMessages: Ref<IErrorMessage[]> = ref([])
+ const editorVariables = reactive({
+ editor: {} as any,
+ language: 'sql',
+ content: '',
+ options: {}
+ })
+
+ const editorMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
+ editorVariables.editor = editor
+ }
+ const handleFormat = () => {
+ toRaw(editorVariables.editor).setValue(
+ format(toRaw(editorVariables.editor).getValue())
+ )
+ }
+
+ const editorSave = () => {
+ handleFormat()
+ }
+
+ const handleContentChange = (value: string) => {
+ editorVariables.content = value
+ }
+
+ const handleQuerySql = async () => {
+ resultLoading.value = true
+ logLoading.value = true
+ errorMessages.value = []
+
+ if (!sessionIdentifier.value) {
+ const openSessionResponse: IResponse = await openSession({
+ 'kyuubi.engine.type': param.engineType
+ }).catch(catchSessionError)
+ if (!openSessionResponse) return
+ sessionIdentifier.value = openSessionResponse.identifier
+ }
+
+ const runSqlResponse: IResponse = await runSql(
+ {
+ statement: editorVariables.content,
+ runAsync: false
+ },
+ sessionIdentifier.value
+ ).catch(catchSessionError)
+ if (!runSqlResponse) return
+
+ const getSqlResultPromise = Promise.all([
+ getSqlRowset({
+ operationHandleStr: runSqlResponse.identifier,
+ fetchorientation: 'FETCH_NEXT',
+ maxrows: limit.value
+ }).catch((err: IError) => {
+ catchOperationError(err, t('message.get_sql_result_failed'))
+ }),
+ getSqlMetadata({
+ operationHandleStr: runSqlResponse.identifier
+ }).catch((err: IError) =>
+ catchOperationError(err, t('message.get_sql_metadata_failed'))
+ )
+ ])
+ .then((result) => {
+ sqlResult.value = result[0]?.rows?.map((row: IFields) => {
+ const map: { [key: string]: any } = {}
+ row.fields?.forEach(({ value }: ISqlResult, index: number) => {
+ map[result[1].columns[index]?.columnName] = value
+ })
+ return map
+ })
+ })
+ .finally(() => {
+ resultLoading.value = false
+ })
+
+ const getSqlLogPromise = getLog(runSqlResponse.identifier)
+ .then((res: ILog) => {
+ sqlLog.value = res?.logRowSet?.join('\r\n')
+ })
+ .catch((err: IError) => {
+ postError(err, t('message.get_sql_log_failed'))
+ sqlLog.value = ''
+ })
+ .finally(() => {
+ logLoading.value = false
+ })
+
+ Promise.all([getSqlResultPromise, getSqlLogPromise]).then(() =>
+ closeOperation(runSqlResponse.identifier)
+ )
+ }
+
+ const postError = (err: IError, title = t('message.run_sql_failed')) => {
+ errorMessages.value.push({
+ title,
+ description: err?.response?.data?.message || err?.message || ''
+ })
+ ElMessage({
+ message: title,
+ type: 'error'
+ })
+ }
+
+ const catchSessionError = (err: IError) => {
+ sqlResult.value = []
+ sqlLog.value = ''
+ postError(err)
+ resultLoading.value = false
+ logLoading.value = false
+ }
+
+ const catchOperationError = (err: IError, title: string) => {
+ postError(err, title)
+ sqlResult.value = []
+ }
+
+ const handleChangeLimit = (command: number) => {
+ limit.value = command
+ }
+
+ const customMonacoEditorTheme = () => {
+ monaco.editor.defineTheme(theme.value, {
+ base: 'vs',
+ inherit: true,
+ rules: [],
+ colors: {
+ 'editor.foreground': '#000000',
+ 'editor.background': '#ffffff',
+ 'editor.lineHighlightBackground': '#f6f6f6',
+ 'editorGutter.background': '#e2e2e2'
+ }
+ })
+ monaco.editor.setTheme(theme.value)
+ }
+ customMonacoEditorTheme()
+
+ onUnmounted(() => {
+ if (sessionIdentifier.value) {
+ closeSession(sessionIdentifier.value)
+ }
+ })
+</script>
+
+<style lang="scss" scoped>
+ .editor {
+ > .el-space,
+ > section,
+ > .el-tabs {
+ margin-bottom: 12px;
+ }
+
+ > .el-space {
+ .el-select {
+ width: 180px;
+ }
+ .el-button {
+ width: 120px;
+ }
+ .el-dropdown {
+ margin-left: 4px;
+ margin-right: 8px;
+ }
+ }
+
+ > section {
+ height: 180px;
+ border: 1px solid #e0e0e0;
+ }
+ }
+</style>
diff --git a/kyuubi-server/web-ui/src/views/editor/components/Log.vue
b/kyuubi-server/web-ui/src/views/editor/components/Log.vue
new file mode 100644
index 000000000..d2a403d9e
--- /dev/null
+++ b/kyuubi-server/web-ui/src/views/editor/components/Log.vue
@@ -0,0 +1,56 @@
+<!--
+* 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.
+-->
+
+<template>
+ <div class="log">
+ <pre v-if="data">{{ data }}</pre>
+ <div v-else class="no-data">
+ <img src="@/assets/images/document.svg" />
+ <div>{{ $t('no_log') }}</div>
+ </div>
+ </div>
+</template>
+
+<script lang="ts" setup>
+ defineProps({
+ data: {
+ type: String,
+ default: null,
+ required: true
+ }
+ })
+</script>
+
+<style lang="scss" scoped>
+ @import '../styles/shared-styles.scss';
+ .log {
+ min-height: 200px;
+ position: relative;
+ pre {
+ overflow: auto;
+ padding: 0 24px;
+ font-size: 12px;
+ line-height: 20px;
+ color: #444;
+ white-space: pre-wrap;
+ }
+ .no-data {
+ @include sharedNoData;
+ }
+ }
+</style>
diff --git a/kyuubi-server/web-ui/src/views/editor/components/Result.vue
b/kyuubi-server/web-ui/src/views/editor/components/Result.vue
new file mode 100644
index 000000000..de103ff13
--- /dev/null
+++ b/kyuubi-server/web-ui/src/views/editor/components/Result.vue
@@ -0,0 +1,144 @@
+<!--
+* 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.
+-->
+
+<template>
+ <div class="result">
+ <el-table v-if="data?.length" stripe :data="data" class="editor-el-table">
+ <el-table-column type="index" class-name="index" :width="indexWidth" />
+ <el-table-column
+ v-for="(width, key) in columns"
+ :key="key"
+ :prop="key"
+ :label="key"
+ :width="width"
+ show-overflow-tooltip
+ sortable />
+ </el-table>
+ <template v-else>
+ <template v-if="errorMessages.length">
+ <el-alert
+ v-for="(m, idx) in errorMessages"
+ :key="idx"
+ :title="m.title"
+ type="error"
+ show-icon>
+ <template #default>
+ <pre> {{ m.description }} </pre>
+ </template>
+ </el-alert>
+ </template>
+ <div v-else class="no-data">
+ <img src="@/assets/images/document.svg" />
+ <div>{{ data === null ? $t('run_sql_tips') : $t('no_data') }}</div>
+ </div>
+ </template>
+ </div>
+</template>
+
+<script lang="ts" setup>
+ import { PropType, computed } from 'vue'
+ import type { IErrorMessage } from './types'
+
+ const font = '14px Myriad Pro,Helvetica Neue,Arial,Helvetica,sans-serif'
+ const props = defineProps({
+ data: {
+ type: [Array, null] as PropType<Array<Object> | null>,
+ default: null,
+ required: true
+ },
+ errorMessages: {
+ type: Array as PropType<Array<IErrorMessage>>,
+ default: [],
+ required: true
+ }
+ })
+
+ const measureTextWidth = (text: string, font: string, deviation = 48) => {
+ const canvas = document.createElement('canvas')
+ const context: CanvasRenderingContext2D = canvas.getContext(
+ '2d'
+ ) as CanvasRenderingContext2D
+ context.font = font
+ const width = context.measureText(text).width
+ return width + deviation
+ }
+
+ const indexWidth = computed(() =>
+ props.data?.length ? measureTextWidth(String(props.data?.length), font) : 0
+ )
+
+ const columns = computed(() => {
+ if (props.data?.length) {
+ const maxColumnLength = 600
+ const obj: { [key: string]: number } = {}
+ props.data?.forEach((item: { [key: string]: any }) => {
+ for (const key in item) {
+ if (!obj[key]) {
+ obj[key] = measureTextWidth(key, font, 80)
+ }
+ obj[key] = Math.min(
+ maxColumnLength,
+ Math.max(obj[key], measureTextWidth(String(item[key]), font))
+ )
+ }
+ })
+ return obj
+ } else {
+ return {}
+ }
+ })
+</script>
+
+<style lang="scss" scoped>
+ @import '../styles/shared-styles.scss';
+
+ :deep(.editor-el-table) {
+ .el-table__header,
+ .el-table__body,
+ .el-table__body-wrapper .el-scrollbar__view,
+ tbody td .cell {
+ min-width: 100%;
+ }
+
+ tbody td.index .cell {
+ text-align: center;
+ }
+ }
+ .result {
+ min-height: 200px;
+ position: relative;
+ .el-alert {
+ width: auto;
+ margin: 10px;
+ border: 1px solid #db2828;
+
+ :deep(.el-alert__description) {
+ max-height: 300px;
+ overflow: auto;
+
+ pre {
+ margin-top: 0;
+ white-space: pre-wrap;
+ }
+ }
+ }
+ .no-data {
+ @include sharedNoData;
+ }
+ }
+</style>
diff --git a/kyuubi-server/web-ui/src/router/lab/index.ts
b/kyuubi-server/web-ui/src/views/editor/components/types.ts
similarity index 63%
rename from kyuubi-server/web-ui/src/router/lab/index.ts
rename to kyuubi-server/web-ui/src/views/editor/components/types.ts
index d78838079..42475bf4a 100644
--- a/kyuubi-server/web-ui/src/router/lab/index.ts
+++ b/kyuubi-server/web-ui/src/views/editor/components/types.ts
@@ -15,12 +15,36 @@
* limitations under the License.
*/
-const routes = [
- {
- path: '/lab',
- name: 'lab',
- component: () => import('@/views/lab/index.vue')
+interface IResponse {
+ identifier: string
+}
+
+interface ISqlResult {
+ dataName?: string
+ dataType: string
+ value: any
+}
+
+interface IFields {
+ fields: ISqlResult[]
+}
+
+interface ILog {
+ logRowSet: string[]
+ rowCount: number
+}
+
+interface IErrorMessage {
+ title: string
+ description: string
+}
+
+interface IError extends Error {
+ response?: {
+ data?: {
+ message?: string
+ }
}
-]
+}
-export default routes
+export { IResponse, ISqlResult, IFields, ILog, IErrorMessage, IError }
diff --git a/kyuubi-server/web-ui/src/views/editor/index.vue
b/kyuubi-server/web-ui/src/views/editor/index.vue
new file mode 100644
index 000000000..424d3e929
--- /dev/null
+++ b/kyuubi-server/web-ui/src/views/editor/index.vue
@@ -0,0 +1,141 @@
+<!--
+* 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.
+-->
+
+<template>
+ <div class="container">
+ <el-tabs
+ v-model="editableTabsValue"
+ type="border-card"
+ class="editor-el-tabs"
+ editable
+ @edit="handleTabsEdit">
+ <el-tab-pane
+ v-for="item in editableTabs"
+ :key="item.name"
+ :label="item.title"
+ :name="item.name">
+ <Editor />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script lang="ts" setup>
+ import Editor from './components/Editor.vue'
+ import { ref } from 'vue'
+ import { TabPanelName } from 'element-plus'
+
+ const editableTabsValue = ref('1')
+ const editableTabs = ref([
+ {
+ title: 'Session 1',
+ name: '1'
+ }
+ ])
+
+ const handleTabsEdit = (
+ targetName: TabPanelName | undefined,
+ action: 'remove' | 'add'
+ ) => {
+ if (action === 'add') {
+ const tabLength = editableTabs.value.length + 1
+ const newTabName = `${tabLength}`
+ editableTabs.value.push({
+ title: `Session ${tabLength}`,
+ name: newTabName
+ })
+ editableTabsValue.value = newTabName
+ } else if (action === 'remove') {
+ const tabs = editableTabs.value
+ let activeName = editableTabsValue.value
+ if (activeName === targetName) {
+ tabs.forEach((tab, index) => {
+ if (tab.name === targetName) {
+ const nextTab = tabs[index + 1] || tabs[index - 1]
+ if (nextTab) {
+ activeName = nextTab.name
+ }
+ }
+ })
+ }
+
+ editableTabsValue.value = activeName
+ editableTabs.value = tabs.filter((tab) => tab.name !== targetName)
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .container {
+ height: calc(100vh - 66px);
+ margin: -20px;
+ }
+
+ :deep(.editor-el-tabs) {
+ height: 100%;
+ overflow: auto;
+ .el-tabs__header {
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: flex-end;
+ background: #f5f5f5;
+ }
+ .el-tabs__content {
+ overflow: auto;
+ padding: 12px;
+ .el-tab-pane {
+ height: 100%;
+ }
+ }
+ }
+
+ :deep(.result-el-tabs) {
+ $border: 1px solid #e4e7ed;
+ .el-tabs__header {
+ margin-bottom: 0;
+ background: transparent;
+ .el-tabs__nav-wrap {
+ border-bottom: $border;
+ }
+ .el-tabs__nav {
+ border: 0;
+
+ .el-tabs__item {
+ border: $border;
+ border-bottom: 0;
+ margin-right: 4px;
+ background: #f5f5f5;
+ border-radius: 2px 2px 0 0;
+
+ &.is-active {
+ background: #fff;
+ }
+
+ &:not(.is-active) {
+ color: #909399;
+ }
+ }
+ }
+ }
+ .el-tabs__content {
+ padding: 10px 0 0 0;
+ border: $border;
+ border-top: none;
+ }
+ }
+</style>
diff --git a/kyuubi-server/web-ui/src/api/server/index.ts
b/kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss
similarity index 76%
copy from kyuubi-server/web-ui/src/api/server/index.ts
copy to kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss
index e2d74d7db..9027ef69a 100644
--- a/kyuubi-server/web-ui/src/api/server/index.ts
+++ b/kyuubi-server/web-ui/src/views/editor/styles/shared-styles.scss
@@ -14,12 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
-import request from '@/utils/request'
-
-export function getAllServer() {
- return request({
- url: 'api/v1/admin/server',
- method: 'get'
- })
-}
+
+@mixin sharedNoData {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ font-size: 14px;
+ color: #999;
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/kyuubi-server/web-ui/src/views/lab/index.vue
b/kyuubi-server/web-ui/src/views/lab/index.vue
deleted file mode 100644
index 26ecfac0d..000000000
--- a/kyuubi-server/web-ui/src/views/lab/index.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<!--
-* 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.
--->
-
-<template>
- <div class="container">
- <MonacoEditor
- v-model="editorVariables.content"
- :language="editorVariables.language"
- @editor-mounted="editorMounted"
- @change="handleContentChange"
- @editor-save="editorSave" />
- </div>
-</template>
-
-<script lang="ts" setup>
- import MonacoEditor from '@/components/monaco-editor/index.vue'
- import { reactive, toRaw } from 'vue'
- import * as monaco from 'monaco-editor'
- import { format } from 'sql-formatter'
-
- const editorVariables = reactive({
- editor: {} as any,
- language: 'sql',
- content: ''
- })
-
- const editorMounted = (editor: monaco.editor.IStandaloneCodeEditor) => {
- editorVariables.editor = editor
- }
- const handleFormat = () => {
- toRaw(editorVariables.editor).setValue(
- format(toRaw(editorVariables.editor).getValue())
- )
- }
-
- const editorSave = () => {
- handleFormat()
- }
-
- const handleContentChange = (value: string) => {
- editorVariables.content = value
- }
-</script>
-
-<style lang="scss" scoped>
- .container {
- height: 70%;
- }
-</style>