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 031bd2e624e2e06a110f865cee119d9b71a9f227 Author: dark <[email protected]> AuthorDate: Fri Aug 14 12:39:02 2026 +0800 fix(hubble): recover schema and task views --- .../src/pages/Schema/schema-template-error.test.js | 23 +++- .../Schema/schema-template-starting-point.test.js | 125 ++++++++++++++++++++- .../src/pages/Task/components/ViewLayer.js | 61 ++++++++-- 3 files changed, 198 insertions(+), 11 deletions(-) diff --git a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-error.test.js b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-error.test.js index 3bcec1bdc..41783dd34 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-error.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-error.test.js @@ -16,7 +16,11 @@ * under the License. */ -import {BUILTIN_SCHEMA_TEMPLATES, schemaTemplateBusinessError} from './EditLayer'; +import { + BUILTIN_SCHEMA_TEMPLATES, + schemaTemplateBusinessError, + validateSchemaTemplateFields, +} from './EditLayer'; jest.mock('../../components/CodeEditor', () => () => null); @@ -42,6 +46,23 @@ test('uses an input-oriented fallback for other business failures', () => { }); }); +test('handles only Ant Design field-validation rejections', async () => { + const validationError = { + errorFields: [{name: ['name'], errors: ['Required']}], + values: {name: ''}, + }; + const form = {validateFields: jest.fn().mockRejectedValue(validationError)}; + + await expect(validateSchemaTemplateFields(form)).resolves.toBeNull(); +}); + +test('does not swallow unexpected validation or application failures', async () => { + const error = new Error('unexpected'); + const form = {validateFields: jest.fn().mockRejectedValue(error)}; + + await expect(validateSchemaTemplateFields(form)).rejects.toBe(error); +}); + test.each(Object.entries(BUILTIN_SCHEMA_TEMPLATES))( '%s is a small idempotent complete graph starting point', (name, script) => { diff --git a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-starting-point.test.js b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-starting-point.test.js index 87a905f4e..79495cf4a 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-starting-point.test.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Schema/schema-template-starting-point.test.js @@ -15,9 +15,11 @@ * limitations under the License. */ -import {render, screen} from '@testing-library/react'; +import {render, screen, waitFor} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import {message} from 'antd'; import EditLayer, {BUILTIN_SCHEMA_TEMPLATES} from './EditLayer'; +import * as api from '../../api/index'; jest.mock('../../api/index', () => ({ manage: { @@ -52,6 +54,10 @@ beforeAll(() => { })); }); +beforeEach(() => { + jest.clearAllMocks(); +}); + test('offers built-in starting points in create mode and fills an empty draft', async () => { render( <EditLayer @@ -130,3 +136,120 @@ test('does not show the starting-point selector while editing', () => { expect(screen.queryByText('schema_template.form.starting_point')).not.toBeInTheDocument(); }); + +test('keeps empty-field errors in the form without rejecting or calling the API', async () => { + const unhandledRejection = jest.fn(event => event.preventDefault()); + window.addEventListener('unhandledrejection', unhandledRejection); + render( + <EditLayer + visible + mode='create' + detail={{}} + graphspace='DEFAULT' + onCancel={jest.fn()} + refresh={jest.fn()} + /> + ); + + await userEvent.click(screen.getByRole('button', {name: 'OK'})); + + await waitFor(() => expect(screen.getAllByRole('alert').length).toBeGreaterThan(0)); + expect(api.manage.addSchema).not.toHaveBeenCalled(); + expect(api.manage.updateSchema).not.toHaveBeenCalled(); + expect(unhandledRejection).not.toHaveBeenCalled(); + window.removeEventListener('unhandledrejection', unhandledRejection); +}); + +test('keeps invalid-name errors in the form without calling the API', async () => { + render( + <EditLayer + visible + mode='create' + detail={{}} + graphspace='DEFAULT' + onCancel={jest.fn()} + refresh={jest.fn()} + /> + ); + + await userEvent.type( + screen.getByPlaceholderText('schema_template.form.name_placeholder'), + 'INVALID-NAME' + ); + await userEvent.type( + screen.getByPlaceholderText('schema_template.form.schema_placeholder'), + 'schema = graph.schema()' + ); + await userEvent.click(screen.getByRole('button', {name: 'OK'})); + + await waitFor(() => expect(screen.getAllByRole('alert').length).toBeGreaterThan(0)); + expect(api.manage.addSchema).not.toHaveBeenCalled(); + expect(api.manage.updateSchema).not.toHaveBeenCalled(); +}); + +test('keeps valid create submission behavior after validation passes', async () => { + api.manage.addSchema.mockResolvedValue({status: 200}); + const onCancel = jest.fn(); + const refresh = jest.fn(); + render( + <EditLayer + visible + mode='create' + detail={{}} + graphspace='DEFAULT' + onCancel={onCancel} + refresh={refresh} + /> + ); + + await userEvent.type( + screen.getByPlaceholderText('schema_template.form.name_placeholder'), + 'valid_name' + ); + await userEvent.type( + screen.getByPlaceholderText('schema_template.form.schema_placeholder'), + 'schema = graph.schema()' + ); + await userEvent.click(screen.getByRole('button', {name: 'OK'})); + + await waitFor(() => expect(api.manage.addSchema).toHaveBeenCalledWith( + 'DEFAULT', + { + name: 'valid_name', + schema: 'schema = graph.schema()', + }, + {suppressBusinessErrorToast: true} + )); + await waitFor(() => expect(onCancel).toHaveBeenCalled()); + expect(refresh).toHaveBeenCalled(); +}); + +test('reports unexpected validation failures without an unhandled rejection', async () => { + const error = new Error('validation infrastructure failed'); + const validateForm = jest.fn().mockRejectedValue(error); + const unhandledRejection = jest.fn(event => event.preventDefault()); + const messageError = jest.spyOn(message, 'error').mockImplementation(() => undefined); + window.addEventListener('unhandledrejection', unhandledRejection); + render( + <EditLayer + visible + mode='create' + detail={{}} + graphspace='DEFAULT' + onCancel={jest.fn()} + refresh={jest.fn()} + validateForm={validateForm} + /> + ); + + await userEvent.click(screen.getByRole('button', {name: 'OK'})); + + await waitFor(() => expect(messageError).toHaveBeenCalledWith( + 'common.msg.operation_failed' + )); + expect(validateForm).toHaveBeenCalled(); + expect(api.manage.addSchema).not.toHaveBeenCalled(); + expect(api.manage.updateSchema).not.toHaveBeenCalled(); + expect(unhandledRejection).not.toHaveBeenCalled(); + window.removeEventListener('unhandledrejection', unhandledRejection); +}); diff --git a/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.js b/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.js index 99de3ee66..a2201837e 100644 --- a/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.js +++ b/hugegraph-hubble/hubble-fe/src/pages/Task/components/ViewLayer.js @@ -19,6 +19,8 @@ import { Alert, Button, + Collapse, + Descriptions, Modal, Spin, } from 'antd'; @@ -26,6 +28,14 @@ import {useCallback, useEffect, useRef, useState} from 'react'; import * as api from '../../../api'; import ReactJsonView from 'react-json-view'; import {useTranslation} from 'react-i18next'; +import {TaskStatus} from '../status'; +import style from './index.module.scss'; + +const {Panel} = Collapse; + +const displayValue = (value, fallback) => ( + value === null || value === undefined || value === '' ? fallback : value +); const ViewLayer = ({visible, onCancel, task_id}) => { const {t} = useTranslation(); @@ -33,6 +43,7 @@ const ViewLayer = ({visible, onCancel, task_id}) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const detailRequest = useRef(null); + const unavailable = t('task.view.unavailable_value'); const onFinish = useCallback(() => { onCancel(); @@ -103,15 +114,47 @@ const ViewLayer = ({visible, onCancel, task_id}) => { /> )} {data && ( - <div style={{height: 400, overflow: 'scroll'}}> - <ReactJsonView - src={data} - name={false} - displayObjectSize={false} - displayDataTypes={false} - groupArraysAfterLength={50} - /> - </div> + <> + <Descriptions bordered size='small' column={1}> + <Descriptions.Item label={t('task.view.name')}> + {displayValue(data.job_name ?? data.name, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.target_space')}> + {displayValue(data.graphspace, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.target_graph')}> + {displayValue(data.graph, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.status')}> + <TaskStatus status={data.job_status ?? data.status} /> + </Descriptions.Item> + <Descriptions.Item label={t('task.view.data_size')}> + {displayValue(data.job_size, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.duration')}> + {displayValue(data.job_duration, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.create_time')}> + {displayValue(data.create_time, unavailable)} + </Descriptions.Item> + <Descriptions.Item label={t('task.view.update_time')}> + {displayValue(data.update_time, unavailable)} + </Descriptions.Item> + </Descriptions> + <Collapse className={style.technical_details} ghost> + <Panel header={t('task.view.technical_details')} key='technical'> + <div className={style.raw_json}> + <ReactJsonView + src={data} + name={false} + displayObjectSize={false} + displayDataTypes={false} + groupArraysAfterLength={50} + /> + </div> + </Panel> + </Collapse> + </> )} </Spin> </Modal>
