disable input control when field contains no values in index pattern (#15317) (#15621)

* disable range control when no min and max

* use message text provided by gchaps

* use pui-react-tooltip instead of waiting for UI-framework tooltip

* disable list control when no terms returned

* fix jest tests and add test case disabled controls

* fix pui tooltip styling

* set disable to true since its inside if check for same value

* update jest snapshot
This commit is contained in:
Nathan Reese 2017-12-14 16:42:33 -07:00 committed by GitHub
parent a287eef7d4
commit ef0cdaad10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 347 additions and 94 deletions

View file

@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders disabled control with tooltip 1`] = `
<div
className="kuiVerticalRhythm"
data-test-subj="inputControl0"
>
<label
className="kuiLabel kuiVerticalRhythmSmall"
htmlFor="controlId"
>
test control
</label>
<div
className="kuiVerticalRhythmSmall"
>
<OverlayTrigger
display={false}
isSticky={false}
overlay={
<Tooltip
className="inputControlDisabledTooltip"
isSticky={false}
size="auto"
visible={true}
/>
}
pin={true}
placement="top"
theme="dark"
trigger="hover"
>
<div>
My Control
</div>
</OverlayTrigger>
</div>
</div>
`;
exports[`renders enabled control 1`] = `
<div
className="kuiVerticalRhythm"
data-test-subj="inputControl0"
>
<label
className="kuiLabel kuiVerticalRhythmSmall"
htmlFor="controlId"
>
test control
</label>
<div
className="kuiVerticalRhythmSmall"
>
<div>
My Control
</div>
</div>
</div>
`;

View file

@ -24,6 +24,7 @@ exports[`Apply and Cancel change btns enabled when there are changes 1`] = `
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
@ -119,6 +120,7 @@ exports[`Clear btns enabled when there are values 1`] = `
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
@ -214,6 +216,7 @@ exports[`Renders list control 1`] = `
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
@ -308,6 +311,7 @@ exports[`Renders range control 1`] = `
control={
Object {
"id": "mock-range-control",
"isEnabled": [Function],
"label": "ragne control",
"max": 100,
"min": 0,

View file

@ -1,7 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ListControl 1`] = `
<Component
<FormRow
control={
Object {
"getMultiSelectDelimiter": [Function],
"id": "mock-list-control",
"isEnabled": [Function],
"label": "list control",
"options": Object {
"multiselect": true,
"type": "terms",
},
"selectOptions": Array [
Object {
"label": "choice1",
"value": "choice1",
},
Object {
"label": "choice2",
"value": "choice2",
},
],
"type": "list",
"value": "",
}
}
controlIndex={0}
id="mock-list-control"
label="list control"
@ -66,5 +90,5 @@ exports[`renders ListControl 1`] = `
valueKey="value"
valueRenderer={[Function]}
/>
</Component>
</FormRow>
`;

View file

@ -1,65 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders RangeControl 1`] = `
<Component
<FormRow
control={
Object {
"hasValue": [Function],
"id": "mock-range-control",
"isEnabled": [Function],
"label": "range control",
"max": 100,
"min": 0,
"options": Object {
"decimalPlaces": 0,
"step": 1,
},
"type": "range",
"value": Object {
"max": 0,
"min": 0,
},
}
}
controlIndex={0}
id="mock-range-control"
label="range control"
>
<input
className="kuiTextInput"
id="mock-range-control_min"
max={100}
min={0}
name="min"
onChange={[Function]}
type="number"
value=""
/>
<div
className="inputRangeContainer"
>
<InputRange
ariaLabelledby="mock-range-control"
classNames={
Object {
"activeTrack": "input-range__track input-range__track--active",
"disabledInputRange": "input-range input-range--disabled",
"inputRange": "input-range",
"labelContainer": "input-range__label-container",
"maxLabel": "input-range__label input-range__label--max",
"minLabel": "input-range__label input-range__label--min",
"slider": "input-range__slider",
"sliderContainer": "input-range__slider-container",
"track": "input-range__track input-range__track--background",
"valueLabel": "input-range__label input-range__label--value",
}
}
<div>
<input
className="kuiTextInput"
disabled={false}
draggableTrack={true}
formatLabel={[Function]}
maxValue={100}
minValue={0}
id="mock-range-control_min"
max={100}
min={0}
name="min"
onChange={[Function]}
onChangeComplete={[Function]}
step={1}
value={
Object {
"max": 0,
"min": 0,
type="number"
value=""
/>
<div
className="inputRangeContainer"
>
<InputRange
ariaLabelledby="mock-range-control"
classNames={
Object {
"activeTrack": "input-range__track input-range__track--active",
"disabledInputRange": "input-range input-range--disabled",
"inputRange": "input-range",
"labelContainer": "input-range__label-container",
"maxLabel": "input-range__label input-range__label--max",
"minLabel": "input-range__label input-range__label--min",
"slider": "input-range__slider",
"sliderContainer": "input-range__slider-container",
"track": "input-range__track input-range__track--background",
"valueLabel": "input-range__label input-range__label--value",
}
}
}
disabled={false}
draggableTrack={true}
formatLabel={[Function]}
maxValue={100}
minValue={0}
onChange={[Function]}
onChangeComplete={[Function]}
step={1}
value={
Object {
"max": 0,
"min": 0,
}
}
/>
</div>
<input
className="kuiTextInput"
disabled={false}
id="mock-range-control_max"
max={100}
min={0}
name="max"
onChange={[Function]}
type="number"
value=""
/>
</div>
<input
className="kuiTextInput"
id="mock-range-control_max"
max={100}
min={0}
name="max"
onChange={[Function]}
type="number"
value=""
/>
</Component>
</FormRow>
`;

