Copilot commented on code in PR #7505:
URL: https://github.com/apache/kyuubi/pull/7505#discussion_r3378012340
##########
kyuubi-server/web-ui/src/utils/use-table.ts:
##########
@@ -16,6 +16,7 @@
*/
import { ref, Ref } from 'vue'
+import { ElMessage } from 'element-plus'
Review Comment:
`useTable` now imports Element Plus and shows an `ElMessage` toast with a
hard-coded English fallback ('Request failed'). This couples a generic
composable to UI messaging and bypasses i18n; it can also cause unexpected
global toasts anywhere `useTable()` is used. Prefer keeping `useTable`
UI-agnostic and letting callers decide how to surface errors.
##########
kyuubi-server/web-ui/src/views/management/batch/index.vue:
##########
@@ -0,0 +1,647 @@
+<!--
+* 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' }" class="filter-card">
+ <header>
+ <el-space class="search-box" wrap>
+ <el-input
+ v-model="searchParam.batchUser"
+ :placeholder="$t('user')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-input
+ v-model="searchParam.batchName"
+ :placeholder="$t('batch_name')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-select
+ v-model="searchParam.batchType"
+ :placeholder="$t('batch_type')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option label="SPARK" value="SPARK" />
+ <el-option label="PYSPARK" value="PYSPARK" />
+ </el-select>
+ <el-select
+ v-model="searchParam.batchState"
+ :placeholder="$t('state')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option
+ v-for="state in batchStates"
+ :key="state"
+ :label="state"
+ :value="state" />
+ </el-select>
+ <el-button type="primary" icon="Search" @click="searchBatches" />
+ <el-button icon="Refresh" @click="getList" />
+ </el-space>
+ </header>
+ </el-card>
+
+ <el-card class="table-container">
+ <el-table
+ v-loading="loading"
+ class="batch-table"
+ :data="tableData"
+ style="width: 100%">
+ <el-table-column prop="user" :label="$t('user')" min-width="120" />
+ <el-table-column :label="$t('batch_id')" min-width="300">
+ <template #default="scope">
+ <span class="truncate-text">{{ scope.row.id }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column :label="$t('batch_name')" min-width="180">
+ <template #default="scope">{{ display(scope.row.name) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="batchType"
+ :label="$t('batch_type')"
+ min-width="130" />
+ <el-table-column prop="state" :label="$t('state')" min-width="120" />
+ <el-table-column :label="$t('app_state')" min-width="170">
+ <template #default="scope">{{ display(scope.row.appState) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_id')" min-width="210">
+ <template #default="scope">{{ display(scope.row.appId) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="kyuubiInstance"
+ :label="$t('kyuubi_instance')"
+ min-width="220"
+ show-overflow-tooltip />
+ <el-table-column :label="$t('create_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.createTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('end_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.endTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('duration')" min-width="130">
+ <template #default="scope">{{ formatDuration(scope.row) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_diagnostic')" min-width="240">
+ <template #default="scope">
+ <el-tooltip
+ v-if="scope.row.appDiagnostic"
+ effect="dark"
+ :content="scope.row.appDiagnostic"
+ placement="top">
+ <span class="truncate-text">{{ scope.row.appDiagnostic }}</span>
+ </el-tooltip>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" :label="$t('operation.text')" width="160">
+ <template #default="scope">
+ <el-space wrap>
+ <el-tooltip
+ v-if="canShowAppUI(scope.row)"
+ effect="dark"
+ :content="$t('app_url') + ': ' + getAppUI(scope.row.appUrl)"
+ placement="top">
+ <el-button
+ type="primary"
+ icon="Link"
+ circle
+ @click="openAppUI(scope.row.appUrl)" />
+ </el-tooltip>
+ <el-tooltip effect="dark" :content="$t('log')" placement="top">
+ <el-button icon="Document" circle @click="openLog(scope.row)" />
+ </el-tooltip>
+ <el-popconfirm
+ v-if="!isTerminalState(scope.row.state)"
+ :title="$t('operation.cancel_confirm')"
+ @confirm="handleDeleteBatch(scope.row)">
+ <template #reference>
+ <span>
+ <el-tooltip
+ effect="dark"
+ :content="$t('operation.cancel')"
+ placement="top">
+ <template #default>
+ <el-button type="danger" icon="Remove" circle />
+ </template>
+ </el-tooltip>
+ </span>
+ </template>
+ </el-popconfirm>
+ </el-space>
+ </template>
+ </el-table-column>
+ </el-table>
+ <footer class="pager">
+ <span>{{ pageRangeText }}</span>
+ <el-space>
+ <el-button
+ icon="ArrowLeft"
+ :disabled="!hasPreviousPage || loading"
+ @click="previousPage">
+ {{ $t('previous') }}
+ </el-button>
+ <el-button
+ icon="ArrowRight"
+ :disabled="!hasNextPage || loading"
+ @click="nextPage">
+ {{ $t('next') }}
+ </el-button>
+ </el-space>
+ </footer>
+ </el-card>
+
+ <el-drawer
+ v-model="logDrawerVisible"
+ :title="selectedBatch ? `${$t('log')} - ${selectedBatch.id}` : $t('log')"
+ size="60%"
+ @close="onLogDrawerClose">
+ <div class="log-body">
+ <div class="log-toolbar">
+ <el-switch
+ v-model="logFollowing"
+ :disabled="logFollowDisabled"
+ :active-text="$t('follow')"
+ @change="onFollowChange" />
+ <el-button
+ icon="Refresh"
+ :disabled="logFollowing || logLoading"
+ @click="refreshLog">
+ {{ $t('refresh') }}
+ </el-button>
+ <span v-if="logTruncated" class="log-truncated">
+ {{ $t('log_truncated', { count: LOG_MAX_ROWS }) }}
+ </span>
+ </div>
+ <el-scrollbar
+ ref="logScrollbarRef"
+ v-loading="logLoading"
+ class="log-container">
+ <pre v-if="batchLogs.length > 0">{{ batchLogs.join('\n') }}</pre>
+ <el-empty v-else :description="$t('no_log')" />
+ </el-scrollbar>
+ </div>
+ </el-drawer>
+</template>
+
+<script lang="ts" setup>
+ import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue'
+ import { format } from 'date-fns'
+ import { ElMessage } from 'element-plus'
+ import { useI18n } from 'vue-i18n'
+ import {
+ deleteBatch,
+ getAllBatches,
+ getBatch,
+ getBatchLocalLog
+ } from '@/api/batch'
+ import { IBatch, IBatchSearch } from '@/api/batch/types'
+ import { getWebUIConfig } from '@/api/server'
+ import { IWebUIConfig } from '@/api/server/types'
+ import { getEngineUIUrl } from '@/utils/engine-ui'
+ import { millTransfer } from '@/utils/unit'
+
+ const { t } = useI18n()
+ const loading = ref(false)
+ const tableData = ref<IBatch[]>([])
+ const pageSize = ref(100)
+ const logDrawerVisible = ref(false)
+ const logLoading = ref(false)
Review Comment:
Server-side pagination exposes `total` in the `/api/v1/batches` response,
but the view doesn’t track it. Without keeping `total`, the pager can’t
reliably know whether more pages exist (e.g., when `total` is an exact multiple
of `pageSize`). Store `total` alongside `tableData` so the Next button can be
computed correctly.
##########
kyuubi-server/web-ui/src/utils/use-table.ts:
##########
@@ -56,7 +57,16 @@ export function useTable() {
loading.value = true
func(data)
.then((res: any[]) => (list.value = res || []))
- .catch(() => (list.value = []))
+ .catch((error: unknown) => {
+ list.value = []
+ ElMessage({
+ message:
+ error instanceof Error && error.message
+ ? error.message
+ : 'Request failed',
+ type: 'error'
+ })
+ })
.finally(() => {
Review Comment:
The error handler in `useTable.getList` now always shows an `ElMessage`
toast. This changes behavior app-wide and uses a non-localized fallback
message. If you want error toasts, it’s better to handle them in the calling
view where you have i18n context and can tailor the message.
##########
kyuubi-server/web-ui/src/views/management/batch/index.vue:
##########
@@ -0,0 +1,647 @@
+<!--
+* 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' }" class="filter-card">
+ <header>
+ <el-space class="search-box" wrap>
+ <el-input
+ v-model="searchParam.batchUser"
+ :placeholder="$t('user')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-input
+ v-model="searchParam.batchName"
+ :placeholder="$t('batch_name')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-select
+ v-model="searchParam.batchType"
+ :placeholder="$t('batch_type')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option label="SPARK" value="SPARK" />
+ <el-option label="PYSPARK" value="PYSPARK" />
+ </el-select>
+ <el-select
+ v-model="searchParam.batchState"
+ :placeholder="$t('state')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option
+ v-for="state in batchStates"
+ :key="state"
+ :label="state"
+ :value="state" />
+ </el-select>
+ <el-button type="primary" icon="Search" @click="searchBatches" />
+ <el-button icon="Refresh" @click="getList" />
+ </el-space>
+ </header>
+ </el-card>
+
+ <el-card class="table-container">
+ <el-table
+ v-loading="loading"
+ class="batch-table"
+ :data="tableData"
+ style="width: 100%">
+ <el-table-column prop="user" :label="$t('user')" min-width="120" />
+ <el-table-column :label="$t('batch_id')" min-width="300">
+ <template #default="scope">
+ <span class="truncate-text">{{ scope.row.id }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column :label="$t('batch_name')" min-width="180">
+ <template #default="scope">{{ display(scope.row.name) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="batchType"
+ :label="$t('batch_type')"
+ min-width="130" />
+ <el-table-column prop="state" :label="$t('state')" min-width="120" />
+ <el-table-column :label="$t('app_state')" min-width="170">
+ <template #default="scope">{{ display(scope.row.appState) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_id')" min-width="210">
+ <template #default="scope">{{ display(scope.row.appId) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="kyuubiInstance"
+ :label="$t('kyuubi_instance')"
+ min-width="220"
+ show-overflow-tooltip />
+ <el-table-column :label="$t('create_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.createTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('end_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.endTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('duration')" min-width="130">
+ <template #default="scope">{{ formatDuration(scope.row) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_diagnostic')" min-width="240">
+ <template #default="scope">
+ <el-tooltip
+ v-if="scope.row.appDiagnostic"
+ effect="dark"
+ :content="scope.row.appDiagnostic"
+ placement="top">
+ <span class="truncate-text">{{ scope.row.appDiagnostic }}</span>
+ </el-tooltip>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" :label="$t('operation.text')" width="160">
+ <template #default="scope">
+ <el-space wrap>
+ <el-tooltip
+ v-if="canShowAppUI(scope.row)"
+ effect="dark"
+ :content="$t('app_url') + ': ' + getAppUI(scope.row.appUrl)"
+ placement="top">
+ <el-button
+ type="primary"
+ icon="Link"
+ circle
+ @click="openAppUI(scope.row.appUrl)" />
+ </el-tooltip>
+ <el-tooltip effect="dark" :content="$t('log')" placement="top">
+ <el-button icon="Document" circle @click="openLog(scope.row)" />
+ </el-tooltip>
+ <el-popconfirm
+ v-if="!isTerminalState(scope.row.state)"
+ :title="$t('operation.cancel_confirm')"
+ @confirm="handleDeleteBatch(scope.row)">
+ <template #reference>
+ <span>
+ <el-tooltip
+ effect="dark"
+ :content="$t('operation.cancel')"
+ placement="top">
+ <template #default>
+ <el-button type="danger" icon="Remove" circle />
+ </template>
+ </el-tooltip>
+ </span>
+ </template>
+ </el-popconfirm>
+ </el-space>
+ </template>
+ </el-table-column>
+ </el-table>
+ <footer class="pager">
+ <span>{{ pageRangeText }}</span>
+ <el-space>
+ <el-button
+ icon="ArrowLeft"
+ :disabled="!hasPreviousPage || loading"
+ @click="previousPage">
+ {{ $t('previous') }}
+ </el-button>
+ <el-button
+ icon="ArrowRight"
+ :disabled="!hasNextPage || loading"
+ @click="nextPage">
+ {{ $t('next') }}
+ </el-button>
+ </el-space>
+ </footer>
+ </el-card>
+
+ <el-drawer
+ v-model="logDrawerVisible"
+ :title="selectedBatch ? `${$t('log')} - ${selectedBatch.id}` : $t('log')"
+ size="60%"
+ @close="onLogDrawerClose">
+ <div class="log-body">
+ <div class="log-toolbar">
+ <el-switch
+ v-model="logFollowing"
+ :disabled="logFollowDisabled"
+ :active-text="$t('follow')"
+ @change="onFollowChange" />
+ <el-button
+ icon="Refresh"
+ :disabled="logFollowing || logLoading"
+ @click="refreshLog">
+ {{ $t('refresh') }}
+ </el-button>
+ <span v-if="logTruncated" class="log-truncated">
+ {{ $t('log_truncated', { count: LOG_MAX_ROWS }) }}
+ </span>
+ </div>
+ <el-scrollbar
+ ref="logScrollbarRef"
+ v-loading="logLoading"
+ class="log-container">
+ <pre v-if="batchLogs.length > 0">{{ batchLogs.join('\n') }}</pre>
+ <el-empty v-else :description="$t('no_log')" />
+ </el-scrollbar>
+ </div>
+ </el-drawer>
+</template>
+
+<script lang="ts" setup>
+ import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue'
+ import { format } from 'date-fns'
+ import { ElMessage } from 'element-plus'
+ import { useI18n } from 'vue-i18n'
+ import {
+ deleteBatch,
+ getAllBatches,
+ getBatch,
+ getBatchLocalLog
+ } from '@/api/batch'
+ import { IBatch, IBatchSearch } from '@/api/batch/types'
+ import { getWebUIConfig } from '@/api/server'
+ import { IWebUIConfig } from '@/api/server/types'
+ import { getEngineUIUrl } from '@/utils/engine-ui'
+ import { millTransfer } from '@/utils/unit'
+
+ const { t } = useI18n()
+ const loading = ref(false)
+ const tableData = ref<IBatch[]>([])
+ const pageSize = ref(100)
+ const logDrawerVisible = ref(false)
+ const logLoading = ref(false)
+ const batchLogs = ref<string[]>([])
+ const selectedBatch = ref<IBatch>()
+ const logFollowing = ref(false)
+ const logTruncated = ref(false)
+ const logScrollbarRef = ref()
+ const logNextFrom = ref(0)
+ let logTimer: ReturnType<typeof setTimeout> | undefined
+ // Bumped whenever the log view is reset (open / refresh / close) so
in-flight
+ // fetches from a previous view discard their results instead of appending.
+ let logEpoch = 0
+
+ const batchStates = ['PENDING', 'RUNNING', 'FINISHED', 'ERROR', 'CANCELED']
+ const terminalBatchStates = new Set(['FINISHED', 'ERROR', 'CANCELED'])
+
+ // Batch log viewer: fetch the operation log by absolute line offset (server
+ // OperationLog.read(from, size) treats `from` as a line index), tracking the
+ // cursor on the client so a live tail never fights the shared server cursor.
+ const LOG_FETCH_SIZE = 1000
+ const LOG_POLL_INTERVAL_MS = 2000
+ const LOG_MAX_ROWS = 5000
+
+ const searchParam: IBatchSearch = reactive({
+ from: 0,
+ size: 100,
+ desc: true
+ })
+
+ const hasPreviousPage = computed(() => (searchParam.from || 0) > 0)
+ const hasNextPage = computed(() => tableData.value.length === pageSize.value)
+ const pageRangeText = computed(() => {
Review Comment:
`hasNextPage` is currently inferred from `tableData.length === pageSize`,
which can enable Next even when the current page is the last page (when `total`
is an exact multiple of `pageSize`). Compute Next availability using `from +
tableData.length < total` instead.
##########
kyuubi-server/web-ui/src/views/management/batch/index.vue:
##########
@@ -0,0 +1,647 @@
+<!--
+* 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' }" class="filter-card">
+ <header>
+ <el-space class="search-box" wrap>
+ <el-input
+ v-model="searchParam.batchUser"
+ :placeholder="$t('user')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-input
+ v-model="searchParam.batchName"
+ :placeholder="$t('batch_name')"
+ clearable
+ style="width: 180px"
+ @keyup.enter="searchBatches" />
+ <el-select
+ v-model="searchParam.batchType"
+ :placeholder="$t('batch_type')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option label="SPARK" value="SPARK" />
+ <el-option label="PYSPARK" value="PYSPARK" />
+ </el-select>
+ <el-select
+ v-model="searchParam.batchState"
+ :placeholder="$t('state')"
+ clearable
+ style="width: 150px"
+ @change="searchBatches">
+ <el-option
+ v-for="state in batchStates"
+ :key="state"
+ :label="state"
+ :value="state" />
+ </el-select>
+ <el-button type="primary" icon="Search" @click="searchBatches" />
+ <el-button icon="Refresh" @click="getList" />
+ </el-space>
+ </header>
+ </el-card>
+
+ <el-card class="table-container">
+ <el-table
+ v-loading="loading"
+ class="batch-table"
+ :data="tableData"
+ style="width: 100%">
+ <el-table-column prop="user" :label="$t('user')" min-width="120" />
+ <el-table-column :label="$t('batch_id')" min-width="300">
+ <template #default="scope">
+ <span class="truncate-text">{{ scope.row.id }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column :label="$t('batch_name')" min-width="180">
+ <template #default="scope">{{ display(scope.row.name) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="batchType"
+ :label="$t('batch_type')"
+ min-width="130" />
+ <el-table-column prop="state" :label="$t('state')" min-width="120" />
+ <el-table-column :label="$t('app_state')" min-width="170">
+ <template #default="scope">{{ display(scope.row.appState) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_id')" min-width="210">
+ <template #default="scope">{{ display(scope.row.appId) }}</template>
+ </el-table-column>
+ <el-table-column
+ prop="kyuubiInstance"
+ :label="$t('kyuubi_instance')"
+ min-width="220"
+ show-overflow-tooltip />
+ <el-table-column :label="$t('create_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.createTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('end_time')" min-width="170">
+ <template #default="scope">{{
+ formatTime(scope.row.endTime)
+ }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('duration')" min-width="130">
+ <template #default="scope">{{ formatDuration(scope.row) }}</template>
+ </el-table-column>
+ <el-table-column :label="$t('app_diagnostic')" min-width="240">
+ <template #default="scope">
+ <el-tooltip
+ v-if="scope.row.appDiagnostic"
+ effect="dark"
+ :content="scope.row.appDiagnostic"
+ placement="top">
+ <span class="truncate-text">{{ scope.row.appDiagnostic }}</span>
+ </el-tooltip>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" :label="$t('operation.text')" width="160">
+ <template #default="scope">
+ <el-space wrap>
+ <el-tooltip
+ v-if="canShowAppUI(scope.row)"
+ effect="dark"
+ :content="$t('app_url') + ': ' + getAppUI(scope.row.appUrl)"
+ placement="top">
+ <el-button
+ type="primary"
+ icon="Link"
+ circle
+ @click="openAppUI(scope.row.appUrl)" />
+ </el-tooltip>
+ <el-tooltip effect="dark" :content="$t('log')" placement="top">
+ <el-button icon="Document" circle @click="openLog(scope.row)" />
+ </el-tooltip>
+ <el-popconfirm
+ v-if="!isTerminalState(scope.row.state)"
+ :title="$t('operation.cancel_confirm')"
+ @confirm="handleDeleteBatch(scope.row)">
+ <template #reference>
+ <span>
+ <el-tooltip
+ effect="dark"
+ :content="$t('operation.cancel')"
+ placement="top">
+ <template #default>
+ <el-button type="danger" icon="Remove" circle />
+ </template>
+ </el-tooltip>
+ </span>
+ </template>
+ </el-popconfirm>
+ </el-space>
+ </template>
+ </el-table-column>
+ </el-table>
+ <footer class="pager">
+ <span>{{ pageRangeText }}</span>
+ <el-space>
+ <el-button
+ icon="ArrowLeft"
+ :disabled="!hasPreviousPage || loading"
+ @click="previousPage">
+ {{ $t('previous') }}
+ </el-button>
+ <el-button
+ icon="ArrowRight"
+ :disabled="!hasNextPage || loading"
+ @click="nextPage">
+ {{ $t('next') }}
+ </el-button>
+ </el-space>
+ </footer>
+ </el-card>
+
+ <el-drawer
+ v-model="logDrawerVisible"
+ :title="selectedBatch ? `${$t('log')} - ${selectedBatch.id}` : $t('log')"
+ size="60%"
+ @close="onLogDrawerClose">
+ <div class="log-body">
+ <div class="log-toolbar">
+ <el-switch
+ v-model="logFollowing"
+ :disabled="logFollowDisabled"
+ :active-text="$t('follow')"
+ @change="onFollowChange" />
+ <el-button
+ icon="Refresh"
+ :disabled="logFollowing || logLoading"
+ @click="refreshLog">
+ {{ $t('refresh') }}
+ </el-button>
+ <span v-if="logTruncated" class="log-truncated">
+ {{ $t('log_truncated', { count: LOG_MAX_ROWS }) }}
+ </span>
+ </div>
+ <el-scrollbar
+ ref="logScrollbarRef"
+ v-loading="logLoading"
+ class="log-container">
+ <pre v-if="batchLogs.length > 0">{{ batchLogs.join('\n') }}</pre>
+ <el-empty v-else :description="$t('no_log')" />
+ </el-scrollbar>
+ </div>
+ </el-drawer>
+</template>
+
+<script lang="ts" setup>
+ import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue'
+ import { format } from 'date-fns'
+ import { ElMessage } from 'element-plus'
+ import { useI18n } from 'vue-i18n'
+ import {
+ deleteBatch,
+ getAllBatches,
+ getBatch,
+ getBatchLocalLog
+ } from '@/api/batch'
+ import { IBatch, IBatchSearch } from '@/api/batch/types'
+ import { getWebUIConfig } from '@/api/server'
+ import { IWebUIConfig } from '@/api/server/types'
+ import { getEngineUIUrl } from '@/utils/engine-ui'
+ import { millTransfer } from '@/utils/unit'
+
+ const { t } = useI18n()
+ const loading = ref(false)
+ const tableData = ref<IBatch[]>([])
+ const pageSize = ref(100)
+ const logDrawerVisible = ref(false)
+ const logLoading = ref(false)
+ const batchLogs = ref<string[]>([])
+ const selectedBatch = ref<IBatch>()
+ const logFollowing = ref(false)
+ const logTruncated = ref(false)
+ const logScrollbarRef = ref()
+ const logNextFrom = ref(0)
+ let logTimer: ReturnType<typeof setTimeout> | undefined
+ // Bumped whenever the log view is reset (open / refresh / close) so
in-flight
+ // fetches from a previous view discard their results instead of appending.
+ let logEpoch = 0
+
+ const batchStates = ['PENDING', 'RUNNING', 'FINISHED', 'ERROR', 'CANCELED']
+ const terminalBatchStates = new Set(['FINISHED', 'ERROR', 'CANCELED'])
+
+ // Batch log viewer: fetch the operation log by absolute line offset (server
+ // OperationLog.read(from, size) treats `from` as a line index), tracking the
+ // cursor on the client so a live tail never fights the shared server cursor.
+ const LOG_FETCH_SIZE = 1000
+ const LOG_POLL_INTERVAL_MS = 2000
+ const LOG_MAX_ROWS = 5000
+
+ const searchParam: IBatchSearch = reactive({
+ from: 0,
+ size: 100,
+ desc: true
+ })
+
+ const hasPreviousPage = computed(() => (searchParam.from || 0) > 0)
+ const hasNextPage = computed(() => tableData.value.length === pageSize.value)
+ const pageRangeText = computed(() => {
+ const from = searchParam.from || 0
+ if (tableData.value.length === 0) {
+ return '0 - 0'
+ }
+ return `${from + 1} - ${from + tableData.value.length}`
+ })
+
+ const engineUIProxyConfig: IWebUIConfig = reactive({
+ engineUIProxyEnabled: false
+ })
+
+ function display(value: string | null | undefined): string {
+ return value ? value : '-'
+ }
+
+ function formatTime(value: number): string {
+ return value != null && value > 0
+ ? format(value, 'yyyy-MM-dd HH:mm:ss')
+ : '-'
+ }
+
+ function formatDuration(batch: IBatch): string {
+ return batch.createTime > 0 && batch.endTime > 0
+ ? millTransfer(batch.endTime - batch.createTime)
+ : '-'
+ }
+
+ function isTerminalState(state: string): boolean {
+ return terminalBatchStates.has(state)
+ }
+
+ // The driver UI is only reachable while the engine is alive; once the batch
is
+ // terminal the app/driver is gone, so hide the link rather than open a 502.
+ function canShowAppUI(batch: IBatch): boolean {
+ return Boolean(batch.appUrl) && !isTerminalState(batch.state)
+ }
+
+ function getErrorMessage(error: unknown, fallback: string): string {
+ if (error instanceof Error && error.message) {
+ return error.message
+ }
+ return fallback
+ }
+
+ function getAppUI(url: string): string {
+ return getEngineUIUrl(url, engineUIProxyConfig.engineUIProxyEnabled)
+ }
+
+ function openAppUI(url: string) {
+ window.open(getAppUI(url))
+ }
+
+ function getList() {
+ loading.value = true
+ searchParam.size = pageSize.value
+ getAllBatches(searchParam)
+ .then((res) => {
+ tableData.value = res.batches || []
+ })
+ .catch((error) => {
+ tableData.value = []
+ ElMessage({
+ message: getErrorMessage(error, t('message.get_batches_failed')),
+ type: 'error'
+ })
+ })
Review Comment:
`getList()` ignores the `total` returned by `/api/v1/batches`, so pagination
state can’t be computed accurately. Capture `res.total` (and reset it on error)
so the pager reflects the real last page.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]