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

[Dashboard] [Controls] Refactor options list to align with new code standards #138336

Merged
merged 11 commits into from
Aug 12, 2022
47 changes: 47 additions & 0 deletions src/plugins/controls/common/control_group/mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getDefaultControlGroupInput } from '..';
import { ControlGroupInput } from './types';

export const mockControlGroupInput = (partial?: Partial<ControlGroupInput>): ControlGroupInput => ({
id: 'mocked_control_group',
...getDefaultControlGroupInput(),
...{
panels: {
control1: {
order: 0,
width: 'medium',
grow: true,
type: 'mockedOptionsList',
explicitInput: {
id: 'control1',
},
},
control2: {
order: 1,
width: 'large',
grow: true,
type: 'mockedRangeSlider',
explicitInput: {
id: 'control2',
},
},
control3: {
order: 2,
width: 'small',
grow: true,
type: 'mockedOptionsList',
explicitInput: {
id: 'control3',
},
},
},
},
...(partial ?? {}),
});
5 changes: 1 addition & 4 deletions src/plugins/controls/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ export {
} from './control_group/control_group_constants';

// Control Type exports
export {
OPTIONS_LIST_CONTROL,
type OptionsListEmbeddableInput,
} from './control_types/options_list/types';
export { OPTIONS_LIST_CONTROL, type OptionsListEmbeddableInput } from './options_list/types';
export {
type RangeSliderEmbeddableInput,
RANGE_SLIDER_CONTROL,
Expand Down
41 changes: 2 additions & 39 deletions src/plugins/controls/common/mocks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,5 @@
* Side Public License, v 1.
*/

import { getDefaultControlGroupInput } from '.';
import { ControlGroupInput } from './control_group/types';

export const mockControlGroupInput = (partial?: Partial<ControlGroupInput>): ControlGroupInput => ({
id: 'mocked_control_group',
...getDefaultControlGroupInput(),
...{
panels: {
control1: {
order: 0,
width: 'medium',
grow: true,
type: 'mockedOptionsList',
explicitInput: {
id: 'control1',
},
},
control2: {
order: 1,
width: 'large',
grow: true,
type: 'mockedRangeSlider',
explicitInput: {
id: 'control2',
},
},
control3: {
order: 2,
width: 'small',
grow: true,
type: 'mockedOptionsList',
explicitInput: {
id: 'control3',
},
},
},
},
...(partial ?? {}),
});
export * from './control_group/mocks';
export * from './options_list/mocks';
63 changes: 63 additions & 0 deletions src/plugins/controls/common/options_list/mocks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { ReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public/redux_embeddables/types';
import { ControlOutput } from '../../public/types';
import {
OptionsListComponentState,
OptionsListEmbeddableInput,
OptionsListReduxState,
} from '../../public/options_list/types';
import { optionsListReducers } from '../../public/options_list/options_list_reducers';

const mockOptionsListComponentState = {
field: undefined,
totalCardinality: 0,
availableOptions: ['woof', 'bark', 'meow', 'quack', 'moo'],
invalidSelections: [],
validSelections: [],
searchString: '',
} as OptionsListComponentState;

const mockOptionsListEmbeddableInput = {
id: 'sample options list',
fieldName: 'sample field',
dataViewId: 'sample id',
selectedOptions: [],
runPastTimeout: false,
singleSelect: false,
} as OptionsListEmbeddableInput;

const mockOptionsListOutput = {
loading: false,
} as ControlOutput;

export const mockOptionsListContext = (
partialState?: Partial<OptionsListReduxState>
): ReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers> => {
const mockReduxState = {
componentState: {
...mockOptionsListComponentState,
...partialState?.componentState,
},
explicitInput: {
...mockOptionsListEmbeddableInput,
...partialState?.explicitInput,
},
output: {
...mockOptionsListOutput,
...partialState?.output,
},
} as OptionsListReduxState;

return {
actions: {},
useEmbeddableDispatch: () => {},
useEmbeddableSelector: (selector: any) => selector(mockReduxState),
} as unknown as ReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import type { Filter, Query, BoolQuery, TimeRange } from '@kbn/es-query';
import { FieldSpec, DataView } from '@kbn/data-views-plugin/common';

import { DataControlInput } from '../../types';
import { DataControlInput } from '../types';

export const OPTIONS_LIST_CONTROL = 'optionsListControl';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { injectStorybookDataView } from '../services/storybook/data_views';
import { replaceOptionsListMethod } from '../services/storybook/options_list';
import { populateStorybookControlFactories } from './storybook_control_factories';
import { replaceValueSuggestionMethod } from '../services/storybook/unified_search';
import { OptionsListResponse, OptionsListRequest } from '../control_types/options_list/types';
import { OptionsListResponse, OptionsListRequest } from '../options_list/types';

export default {
title: 'Controls',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Side Public License, v 1.
*/

import { OptionsListEmbeddableFactory } from '../control_types/options_list';
import { OptionsListEmbeddableFactory } from '../options_list';
import { RangeSliderEmbeddableFactory } from '../control_types/range_slider';
import { TimesliderEmbeddableFactory } from '../control_types/time_slider';
import { ControlsService } from '../services/controls';
Expand Down
1 change: 0 additions & 1 deletion src/plugins/controls/public/control_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
* Side Public License, v 1.
*/

export * from './options_list';
export * from './range_slider';
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ControlEmbeddable, DataControlField, IEditableControlFactory } from '..
import {
createOptionsListExtract,
createOptionsListInject,
} from '../../../common/control_types/options_list/options_list_persistable_state';
} from '../../../common/options_list/options_list_persistable_state';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

LOL why is time slider using the options list persistable state? Definitely something to fix in the time slider refactor @cqliu1 @ThomThomson 😆

import { TimeSliderControlEmbeddableInput } from '../../../common/control_types/time_slider/types';
import { TimeSliderStrings } from './time_slider_strings';

Expand Down
3 changes: 3 additions & 0 deletions src/plugins/controls/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export {
OptionsListEmbeddableFactory,
OptionsListEmbeddable,
type OptionsListEmbeddableInput,
} from './options_list';

export {
RangeSliderEmbeddableFactory,
RangeSliderEmbeddable,
type RangeSliderEmbeddableInput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,22 @@
* Side Public License, v 1.
*/

import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Subject } from 'rxjs';
import classNames from 'classnames';
import { debounce, isEmpty } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';

import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';

import { OptionsListStrings } from './options_list_strings';
import { optionsListReducers } from './options_list_reducers';
import { OptionsListPopover } from './options_list_popover_component';
import { OptionsListPopover } from './options_list_popover';
import { optionsListReducers } from '../options_list_reducers';
import { OptionsListReduxState } from '../types';

import './options_list.scss';
import { OptionsListReduxState } from './types';

export const OptionsListComponent = ({
typeaheadSubject,
}: {
typeaheadSubject: Subject<string>;
}) => {
export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Subject<string> }) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);

