diff --git a/seatunnel-engine/seatunnel-engine-ui/src/tests/detail.spec.ts b/seatunnel-engine/seatunnel-engine-ui/src/tests/detail.spec.ts new file mode 100644 index 000000000000..ce35da24051e --- /dev/null +++ b/seatunnel-engine/seatunnel-engine-ui/src/tests/detail.spec.ts @@ -0,0 +1,112 @@ +/* + * 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 { describe, test, expect, vi, beforeEach } from 'vitest' +import { flushPromises, mount } from '@vue/test-utils' +import { createApp } from 'vue' +import { createPinia, setActivePinia } from 'pinia' +import i18n from '@/locales' +import detail from '@/views/jobs/detail' +import { getJobInfo } from '@/service/job' +import type { Job } from '@/service/job/types' + +vi.mock('@/service/job', () => ({ + getJobInfo: vi.fn(), + getRunningJobInfo: vi.fn() +})) + +vi.mock('vue-router', () => ({ + useRoute: () => ({ params: { jobId: '123456789' } }), + useRouter: () => ({ push: vi.fn() }) +})) + +vi.mock('@/components/directed-acyclic-graph', () => ({ + default: { template: '
' } +})) + +describe('detail', () => { + const app = createApp({}) + beforeEach(() => { + const pinia = createPinia() + app.use(pinia) + setActivePinia(createPinia()) + }) + + test('should not display NaN when tablePaths does not match metrics keys', async () => { + const mockJob = { + jobId: '123456789', + jobName: 'Oracle-CDC-Test', + jobStatus: 'FINISHED', + errorMsg: '', + createTime: '2026-05-21 10:00:00', + finishTime: '2026-05-21 11:00:00', + metrics: { + SourceReceivedBytes: '119028', + SourceReceivedBytesPerSeconds: '1024', + SourceReceivedCount: '141', + SourceReceivedQPS: '10', + SinkWriteBytes: '98304', + SinkWriteBytesPerSeconds: '512', + SinkWriteCount: '138', + SinkWriteQPS: '9', + TableSourceReceivedBytes: { 'Source[0].fake': '119028' }, + TableSourceReceivedCount: { 'Source[0].fake': '141' }, + TableSourceReceivedQPS: { 'Source[0].fake': '10' }, + TableSourceReceivedBytesPerSeconds: { 'Source[0].fake': '1024' }, + TableSinkWriteBytes: { 'Sink[0].fake': '98304' }, + TableSinkWriteCount: { 'Sink[0].fake': '138' }, + TableSinkWriteQPS: { 'Sink[0].fake': '9' }, + TableSinkWriteBytesPerSeconds: { 'Sink[0].fake': '512' } + }, + jobDag: { + jobId: '123456789', + pipelineEdges: {}, + vertexInfoMap: [ + { + vertexId: 1, + type: 'source', + vertexName: 'pipeline-1 [Source[0]-FakeSource]', + tablePaths: ['fake'] + }, + { + vertexId: 2, + type: 'sink', + vertexName: 'pipeline-1 [Sink[0]-FakeSink]', + tablePaths: ['fake'] + } + ] + }, + pluginJarsUrls: [] + } as unknown as Job + + vi.mocked(getJobInfo).mockResolvedValue(mockJob) + + const wrapper = mount(detail, { + global: { + plugins: [i18n] + } + }) + + await flushPromises() + + + expect(wrapper.text()).toContain('Oracle-CDC-Test') + + + expect(wrapper.text()).not.toContain('NaN') + }) +}) \ No newline at end of file diff --git a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx index f8f14b101f36..6353f6a03195 100644 --- a/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx +++ b/seatunnel-engine/seatunnel-engine-ui/src/views/jobs/detail.tsx @@ -92,31 +92,40 @@ export default defineComponent({ return job.jobDag?.vertexInfoMap?.filter((v) => v.type !== 'transform') || [] }) const sourceCell = ( - row: Vertex, - key: - | 'TableSourceReceivedBytes' - | 'TableSourceReceivedCount' - | 'TableSourceReceivedQPS' - | 'TableSourceReceivedBytesPerSeconds' - ) => { - if (row.type === 'source') { - return row.tablePaths.reduce((s, path) => s + Number(job.metrics?.[key][path]), 0) - } - return 0 - } - const sinkCell = ( - row: Vertex, - key: - | 'TableSinkWriteBytes' - | 'TableSinkWriteCount' - | 'TableSinkWriteQPS' - | 'TableSinkWriteBytesPerSeconds' - ) => { - if (row.type === 'sink') { - return row.tablePaths.reduce((s, path) => s + Number(job.metrics?.[key][path]), 0) - } - return 0 - } + row: Vertex, + key: + | 'TableSourceReceivedBytes' + | 'TableSourceReceivedCount' + | 'TableSourceReceivedQPS' + | 'TableSourceReceivedBytesPerSeconds' +) => { + if (row.type === 'source') { + return row.tablePaths.reduce((s, path) => { + const metrics = job.metrics?.[key] || {} + const matchedKey = Object.keys(metrics).find(k => k.endsWith(path)) + return s + Number(matchedKey ? metrics[matchedKey] : 0) + }, 0) + } + return 0 +} + +const sinkCell = ( + row: Vertex, + key: + | 'TableSinkWriteBytes' + | 'TableSinkWriteCount' + | 'TableSinkWriteQPS' + | 'TableSinkWriteBytesPerSeconds' +) => { + if (row.type === 'sink') { + return row.tablePaths.reduce((s, path) => { + const metrics = job.metrics?.[key] || {} + const matchedKey = Object.keys(metrics).find(k => k.endsWith(path)) + return s + Number(matchedKey ? metrics[matchedKey] : 0) + }, 0) + } + return 0 +} const columns: DataTableColumns