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}

Reply via email to