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 e65f2191ba2a8a561c278ef1dda4a5ed43a68a68
Author: dark <[email protected]>
AuthorDate: Fri Aug 14 12:39:02 2026 +0800

    fix(hubble): restore graph sample workflows
---
 .../hubble-fe/src/pages/Graph/EditLayer.test.js    | 288 ++++++++++++++++++++-
 .../src/pages/Graph/default-card-actions.test.js   |  61 ++++-
 .../hubble-fe/src/pages/Graph/index.js             | 164 ++++++++----
 3 files changed, 459 insertions(+), 54 deletions(-)

diff --git a/hugegraph-hubble/hubble-fe/src/pages/Graph/EditLayer.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Graph/EditLayer.test.js
index 764b6a348..320332f2b 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Graph/EditLayer.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Graph/EditLayer.test.js
@@ -18,7 +18,8 @@
 
 import {fireEvent, render, screen, waitFor} from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import {EditLayer, ViewLayer} from './EditLayer';
+import {message} from 'antd';
+import {EditLayer, validateGraphFields, ViewLayer} from './EditLayer';
 import * as api from '../../api';
 import {BUILTIN_SCHEMA_TEMPLATES} from '../Schema/builtinSchemaTemplates';
 
@@ -36,7 +37,9 @@ jest.mock('../../api', () => ({
         addSchema: jest.fn(),
         addGraphSchema: jest.fn(),
         addGraph: jest.fn(),
+        loadSampleGraph: jest.fn(),
         getGraph: jest.fn(),
+        updateGraph: jest.fn(),
     },
 }));
 
@@ -143,11 +146,220 @@ test('offers built-in Graph Schema choices and a direct 
create-template route',
     expect(await screen.findByRole('link', {name: 'graph.form.schema_create'}))
         .toHaveAttribute('href', '/graphspace/DEFAULT/schema?create=true');
     await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
+    await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
     await userEvent.click(screen.getByRole('combobox'));
     
expect(screen.getByText('schema_template.builtin.people_network')).toBeInTheDocument();
     
expect(screen.getByText('schema_template.builtin.product_catalog')).toBeInTheDocument();
 });
 
