Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[VisBuilder-Next] Migrate legacy visualizations to visbuilder by mapping the url #7529

Merged
merged 4 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/7529.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- [VisBuilder-Next] Migration of legacy visualizations to VisBuilder by constructing the URL. ([#7529](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7529))
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import { EditActionDropdown, VisualizationItem } from './edit_action_dropdown';
import {
EuiContextMenuPanel,
EuiIcon,
EuiPopover,
EuiContextMenuItem,
EuiConfirmModal,
} from '@elastic/eui';
import { useOpenSearchDashboards } from '../context';

// Mock the useOpenSearchDashboards hook
jest.mock('../context', () => ({
useOpenSearchDashboards: jest.fn(),
}));

describe('EditActionDropdown', () => {
let component: any;
const mockEditItem = jest.fn();
const mockVisbuilderEditItem = jest.fn();
const mockOpenModal = jest.fn();
const mockCloseModal = jest.fn();

const defaultItem: VisualizationItem = {
typeTitle: 'Area',
id: '1',
version: 1,
};

const mockOverlays = {
openModal: mockOpenModal.mockReturnValue({ close: mockCloseModal }),
};

beforeEach(() => {
// Cast the mocked function to any to avoid TypeScript errors
(useOpenSearchDashboards as jest.Mock).mockReturnValue({ overlays: mockOverlays });

component = mount(
<EditActionDropdown
item={defaultItem}
editItem={mockEditItem}
visbuilderEditItem={mockVisbuilderEditItem}
/>
);
});

afterEach(() => {
jest.clearAllMocks();
});

it('renders the edit icon', () => {
expect(component.find(EuiIcon).first().prop('type')).toBe('pencil');
});

it('opens the popover when icon is clicked', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
expect(component.find(EuiPopover).prop('isOpen')).toBe(true);
});

it('renders context menu panel with correct options for VisBuilder compatible item', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
const contextMenuPanel = component.find(EuiContextMenuPanel);
expect(contextMenuPanel.exists()).toBe(true);
expect(contextMenuPanel.prop('items')).toHaveLength(2);
expect(contextMenuPanel.find(EuiContextMenuItem).at(0).text()).toBe('Edit');
expect(contextMenuPanel.find(EuiContextMenuItem).at(1).text()).toBe('Import to VisBuilder');
});

it('does not render VisBuilder option for incompatible item', () => {
const incompatibleItem: VisualizationItem = {
typeTitle: 'Pie',
id: '2',
version: 2,
};
component.setProps({ item: incompatibleItem });
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
const contextMenuPanel = component.find(EuiContextMenuPanel);
expect(contextMenuPanel.prop('items')).toHaveLength(1);
expect(contextMenuPanel.find(EuiContextMenuItem).at(0).text()).toBe('Edit');
});

it('calls editItem when Edit option is clicked', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
act(() => {
component.find(EuiContextMenuItem).at(0).simulate('click');
});
expect(mockEditItem).toHaveBeenCalledWith(defaultItem);
});

it('opens a confirmation modal when Import to VisBuilder option is clicked', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
act(() => {
component.find(EuiContextMenuItem).at(1).simulate('click');
});
expect(mockOpenModal).toHaveBeenCalled();
expect(mockOpenModal.mock.calls[0][0].type).toBe(EuiConfirmModal);
});

it('calls visbuilderEditItem when confirmation modal is confirmed', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
act(() => {
component.find(EuiContextMenuItem).at(1).simulate('click');
});

const modalProps = mockOpenModal.mock.calls[0][0].props;
act(() => {
modalProps.onConfirm();
});

expect(mockVisbuilderEditItem).toHaveBeenCalledWith(defaultItem);
expect(mockCloseModal).toHaveBeenCalled();
});

it('does not call visbuilderEditItem when confirmation modal is cancelled', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
act(() => {
component.find(EuiContextMenuItem).at(1).simulate('click');
});

const modalProps = mockOpenModal.mock.calls[0][0].props;
act(() => {
modalProps.onCancel();
});

expect(mockVisbuilderEditItem).not.toHaveBeenCalled();
expect(mockCloseModal).toHaveBeenCalled();
});

