Skip to content

Commit

Permalink
[Controls] Range slider (#125584)
Browse files Browse the repository at this point in the history
* Adds range slider control

Fix ts error

Fix ref type error

Extracted i18n strings

Fixed number rounding

Fixed missing i18n string

Add loading state to range slider control output

Remove unnecessary change

Fix i18n errors

Apply formatter to range slider tick labels

* Apply comment updates from code review

Co-authored-by: Devon Thomson <[email protected]>

* Remove extra fetches

* set min width for panel

* Fix functional tests

* Fixed controls page object

Co-authored-by: Devon Thomson <[email protected]>
Co-authored-by: andreadelrio <[email protected]>
Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
4 people authored Mar 28, 2022
1 parent e67a782 commit ba6be79
Show file tree
Hide file tree
Showing 30 changed files with 1,497 additions and 30 deletions.
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 {
EmbeddableStateWithType,
EmbeddablePersistableStateService,
} from '../../../../embeddable/common';
import { RangeSliderEmbeddableInput } from './types';
import { SavedObjectReference } from '../../../../../core/types';
import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../../../data_views/common';

type RangeSliderInputWithType = Partial<RangeSliderEmbeddableInput> & { type: string };
const dataViewReferenceName = 'optionsListDataView';

export const createRangeSliderInject = (): EmbeddablePersistableStateService['inject'] => {
return (state: EmbeddableStateWithType, references: SavedObjectReference[]) => {
const workingState = { ...state } as EmbeddableStateWithType | RangeSliderInputWithType;
references.forEach((reference) => {
if (reference.name === dataViewReferenceName) {
(workingState as RangeSliderInputWithType).dataViewId = reference.id;
}
});
return workingState as EmbeddableStateWithType;
};
};

export const createRangeSliderExtract = (): EmbeddablePersistableStateService['extract'] => {
return (state: EmbeddableStateWithType) => {
const workingState = { ...state } as EmbeddableStateWithType | RangeSliderInputWithType;
const references: SavedObjectReference[] = [];

if ('dataViewId' in workingState) {
references.push({
name: dataViewReferenceName,
type: DATA_VIEW_SAVED_OBJECT_TYPE,
id: workingState.dataViewId!,
});
delete workingState.dataViewId;
}
return { state: workingState as EmbeddableStateWithType, references };
};
};
19 changes: 19 additions & 0 deletions src/plugins/controls/common/control_types/range_slider/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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 { ControlInput } from '../../types';

export const RANGE_SLIDER_CONTROL = 'rangeSliderControl';

export type RangeValue = [string, string];

export interface RangeSliderEmbeddableInput extends ControlInput {
fieldName: string;
dataViewId: string;
value: RangeValue;
}
7 changes: 5 additions & 2 deletions src/plugins/controls/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
* Side Public License, v 1.
*/

export type { ControlWidth } from './types';
export type { ControlPanelState, ControlsPanels, ControlGroupInput } from './control_group/types';
export type { OptionsListEmbeddableInput } from './control_types/options_list/types';
export type { ControlWidth } from './types';
export type { RangeSliderEmbeddableInput } from './control_types/range_slider/types';

export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types';
export { CONTROL_GROUP_TYPE } from './control_group/types';
export { OPTIONS_LIST_CONTROL } from './control_types/options_list/types';
export { RANGE_SLIDER_CONTROL } from './control_types/range_slider/types';

export { getDefaultControlGroupInput } from './control_group/control_group_constants';
61 changes: 61 additions & 0 deletions src/plugins/controls/public/__stories__/controls.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import {
import {
ControlGroupContainerFactory,
OptionsListEmbeddableInput,
RangeSliderEmbeddableInput,
OPTIONS_LIST_CONTROL,
RANGE_SLIDER_CONTROL,
} from '../';

import { ViewMode } from '../../../embeddable/public';
Expand Down Expand Up @@ -169,6 +171,65 @@ export const ConfiguredControlGroupStory = () => (
fieldName: 'Carrier',
} as OptionsListEmbeddableInput,
},
rangeSlider1: {
type: RANGE_SLIDER_CONTROL,
order: 4,
width: 'auto',
explicitInput: {
id: 'rangeSlider1',
title: 'Average ticket price',
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['4', '12'],
step: 2,
} as RangeSliderEmbeddableInput,
},
}}
/>
);