View file

@ -1,23 +1,40 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Tooltip } from 'pui-react-tooltip';
import { OverlayTrigger } from 'pui-react-overlay-trigger';
export const FormRow = (props) => (
<div
className="kuiVerticalRhythm"
data-test-subj={'inputControl' + props.controlIndex}
>
<label className="kuiLabel kuiVerticalRhythmSmall" htmlFor={props.id}>
{props.label}
</label>
<div className="kuiVerticalRhythmSmall">
{props.children}
export function FormRow(props) {
let control = props.children;
if (!props.control.isEnabled()) {
const tooltip = (
<Tooltip className="inputControlDisabledTooltip" >{props.control.disabledReason}</Tooltip>
);
control = (
<OverlayTrigger placement="top" overlay={tooltip}>
{control}
</OverlayTrigger>
);
}
return (
<div
className="kuiVerticalRhythm"
data-test-subj={'inputControl' + props.controlIndex}
>
<label className="kuiLabel kuiVerticalRhythmSmall" htmlFor={props.id}>
{props.label}
</label>
<div className="kuiVerticalRhythmSmall">
{control}
</div>
</div>
</div>
);
);
}
FormRow.propTypes = {
label: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
controlIndex: PropTypes.number.isRequired
controlIndex: PropTypes.number.isRequired,
control: PropTypes.object.isRequired,
};

View file

