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

[WIP] Terms Filter Control #11873

Closed
wants to merge 17 commits into from
Closed
12 changes: 12 additions & 0 deletions src/core_plugins/control_visualizations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default function (kibana) {

return new kibana.Plugin({
require: ['kibana','elasticsearch'],

uiExports: {
visTypes: [
'plugins/terms/terms_vis/vis'
]
}
});
}
4 changes: 4 additions & 0 deletions src/core_plugins/control_visualizations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "terms",
"version": "kibana"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';

export class FieldSelect extends Component {
constructor(props) {
super(props);

this.loadFields = this.loadFields.bind(this);
}

loadFields(input, callback) {
if (!this.props.indexPatternId || this.props.indexPatternId.length === 0) {
callback(null, { options: [] });
return;
}

this.props.getIndexPattern(this.props.indexPatternId).then(index => {
const fields = index.fields.filter(function (field) {
return field.aggregatable && ['number', 'boolean', 'date', 'ip', 'string'].includes(field.type);
}).sort((a, b) => {
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
}).map(function (field) {
return { label: field.name, value: field.name };
});
//Setting complete=true means loadOptions will never be called again.
callback(null, { options: fields, complete: true });
});
}

render() {
if (!this.props.indexPatternId || this.props.indexPatternId.trim().length === 0) {
return null;
}

return (
<div className="kuiFieldGroup">
<div className="kuiFieldGroupSection">
<label>
Terms Field
</label>
</div>
<div className="kuiFieldGroupSection kuiFieldGroupSection--wide">
<Select.Async
placeholder="Select..."
value={this.props.value}
loadOptions={this.loadFields}
onChange={this.props.onChange}
resetValue={''}/>
</div>
</div>
);
}
}

