Skip to content

Commit

Permalink
[VisBuilder-Next] Migrate legacy visualizations to visbuilder by mapp…
Browse files Browse the repository at this point in the history
…ing the url (opensearch-project#7529)

* migration of visualization objects to visbuilder
Signed-off-by: Shey Gao <[email protected]>

* update layover

Signed-off-by: Shey Gao <[email protected]>

* fixed a bug

Signed-off-by: Shey Gao <[email protected]>

* Changeset file for PR opensearch-project#7529 created/updated

---------

Signed-off-by: Shey Gao <[email protected]>
Co-authored-by: Shey Gao <[email protected]>
Co-authored-by: opensearch-changeset-bot[bot] <154024398+opensearch-changeset-bot[bot]@users.noreply.github.com>
(cherry picked from commit 2313372)
  • Loading branch information
SheyGao authored and Shey Gao committed Aug 23, 2024
1 parent 90ffd77 commit 5807d52
Show file tree
Hide file tree
Showing 16 changed files with 513 additions and 31 deletions.
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');
});
});
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

0 comments on commit 5807d52

Please sign in to comment.