+test('keeps empty create errors in the form without an unhandled rejection or 
API call', async () => {
+    api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
+    const unhandledRejection = jest.fn(event => event.preventDefault());
+    window.addEventListener('unhandledrejection', unhandledRejection);
+    render(
+        <EditLayer
+            visible
+            onCancel={jest.fn()}
+            refresh={jest.fn()}
+            graphspace='DEFAULT'
+        />
+    );
+
+    await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
+    await userEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => 
expect(screen.getAllByRole('alert').length).toBeGreaterThan(0));
+    expect(api.manage.addGraph).not.toHaveBeenCalled();
+    expect(api.manage.updateGraph).not.toHaveBeenCalled();
+    expect(unhandledRejection).not.toHaveBeenCalled();
+    window.removeEventListener('unhandledrejection', unhandledRejection);
+});
+
+test('reports unexpected validation rejection at the Modal event boundary', 
async () => {
+    api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
+    const validateForm = jest.fn().mockRejectedValue(new Error('validation 
failed'));
+    const unhandledRejection = jest.fn(event => event.preventDefault());
+    const messageError = jest.spyOn(message, 'error').mockImplementation(() => 
undefined);
+    window.addEventListener('unhandledrejection', unhandledRejection);
+    render(
+        <EditLayer
+            visible
+            onCancel={jest.fn()}
+            refresh={jest.fn()}
+            graphspace='DEFAULT'
+            validateForm={validateForm}
+        />
+    );
+
+    await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
+    await userEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => expect(messageError).toHaveBeenCalledWith(
+        'common.msg.operation_failed'
+    ));
+    expect(api.manage.addGraph).not.toHaveBeenCalled();
+    expect(api.manage.updateGraph).not.toHaveBeenCalled();
+    expect(unhandledRejection).not.toHaveBeenCalled();
+    window.removeEventListener('unhandledrejection', unhandledRejection);
+    messageError.mockRestore();
+});
+
+test('recognizes only Ant Design field-validation rejection as a form error', 
async () => {
+    const form = {
+        validateFields: jest.fn().mockRejectedValue({
+            errorFields: [{name: ['graph'], errors: ['Required']}],
+        }),
+    };
+
+    await expect(validateGraphFields(form)).resolves.toBeNull();
+});
+
+test('submits a valid create after field validation passes', async () => {
+    api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
+    api.manage.addGraph.mockResolvedValue({status: 200});
+    render(
+        <EditLayer
+            visible
+            onCancel={jest.fn()}
+            refresh={jest.fn()}
+            graphspace='DEFAULT'
+        />
+    );
+
+    await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
+    await userEvent.type(
+        screen.getByPlaceholderText('graph.form.name_placeholder'),
+        'valid_graph'
+    );
+    await userEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => expect(api.manage.addGraph).toHaveBeenCalledWith(
+        'DEFAULT',
+        expect.objectContaining({
+            graph: 'valid_graph',
+            auth: false,
+            graphspace: 'DEFAULT',
+        })
+    ));
+});
+
+test('submits a valid alias update after field validation passes', async () => 
{
+    api.manage.getGraph.mockResolvedValue({
+        status: 200,
+        data: {name: 'movie_graph', nickname: 'Movies'},
+    });
+    api.manage.updateGraph.mockResolvedValue({status: 200});
+    const onCancel = jest.fn();
+    const refresh = jest.fn();
+    render(
+        <EditLayer
+            visible
+            onCancel={onCancel}
+            refresh={refresh}
+            graphspace='DEFAULT'
+            graph='movie_graph'
+        />
+    );
+
+    const alias = 
screen.getByPlaceholderText('graph.form.nickname_placeholder');
+    await waitFor(() => expect(alias).toHaveValue('Movies'));
+    await userEvent.clear(alias);
+    await userEvent.type(alias, 'Movie_Catalog');
+    await userEvent.click(screen.getByRole('button', {name: 'OK'}));
+
+    await waitFor(() => expect(api.manage.updateGraph).toHaveBeenCalledWith(
+        'DEFAULT',
+        'movie_graph',
+        {nickname: 'Movie_Catalog'},
+        {suppressBusinessErrorToast: true}
+    ));
+    await waitFor(() => expect(onCancel).toHaveBeenCalled());
+    expect(refresh).toHaveBeenCalled();
+});
+
+test('recovers from an update request rejection and allows retry', async () => 
{
+    api.manage.getGraph.mockResolvedValue({
+        status: 200,
+        data: {name: 'movie_graph', nickname: 'Movies'},
+    });
+    api.manage.updateGraph
+        .mockRejectedValueOnce(new Error('offline'))
+        .mockResolvedValueOnce({status: 200});
+    const onCancel = jest.fn();
+    const refresh = jest.fn();
+    const unhandledRejection = jest.fn(event => event.preventDefault());
+    const messageError = jest.spyOn(message, 'error').mockImplementation(() => 
undefined);
+    window.addEventListener('unhandledrejection', unhandledRejection);
+    render(
+        <EditLayer
+            visible
+            onCancel={onCancel}
+            refresh={refresh}
+            graphspace='DEFAULT'
+            graph='movie_graph'
+        />
+    );
+
+    const alias = 
screen.getByPlaceholderText('graph.form.nickname_placeholder');
+    await waitFor(() => expect(alias).toHaveValue('Movies'));
+    await userEvent.clear(alias);
+    await userEvent.type(alias, 'Movie_Catalog');
+    const confirm = screen.getByRole('button', {name: 'OK'});
+    await userEvent.click(confirm);
+
+    await waitFor(() => expect(messageError).toHaveBeenCalledWith(
+        'common.msg.operation_failed'
+    ));
+    expect(confirm).not.toHaveClass('ant-btn-loading');
+    expect(onCancel).not.toHaveBeenCalled();
+    expect(refresh).not.toHaveBeenCalled();
+    expect(unhandledRejection).not.toHaveBeenCalled();
+
+    await userEvent.click(confirm);
+
+    await waitFor(() => 
expect(api.manage.updateGraph).toHaveBeenCalledTimes(2));
+    await waitFor(() => expect(onCancel).toHaveBeenCalled());
+    expect(refresh).toHaveBeenCalled();
+    window.removeEventListener('unhandledrejection', unhandledRejection);
+    messageError.mockRestore();
+});
+
+test('keeps the resolved update business-error message and resets loading', 
async () => {
+    api.manage.getGraph.mockResolvedValue({
+        status: 200,
+        data: {name: 'movie_graph', nickname: 'Movies'},
+    });
+    api.manage.updateGraph.mockResolvedValue({
+        status: 409,
+        message: 'nickname conflict',
+    });
+    const onCancel = jest.fn();
+    const refresh = jest.fn();
+    const messageError = jest.spyOn(message, 'error').mockImplementation(() => 
undefined);
+    render(
+        <EditLayer
+            visible
+            onCancel={onCancel}
+            refresh={refresh}
+            graphspace='DEFAULT'
+            graph='movie_graph'
+        />
+    );
+
+    const alias = 
screen.getByPlaceholderText('graph.form.nickname_placeholder');
+    await waitFor(() => expect(alias).toHaveValue('Movies'));
+    const confirm = screen.getByRole('button', {name: 'OK'});
+    await userEvent.click(confirm);
+
+    await waitFor(() => expect(messageError).toHaveBeenCalledWith(
+        'nickname conflict'
+    ));
+    expect(confirm).not.toHaveClass('ant-btn-loading');
+    expect(onCancel).not.toHaveBeenCalled();
+    expect(refresh).not.toHaveBeenCalled();
+    messageError.mockRestore();
+});
+
 test('persists a selected built-in template before creating the graph', async 
() => {
     api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
     api.manage.addSchema.mockResolvedValue({status: 200});
@@ -419,3 +631,77 @@ test('does not leak an old graph export failure into the 
next graph', async () =
     expect(screen.getByRole('button', {name: 'graph.schema_view.export'}))
         .not.toHaveClass('ant-btn-loading');
 });
+
+test('creates the graph before importing a selected example dataset', async () 
=> {
+    api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
+    api.manage.addGraph.mockResolvedValue({status: 200, data: {name: 
'demo_hlm'}});
+    api.manage.loadSampleGraph.mockResolvedValue({
+        status: 200, data: {vertices: 14, edges: 15},
+    });
+    const onCancel = jest.fn();
+
+    render(
+        <EditLayer
+            visible
+            onCancel={onCancel}
+            refresh={jest.fn()}
+            graphspace='DEFAULT'
+        />
+    );
+
+    await userEvent.type(
+        screen.getByPlaceholderText('graph.form.name_placeholder'),
+        'demo_hlm'
+    );
+    await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled());
+    await userEvent.click(screen.getByRole('combobox'));
+    await 
userEvent.click(screen.getByText('schema_template.builtin.people_network'));
+    await userEvent.click(screen.getByRole('radio', {
+        name: 'graph.form.sample_hlm',
+    }));
+    fireEvent.click(document.querySelector('.ant-modal-footer 
.ant-btn-primary'));
+
+    await waitFor(() => expect(api.manage.addGraph).toHaveBeenCalled());
+    expect(api.manage.addGraph).toHaveBeenCalledWith(
+        'DEFAULT',
+        expect.not.objectContaining({schema: 'people_network'})
+    );
+    expect(api.manage.loadSampleGraph).toHaveBeenCalledWith(
+        'DEFAULT',
+        'demo_hlm',
+        'hlm',
+        {suppressBusinessErrorToast: true}
+    );
+    expect(onCancel).toHaveBeenCalled();
+});
+
+test('lets users clear the optional example dataset choice', async () => {
+    api.manage.getSchemaList.mockResolvedValue({status: 200, data: {records: 
[]}});
+    api.manage.addGraph.mockResolvedValue({status: 200, data: {name: 
'empty_graph'}});
+    const onCancel = jest.fn();
+
+    render(
+        <EditLayer
+            visible
+            onCancel={onCancel}
+            refresh={jest.fn()}
+            graphspace='DEFAULT'
+        />
+    );
+
+    await userEvent.type(
+        screen.getByPlaceholderText('graph.form.name_placeholder'),
+        'empty_graph'
+    );
+    await userEvent.click(screen.getByRole('radio', {
+        name: 'graph.form.sample_hlm',
+    }));
+    await userEvent.click(screen.getByRole('radio', {
+        name: 'graph.form.sample_none',
+    }));
+    fireEvent.click(document.querySelector('.ant-modal-footer 
.ant-btn-primary'));
+
+    await waitFor(() => expect(api.manage.addGraph).toHaveBeenCalled());
+    expect(api.manage.loadSampleGraph).not.toHaveBeenCalled();
+    expect(onCancel).toHaveBeenCalled();
+});
diff --git 
a/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js 
b/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js
index 13a4b8ca4..33e1d4783 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Graph/default-card-actions.test.js
@@ -20,6 +20,12 @@ import {fireEvent, render, screen, waitFor, within} from 
'@testing-library/react
 import Graph from './index';
 import * as api from '../../api';
 
+let mockAuthContext;
+
+jest.mock('../../auth/AuthContext', () => ({
+    useAuthContext: () => mockAuthContext,
+}));
+
 jest.mock('react-i18next', () => ({
     useTranslation: () => ({t: key => key}),
 }));
@@ -91,6 +97,9 @@ beforeEach(() => {
     jest.clearAllMocks();
     installMatchMedia();
     sessionStorage.setItem('hubble_config_', JSON.stringify({pd_enabled: 
true}));
+    mockAuthContext = {
+        context: {role: 'SUPERADMIN', scopes: {all_graphspaces: true}},
+    };
     api.manage.getGraphSpace.mockResolvedValue({
         status: 200,
         data: {name: 'space', nickname: 'Space'},
@@ -170,18 +179,66 @@ test('shows clone as unavailable instead of exposing a 
failing action', async ()
     expect(clone.closest('a')).toBeNull();
 });
 
-test('keeps exactly the five requested graph card actions', async () => {
+test('keeps the graph card actions including example datasets', async () => {
     render(<Graph />);
 
     const menu = await screen.findByTestId('graph-card-menu');
-    expect(within(menu).getAllByRole('menuitem')).toHaveLength(5);
+    expect(within(menu).getAllByRole('menuitem')).toHaveLength(8);
     
expect(within(menu).getByText('graph.menu.clear_graph')).toBeInTheDocument();
     
expect(within(menu).getByText('graph.menu.set_default')).toBeInTheDocument();
     expect(within(menu).getByText('common.action.edit')).toBeInTheDocument();
+    
expect(within(menu).getByText('graph.menu.load_hlm_sample')).toBeInTheDocument();
+    
expect(within(menu).getByText('graph.menu.load_rank_sample')).toBeInTheDocument();
+    
expect(within(menu).getByText('graph.menu.load_loader_sample')).toBeInTheDocument();
     expect(within(menu).getByText('common.action.delete')).toBeInTheDocument();
     expect(within(menu).getByText('graph.menu.clone')).toBeInTheDocument();
 });
 
+test('disables example dataset writes for the protected built-in graphspace', 
async () => {
+    api.manage.getGraphList.mockResolvedValue({
+        status: 200,
+        data: {
+            records: [{
+                name: 'protected-graph',
+                nickname: 'Protected graph',
+                graphspace: 'neizhianli',
+                default: false,
+            }],
+            total: 1,
+        },
+    });
+
+    render(<Graph />);
+
+    const menu = await screen.findByTestId('graph-card-menu');
+    [
+        'graph.menu.load_hlm_sample',
+        'graph.menu.load_rank_sample',
+        'graph.menu.load_loader_sample',
+    ].forEach(label => {
+        expect(within(menu).getByText(label).closest('[role="menuitem"]'))
+            .toHaveAttribute('aria-disabled', 'true');
+    });
+});
+
+test('disables example dataset writes without graphspace update permission', 
async () => {
+    mockAuthContext = {
+        context: {role: 'USER', scopes: {admin_graphspaces: []}},
+    };
+
+    render(<Graph />);
+
+    const menu = await screen.findByTestId('graph-card-menu');
+    [
+        'graph.menu.load_hlm_sample',
+        'graph.menu.load_rank_sample',
+        'graph.menu.load_loader_sample',
+    ].forEach(label => {
+        expect(within(menu).getByText(label).closest('[role="menuitem"]'))
+            .toHaveAttribute('aria-disabled', 'true');
+    });
+});
+
 test('places the new-graph card after existing graphs', async () => {
     render(<Graph />);
 
diff --git a/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js 
b/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js
index a07696bfa..770fdbca6 100644
--- a/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js
+++ b/hugegraph-hubble/hubble-fe/src/pages/Graph/index.js
@@ -103,6 +103,7 @@ const Graph = () => {
     const [loading, setLoading] = useState(false);
     const [listUnavailable, setListUnavailable] = useState(false);
     const [clearSelection, setClearSelection] = useState(null);
+    const [sampleLoading, setSampleLoading] = useState('');
     const {graphspace} = useParams();
     const navigate = useNavigate();
     const {context: authContext} = useAuthContext();
@@ -172,6 +173,40 @@ const Graph = () => {
         return api.manage.clearGraph(graphspace, clearSelection.graph);
     }, [clearSelection, graphspace]);
 
+    const loadSample = useCallback((graph, dataset) => {
+        Modal.confirm({
+            title: t(`graph.sample.${dataset}_title`),
+            content: t(`graph.sample.${dataset}_description`, {graph}),
+            okText: t('graph.sample.confirm'),
+            cancelText: t('common.action.cancel'),
+            onOk: async () => {
+                setSampleLoading(`${graph}:${dataset}`);
+                try {
+                    const res = await api.manage.loadSampleGraph(
+                        graphspace,
+                        graph,
+                        dataset,
+                        {suppressBusinessErrorToast: true}
+                    );
+                    if (res.status !== 200) {
+                        throw new Error(res.message || 
t('graph.sample.failed'));
+                    }
+                    message.success(t('graph.sample.success', {
+                        vertices: res.data.vertices,
+                        edges: res.data.edges,
+                    }));
+                    setRefresh(value => !value);
+                }
+                catch (error) {
+                    message.error(error.message || t('graph.sample.failed'));
+                }
+                finally {
+                    setSampleLoading('');
+                }
+            },
+        });
+    }, [graphspace, t]);
+
     const showSchema = useCallback(graph => {
         setViewLayer(true);
         setSelectGraph(graph);
@@ -421,57 +456,84 @@ const Graph = () => {
         },
     ];
 
-    const getMenus = item => [
-        {
-            key: 'clear',
-            danger: true,
-            disabled: item.default,
-            label: item.default
-                ? <span 
className={style.disable}>{t('graph.menu.clear_graph')}</span>
-                : t('graph.menu.clear_graph'),
-            onClick: item.default ? undefined : () => clearGraph(item.name),
-        },
-        graphDefaultMutationEnabled && {
-            key: 'default',
-            disabled: item.default,
-            label: item.default
-                ? <span 
className={style.disable}>{t('graph.menu.set_default')}</span>
-                : t('graph.menu.set_default'),
-            onClick: item.default ? undefined : () => 
handleSetDefault(item.name),
-        },
-        {
-            key: 'edit',
-            disabled: item.graphspace === 'neizhianli',
-            label: item.graphspace === 'neizhianli'
-                ? <span 
className={style.disable}>{t('common.action.edit')}</span>
-                : t('common.action.edit'),
-            onClick: item.graphspace === 'neizhianli'
-                ? undefined : () => editGraph(item.name),
-        },
-        {
-            key: 'delete',
-            danger: true,
-            disabled: item.graphspace === 'neizhianli',
-            label: item.graphspace === 'neizhianli'
-                ? <span 
className={style.disable}>{t('common.action.delete')}</span>
-                : t('common.action.delete'),
-            onClick: item.graphspace === 'neizhianli'
-                ? undefined : () => deleteGraph(item.name),
-        },
-        graphCreateEnabled && {
-            key: 'clone',
-            disabled: true,
-            label: (
-                <Tooltip title={t('graph.clone.unavailable')}>
-                    <span
-                        aria-label={`${t('graph.menu.clone')}: 
${t('graph.clone.unavailable')}`}
-                    >
-                        {t('graph.menu.clone')}
-                    </span>
-                </Tooltip>
-            ),
-        },
-    ].filter(Boolean);
+    const getMenus = item => {
+        const itemGraphspace = item.graphspace || graphspace;
+        const immutable = itemGraphspace === 'neizhianli';
+        const readOnly = !canUpdateGraphspace(itemGraphspace);
+        const sampleDisabled = immutable || readOnly || Boolean(sampleLoading);
+        return [
+            {
+                key: 'clear',
+                danger: true,
+                disabled: item.default,
+                label: item.default
+                    ? <span 
className={style.disable}>{t('graph.menu.clear_graph')}</span>
+                    : t('graph.menu.clear_graph'),
+                onClick: item.default ? undefined : () => 
clearGraph(item.name),
+            },
+            graphDefaultMutationEnabled && {
+                key: 'default',
+                disabled: item.default,
+                label: item.default
+                    ? <span 
className={style.disable}>{t('graph.menu.set_default')}</span>
+                    : t('graph.menu.set_default'),
+                onClick: item.default ? undefined : () => 
handleSetDefault(item.name),
+            },
+            {
+                key: 'edit',
+                disabled: immutable,
+                label: immutable
+                    ? <span 
className={style.disable}>{t('common.action.edit')}</span>
+                    : t('common.action.edit'),
+                onClick: immutable
+                    ? undefined : () => editGraph(item.name),
+            },
+            {
+                key: 'sample-hlm',
+                disabled: sampleDisabled,
+                label: sampleLoading === `${item.name}:hlm`
+                    ? t('graph.sample.loading') : 
t('graph.menu.load_hlm_sample'),
+                onClick: sampleDisabled ? undefined : () => 
loadSample(item.name, 'hlm'),
+            },
+            {
+                key: 'sample-rank',
+                disabled: sampleDisabled,
+                label: sampleLoading === `${item.name}:rank`
+                    ? t('graph.sample.loading') : 
t('graph.menu.load_rank_sample'),
+                onClick: sampleDisabled ? undefined : () => 
loadSample(item.name, 'rank'),
+            },
+            {
+                key: 'sample-loader',
+                disabled: sampleDisabled,
+                label: sampleLoading === `${item.name}:loader`
+                    ? t('graph.sample.loading') : 
t('graph.menu.load_loader_sample'),
+                onClick: sampleDisabled ? undefined : () => 
loadSample(item.name, 'loader'),
+            },
+            {
+                key: 'delete',
+                danger: true,
+                disabled: immutable,
+                label: immutable
+                    ? <span 
className={style.disable}>{t('common.action.delete')}</span>
+                    : t('common.action.delete'),
+                onClick: immutable
+                    ? undefined : () => deleteGraph(item.name),
+            },
+            graphCreateEnabled && {
+                key: 'clone',
+                disabled: true,
+                label: (
+                    <Tooltip title={t('graph.clone.unavailable')}>
+                        <span
+                            aria-label={`${t('graph.menu.clone')}: 
${t('graph.clone.unavailable')}`}
+                        >
+                            {t('graph.menu.clone')}
+                        </span>
+                    </Tooltip>
+                ),
+            },
+        ].filter(Boolean);
+    };
 
     useEffect(() => {
         setLoading(true);

Reply via email to