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

[Workspace] Refactor 'Associate data sources' to support multi-select #7696

Closed
Show file tree
Hide file tree
Changes from 7 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/7696.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Refactor Associate data sources to support multi-select ([#7696](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/7696))
Original file line number Diff line number Diff line change
Expand Up @@ -274,39 +274,33 @@ describe('WorkspaceCreator', () => {
});

it('create workspace with customized selected dataSources', async () => {
const { getByTestId, getByTitle, getByText } = render(
<WorkspaceCreator isDashboardAdmin={true} />
);
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByTestId('workspaceUseCase-observability'));
fireEvent.click(getByTestId('workspaceForm-select-dataSource-addNew'));
fireEvent.click(getByTestId('workspaceForm-select-dataSource-comboBox'));
await act(() => {
fireEvent.click(getByText('Select'));
});
fireEvent.click(getByTitle(dataSourcesList[0].title));
const { getByTestId, getByText } = render(<WorkspaceCreator isDashboardAdmin={true} />);

fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton'));
expect(workspaceClientCreate).toHaveBeenCalledWith(
expect.objectContaining({
name: 'test workspace name',
}),
{
dataSources: ['id1'],
permissions: {
library_write: {
users: ['%me%'],
},
write: {
users: ['%me%'],
waitFor(() => {
const nameInput = getByTestId('workspaceForm-workspaceDetails-nameInputText');
fireEvent.input(nameInput, {
target: { value: 'test workspace name' },
});
fireEvent.click(getByTestId('workspaceUseCase-observability'));
fireEvent.click(getByText(dataSourcesList[0].title));

fireEvent.click(getByTestId('workspaceForm-bottomBar-createButton'));
expect(workspaceClientCreate).toHaveBeenCalledWith(
expect.objectContaining({
name: 'test workspace name',
}),
{
dataSources: ['id1'],
permissions: {
library_write: {
users: ['%me%'],
},
write: {
users: ['%me%'],
},
},
},
}
);
await waitFor(() => {
}
);
expect(notificationToastsAddSuccess).toHaveBeenCalled();
});
expect(notificationToastsAddDanger).not.toHaveBeenCalled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,6 @@ describe('WorkspaceUpdater', () => {
fireEvent.click(getByTestId('workspaceUseCase-observability'));
fireEvent.click(getByTestId('workspaceUseCase-analytics'));

act(() => {
fireEvent.click(getAllByLabelText('Delete data source')[0]);
});

fireEvent.click(getByTestId('workspaceForm-bottomBar-updateButton'));
expect(workspaceClientUpdate).toHaveBeenCalledWith(
expect.any(String),
Expand All @@ -245,7 +241,7 @@ describe('WorkspaceUpdater', () => {
users: ['foo'],
},
},
dataSources: ['id2'],
dataSources: ['id1', 'id2'],
}
);
await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const workspaceUseCaseTitle = i18n.translate('workspace.form.workspaceUse
});

export const selectDataSourceTitle = i18n.translate('workspace.form.selectDataSource.title', {
defaultMessage: 'Associate data source',
defaultMessage: 'Associate data sources',
});

export const usersAndPermissionsTitle = i18n.translate('workspace.form.usersAndPermissions.title', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import React from 'react';
import { fireEvent, render, act } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { SelectDataSourcePanel, SelectDataSourcePanelProps } from './select_data_source_panel';
import { coreMock } from '../../../../../core/public/mocks';

Expand Down Expand Up @@ -39,51 +39,50 @@ const setup = ({
};

describe('SelectDataSourcePanel', () => {
it('should render consistent data sources when selected data sources passed', () => {
const { getByText } = setup({ selectedDataSources: dataSources });
const originalOffsetHeight = Object.getOwnPropertyDescriptor(
HTMLElement.prototype,
'offsetHeight'
);
const originalOffsetWidth = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetWidth');
beforeEach(() => {
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
configurable: true,
value: 600,
});
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
configurable: true,
value: 600,
});
});

expect(getByText(dataSources[0].title)).toBeInTheDocument();
expect(getByText(dataSources[1].title)).toBeInTheDocument();
afterEach(() => {
Object.defineProperty(
HTMLElement.prototype,
'offsetHeight',
originalOffsetHeight as PropertyDescriptor
);
Object.defineProperty(
HTMLElement.prototype,
'offsetWidth',
originalOffsetWidth as PropertyDescriptor
);
});

it('should call onChange when clicking add new data source button', () => {
const onChangeMock = jest.fn();
const { getByTestId } = setup({ onChange: onChangeMock });
it('should render consistent data sources when selected data sources passed', async () => {
const { getByText } = await setup({ selectedDataSources: [] });

expect(onChangeMock).not.toHaveBeenCalled();
fireEvent.click(getByTestId('workspaceForm-select-dataSource-addNew'));
expect(onChangeMock).toHaveBeenCalledWith([
{
id: '',
title: '',
},
]);
expect(getByText(dataSources[0].title)).toBeInTheDocument();
expect(getByText(dataSources[1].title)).toBeInTheDocument();
});

it('should call onChange when updating selected data sources in combo box', async () => {
it('should call onChange when updating selected data sources in selectable', async () => {
const onChangeMock = jest.fn();
const { getByTitle, getByText } = setup({
const { getByTitle } = await setup({
onChange: onChangeMock,
selectedDataSources: [{ id: '', title: '' }],
selectedDataSources: [],
});
expect(onChangeMock).not.toHaveBeenCalled();
await act(() => {
fireEvent.click(getByText('Select'));
});
fireEvent.click(getByTitle(dataSources[0].title));
expect(onChangeMock).toHaveBeenCalledWith([{ id: 'id1', title: 'title1' }]);
});

it('should call onChange when deleting selected data source', async () => {
const onChangeMock = jest.fn();
const { getByLabelText } = setup({
onChange: onChangeMock,
selectedDataSources: [{ id: '', title: '' }],
});
expect(onChangeMock).not.toHaveBeenCalled();
await act(() => {
fireEvent.click(getByLabelText('Delete data source'));
});
expect(onChangeMock).toHaveBeenCalledWith([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,7 @@
*/

import React, { useCallback, useEffect, useState } from 'react';
import {
EuiSmallButton,
EuiCompressedFormRow,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiButtonIcon,
EuiCompressedComboBox,
EuiComboBoxOptionOption,
EuiFormLabel,
} from '@elastic/eui';
import { EuiSpacer, EuiFormLabel, EuiSelectable, EuiText, EuiSelectableOption } from '@elastic/eui';
import { i18n } from '@osd/i18n';
import { SavedObjectsStart } from '../../../../../core/public';
import { getDataSourcesList } from '../../utils';
Expand All @@ -34,118 +24,61 @@ export const SelectDataSourcePanel = ({
selectedDataSources,
savedObjects,
}: SelectDataSourcePanelProps) => {
const [dataSourcesOptions, setDataSourcesOptions] = useState<EuiComboBoxOptionOption[]>([]);
const [dataSourcesOptions, setDataSourcesOptions] = useState<EuiSelectableOption[]>([]);
useEffect(() => {
if (!savedObjects) return;
getDataSourcesList(savedObjects.client, ['*']).then((result) => {
const options = result.map(({ title, id }) => ({
label: title,
value: id,
}));

setDataSourcesOptions(options);
});
}, [savedObjects, setDataSourcesOptions]);
const handleAddNewOne = useCallback(() => {
onChange?.([
...selectedDataSources,
{
title: '',
id: '',
},
]);
}, [onChange, selectedDataSources]);

const handleSelect = useCallback(
(selectedOptions, index) => {
const newOption = selectedOptions[0]
? // Select new data source
{
title: selectedOptions[0].label,
id: selectedOptions[0].value,
}
: // Click reset button
{
title: '',
id: '',
};
const newSelectedOptions = [...selectedDataSources];
newSelectedOptions.splice(index, 1, newOption);

onChange(newSelectedOptions);
},
[onChange, selectedDataSources]
);

const handleDelete = useCallback(
(index) => {
const newSelectedOptions = [...selectedDataSources];
newSelectedOptions.splice(index, 1);

onChange(newSelectedOptions);
(options) => {
setDataSourcesOptions(options);
const selectedOptions = [];
for (const option of options) {
if (option.checked === 'on')
selectedOptions.push({ title: option.label, id: option.value });
}
onChange(selectedOptions);
},
[onChange, selectedDataSources]
[onChange]
);

return (
<div>
<EuiFormLabel>
{i18n.translate('workspace.form.selectDataSource.subTitle', {
defaultMessage: 'Data source',
})}
<EuiText size="xs">
{i18n.translate('workspace.form.selectDataSource.subTitle', {
defaultMessage: 'Add data sources that will be available in the workspace',
})}
</EuiText>
</EuiFormLabel>
<EuiSpacer size="s" />
{selectedDataSources.map(({ id, title }, index) => (
<EuiCompressedFormRow
key={index}
isInvalid={!!errors?.[index]}
error={errors?.[index]?.message}
fullWidth
>
<EuiFlexGroup alignItems="flexEnd" gutterSize="m">
<EuiFlexItem style={{ maxWidth: 400 }}>
<EuiCompressedComboBox
data-test-subj="workspaceForm-select-dataSource-comboBox"
singleSelection
options={dataSourcesOptions}
selectedOptions={
id
? [
{
label: title,
value: id,
},
]
: []
}
onChange={(selectedOptions) => handleSelect(selectedOptions, index)}
placeholder="Select"
/>
</EuiFlexItem>
<EuiFlexItem style={{ maxWidth: 332 }}>
<EuiButtonIcon
color="danger"
aria-label="Delete data source"
iconType="trash"
display="empty"
size="m"
onClick={() => handleDelete(index)}
isDisabled={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiCompressedFormRow>
))}

<EuiSmallButton
fill
fullWidth={false}
onClick={handleAddNewOne}
data-test-subj={`workspaceForm-select-dataSource-addNew`}
<EuiSpacer size="m" />
<EuiSelectable
style={{ maxWidth: 400 }}
searchable
searchProps={{
placeholder: i18n.translate('workspace.form.selectDataSource.searchBar', {
defaultMessage: 'Search',
}),
}}
listProps={{ bordered: true, rowHeight: 32, showIcons: true }}
options={dataSourcesOptions}
onChange={handleSelect}
>
{i18n.translate('workspace.form.selectDataSourcePanel.addNew', {
defaultMessage: 'Add New',
})}
</EuiSmallButton>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,17 @@ describe('WorkspaceForm', () => {
it('should enable data source panel for dashboard admin and when data source is enabled', () => {
const { getByText } = setup(true, mockDataSourceManagementSetup);

expect(getByText('Associate data source')).toBeInTheDocument();
expect(getByText('Associate data sources')).toBeInTheDocument();
});

it('should not display data source panel for non dashboard admin', () => {
const { queryByText } = setup(false, mockDataSourceManagementSetup);

expect(queryByText('Associate data source')).not.toBeInTheDocument();
expect(queryByText('Associate data sources')).not.toBeInTheDocument();
});
it('should not display data source panel when data source is disabled', () => {
const { queryByText } = setup(true, undefined);

expect(queryByText('Associate data source')).not.toBeInTheDocument();
expect(queryByText('Associate data sources')).not.toBeInTheDocument();
});
});
Loading