export const RangeSliderControlGroupStory = () => (
<ControlGroupStoryComponent
panels={{
rangeSlider1: {
type: RANGE_SLIDER_CONTROL,
order: 1,
width: 'auto',
explicitInput: {
id: 'rangeSlider1',
title: 'Average ticket price',
dataViewId: 'demoDataFlights',
fieldName: 'AvgTicketPrice',
value: ['4', '12'],
step: 2,
} as RangeSliderEmbeddableInput,
},
rangeSlider2: {
type: RANGE_SLIDER_CONTROL,
order: 2,
width: 'auto',
explicitInput: {
id: 'rangeSlider2',
title: 'Total distance in miles',
dataViewId: 'demoDataFlights',
fieldName: 'DistanceMiles',
value: ['0', '100'],
step: 10,
} as RangeSliderEmbeddableInput,
},
rangeSlider3: {
type: RANGE_SLIDER_CONTROL,
order: 3,
width: 'auto',
explicitInput: {
id: 'rangeSlider3',
title: 'Flight duration in hour',
dataViewId: 'demoDataFlight',
fieldName: 'FlightTimeHour',
value: ['30', '600'],
step: 30,
} as RangeSliderEmbeddableInput,
},
}}
/>
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { OptionsListEmbeddableFactory } from '../control_types/options_list';
import { RangeSliderEmbeddableFactory } from '../control_types/range_slider';
import { ControlsService } from '../services/controls';
import { ControlFactory } from '..';

Expand All @@ -17,4 +18,11 @@ export const populateStorybookControlFactories = (controlsServiceStub: ControlsS
const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory;
optionsListControlFactory.getDefaultInput = () => ({});
controlsServiceStub.registerControlType(optionsListControlFactory);

const rangeSliderFactoryStub = new RangeSliderEmbeddableFactory();

// cast to unknown because the stub cannot use the embeddable start contract to transform the EmbeddableFactoryDefinition into an EmbeddableFactory
const rangeSliderControlFactory = rangeSliderFactoryStub as unknown as ControlFactory;
rangeSliderControlFactory.getDefaultInput = () => ({});
controlsServiceStub.registerControlType(rangeSliderControlFactory);
};
1 change: 1 addition & 0 deletions src/plugins/controls/public/control_types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export * from './options_list';
export * from './range_slider';
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,6 @@ export const OptionsListStrings = {
}),
},
editor: {
getIndexPatternTitle: () =>
i18n.translate('controls.optionsList.editor.indexPatternTitle', {
defaultMessage: 'Index pattern',
}),
getDataViewTitle: () =>
i18n.translate('controls.optionsList.editor.dataViewTitle', {
defaultMessage: 'Data view',
Expand Down
13 changes: 13 additions & 0 deletions src/plugins/controls/public/control_types/range_slider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* 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.
*/

export { RANGE_SLIDER_CONTROL } from '../../../common/control_types/range_slider/types';
export { RangeSliderEmbeddableFactory } from './range_slider_embeddable_factory';

export type { RangeSliderEmbeddable } from './range_slider_embeddable';
export type { RangeSliderEmbeddableInput } from '../../../common/control_types/range_slider/types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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, { FC, useCallback, useState } from 'react';
import { BehaviorSubject } from 'rxjs';

import { DataViewField } from '../../../../data_views/public';
import { useReduxEmbeddableContext } from '../../../../presentation_util/public';
import { useStateObservable } from '../../hooks/use_state_observable';
import { RangeSliderPopover } from './range_slider_popover';
import { rangeSliderReducers } from './range_slider_reducers';
import { RangeSliderEmbeddableInput, RangeValue } from './types';

import './range_slider.scss';

interface Props {
componentStateSubject: BehaviorSubject<RangeSliderComponentState>;
}
// Availableoptions and loading state is controled by the embeddable, but is not considered embeddable input.
export interface RangeSliderComponentState {
field?: DataViewField;
fieldFormatter: (value: string) => string;
min: string;
max: string;
loading: boolean;
}

export const RangeSliderComponent: FC<Props> = ({ componentStateSubject }) => {
// Redux embeddable Context to get state from Embeddable input
const {
useEmbeddableDispatch,
useEmbeddableSelector,
actions: { selectRange },
} = useReduxEmbeddableContext<RangeSliderEmbeddableInput, typeof rangeSliderReducers>();
const dispatch = useEmbeddableDispatch();

// useStateObservable to get component state from Embeddable
const { loading, min, max, fieldFormatter } = useStateObservable<RangeSliderComponentState>(
componentStateSubject,
componentStateSubject.getValue()
);

const { value = ['', ''], id, title } = useEmbeddableSelector((state) => state);

const [selectedValue, setSelectedValue] = useState<RangeValue>(value || ['', '']);

const onChangeComplete = useCallback(
(range: RangeValue) => {
dispatch(selectRange(range));
setSelectedValue(range);
},
[selectRange, setSelectedValue, dispatch]
);

return (
<RangeSliderPopover
id={id}
isLoading={loading}
min={min}
max={max}
title={title}
value={selectedValue}
onChange={onChangeComplete}
fieldFormatter={fieldFormatter}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
.rangeSlider__popoverOverride {
height: 100%;
max-width: 100%;
width: 100%;
}

@include euiBreakpoint('m', 'l', 'xl') {
.rangeSlider__panelOverride {
min-width: $euiSizeXXL * 12;
}
}

.rangeSlider__anchorOverride {
>div {
height: 100%;
}
}

.rangeSliderAnchor__button {
display: flex;
align-items: center;
width: 100%;
height: 100%;
justify-content: space-between;
background-color: $euiFormBackgroundColor;
@include euiFormControlSideBorderRadius($euiFormControlBorderRadius, $side: 'right', $internal: true);

.euiToolTipAnchor {
width: 100%;
}

.rangeSliderAnchor__delimiter {
background-color: unset;
}
.rangeSliderAnchor__fieldNumber {
font-weight: $euiFontWeightBold;
box-shadow: none;
text-align: center;
background-color: unset;

&::placeholder {
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
text-decoration: none;
}
}

.rangeSliderAnchor__fieldNumber--invalid {
text-decoration: line-through;
font-weight: $euiFontWeightRegular;
color: $euiColorMediumShade;
}

.rangeSliderAnchor__spinner {
padding-right: $euiSizeS;
}
}
Loading

0 comments on commit ba6be79

Please sign in to comment.