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 768b8ff7c [KYUUBI #3648][UI] Add Session Detail Page
768b8ff7c is described below

commit 768b8ff7cabe229418621f059de1ca0249bd71c4
Author: zwangsheng <[email protected]>
AuthorDate: Wed Jun 14 14:56:53 2023 +0800

    [KYUUBI #3648][UI] Add Session Detail Page
    
    ### _Why are the changes needed?_
    
    Close #3648
    
    ### _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/develop_tools/testing.html#running-tests)
 locally before make a pull request
    
    Closes #4793 from zwangsheng/KYUUBI_3648.
    
    Closes #3648
    
    e5fe04f7a [zwangsheng] debug
    a14c6d325 [zwangsheng] debug
    eb96cf0dc [zwangsheng] retest
    5cfbb64c5 [zwangsheng] Add UT
    7a83a01b0 [zwangsheng] [KYUUBI #3648][UI] Add Session Detail Page
    
    Authored-by: zwangsheng <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
---
 .../kyuubi/server/api/v1/SessionsResource.scala    |  30 ++++-
 .../server/api/v1/SessionsResourceSuite.scala      |  22 ++++
 kyuubi-server/web-ui/src/api/session/index.ts      |  14 ++
 kyuubi-server/web-ui/src/locales/en_US/index.ts    |   2 +
 kyuubi-server/web-ui/src/locales/zh_CN/index.ts    |   2 +
 .../src/{api/session => router/detail}/index.ts    |  22 ++--
 kyuubi-server/web-ui/src/router/index.ts           |   2 +
 .../web-ui/src/views/detail/session/index.vue      | 146 +++++++++++++++++++++
 .../web-ui/src/views/management/session/index.vue  |  24 +++-
 9 files changed, 244 insertions(+), 20 deletions(-)

diff --git 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala
 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala
index 7866744dc..d735b87d8 100644
--- 
a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala
+++ 
b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/api/v1/SessionsResource.scala
@@ -27,13 +27,14 @@ import scala.util.control.NonFatal
 import io.swagger.v3.oas.annotations.media.{ArraySchema, Content, Schema}
 import io.swagger.v3.oas.annotations.responses.ApiResponse
 import io.swagger.v3.oas.annotations.tags.Tag
+import org.apache.commons.lang3.StringUtils
 import org.apache.hive.service.rpc.thrift.{TGetInfoType, TProtocolVersion}
 
 import org.apache.kyuubi.Logging
 import org.apache.kyuubi.client.api.v1.dto
 import org.apache.kyuubi.client.api.v1.dto._
 import org.apache.kyuubi.config.KyuubiReservedKeys._
-import org.apache.kyuubi.operation.OperationHandle
+import org.apache.kyuubi.operation.{KyuubiOperation, OperationHandle}
 import org.apache.kyuubi.server.api.{ApiRequestContext, ApiUtils}
 import org.apache.kyuubi.session.{KyuubiSession, SessionHandle}
 
@@ -414,6 +415,33 @@ private[v1] class SessionsResource extends 
ApiRequestContext with Logging {
         throw new NotFoundException(errorMsg)
     }
   }
+
+  @ApiResponse(
+    responseCode = "200",
+    content = Array(new Content(
+      mediaType = MediaType.APPLICATION_JSON,
+      array = new ArraySchema(schema = new Schema(implementation =
+        classOf[OperationData])))),
+    description =
+      "get the list of all type operations belong to session")
+  @GET
+  @Path("{sessionHandle}/operations")
+  def getOperation(@PathParam("sessionHandle") sessionHandleStr: String): 
Seq[OperationData] = {
+    try {
+      fe.be.sessionManager.operationManager.allOperations().map { operation =>
+        if (StringUtils.equalsIgnoreCase(
+            operation.getSession.handle.identifier.toString,
+            sessionHandleStr)) {
+          ApiUtils.operationData(operation.asInstanceOf[KyuubiOperation])
+        }
+      }.toSeq.asInstanceOf[Seq[OperationData]]
+    } catch {
+      case NonFatal(e) =>
+        val errorMsg = "Error getting the list of all type operations belong 
to session"
+        error(errorMsg, e)
+        throw new NotFoundException(errorMsg)
+    }
+  }
 }
 
 object SessionsResource {
diff --git 
a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala
 
b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala
index b197a489c..b58e87bc8 100644
--- 
a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala
+++ 
b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/api/v1/SessionsResourceSuite.scala
@@ -364,4 +364,26 @@ class SessionsResourceSuite extends KyuubiFunSuite with 
RestFrontendTestHelper {
       assert(sessionDataList.isEmpty)
     }
   }
+
+  test("list all type operations under session") {
+    val sessionOpenRequest = new SessionOpenRequest(Map("testConfig" -> 
"testValue").asJava)
+    val user = "kyuubi".getBytes()
+    val sessionOpenResp = webTarget.path("api/v1/sessions")
+      .request(MediaType.APPLICATION_JSON_TYPE)
+      .header(
+        AUTHORIZATION_HEADER,
+        s"Basic ${new String(Base64.getEncoder.encode(user), 
StandardCharsets.UTF_8)}")
+      .post(Entity.entity(sessionOpenRequest, MediaType.APPLICATION_JSON_TYPE))
+
+    val sessionHandle = 
sessionOpenResp.readEntity(classOf[SessionHandle]).getIdentifier
+
+    // get operations belongs to specified session
+    val response = webTarget
+      .path(s"api/v1/sessions/${sessionHandle.toString}/operations")
+      .request().get()
+    assert(200 == response.getStatus)
+    val operations = response.readEntity(new GenericType[Seq[OperationData]]() 
{})
+    assert(operations.size == 1)
+    assert(sessionHandle.toString.equals(operations.head.getSessionId))
+  }
 }
