This is an automated email from the ASF dual-hosted git repository. imbajin pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/hugegraph-toolchain.git
commit 4a0d0da4925e4fa78e7fb04d9412a5631c7d115a Author: dark <[email protected]> AuthorDate: Fri Aug 14 12:39:02 2026 +0800 test(task): cover list and detail recovery --- .../src/pages/Task/task-list-layout.test.js | 70 +++++++++++++++++ .../hubble-fe/src/pages/Task/task-recovery.test.js | 87 +++++++++++++++++++++- .../hubble-fe/src/pages/TaskDetail/index.js | 33 ++++++-- 3 files changed, 183 insertions(+), 7 deletions(-) diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/task-list-layout.test.js b/hugegraph-hubble/hubble-fe/src/pages/Task/task-list-layout.test.js new file mode 100644 index 000000000..dbf236dc7 --- /dev/null +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/task-list-layout.test.js @@ -0,0 +1,70 @@ +/* + * 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 fs from 'fs'; +import path from 'path'; + +const component = fs.readFileSync(path.join(__dirname, 'index.js'), 'utf8'); +const stylesheet = fs.readFileSync(path.join(__dirname, 'index.module.scss'), 'utf8'); + +const widthConfig = component.match( + /const TASK_COLUMN_WIDTHS = \{([\s\S]*?)\};/ +)?.[1]; +const widths = Object.fromEntries( + [...(widthConfig?.matchAll(/(\w+):\s*(\d+)/g) ?? [])] + .map(([, key, value]) => [key, Number(value)]) +); + +test('declares bounded task column widths and nowrap overflow behavior', () => { + // This is a static source contract. It does not prove that every column is + // visible at 1280px; viewport visibility requires browser evidence. + expect(component).toContain('className={style.task_table}'); + expect(component).toContain("scroll={{x: 'max-content'}}"); + expect(component).not.toContain("fixed: 'right'"); + expect(component).toContain('const hasCreator = data.some'); + expect(component).toContain('...(hasCreator ? [{'); + expect(component.match(/className: style\.no_wrap/g)?.length).toBeGreaterThanOrEqual(8); + expect(component.match(/ellipsis: true/g)?.length).toBeGreaterThanOrEqual(7); + expect(Object.keys(widths)).toEqual([ + 'name', + 'source', + 'graphspace', + 'graph', + 'created', + 'creator', + 'status', + 'sync', + 'actions', + ]); + const declaredPrimaryWidth = widths.name + widths.source + widths.graphspace + + widths.graph + widths.created + widths.status + widths.sync; + expect(declaredPrimaryWidth).toBeLessThanOrEqual(780); + expect(widths.actions).toBeGreaterThanOrEqual(180); + expect(widths.actions).toBeLessThanOrEqual(200); + expect(declaredPrimaryWidth + widths.actions).toBeLessThanOrEqual(950); + expect(declaredPrimaryWidth + widths.creator + widths.actions) + .toBeLessThanOrEqual(1020); + expect(component).toMatch( + /title: t\('graphspace\.col\.operation'\),\s*align: 'center',\s*width: TASK_COLUMN_WIDTHS\.actions/ + ); + expect(stylesheet).toMatch( + /\.task_table\s*\{[\s\S]*\.ant-table-cell[\s\S]*white-space:\s*nowrap/ + ); + expect(stylesheet).toMatch( + /\.no_wrap\s*\{[\s\S]*text-overflow:\s*ellipsis/ + ); +}); diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/task-recovery.test.js b/hugegraph-hubble/hubble-fe/src/pages/Task/task-recovery.test.js index 58d836044..292a319e3 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Task/task-recovery.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/task-recovery.test.js @@ -56,6 +56,7 @@ jest.mock('react-i18next', () => ({ 'task.demo.select_graph': 'Choose graph', 'graph.menu.load_hlm_sample': 'Build Red Chamber Demo', 'graph.menu.load_loader_sample': 'Build People & Software Demo', + 'graph.menu.load_rank_sample': 'Build Rank Movie Demo', 'graph.sample.hlm_title': 'Build demo?', 'graph.sample.hlm_description': 'Safe demo', 'graph.sample.confirm': 'Build demo', @@ -67,12 +68,23 @@ jest.mock('react-i18next', () => ({ 'task.col.target_space': 'Graph space', 'task.col.target_graph': 'Graph', 'task.col.create_time': 'Created', + 'task.col.creator': 'Creator', 'task.col.status': 'Status', 'task.col.sync_type': 'Schedule', + 'task.status.pending': 'Pending', + 'task.status.running': 'Running', + 'task.status.success': 'Completed', + 'task.status.failed': 'Failed', + 'task.status.paused': 'Paused', + 'task.status.stopped': 'Stopped', + 'task.status.initializing': 'Initializing', + 'task.status.cancelling': 'Cancelling', + 'task.status.cancelled': 'Cancelled', + 'task.status.unknown': 'Unknown', 'account.col.id': 'Creator', 'graphspace.col.operation': 'Actions', 'task.action.detail': 'View execution history', - 'task.action.config': 'View task configuration', + 'task.action.config': 'View task information', 'task.action.edit': 'Edit task', 'task.action.pause': 'Pause task', 'task.action.run': 'Run task', @@ -114,6 +126,8 @@ it('offers quick demos for the current target graph on the import page', async ( .toBeEnabled(); expect(screen.getByRole('button', {name: 'Build People & Software Demo'})) .toBeEnabled(); + expect(screen.getByRole('button', {name: 'Build Rank Movie Demo'})) + .toBeEnabled(); await act(async () => Promise.resolve()); }); @@ -238,9 +252,78 @@ it('gives every task-row action an accessible name and disables unsafe actions', name: 'View execution history', })).toBeInTheDocument(); expect(screen.getByRole('button', { - name: 'View task configuration', + name: 'View task information', })).toBeEnabled(); expect(screen.getByRole('button', {name: 'Edit task'})).toBeDisabled(); expect(screen.getByRole('button', {name: 'Pause task'})).toBeEnabled(); expect(screen.getByRole('button', {name: 'Delete task'})).toBeDisabled(); + expect(screen.queryByRole('columnheader', {name: 'Creator'})) + .not.toBeInTheDocument(); +}); + +it('shows the compact creator column only when task data contains it', async () => { + api.manage.getTaskList.mockResolvedValue({ + status: 200, + data: { + records: [{ + task_id: 8, + task_name: 'owned import', + ingestion_mapping: {structs: []}, + ingestion_option: {graphspace: 'DEFAULT', graph: 'hugegraph'}, + task_schedule_status: 'DISABLE', + task_schedule_type: 'ONCE', + creator: 'admin', + }], + total: 1, + size: 10, + }, + }); + api.manage.getMetricsTask.mockResolvedValue({status: 200, data: {}}); + + render(<Task />); + + expect(await screen.findByRole('columnheader', {name: 'Creator'})) + .toBeInTheDocument(); + expect(screen.getByText('admin')).toBeInTheDocument(); +}); + +it('localizes common Loader task statuses instead of exposing backend codes', async () => { + const statuses = [ + ['NEW', 'Pending'], + ['SUCCEED', 'Completed'], + ['SUCCESS', 'Completed'], + ['FAILED', 'Failed'], + ['PAUSED', 'Paused'], + ['STOPPED', 'Stopped'], + ['INIT', 'Initializing'], + ['CANCELLING', 'Cancelling'], + ['UNRECOGNIZED', 'Unknown'], + ['CANCELLED', 'Cancelled'], + ['RUNNING', 'Running'], + ]; + api.manage.getTaskList.mockResolvedValue({ + status: 200, + data: { + records: statuses.map(([status], index) => ({ + task_id: index + 1, + task_name: `task ${index + 1}`, + ingestion_mapping: {structs: []}, + ingestion_option: {graphspace: 'DEFAULT', graph: 'hugegraph'}, + task_schedule_status: 'DISABLE', + task_schedule_type: 'ONCE', + last_metrics: {status}, + })), + total: statuses.length, + size: statuses.length, + }, + }); + api.manage.getMetricsTask.mockResolvedValue({status: 200, data: {}}); + + render(<Task />); + + for (const [, label] of statuses) { + expect((await screen.findAllByText(label)).length).toBeGreaterThan(0); + } + expect(screen.queryByText('SUCCEED')).not.toBeInTheDocument(); + expect(screen.queryByText('UNRECOGNIZED')).not.toBeInTheDocument(); }); diff --git a/hugegraph-hubble/hubble-fe/src/pages/TaskDetail/index.js b/hugegraph-hubble/hubble-fe/src/pages/TaskDetail/index.js index 1bf23d70f..f6d9a7f4f 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/TaskDetail/index.js +++ b/hugegraph-hubble/hubble-fe/src/pages/TaskDetail/index.js @@ -16,35 +16,48 @@ * under the License. */ -import {Alert, Button, PageHeader, Spin, Table} from 'antd'; +import {Alert, Button, PageHeader, Spin, Table, Tooltip} from 'antd'; import {useState, useEffect, useCallback, useRef} from 'react'; import {useTranslation} from 'react-i18next'; import {useNavigate, useParams} from 'react-router-dom'; import * as api from '../../api'; -import {StatusField} from '../../components/Status'; import DataPreparationNav from '../../components/DataPreparationNav'; +import {TaskStatus} from '../Task/status'; +import style from './index.module.scss'; const createColumns = t => [ { title: t('task.detail.job_id'), dataIndex: 'job_id', + className: style.no_wrap, + width: 110, + ellipsis: true, render: val => (val === null || val === undefined ? '-' : val.toString()), }, { title: t('task.detail.import_count'), dataIndex: 'job_metrics', + className: style.no_wrap, align: 'right', + width: 120, + ellipsis: true, render: val => val?.total_count, }, { title: t('task.detail.create_time'), dataIndex: 'create_time', + className: style.no_wrap, align: 'center', + width: 180, + ellipsis: true, }, { title: t('task.detail.average_rate'), dataIndex: 'job_metrics', + className: style.no_wrap, align: 'right', + width: 150, + ellipsis: true, render: (val, row) => { if (val) { const rate = row.job_status?.toLowerCase() === 'running' ? val.cur_rate : val.avg_rate; @@ -57,7 +70,10 @@ const createColumns = t => [ { title: t('task.detail.duration'), dataIndex: 'job_metrics', + className: style.no_wrap, align: 'right', + width: 120, + ellipsis: true, render: val => { if (val) { return t('task.detail.seconds', {seconds: val.total_time / 1000}); @@ -69,15 +85,21 @@ const createColumns = t => [ { title: t('task.detail.status'), dataIndex: 'job_status', + className: style.no_wrap, align: 'center', - render: val => <StatusField status={val} />, + width: 110, + render: val => <TaskStatus status={val} />, }, { title: t('task.detail.other'), - width: 400, + className: style.no_wrap, + width: 240, + ellipsis: true, align: 'center', dataIndex: 'job_message', - render: val => val ?? '-', + render: val => ( + val ? <Tooltip title={val}><span>{val}</span></Tooltip> : '-' + ), }, ]; @@ -163,6 +185,7 @@ const TaskDetail = () => { )} <Spin spinning={loading}> <Table + className={style.task_detail_table} rowKey={getRowKey} columns={columns} dataSource={visibleData}