const resizeRef = useRef(null);
Expand Down Expand Up @@ -79,11 +76,11 @@ export const OptionsListComponent = ({
selectionDisplayNode: (
<>
{validSelections && (
<span>{validSelections?.join(OptionsListStrings.summary.getSeparator())}</span>
<span>{validSelections?.join(OptionsListStrings.control.getSeparator())}</span>
)}
{invalidSelections && (
<span className="optionsList__filterInvalid">
{invalidSelections.join(OptionsListStrings.summary.getSeparator())}
{invalidSelections.join(OptionsListStrings.control.getSeparator())}
</span>
)}
</>
Expand All @@ -106,7 +103,7 @@ export const OptionsListComponent = ({
numActiveFilters={validSelectionsCount}
hasActiveFilters={Boolean(validSelectionsCount)}
>
{hasSelections ? selectionDisplayNode : OptionsListStrings.summary.getPlaceholder()}
{hasSelections ? selectionDisplayNode : OptionsListStrings.control.getPlaceholder()}
</EuiFilterButton>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
*/

import React, { useState } from 'react';

import { EuiFormRow, EuiSwitch } from '@elastic/eui';

import { OptionsListEmbeddableInput } from './types';
import { OptionsListStrings } from './options_list_strings';
import { OptionsListEmbeddableInput } from '../types';
import { ControlEditorProps } from '../..';

interface OptionsListEditorState {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { ReactWrapper } from 'enzyme';

import { mountWithIntl } from '@kbn/test-jest-helpers';
import { findTestSubject } from '@elastic/eui/lib/test';
import { EmbeddableReduxContext } from '@kbn/presentation-util-plugin/public/redux_embeddables/use_redux_embeddable_context';

import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover';
import {
OptionsListComponentState,
OptionsListEmbeddableInput,
OptionsListReduxState,
} from '../types';
import { ControlOutput } from '../..';
import { mockOptionsListContext } from '../../../common/mocks';

describe('Options list popover', () => {
const defaultProps = {
width: 500,
updateSearchString: jest.fn(),
};

interface MountOptions {
componentState: Partial<OptionsListComponentState>;
explicitInput: Partial<OptionsListEmbeddableInput>;
output: Partial<ControlOutput>;
popoverProps: Partial<OptionsListPopoverProps>;
}

function mountComponent(options?: Partial<MountOptions>) {
const compProps = { ...defaultProps, ...(options?.popoverProps ?? {}) };
const context = mockOptionsListContext({
componentState: options?.componentState ?? {},
explicitInput: options?.explicitInput ?? {},
output: options?.output ?? {},
} as Partial<OptionsListReduxState>);

return mountWithIntl(
<EmbeddableReduxContext.Provider value={context}>
<OptionsListPopover {...compProps} />
</EmbeddableReduxContext.Provider>
);
}

const clickShowOnlySelections = (popover: ReactWrapper) => {
const showOnlySelectedButton = findTestSubject(
popover,
'optionsList-control-show-only-selected'
);
showOnlySelectedButton.simulate('click');
};

test('available options list width responds to container size', () => {
let popover = mountComponent({ popoverProps: { width: 301 } });
let availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
expect(availableOptionsDiv.getDOMNode().getAttribute('style')).toBe('width: 301px;');

// the div cannot be smaller than 301 pixels wide
popover = mountComponent({ popoverProps: { width: 300 } });
availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
expect(availableOptionsDiv.getDOMNode().getAttribute('style')).toBe(null);
});

test('no available options', () => {
const popover = mountComponent({ componentState: { availableOptions: [] } });
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
const noOptionsDiv = findTestSubject(
availableOptionsDiv,
'optionsList-control-noSelectionsMessage'
);
expect(noOptionsDiv.exists()).toBeTruthy();
});

test('display error message when the show only selected toggle is true but there are no selections', () => {
const popover = mountComponent();
clickShowOnlySelections(popover);
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
const noSelectionsDiv = findTestSubject(
availableOptionsDiv,
'optionsList-control-selectionsEmptyMessage'
);
expect(noSelectionsDiv.exists()).toBeTruthy();
});

test('show only selected options', () => {
const selections = ['woof', 'bark'];
const popover = mountComponent({
explicitInput: { selectedOptions: selections },
});
clickShowOnlySelections(popover);
const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
availableOptionsDiv.children().forEach((child, i) => {
expect(child.text()).toBe(selections[i]);
});
});
});
Loading