diff --git a/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap b/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap
new file mode 100644
index 000000000000..33d770dbaf23
--- /dev/null
+++ b/src/core/public/overlays/sidecar/__snapshots__/sidecar_service.test.tsx.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SidecarService SidecarRef#close() can be called multiple times on the same SidecarRef 1`] = `
+Array [
+ Array [
+
,
+ ],
+]
+`;
+
+exports[`SidecarService openSidecar() renders a sidecar to the DOM 1`] = `"
Sidecar content
"`;
+
+exports[`SidecarService openSidecar() with a currently active sidecar replaces the current sidecar with a new one 1`] = `"
Sidecar content 2
"`;
diff --git a/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap
new file mode 100644
index 000000000000..9d46091330c3
--- /dev/null
+++ b/src/core/public/overlays/sidecar/components/__snapshots__/resizable_button.test.tsx.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`is rendered 1`] = `
+
+`;
+
+exports[`it should be horizontal when docked mode is right 1`] = `
+
+`;
+
+exports[`it should be vertical when docked mode is takeover 1`] = `
+
+`;
+
+exports[`it should emit onResize with min size when drag if new size is below the minimum 1`] = `
+
+`;
+
+exports[`it should emit onResize with new flyout size when drag and horizontal 1`] = `
+
+`;
+
+exports[`it should emit onResize with new flyout size when drag and vertical 1`] = `
+
+`;
diff --git a/src/core/public/overlays/sidecar/resizable_button.scss b/src/core/public/overlays/sidecar/components/resizable_button.scss
similarity index 100%
rename from src/core/public/overlays/sidecar/resizable_button.scss
rename to src/core/public/overlays/sidecar/components/resizable_button.scss
diff --git a/src/core/public/overlays/sidecar/components/resizable_button.test.tsx b/src/core/public/overlays/sidecar/components/resizable_button.test.tsx
new file mode 100644
index 000000000000..addd81e4aa9a
--- /dev/null
+++ b/src/core/public/overlays/sidecar/components/resizable_button.test.tsx
@@ -0,0 +1,92 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+
+import { ResizableButton, MIN_SIDECAR_SIZE } from './resizable_button';
+import { shallow } from 'enzyme';
+import { SIDECAR_DOCKED_MODE } from '../sidecar_service';
+
+const storeWindowEvents = () => {
+ const map: Record = {};
+ window.addEventListener = jest.fn().mockImplementation((event: string, cb) => {
+ map[event] = cb;
+ });
+ return map;
+};
+
+const DEFAULT_FLYOUT_SIZE = 460;
+const props = {
+ dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
+ onResize: jest.fn(),
+ flyoutSize: DEFAULT_FLYOUT_SIZE,
+};
+
+test('is rendered', () => {
+ const component = shallow();
+ expect(component).toMatchSnapshot();
+});
+
+test('it should be horizontal when docked mode is right', () => {
+ const component = shallow();
+ expect(component.hasClass('resizableButton--horizontal')).toBe(true);
+ expect(component.hasClass('resizableButton--vertical')).toBe(false);
+ expect(component).toMatchSnapshot();
+});
+
+test('it should be vertical when docked mode is takeover', () => {
+ const newProps = { ...props, dockedMode: SIDECAR_DOCKED_MODE.TAKEOVER };
+ const component = shallow();
+ expect(component.hasClass('resizableButton--vertical')).toBe(true);
+ expect(component.hasClass('resizableButton--horizontal')).toBe(false);
+ expect(component).toMatchSnapshot();
+});
+
+test('it should emit onResize with new flyout size when drag and horizontal', () => {
+ const windowEvents = storeWindowEvents();
+
+ const onResize = jest.fn();
+ const newProps = { ...props, onResize };
+ const component = shallow();
+ const resizer = component.find(`[data-test-subj~="resizableButton"]`).first();
+ expect(onResize).not.toHaveBeenCalled();
+ resizer.simulate('mousedown', { clientX: 0, pageX: 0, pageY: 0 });
+ windowEvents?.mousemove({ clientX: -1000, pageX: 0, pageY: 0 });
+ windowEvents?.mouseup();
+ const newSize = 1000 + DEFAULT_FLYOUT_SIZE;
+ expect(onResize).toHaveBeenCalledWith(newSize);
+ expect(component).toMatchSnapshot();
+});
+
+test('it should emit onResize with new flyout size when drag and vertical', () => {
+ const windowEvents = storeWindowEvents();
+
+ const onResize = jest.fn();
+ const newProps = { ...props, onResize, dockedMode: SIDECAR_DOCKED_MODE.TAKEOVER };
+ const component = shallow();
+ const resizer = component.find(`[data-test-subj~="resizableButton"]`).first();
+ expect(onResize).not.toHaveBeenCalled();
+ resizer.simulate('mousedown', { clientY: 0, pageX: 0, pageY: 0 });
+ windowEvents?.mousemove({ clientY: -1000, pageX: 0, pageY: 0 });
+ windowEvents?.mouseup();
+ const newSize = 1000 + DEFAULT_FLYOUT_SIZE;
+ expect(onResize).toHaveBeenCalledWith(newSize);
+ expect(component).toMatchSnapshot();
+});
+
+test('it should emit onResize with min size when drag if new size is below the minimum', () => {
+ const windowEvents = storeWindowEvents();
+ const onResize = jest.fn();
+ const newProps = { ...props, onResize };
+ const component = shallow();
+ const resizer = component.find(`[data-test-subj~="resizableButton"]`).first();
+ expect(onResize).not.toHaveBeenCalled();
+ resizer.simulate('mousedown', { clientX: 0, pageX: 0, pageY: 0 });
+ windowEvents?.mousemove({ clientX: 1000, pageX: 0, pageY: 0 });
+ windowEvents?.mouseup();
+ const newSize = MIN_SIDECAR_SIZE;
+ expect(onResize).toHaveBeenCalledWith(newSize);
+ expect(component).toMatchSnapshot();
+});
diff --git a/src/core/public/overlays/sidecar/resizable_button.tsx b/src/core/public/overlays/sidecar/components/resizable_button.tsx
similarity index 91%
rename from src/core/public/overlays/sidecar/resizable_button.tsx
rename to src/core/public/overlays/sidecar/components/resizable_button.tsx
index 12d8c4255967..8741581d97e2 100644
--- a/src/core/public/overlays/sidecar/resizable_button.tsx
+++ b/src/core/public/overlays/sidecar/components/resizable_button.tsx
@@ -6,8 +6,8 @@
import React, { useCallback, useRef } from 'react';
import classNames from 'classnames';
import './resizable_button.scss';
-import { getPosition } from './helper';
-import { ISidecarConfig, SIDECAR_DOCKED_MODE } from './sidecar_service';
+import { getPosition } from '../helper';
+import { ISidecarConfig, SIDECAR_DOCKED_MODE } from '../sidecar_service';
interface Props {
onResize: (size: number) => void;
@@ -15,7 +15,7 @@ interface Props {
dockedMode: ISidecarConfig['dockedMode'] | undefined;
}
-const MIN_SIDECAR_SIZE = 350;
+export const MIN_SIDECAR_SIZE = 350;
export const ResizableButton = ({ dockedMode, onResize, flyoutSize }: Props) => {
const isHorizontal = dockedMode !== SIDECAR_DOCKED_MODE.TAKEOVER;
@@ -40,7 +40,7 @@ export const ResizableButton = ({ dockedMode, onResize, flyoutSize }: Props) =>
};
const onMouseMove = (e: MouseEvent | TouchEvent) => {
let offset;
- if (dockedMode === 'left') {
+ if (dockedMode === SIDECAR_DOCKED_MODE.LEFT) {
offset = getPosition(e, isHorizontal) - initialMouseXorY.current;
} else {
offset = initialMouseXorY.current - getPosition(e, isHorizontal);
diff --git a/src/core/public/overlays/sidecar/sidecar_service.scss b/src/core/public/overlays/sidecar/components/sidecar.scss
similarity index 100%
rename from src/core/public/overlays/sidecar/sidecar_service.scss
rename to src/core/public/overlays/sidecar/components/sidecar.scss
diff --git a/src/core/public/overlays/sidecar/sidecar.tsx b/src/core/public/overlays/sidecar/components/sidecar.tsx
similarity index 89%
rename from src/core/public/overlays/sidecar/sidecar.tsx
rename to src/core/public/overlays/sidecar/components/sidecar.tsx
index 196e19fef3bb..94ea231cdf42 100644
--- a/src/core/public/overlays/sidecar/sidecar.tsx
+++ b/src/core/public/overlays/sidecar/components/sidecar.tsx
@@ -7,12 +7,12 @@ import React, { useCallback, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { BehaviorSubject } from 'rxjs';
import classNames from 'classnames';
-import { I18nStart } from '../../i18n';
-import { MountPoint } from '../../types';
-import { MountWrapper } from '../../utils';
-import './sidecar_service.scss';
+import { I18nStart } from '../../../i18n';
+import { MountPoint } from '../../../types';
+import { MountWrapper } from '../../../utils';
+import './sidecar.scss';
import { ResizableButton } from './resizable_button';
-import { ISidecarConfig, OverlaySidecarOpenOptions } from './sidecar_service';
+import { ISidecarConfig, OverlaySidecarOpenOptions } from '../sidecar_service';
interface Props {
sidecarConfig$: BehaviorSubject;
diff --git a/src/core/public/overlays/sidecar/helper.ts b/src/core/public/overlays/sidecar/helper.ts
index 17068b7dce63..6b2fbc2eea4f 100644
--- a/src/core/public/overlays/sidecar/helper.ts
+++ b/src/core/public/overlays/sidecar/helper.ts
@@ -3,7 +3,6 @@
* SPDX-License-Identifier: Apache-2.0
*/
-// import { MouseEvent, TouchEvent } from 'react';
import { ISidecarConfig } from './sidecar_service';
function isMouseEvent(
diff --git a/src/core/public/overlays/sidecar/sidecar_service.test.tsx b/src/core/public/overlays/sidecar/sidecar_service.test.tsx
new file mode 100644
index 000000000000..7229f3589f82
--- /dev/null
+++ b/src/core/public/overlays/sidecar/sidecar_service.test.tsx
@@ -0,0 +1,107 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
+
+import { mount } from 'enzyme';
+import { i18nServiceMock } from '../../i18n/i18n_service.mock';
+import {
+ SidecarService,
+ OverlaySidecarStart,
+ OverlaySidecarOpenOptions,
+ SIDECAR_DOCKED_MODE,
+} from './sidecar_service';
+import { OverlayRef } from '../types';
+
+const i18nMock = i18nServiceMock.createStartContract();
+
+beforeEach(() => {
+ mockReactDomRender.mockClear();
+ mockReactDomUnmount.mockClear();
+});
+
+const mountText = (text: string) => (container: HTMLElement) => {
+ const content = document.createElement('span');
+ content.textContent = text;
+ container.append(content);
+ return () => {};
+};
+
+const getServiceStart = () => {
+ const service = new SidecarService();
+ return service.start({ i18n: i18nMock, targetDomElement: document.createElement('div') });
+};
+
+describe('SidecarService', () => {
+ let sidecar: OverlaySidecarStart;
+ const options: OverlaySidecarOpenOptions = {
+ config: {
+ dockedMode: SIDECAR_DOCKED_MODE.RIGHT,
+ paddingSize: 460,
+ },
+ };
+ beforeEach(() => {
+ sidecar = getServiceStart();
+ });
+
+ describe('openSidecar()', () => {
+ it('renders a sidecar to the DOM', () => {
+ expect(mockReactDomRender).not.toHaveBeenCalled();
+ sidecar.open(mountText('Sidecar content'), options);
+ const content = mount(mockReactDomRender.mock.calls[0][0]);
+ expect(content.html()).toMatchSnapshot();
+ });
+ describe('with a currently active sidecar', () => {
+ let ref1: OverlayRef;
+ beforeEach(() => {
+ ref1 = sidecar.open(mountText('Sidecar content'), options);
+ });
+ it('replaces the current sidecar with a new one', () => {
+ sidecar.open(mountText('Sidecar content 2'), options);
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ const modalContent = mount(mockReactDomRender.mock.calls[1][0]);
+ expect(modalContent.html()).toMatchSnapshot();
+ expect(() => ref1.close()).not.toThrowError();
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ });
+ it('resolves onClose on the previous ref', async () => {
+ const onCloseComplete = jest.fn();
+ ref1.onClose.then(onCloseComplete);
+ sidecar.open(mountText('Sidecar content 2'), options);
+ await ref1.onClose;
+ expect(onCloseComplete).toBeCalledTimes(1);
+ });
+ });
+ });
+ describe('SidecarRef#close()', () => {
+ it('resolves the onClose Promise', async () => {
+ const ref = sidecar.open(mountText('Sidecar content'), options);
+
+ const onCloseComplete = jest.fn();
+ ref.onClose.then(onCloseComplete);
+ await ref.close();
+ await ref.close();
+ expect(onCloseComplete).toHaveBeenCalledTimes(1);
+ });
+ it('can be called multiple times on the same SidecarRef', async () => {
+ const ref = sidecar.open(mountText('Sidecar content'), options);
+ expect(mockReactDomUnmount).not.toHaveBeenCalled();
+ await ref.close();
+ expect(mockReactDomUnmount.mock.calls).toMatchSnapshot();
+ await ref.close();
+ expect(mockReactDomUnmount).toHaveBeenCalledTimes(1);
+ });
+ it("on a stale Sidecar doesn't affect the active sidecar", async () => {
+ const ref1 = sidecar.open(mountText('Sidecar content 1'), options);
+ const ref2 = sidecar.open(mountText('Sidecar content 2'), options);
+ const onCloseComplete = jest.fn();
+ ref2.onClose.then(onCloseComplete);
+ mockReactDomUnmount.mockClear();
+ await ref1.close();
+ expect(mockReactDomUnmount).toBeCalledTimes(0);
+ expect(onCloseComplete).toBeCalledTimes(0);
+ });
+ });
+});
diff --git a/src/core/public/overlays/sidecar/sidecar_service.tsx b/src/core/public/overlays/sidecar/sidecar_service.tsx
index 5d652ced40e8..adbd0d327a92 100644
--- a/src/core/public/overlays/sidecar/sidecar_service.tsx
+++ b/src/core/public/overlays/sidecar/sidecar_service.tsx
@@ -11,9 +11,8 @@ import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
import { I18nStart } from '../../i18n';
import { MountPoint } from '../../types';
-import './sidecar_service.scss';
import { OverlayRef } from '../types';
-import { Sidecar } from './sidecar';
+import { Sidecar } from './components/sidecar';
/**
* A SidecarRef is a reference to an opened sidecar panel. It offers methods to
* close the sidecar panel again. If you open a sidecar panel you should make
@@ -68,7 +67,7 @@ export interface OverlaySidecarStart {
* @param options {@link OverlaySidecarOpenOptions} - options for the sidecar
* @return {@link SidecarRef} A reference to the opened sidecar panel.
*/
- open(mount: MountPoint, options?: OverlaySidecarOpenOptions): SidecarRef;
+ open(mount: MountPoint, options: OverlaySidecarOpenOptions): SidecarRef;
/**
* Override the default support config
@@ -147,6 +146,7 @@ export class SidecarService {
if (this.activeSidecar) {
setSidecarConfig({ paddingSize: 0 });
this.activeSidecar.close();
+ this.cleanupDom();
}
const sidecar = new SidecarRef();