diff --git a/LICENSE-binary b/LICENSE-binary
index 7322dec51..065fc6499 100644
--- a/LICENSE-binary
+++ b/LICENSE-binary
@@ -261,6 +261,7 @@ io.etcd:jetcd-api
io.etcd:jetcd-common
io.etcd:jetcd-core
io.etcd:jetcd-grpc
+org.eclipse.jetty:jetty-client
org.eclipse.jetty:jetty-http
org.eclipse.jetty:jetty-io
org.eclipse.jetty:jetty-security
@@ -268,6 +269,7 @@ org.eclipse.jetty:jetty-server
org.eclipse.jetty:jetty-servlet
org.eclipse.jetty:jetty-util-ajax
org.eclipse.jetty:jetty-util
+org.eclipse.jetty:jetty-proxy
org.apache.thrift:libfb303
org.apache.thrift:libthrift
org.apache.logging.log4j:log4j-1.2-api
diff --git a/dev/dependencyList b/dev/dependencyList
index ab80bcfab..6ec0464c0 100644
--- a/dev/dependencyList
+++ b/dev/dependencyList
@@ -95,8 +95,10 @@ jetcd-api/0.7.3//jetcd-api-0.7.3.jar
jetcd-common/0.7.3//jetcd-common-0.7.3.jar
jetcd-core/0.7.3//jetcd-core-0.7.3.jar
jetcd-grpc/0.7.3//jetcd-grpc-0.7.3.jar
+jetty-client/9.4.51.v20230217//jetty-client-9.4.51.v20230217.jar
jetty-http/9.4.51.v20230217//jetty-http-9.4.51.v20230217.jar
jetty-io/9.4.51.v20230217//jetty-io-9.4.51.v20230217.jar
+jetty-proxy/9.4.51.v20230217//jetty-proxy-9.4.51.v20230217.jar
jetty-security/9.4.51.v20230217//jetty-security-9.4.51.v20230217.jar
jetty-server/9.4.51.v20230217//jetty-server-9.4.51.v20230217.jar
jetty-servlet/9.4.51.v20230217//jetty-servlet-9.4.51.v20230217.jar
diff --git a/kyuubi-server/pom.xml b/kyuubi-server/pom.xml
index 2a7ce2270..e1a9c3312 100644
--- a/kyuubi-server/pom.xml
+++ b/kyuubi-server/pom.xml
@@ -257,6 +257,11 @@
trino-client
+
+ org.eclipse.jetty
+ jetty-proxy
+
+
org.glassfish.jersey.test-framework
jersey-test-framework-core
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala
index c9d571008..86cb28aed 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/KyuubiRestFrontendService.scala
@@ -90,6 +90,9 @@ class KyuubiRestFrontendService(override val serverable: Serverable)
val authenticationFactory = new KyuubiHttpAuthenticationFactory(conf)
server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(contextHandler))
+ val proxyHandler = ApiRootResource.getEngineUIProxyHandler(this)
+ server.addHandler(authenticationFactory.httpHandlerWrapperFactory.wrapHandler(proxyHandler))
+
server.addStaticHandler("org/apache/kyuubi/ui/static", "/static/")
server.addRedirectHandler("/", "/static/")
server.addRedirectHandler("/static", "/static/")
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala
new file mode 100644
index 000000000..65fc04f58
--- /dev/null
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/EngineUIProxyServlet.scala
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package org.apache.kyuubi.server.api
+
+import java.net.URL
+import javax.servlet.http.HttpServletRequest
+
+import org.eclipse.jetty.client.api.Request
+import org.eclipse.jetty.proxy.ProxyServlet
+
+import org.apache.kyuubi.Logging
+
+private[api] class EngineUIProxyServlet extends ProxyServlet with Logging {
+
+ override def rewriteTarget(request: HttpServletRequest): String = {
+ val requestURL = request.getRequestURL
+ val requestURI = request.getRequestURI
+ var targetURL = "/no-ui-error"
+ extractTargetAddress(requestURI).foreach { case (host, port) =>
+ val targetURI = requestURI.stripPrefix(s"/engine-ui/$host:$port") match {
+ // for some reason, the proxy can not handle redirect well, as a workaround,
+ // we simulate the Spark UI redirection behavior and forcibly rewrite the
+ // empty URI to the Spark Jobs page.
+ case "" | "/" => "/jobs/"
+ case path => path
+ }
+ targetURL = new URL("http", host, port, targetURI).toString
+ }
+ debug(s"rewrite $requestURL => $targetURL")
+ targetURL
+ }
+
+ override def addXForwardedHeaders(
+ clientRequest: HttpServletRequest,
+ proxyRequest: Request): Unit = {
+ val requestURI = clientRequest.getRequestURI
+ extractTargetAddress(requestURI).foreach { case (host, port) =>
+ // SPARK-24209: Knox uses X-Forwarded-Context to notify the application the base path
+ proxyRequest.header("X-Forwarded-Context", s"/engine-ui/$host:$port")
+ }
+ super.addXForwardedHeaders(clientRequest, proxyRequest)
+ }
+
+ private val r = "^/engine-ui/([^/:]+):(\\d+)/?.*".r
+ private def extractTargetAddress(requestURI: String): Option[(String, Int)] =
+ requestURI match {
+ case r(host, port) => Some(host -> port.toInt)
+ case _ => None
+ }
+}
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala
index d8b997e86..fc3150355 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/ApiRootResource.scala
@@ -30,7 +30,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import org.apache.kyuubi.KYUUBI_VERSION
import org.apache.kyuubi.client.api.v1.dto._
import org.apache.kyuubi.server.KyuubiRestFrontendService
-import org.apache.kyuubi.server.api.{ApiRequestContext, FrontendServiceContext, OpenAPIConfig}
+import org.apache.kyuubi.server.api.{ApiRequestContext, EngineUIProxyServlet, FrontendServiceContext, OpenAPIConfig}
@Path("/v1")
private[v1] class ApiRootResource extends ApiRequestContext {
@@ -82,4 +82,13 @@ private[server] object ApiRootResource {
handler.addServlet(holder, "/*")
handler
}
+
+ def getEngineUIProxyHandler(fe: KyuubiRestFrontendService): ServletContextHandler = {
+ val proxyServlet = new EngineUIProxyServlet()
+ val holder = new ServletHolder(proxyServlet)
+ val proxyHandler = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
+ proxyHandler.setContextPath("/engine-ui");
+ proxyHandler.addServlet(holder, "/*")
+ proxyHandler
+ }
}
diff --git a/kyuubi-server/web-ui/package.json b/kyuubi-server/web-ui/package.json
index 131e69b7f..3814c49cd 100644
--- a/kyuubi-server/web-ui/package.json
+++ b/kyuubi-server/web-ui/package.json
@@ -11,7 +11,7 @@
"lint": "eslint --ext .ts,vue --ignore-path .gitignore .",
"lint-fix": "eslint --fix --ext .ts,vue --ignore-path .gitignore .",
"prettier": "prettier --write \"src/**/*.{vue,ts,tsx}\"",
- "test": "vitest",
+ "test": "vitest --mode development",
"coverage": "vitest run --coverage"
},
"dependencies": {
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 0501078d0..e291b62af 100644
--- a/kyuubi-server/web-ui/src/locales/en_US/index.ts
+++ b/kyuubi-server/web-ui/src/locales/en_US/index.ts
@@ -34,6 +34,7 @@ export default {
engine_type: 'Engine Type',
share_level: 'Share Level',
version: 'Version',
+ engine_ui: 'Engine UI',
operation: {
text: 'Operation',
delete_confirm: 'Delete Confirm',
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 0e7521794..e6dd8fe62 100644
--- a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
+++ b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
@@ -34,6 +34,7 @@ export default {
engine_type: 'Engine 类型',
share_level: '共享级别',
version: '版本',
+ engine_ui: 'Engine UI',
operation: {
text: '操作',
delete_confirm: '确认删除',
diff --git a/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts b/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts
new file mode 100644
index 000000000..b19c83bd9
--- /dev/null
+++ b/kyuubi-server/web-ui/src/test/unit/views/management/engine/index.spec.ts
@@ -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 Engine from '@/views/management/engine/index.vue'
+import { shallowMount } from '@vue/test-utils'
+import { createI18n } from 'vue-i18n'
+import { getStore } from '@/test/unit/utils'
+import { createRouter, createWebHistory } from 'vue-router'
+import ElementPlus from 'element-plus'
+import { expect, test } from 'vitest'
+
+test('proxy ui', async () => {
+ expect(Engine).toBeTruthy()
+ const i18n = createI18n({
+ legacy: false,
+ globalInjection: true
+ })
+
+ const mockRouter = createRouter({ history: createWebHistory(), routes: [] })
+ mockRouter.currentRoute.value.params = {
+ path: '/management/engine'
+ }
+
+ const wrapper = shallowMount(Engine, {
+ global: {
+ plugins: [i18n, mockRouter, getStore(), ElementPlus]
+ }
+ })
+ expect(wrapper.vm.getProxyEngineUI('host:ip')).toEqual(
+ `${import.meta.env.VITE_APP_DEV_WEB_URL}engine-ui/host:ip/`
+ )
+})
diff --git a/kyuubi-server/web-ui/src/views/management/engine/index.vue b/kyuubi-server/web-ui/src/views/management/engine/index.vue
index cecbde709..430482cdd 100644
--- a/kyuubi-server/web-ui/src/views/management/engine/index.vue
+++ b/kyuubi-server/web-ui/src/views/management/engine/index.vue
@@ -81,6 +81,22 @@
+
+
+
@@ -152,7 +168,20 @@
})
}
+ function getProxyEngineUI(url: string): string {
+ url = (url || '').replaceAll(/http:|https:/gi, '')
+ return `${import.meta.env.VITE_APP_DEV_WEB_URL}engine-ui/${url}/`
+ }
+
+ function openEngineUI(url: string) {
+ window.open(getProxyEngineUI(url))
+ }
+
init()
+ // export for test
+ defineExpose({
+ getProxyEngineUI
+ })