@ -0,0 +1,42 @@
import React from 'react';
import { shallow } from 'enzyme';
import {
FormRow,
} from './form_row';
test('renders enabled control', () => {
const enabledControl = {
id: 'mock-enabled-control',
isEnabled: () => { return true; },
};
const component = shallow(
<FormRow
label="test control"
id="controlId"
control={enabledControl}
controlIndex={0}
>
<div>My Control</div>
</FormRow>
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('renders disabled control with tooltip', () => {
const disabledControl = {
id: 'mock-disabled-control',
isEnabled: () => { return false; },
};
const component = shallow(
<FormRow
label="test control"
id="controlId"
control={disabledControl}
controlIndex={0}
>
<div>My Control</div>
</FormRow>
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});

View file

@ -9,6 +9,7 @@ import {
const mockListControl = {
id: 'mock-list-control',
isEnabled: () => { return true; },
options: {
type: 'terms',
multiselect: true
@ -24,6 +25,7 @@ const mockListControl = {
};
const mockRangeControl = {
id: 'mock-range-control',
isEnabled: () => { return true; },
options: {
decimalPlaces: 0,
step: 1

View file

@ -26,25 +26,43 @@ export class ListControl extends Component {
return `${selected.label.substring(0, 23)}...`;
}
renderControl() {
if (!this.props.control.isEnabled()) {
// react-select clobbers the tooltip, so just returning a disabled input instead
return (
<input
disabled={true}
className="kuiTextInput"
style={{ width: '100%' }}
/>
);
}
return (
<Select
className="list-control-react-select"
placeholder="Select..."
multi={this.props.control.options.multiselect}
simpleValue={true}
delimiter={this.props.control.getMultiSelectDelimiter()}
value={this.props.control.value}
options={this.props.control.selectOptions}
onChange={this.handleOnChange}
valueRenderer={this.truncate}
inputProps={{ id: this.props.control.id }}
/>
);
}
render() {
return (
<FormRow
id={this.props.control.id}
label={this.props.control.label}
controlIndex={this.props.controlIndex}
control={this.props.control}
>
<Select
className="list-control-react-select"
placeholder="Select..."
multi={this.props.control.options.multiselect}
simpleValue={true}
delimiter={this.props.control.getMultiSelectDelimiter()}
value={this.props.control.value}
options={this.props.control.selectOptions}
onChange={this.handleOnChange}
valueRenderer={this.truncate}
inputProps={{ id: this.props.control.id }}
/>
{this.renderControl()}
</FormRow>
);
}

View file

@ -8,6 +8,7 @@ import {
const control = {
id: 'mock-list-control',
isEnabled: () => { return true; },
options: {
type: 'terms',
multiselect: true

View file

@ -79,15 +79,12 @@ export class RangeControl extends Component {
return formatedValue;
}
render() {
renderControl() {
return (
<FormRow
id={this.props.control.id}
label={this.props.control.label}
controlIndex={this.props.controlIndex}
>
<div>
<input
id={`${this.props.control.id}_min`}
disabled={!this.props.control.isEnabled()}
name="min"
type="number"
className="kuiTextInput"
@ -98,6 +95,7 @@ export class RangeControl extends Component {
/>
<div className="inputRangeContainer">
<InputRange
disabled={!this.props.control.isEnabled()}
maxValue={this.props.control.max}
minValue={this.props.control.min}
step={this.props.control.options.step}
@ -111,6 +109,7 @@ export class RangeControl extends Component {
</div>
<input
id={`${this.props.control.id}_max`}
disabled={!this.props.control.isEnabled()}
name="max"
type="number"
className="kuiTextInput"
@ -119,6 +118,19 @@ export class RangeControl extends Component {
max={this.props.control.max}
onChange={this.handleInputChange}
/>
</div>
);
}
render() {
return (
<FormRow
id={this.props.control.id}
label={this.props.control.label}
controlIndex={this.props.controlIndex}
control={this.props.control}
>
{this.renderControl()}
</FormRow>
);
}

View file

@ -8,6 +8,7 @@ import {
const control = {
id: 'mock-range-control',
isEnabled: () => { return true; },
options: {
decimalPlaces: 0,
step: 1

View file

@ -1,5 +1,11 @@
import _ from 'lodash';
export function noValuesDisableMsg(fieldName, indexPatternName) {
return `Filtering occurs on the "${fieldName}" field,
which doesn't exist on any documents in the "${indexPatternName}" index pattern.
Choose a different field or index documents that contain values for this field.`;
}
export class Control {
constructor(controlParams, filterManager) {
this.id = controlParams.id;
@ -7,10 +13,20 @@ export class Control {
this.type = controlParams.type;
this.label = controlParams.label ? controlParams.label : controlParams.fieldName;
this.filterManager = filterManager;
this.enable = true;
// restore state from kibana filter context
this.reset();
}
isEnabled() {
return this.enable;
}
disable(reason) {
this.enable = false;
this.disabledReason = reason;
}
set(newValue) {
this.value = newValue;
this._hasChanged = true;

View file

@ -10,6 +10,10 @@ export class FilterManager {
this.unsetValue = unsetValue;
}
getIndexPattern() {
return this.indexPattern;
}
createFilter() {
throw new Error('Must implement createFilter.');
}

View file

@ -1,5 +1,8 @@
import _ from 'lodash';
import { Control } from './control';
import {
Control,
noValuesDisableMsg
} from './control';
import { PhraseFilterManager } from './filter_manager/phrase_filter_manager';
const termsAgg = (field, size, direction) => {
@ -58,12 +61,17 @@ export async function listControlFactory(controlParams, kbnApi) {
'desc'));
const resp = await searchSource.fetch();
const termsSelectOptions = _.get(resp, 'aggregations.termsAgg.buckets', []).map((bucket) => {
return { label: bucket.key.toString(), value: bucket.key.toString() };
});
return new ListControl(
const listControl = new ListControl(
controlParams,
new PhraseFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter, listControlDelimiter),
_.get(resp, 'aggregations.termsAgg.buckets', []).map((bucket) => {
return { label: bucket.key.toString(), value: bucket.key.toString() };
})
termsSelectOptions
);
if (termsSelectOptions.length === 0) {
listControl.disable(noValuesDisableMsg(controlParams.fieldName, indexPattern.title));
}
return listControl;
}

View file

@ -1,5 +1,8 @@
import _ from 'lodash';
import { Control } from './control';
import {
Control,
noValuesDisableMsg
} from './control';
import { RangeFilterManager } from './filter_manager/range_filter_manager';
const minMaxAgg = (field) => {
@ -40,13 +43,23 @@ export async function rangeControlFactory(controlParams, kbnApi) {
const resp = await searchSource.fetch();
const min = _.get(resp, 'aggregations.minAgg.value');
const max = _.get(resp, 'aggregations.maxAgg.value');
let minMaxReturnedFromAggregation = true;
let min = _.get(resp, 'aggregations.minAgg.value');
let max = _.get(resp, 'aggregations.maxAgg.value');
if (min === null || max === null) {
min = 0;
max = 1;
minMaxReturnedFromAggregation = false;
}
const emptyValue = { min: min, max: min };
return new RangeControl(
const rangeControl = new RangeControl(
controlParams,
new RangeFilterManager(controlParams.id, controlParams.fieldName, indexPattern, kbnApi.queryFilter, emptyValue),
min,
max
);
if (!minMaxReturnedFromAggregation) {
rangeControl.disable(noValuesDisableMsg(controlParams.fieldName, indexPattern.title));
}
return rangeControl;
}

View file

@ -44,4 +44,12 @@ visualization.input_control_vis {
}
}
.inputControlDisabledTooltip {
width: 250px;
.tooltip-content {
white-space: normal !important;
}
}