diff --git a/kyuubi-server/web-ui/src/api/session/index.ts 
b/kyuubi-server/web-ui/src/api/session/index.ts
index 5f3c74fef..fa4759b36 100644
--- a/kyuubi-server/web-ui/src/api/session/index.ts
+++ b/kyuubi-server/web-ui/src/api/session/index.ts
@@ -30,3 +30,17 @@ export function deleteSession(sessionId: string) {
     method: 'delete'
   })
 }
+
+export function getSession(sessionId: string) {
+  return request({
+    url: `api/v1/sessions/${sessionId}`,
+    method: 'get'
+  })
+}
+
+export function getAllTypeOperation(sessionId: string) {
+  return request({
+    url: `api/v1/sessions/${sessionId}/operations`,
+    method: 'get'
+  })
+}
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 e291b62af..8606c74da 100644
--- a/kyuubi-server/web-ui/src/locales/en_US/index.ts
+++ b/kyuubi-server/web-ui/src/locales/en_US/index.ts
@@ -35,6 +35,8 @@ export default {
   share_level: 'Share Level',
   version: 'Version',
   engine_ui: 'Engine UI',
+  failure_reason: 'Failure Reason',
+  session_properties: 'Session Properties',
   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 e6dd8fe62..0c4cb66db 100644
--- a/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
+++ b/kyuubi-server/web-ui/src/locales/zh_CN/index.ts
@@ -35,6 +35,8 @@ export default {
   share_level: '共享级别',
   version: '版本',
   engine_ui: 'Engine UI',
+  failure_reason: '失败原因',
+  session_properties: 'Session 参数',
   operation: {
     text: '操作',
     delete_confirm: '确认删除',
diff --git a/kyuubi-server/web-ui/src/api/session/index.ts 
b/kyuubi-server/web-ui/src/router/detail/index.ts
similarity index 73%
copy from kyuubi-server/web-ui/src/api/session/index.ts
copy to kyuubi-server/web-ui/src/router/detail/index.ts
index 5f3c74fef..5b5508460 100644
--- a/kyuubi-server/web-ui/src/api/session/index.ts
+++ b/kyuubi-server/web-ui/src/router/detail/index.ts
@@ -15,18 +15,12 @@
  * limitations under the License.
  */
 
-import request from '@/utils/request'
+const router = [
+  {
+    path: '/detail/session',
+    name: 'session_detail',
+    component: () => import('@/views/detail/session/index.vue')
+  }
+]
 
-export function getAllSessions() {
-  return request({
-    url: 'api/v1/admin/sessions',
-    method: 'get'
-  })
-}
-
-export function deleteSession(sessionId: string) {
-  return request({
-    url: `api/v1/admin/sessions/${sessionId}`,
-    method: 'delete'
-  })
-}
+export default router
diff --git a/kyuubi-server/web-ui/src/router/index.ts 
b/kyuubi-server/web-ui/src/router/index.ts
index cad831705..0b80aea17 100644
--- a/kyuubi-server/web-ui/src/router/index.ts
+++ b/kyuubi-server/web-ui/src/router/index.ts
@@ -21,6 +21,7 @@ import workloadRoutes from './workload'
 import operationRoutes from './operation'
 import contactRoutes from './contact'
 import managementRoutes from './management'
+import detailRoutes from './detail'
 
 const routes = [
   {
@@ -40,6 +41,7 @@ const routes = [
       ...workloadRoutes,
       ...operationRoutes,
       ...managementRoutes,
+      ...detailRoutes,
       ...contactRoutes
     ]
   }
diff --git a/kyuubi-server/web-ui/src/views/detail/session/index.vue 
b/kyuubi-server/web-ui/src/views/detail/session/index.vue
new file mode 100644
index 000000000..4a77b2a66
--- /dev/null
+++ b/kyuubi-server/web-ui/src/views/detail/session/index.vue
@@ -0,0 +1,146 @@
+<!--
+* 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>
+  <el-card :body-style="{ padding: '10px 14px' }">
+    <header>
+      <el-breadcrumb separator="/">
+        <el-breadcrumb-item :to="{ path: '/management/session' }">{{
+          'Sessions'
+        }}</el-breadcrumb-item>
+        <el-breadcrumb-item>{{ route.query.sessionId }}</el-breadcrumb-item>
+      </el-breadcrumb>
+    </header>
+  </el-card>
+  <el-collapse class="session-properties-container">
+    <el-collapse-item name="1">
+      <template #title>
+        <div class="title">
+          <span>{{ $t('session_properties') }}</span>
+        </div>
+      </template>
+      <el-descriptions :column="1" border>
+        <div v-for="(p, key) in sessionProperties" :key="key">
+          <el-descriptions-item :label="key">
+            {{ p }}
+          </el-descriptions-item></div
+        ></el-descriptions
+      >
+    </el-collapse-item>
+  </el-collapse>
+  <el-card class="table-container">
+    <template #header>
+      <div class="card-header">
+        <span>{{ 'Operations' }}</span>
+      </div>
+    </template>
+    <el-table v-loading="loading" :data="tableData" style="width: 100%">
+      <el-table-column prop="sessionUser" :label="$t('user')" width="160" />
+      <el-table-column
+        prop="identifier"
+        :label="$t('operation_id')"
+        width="300" />
+      <el-table-column :label="$t('create_time')" width="160">
+        <template #default="scope">
+          {{
+            scope.row.createTime != null && scope.row.createTime > -1
+              ? format(scope.row.createTime, 'yyyy-MM-dd HH:mm:ss')
+              : '-'
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('complete_time')" width="160">
+        <template #default="scope">
+          {{
+            scope.row.completeTime != null && scope.row.completeTime > -1
+              ? format(scope.row.completeTime, 'yyyy-MM-dd HH:mm:ss')
+              : '-'
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column :label="$t('duration')" width="130">
+        <template #default="scope">{{
+          scope.row.createTime != null &&
+          scope.row.completeTime != null &&
+          scope.row.createTime > -1 &&
+          scope.row.completeTime > -1
+            ? secondTransfer(
+                (scope.row.completeTime - scope.row.createTime) / 1000
+              )
+            : '-'
+        }}</template>
+      </el-table-column>
+      <el-table-column prop="statement" :label="$t('statement')" width="160" />
+      <el-table-column prop="exception" :label="$t('failure_reason')">
+        <template #default="scope">
+          {{ scope.row.exception == '' ? '-' : scope.row.exception }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-card>
+</template>
+
+<script lang="ts" setup>
+  import { Ref, ref } from 'vue'
+  import { getSession, getAllTypeOperation } from '@/api/session'
+  import { useRoute } from 'vue-router'
+  import { format } from 'date-fns'
+  import { secondTransfer } from '@/utils/unit'
+  import { useTable } from '@/views/common/use-table'
+  const route = useRoute()
+  const sessionProperties: Ref<any> = ref({})
+  const sessionPropertiesLoading = ref(false)
+  const { tableData, loading, getList: _getList } = useTable()
+
+  const getSessionById = () => {
+    const sessionId = route.query.sessionId
+    if (sessionId) {
+      sessionPropertiesLoading.value = true
+      getSession(sessionId as string)
+        .then((res: any) => {
+          sessionProperties.value = res?.conf || {}
+        })
+        .finally(() => {
+          sessionPropertiesLoading.value = false
+        })
+    }
+  }
+  const getList = () => {
+    const sessionId = route.query.sessionId
+    if (sessionId) {
+      _getList(getAllTypeOperation, sessionId)
+    }
+  }
+  getSessionById()
+  getList()
+</script>
+<style lang="scss" scoped>
+  header {
+    display: flex;
+    justify-content: space-between;
+    .el-breadcrumb {
+      line-height: 32px;
+    }
+  }
+  .session-properties-container {
+    margin-bottom: 20px;
+    border-radius: 20px;
+    .title {
+      margin-left: 20px;
+    }
+  }
+</style>
diff --git a/kyuubi-server/web-ui/src/views/management/session/index.vue 
b/kyuubi-server/web-ui/src/views/management/session/index.vue
index 327664dd1..0465c1a4a 100644
--- a/kyuubi-server/web-ui/src/views/management/session/index.vue
+++ b/kyuubi-server/web-ui/src/views/management/session/index.vue
@@ -26,17 +26,20 @@
       style="width: 100%">
       <el-table-column prop="user" :label="$t('user')" width="160px" />
       <!-- TODO need jump to engine page -->
-      <el-table-column prop="engineId" :label="$t('engine_ip')" width="160px" 
/>
+      <el-table-column prop="engineId" :label="$t('engine_id')" width="160px" 
/>
       <el-table-column prop="ipAddr" :label="$t('client_ip')" width="160px" />
       <el-table-column
         prop="kyuubiInstance"
         :label="$t('kyuubi_instance')"
         width="180px" />
       <!-- TODO need jump to session page -->
-      <el-table-column
-        prop="identifier"
-        :label="$t('session_id')"
-        width="300px" />
+      <el-table-column :label="$t('session_id')" width="300px">
+        <template #default="scope">
+          <el-link @click="handleSessionDetailJump(scope.row.identifier)">{{
+            scope.row.identifier
+          }}</el-link>
+        </template>
+      </el-table-column>
       <el-table-column :label="$t('create_time')" width="200">
         <template #default="scope">
           {{
@@ -76,6 +79,7 @@
   import { ElMessage } from 'element-plus'
   import { useI18n } from 'vue-i18n'
   import { useTable } from '@/views/common/use-table'
+  import { Router, useRouter } from 'vue-router'
   const { t } = useI18n()
   const { tableData, loading, getList: _getList } = useTable()
   const handleDeleteSession = (sessionId: string) => {
@@ -100,5 +104,15 @@
   const getList = () => {
     _getList(getAllSessions)
   }
+  const router: Router = useRouter()
+
+  function handleSessionDetailJump(sessionId: string) {
+    router.push({
+      path: '/detail/session',
+      query: {
+        sessionId
+      }
+    })
+  }
   getList()
 </script>

Reply via email to