it('closes the popover after an action is selected', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
act(() => {
component.find(EuiContextMenuItem).at(0).simulate('click');
});
component.update();
expect(component.find(EuiPopover).prop('isOpen')).toBe(false);
});

it('sets correct props on EuiPopover', () => {
const popover = component.find(EuiPopover);
expect(popover.prop('panelPaddingSize')).toBe('none');
expect(popover.prop('anchorPosition')).toBe('downLeft');
expect(popover.prop('initialFocus')).toBe('none');
});

it('sets correct props on EuiContextMenuPanel', () => {
act(() => {
component.find(EuiIcon).first().simulate('click');
});
component.update();
const panel = component.find(EuiContextMenuPanel);
expect(panel.prop('size')).toBe('s');
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some linting errors on this file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, fixed

Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from 'react';
import {
EuiIcon,
EuiPopover,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiText,
EuiConfirmModal,
} from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { useOpenSearchDashboards } from '../context';

// TODO: include more types once VisBuilder supports more visualization types
const types = ['Area', 'Vertical Bar', 'Line', 'Metric', 'Table'];

export interface VisualizationItem {
typeTitle: string;
id?: string;
version?: number;
overlays?: any;
}

interface EditActionDropdownProps {
item: VisualizationItem;
editItem?(item: VisualizationItem): void;
visbuilderEditItem?(item: VisualizationItem): void;
}

export const EditActionDropdown: React.FC<EditActionDropdownProps> = ({
item,
editItem,
visbuilderEditItem,
}) => {
const { overlays } = useOpenSearchDashboards();
const [isPopoverOpen, setPopoverOpen] = useState(false);
const onButtonClick = () => {
setPopoverOpen(!isPopoverOpen);
};

const closePopover = () => {
setPopoverOpen(false);
};
// A saved object will only have the 'Import to VisBuilder' option
// if it is a VisBuilder-compatible type and its version is <= 1.
const typeName = item.typeTitle;
const itemVersion = item.version;
const isVisBuilderCompatible =
types.includes(typeName) && itemVersion !== undefined && itemVersion <= 1;

const handleImportToVisBuilder = () => {
closePopover(); // Close the popover first

const modal = overlays.openModal(
<EuiConfirmModal
title="Partial Import"
onCancel={() => modal.close()}
onConfirm={async () => {
modal.close();
// Call visbuilderEditItem with the item
if (visbuilderEditItem) {
await visbuilderEditItem(item);
}
}}
cancelButtonText="Cancel"
confirmButtonText="Import"
>
<EuiText>
<p>
{' '}
Note that not all settings have been migrated from the original visualization. More will
be included as VisBuilder supports additional settings.{' '}
</p>
</EuiText>
</EuiConfirmModal>
);
};

const items = [
<EuiContextMenuItem
key="edit"
icon={<EuiIcon type="pencil" />}
onClick={() => {
closePopover();
editItem?.(item);
}}
data-test-subj="dashboardEditDashboard"
>
{i18n.translate('editActionDropdown.edit', { defaultMessage: 'Edit' })}
</EuiContextMenuItem>,
];
if (isVisBuilderCompatible) {
items.push(
<EuiContextMenuItem
key="importToVisBuilder"
icon={<EuiIcon type="importAction" />}
onClick={handleImportToVisBuilder}
data-test-subj="dashboardImportToVisBuilder"
>
{i18n.translate('editActionDropdown.importToVisBuilder', {
defaultMessage: 'Import to VisBuilder',
})}
</EuiContextMenuItem>
);
}

return (
<EuiPopover
button={<EuiIcon type="pencil" onClick={onButtonClick} data-test-subj="dashboardEditBtn" />}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
initialFocus="none"
>
<EuiContextMenuPanel items={items} size="s" />
</EuiPopover>
);
};
Loading
Loading