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 3414047e64c155655f8efbd07cec5be425b6b755 Author: dark <[email protected]> AuthorDate: Fri Aug 14 12:39:02 2026 +0800 fix(task): improve import feedback and layout --- .../src/pages/Task/components/ViewLayer.test.js | 107 ++++++++++++++++++++- .../src/pages/Task/components/index.module.scss | 9 ++ hugegraph-hubble/hubble-fe/src/pages/Task/index.js | 56 +++++++++-- 3 files changed, 164 insertions(+), 8 deletions(-) diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.test.js b/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.test.js index 37ac59e81..392d48e61 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.test.js @@ -20,6 +20,8 @@ import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ViewLayer from './ViewLayer'; import * as api from '../../../api'; +import enPages from '../../../i18n/resources/en-US/modules/pages.json'; +import zhPages from '../../../i18n/resources/zh-CN/modules/pages.json'; jest.mock('react-i18next', () => ({ useTranslation: () => ({t: key => key}), @@ -29,7 +31,9 @@ jest.mock('../../../api', () => ({ manage: {getTaskDetail: jest.fn()}, })); -jest.mock('react-json-view', () => ({src}) => <div>{src.name}</div>); +jest.mock('react-json-view', () => ({src}) => ( + <pre data-testid='raw-task-json'>{JSON.stringify(src)}</pre> +)); const deferred = () => { let resolve; @@ -72,3 +76,104 @@ test('clears stale task data and offers retry for the current task', async () => await userEvent.click(screen.getByRole('button', {name: 'task.view.retry'})); await waitFor(() => expect(screen.getByText('Task B')).toBeInTheDocument()); }); + +test('shows a user summary while keeping raw backend fields collapsed', async () => { + api.manage.getTaskDetail.mockResolvedValue({ + status: 200, + data: { + id: 7, + conn_id: 3, + job_name: 'movie_import', + graphspace: 'DEFAULT', + graph: 'hugegraph', + job_status: 'SUCCESS', + job_size: '12 MB', + job_duration: '5 s', + create_time: '2026-08-10 09:00:00', + update_time: '2026-08-10 09:00:05', + }, + }); + + render(<ViewLayer visible task_id='7' onCancel={jest.fn()} />); + + expect(await screen.findByText('movie_import')).toBeVisible(); + expect(screen.getByText('DEFAULT')).toBeVisible(); + expect(screen.getByText('hugegraph')).toBeVisible(); + expect(screen.getByText('task.status.success')).toBeVisible(); + expect(screen.getByText('12 MB')).toBeVisible(); + expect(screen.getByText('5 s')).toBeVisible(); + expect(screen.getByText('2026-08-10 09:00:00')).toBeVisible(); + expect(screen.getByText('2026-08-10 09:00:05')).toBeVisible(); + + const technicalDetails = screen.getByRole('button', { + name: /task\.view\.technical_details/, + }); + expect(technicalDetails).toHaveAttribute('aria-expanded', 'false'); + expect(screen.queryByTestId('raw-task-json')).not.toBeInTheDocument(); + expect(screen.queryByText(/SUCCESS/)).not.toBeInTheDocument(); + + await userEvent.click(technicalDetails); + + expect(technicalDetails).toHaveAttribute('aria-expanded', 'true'); + expect(screen.getByTestId('raw-task-json')).toBeVisible(); + expect(screen.getByText(/SUCCESS/)).toBeVisible(); + expect(screen.getByText(/conn_id/)).toBeVisible(); +}); + +test('labels missing summary values instead of leaving blank fields', async () => { + api.manage.getTaskDetail.mockResolvedValue({ + status: 200, + data: { + job_name: null, + graphspace: '', + graph: undefined, + job_status: null, + job_size: null, + job_duration: '', + create_time: null, + update_time: undefined, + }, + }); + + render(<ViewLayer visible task_id='8' onCancel={jest.fn()} />); + + expect(await screen.findAllByText('task.view.unavailable_value')).toHaveLength(7); + expect(screen.getByText('task.status.unknown')).toBeVisible(); +}); + +test('shows a valid intermediate JobStatus as running instead of unknown', async () => { + api.manage.getTaskDetail.mockResolvedValue({ + status: 200, + data: { + job_name: 'active_import', + graphspace: 'DEFAULT', + graph: 'hugegraph', + job_status: 'LOADING', + }, + }); + + render(<ViewLayer visible task_id='9' onCancel={jest.fn()} />); + + expect(await screen.findByText('task.status.running')).toBeVisible(); + expect(screen.queryByText('task.status.unknown')).not.toBeInTheDocument(); + expect(screen.queryByText('LOADING')).not.toBeInTheDocument(); +}); + +test('ships accurate Task Information copy in both languages', () => { + expect(zhPages.task.action.config).toBe('查看任务信息'); + expect(enPages.task.action.config).toBe('View task information'); + expect(zhPages.task.view).toMatchObject({ + title: '任务信息', + status: '状态', + data_size: '数据量', + duration: '持续时间', + technical_details: '技术详情', + }); + expect(enPages.task.view).toMatchObject({ + title: 'Task Information', + status: 'Status', + data_size: 'Data Size', + duration: 'Duration', + technical_details: 'Technical Details', + }); +}); diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/components/index.module.scss b/hugegraph-hubble/hubble-fe/src/pages/Task/components/index.module.scss index b65f2a368..cc87bf76a 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Task/components/index.module.scss +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/components/index.module.scss @@ -59,3 +59,12 @@ } } + +.technical_details { + margin-top: 12px; +} + +.raw_json { + max-height: 280px; + overflow: auto; +} diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/index.js b/hugegraph-hubble/hubble-fe/src/pages/Task/index.js index db43435cc..a6a4b16d1 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Task/index.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/index.js @@ -48,14 +48,26 @@ import ViewLayer from './components/ViewLayer'; import TopStatistic from './components/TopStatistic'; import {useNavigate, Link} from 'react-router-dom'; import * as api from '../../api'; -import {StatusField} from '../../components/Status'; import {sourceType, syncType} from './config'; import TableHeader from '../../components/TableHeader'; import DataPreparationNav from '../../components/DataPreparationNav'; import {readWorkbenchGraphContext} from '../../utils/workbenchGraphContext'; +import {TaskStatus} from './status'; const {Text} = Typography; +const TASK_COLUMN_WIDTHS = { + name: 150, + source: 80, + graphspace: 100, + graph: 100, + created: 145, + creator: 70, + status: 80, + sync: 90, + actions: 190, +}; + const DetailTip = ({row}) => { const {t} = useTranslation(); const {job_summary} = row; @@ -347,6 +359,7 @@ const Task = () => { }, [t]); const loadHlmDemo = useCallback(() => loadDemo('hlm'), [loadDemo]); const loadLoaderDemo = useCallback(() => loadDemo('loader'), [loadDemo]); + const loadRankDemo = useCallback(() => loadDemo('rank'), [loadDemo]); const chooseDemoGraph = useCallback(() => navigate('/graphspace'), [navigate]); const handleHideEditLayer = useCallback(() => setEditLayer(false), []); @@ -354,12 +367,14 @@ const Task = () => { const handleHideViewLayer = useCallback(() => setViewLayer(false), []); const rowKey = useCallback(record => record.task_id, []); + const hasCreator = data.some(row => Boolean(row?.creator)); const columns = [ { title: t('task.col.name'), dataIndex: 'task_name', - width: 220, + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.name, ellipsis: true, render: task_name => ( <Text @@ -373,6 +388,9 @@ const Task = () => { { title: t('task.col.source_type'), dataIndex: 'ingestion_mapping', + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.source, + ellipsis: true, render: val => { const {structs} = val ?? {}; if (!structs?.length) { @@ -386,31 +404,47 @@ const Task = () => { { title: t('task.col.target_space'), dataIndex: 'ingestion_option', + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.graphspace, + ellipsis: true, render: val => val?.graphspace ?? '-', }, { title: t('task.col.target_graph'), dataIndex: 'ingestion_option', + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.graph, + ellipsis: true, render: val => val?.graph ?? '-', }, { title: t('task.col.create_time'), dataIndex: 'create_time', + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.created, + ellipsis: true, }, - { + ...(hasCreator ? [{ title: t('task.col.creator'), dataIndex: 'creator', - }, + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.creator, + ellipsis: true, + }] : []), { title: t('task.col.status'), dataIndex: 'last_metrics', + className: style.no_wrap, align: 'center', - width: 120, - render: val => <StatusField status={val} />, + width: TASK_COLUMN_WIDTHS.status, + render: val => <TaskStatus status={val} />, }, { title: t('task.col.sync_type'), dataIndex: 'task_schedule_type', + className: style.no_wrap, + width: TASK_COLUMN_WIDTHS.sync, + ellipsis: true, render: val => { return syncTypes.find(item => item.value === val)?.label ?? val; }, @@ -418,7 +452,7 @@ const Task = () => { { title: t('graphspace.col.operation'), align: 'center', - width: 160, + width: TASK_COLUMN_WIDTHS.actions, render: row => { return ( <TaskActions @@ -581,6 +615,13 @@ const Task = () => { > {t('graph.menu.load_loader_sample')} </Button> + <Button + loading={demoLoading === 'rank'} + disabled={Boolean(demoLoading)} + onClick={loadRankDemo} + > + {t('graph.menu.load_rank_sample')} + </Button> </Space> ) : ( <Button onClick={chooseDemoGraph}> @@ -619,6 +660,7 @@ const Task = () => { </Button> </TableHeader> <Table + className={style.task_table} columns={columns} rowKey={rowKey} scroll={{x: 'max-content'}}
