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>

Reply via email to