FieldSelect.propTypes = {
getIndexPattern: PropTypes.func.isRequired,
indexPatternId: PropTypes.string,
onChange: PropTypes.func.isRequired,
value: PropTypes.string
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';

export class IndexPatternSelect extends Component {
constructor(props) {
super(props);

this.loadOptions = this.loadOptions.bind(this);
}

loadOptions(input, callback) {
this.props.getIndexPatternIds().then(ids => {
const options = ids.map(id => {
return { label: id, value: id };
});
//Setting complete=true means loadOptions will never be called again.
callback(null, { options: options, complete: true });
});
}

render() {
return (
<div className="kuiFieldGroup">
<div className="kuiFieldGroupSection">
<label>
Index Pattern
</label>
</div>
<div className="kuiFieldGroupSection kuiFieldGroupSection--wide">
<Select.Async
placeholder="Select..."
value={this.props.value}
loadOptions={this.loadOptions}
onChange={this.props.onChange}
resetValue={''}/>
</div>
</div>
);
}
}

IndexPatternSelect.propTypes = {
getIndexPatternIds: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { Component } from 'react';
import Select from 'react-select';

export class TermsVis extends Component {
constructor(props) {
super(props);

this.handleOnChange = this.handleOnChange.bind(this);
}

handleOnChange(control, evt) {
if (evt) {
this.props.setFilter(control.field, evt.value, control.indexPattern);
} else {
this.props.removeFilter(control.field, control.indexPattern);
}
}

render() {
return (
<div className="vertical-layout">
{this.props.controls.map((control, index) =>
<div key={index} className="terms-field">
<span>{control.label}</span>
<Select
className="terms-select"
placeholder="Select..."
value={control.selected}
options={control.terms}
onChange={this.handleOnChange.bind(null, control)}/>
</div>
)}
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { Component } from 'react';
import { IndexPatternSelect } from './index_pattern_select';
import { FieldSelect } from './field_select';
import {
KuiButton,
KuiButtonIcon,
} from 'ui_framework/components';
import { addField, newField, removeField, setField } from '../lib/editor_utils';

export class TermsVisEditor extends Component {
constructor(props) {
super(props);

this.handleFieldNameChange = this.handleFieldNameChange.bind(this);
this.handleLabelChange = this.handleLabelChange.bind(this);
this.handleIndexPatternChange = this.handleIndexPatternChange.bind(this);
this.handleRemoveField = this.handleRemoveField.bind(this);
this.handleAddField = this.handleAddField.bind(this);
}

handleLabelChange(fieldIndex, evt) {
const updatedField = this.props.visParams.fields[fieldIndex];
updatedField.label = evt.target.value;
this.props.setVisParam('fields', setField(this.props.visParams.fields, fieldIndex, updatedField));
}

handleIndexPatternChange(fieldIndex, evt) {
const updatedField = this.props.visParams.fields[fieldIndex];
updatedField.indexPattern = evt.value;
updatedField.fieldName = '';
this.props.setVisParam('fields', setField(this.props.visParams.fields, fieldIndex, updatedField));
}

handleFieldNameChange(fieldIndex, evt) {
const updatedField = this.props.visParams.fields[fieldIndex];
updatedField.fieldName = evt.value;
this.props.setVisParam('fields', setField(this.props.visParams.fields, fieldIndex, updatedField));
}

handleRemoveField(fieldIndex) {
this.props.setVisParam('fields', removeField(this.props.visParams.fields, fieldIndex));
}

handleAddField() {
this.props.setVisParam('fields', addField(this.props.visParams.fields, newField()));
}

renderFields() {
return this.props.visParams.fields.map((field, index) => {
return (
<div key={index} className="field-section">
<div className="kuiFieldGroup">
<div className="kuiFieldGroupSection">
<label>
Label
</label>
</div>
<div className="kuiFieldGroupSection">
<input
className="kuiTextInput"
type="text"
value={field.label}
onChange={this.handleLabelChange.bind(null, index)} />
</div>
<button
className="kuiButton kuiButton--danger kuiButton--small"
onClick={this.handleRemoveField.bind(null, index)}>
<span className="kuiIcon fa-trash"></span>
</button>
</div>

<IndexPatternSelect
value={field.indexPattern}
onChange={this.handleIndexPatternChange.bind(null, index)}
getIndexPatternIds={this.props.getIndexPatternIds} />

<FieldSelect
value={field.fieldName}
indexPatternId={field.indexPattern}
onChange={this.handleFieldNameChange.bind(null, index)}
getIndexPattern={this.props.getIndexPattern} />
</div>
);
});
}

render() {
return (
<div>

{this.renderFields()}

<KuiButton
type="primary"
icon={<KuiButtonIcon type="create" />}
onClick={this.handleAddField}
>
Add
</KuiButton>
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div ng-controller="KbnTermsEditorController" class="terms-vis-editor">
<react-component name="TermsVisEditor" props="reactProps" />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { uiModules } from 'ui/modules';
import { TermsVisEditor } from './components/terms_vis_editor';

const module = uiModules.get('kibana/terms_vis', ['kibana', 'react']);
module.controller('KbnTermsEditorController', ($scope, indexPatterns) => {
$scope.reactProps = {
indexPatterns: indexPatterns,
visParams: $scope.vis.params,
setVisParam: (paramName, paramValue) => {
$scope.vis.params[paramName] = paramValue;
},
getIndexPatternIds: () => {
return indexPatterns.getIds();
},
getIndexPattern: (indexPatternId) => {
return indexPatterns.get(indexPatternId);
}
};
});

module.value('TermsVisEditor', TermsVisEditor);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const setField = (fields, fieldIndex, field) => [
...fields.slice(0, fieldIndex),
field,
...fields.slice(fieldIndex + 1)
];

export const addField = (fields, field) => [...fields, field];

export const removeField = (fields, fieldIndex) => [
...fields.slice(0, fieldIndex),
...fields.slice(fieldIndex + 1)
];

export const newField = () => ({
indexPattern: '',
fieldName: '',
label: '',
size: 5,
order: 'desc'
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div ng-controller="KbnTermsController" class="terms-vis">
<react-component name="TermsVis" props="reactProps" />
</div>
30 changes: 30 additions & 0 deletions src/core_plugins/control_visualizations/public/terms_vis/vis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import './vis_controller';
import './editor_controller';
import 'react-select/dist/react-select.css';
import './vis.less';
import { VisVisTypeProvider } from 'ui/vis/vis_type';
import { TemplateVisTypeProvider } from 'ui/template_vis_type/template_vis_type';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { newField } from './lib/editor_utils';
VisTypesRegistryProvider.register(TermsProvider);

export default function TermsProvider(Private) {
const VisType = Private(VisVisTypeProvider);
const TemplateVisType = Private(TemplateVisTypeProvider);

return new TemplateVisType({
name: 'terms',
title: 'Terms',
implementsRenderComplete: true,
description: 'Terms filter control',
category: VisType.CATEGORY.CONTROL,
template: require('./vis.html'),
params: {
editor: require('./editor.html'),
defaults: {
fields: [newField()]
}
},
requiresSearch: false
});
}
34 changes: 34 additions & 0 deletions src/core_plugins/control_visualizations/public/terms_vis/vis.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.terms-vis {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: hidden;

.horizontal-layout {
display: flex;
}

.vertical-layout {
display: flex;
flex-direction: column;
}

.terms-field {
display: flex;
flex: 1 1 auto;
align-items: center;
}

.terms-select {
margin-right: 5px;
flex: 1 1 auto;
}
}

.terms-vis-editor {
.field-section {
margin-top: 15px;
}
}
Loading