Time Series Metric Visualizations (#10662)

Backports PR #9725

**Commit 1:**
Initial import

* Original sha: f9d527ecad
* Authored by Chris Cowan <chris@chriscowan.us> on 2016-12-21T00:27:11Z

**Commit 2:**
Merge branch 'master' of github.com:elastic/kibana into metrics

* Original sha: c5e936f272
* Authored by Chris Cowan <chris@chriscowan.us> on 2016-12-21T14:26:16Z

**Commit 3:**
updating the editor width to match the new specs

* Original sha: 0d0f4c6aa8
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:51:06Z

**Commit 4:**
Merge branch 'master' of github.com:elastic/kibana into metrics

* Original sha: 2fb42d0e5d
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:54:40Z

**Commit 5:**
Adding tribe node support

* Original sha: 63a736aa21
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:57:40Z

**Commit 6:**
Adding tests for server libs

* Original sha: 6ede79b8e4
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T22:34:57Z

**Commit 7:**
removing bluebird

* Original sha: 1cb28600fb
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T22:39:23Z

**Commit 8:**
removing extra cruft

* Original sha: 4efe0ecd92
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T17:01:32Z

**Commit 9:**
Fixing the font sizes

* Original sha: 33f4d535b8
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T18:09:08Z

**Commit 10:**
Fixed the updating code

* Original sha: 51ff9f8fa9
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T18:56:29Z

**Commit 11:**
Adding brushing

* Original sha: 2ba0463721
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T20:23:46Z

**Commit 12:**
Fixing linting issues

* Original sha: 09b6ada25d
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T20:33:12Z

**Commit 13:**
Adding global filters

* Original sha: 985ec1c02c
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T21:44:20Z

**Commit 14:**
Adding missing packages

* Original sha: bd87e894cc
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T13:16:15Z

**Commit 15:**
Default gauge style to half circle

* Original sha: 5ce7d856fb
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T15:10:51Z

**Commit 16:**
Fixing the markdown css bug

* Original sha: 0b72290f6a
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T15:26:53Z

**Commit 17:**
Adding tests for the get_vis_data api

* Original sha: a6e91ba86e
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T15:16:05Z

**Commit 18:**
Adding time offset

* Original sha: 5ee12ac7e2
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T17:36:56Z

**Commit 19:**
Adding time offset to each type

* Original sha: ee52e34456
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T17:42:33Z

**Commit 20:**
fixing bugs from time offset

* Original sha: 16b7469d41
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-13T23:12:18Z

**Commit 21:**
adding index pattern option to series

* Original sha: 1e64c7cc70
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-17T21:57:13Z

**Commit 22:**
Merge branch 'master' of github.com:elastic/kibana into metrics

* Original sha: b4ce642e68
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-17T23:05:40Z

**Commit 23:**
Adding index pattern overrides

* Original sha: b5ff6c6f46
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:29:25Z

**Commit 24:**
Adding index pattern overrides

* Original sha: 6641c8e6e4
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:38:12Z

**Commit 25:**
Fixing tests

* Original sha: b7822ecb5c
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:43:56Z

**Commit 26:**
Fixing brushing in the vis editor

* Original sha: 74dc57f460
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T17:14:04Z

**Commit 27:**
Changing the label

* Original sha: 95e58b0c86
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T17:19:03Z

**Commit 28:**
Change the behavior of selecting a pipeline agg when only one exists.

* Original sha: 5d2bf42680
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T18:18:57Z

**Commit 29:**
Refactoring series a bit

* Original sha: f1ff1988ce
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T18:33:15Z

**Commit 30:**
Changing series options to just options

* Original sha: 23821b52bd
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T20:34:59Z
This commit is contained in:
jasper 2017-03-02 17:06:13 -05:00 committed by Chris Cowan
parent 48e0046173
commit 6bb9e1dfcb
247 changed files with 16793 additions and 5 deletions

View file

@ -6,7 +6,7 @@
Timelion is a time series data visualizer that enables you to combine totally
independent data sources within a single visualization. It's driven by a simple
expression language you use to retrieve time series data, perform calculations
to tease out the answers to complex questions, and visualize the results.
to tease out the answers to complex questions, and visualize the results.
For example, Timelion enables you to easily get the answers to questions like:
@ -32,7 +32,7 @@ Timelion expression as a Kibana dashboard panel. You can then add it to
a dashboard like any other visualization.
TIP: You can also create time series visualizations right from the Visualize
app--just select the Timeseries visualization type and enter a Timelion
app--just select the Timelion visualization type and enter a Timelion
expression in the expression field.

View file

@ -34,7 +34,7 @@ instructions.
<<tagcloud-chart,Tag cloud>>:: Display words as a cloud in which the size of the word correspond to its importance
<<tilemap,Tile map>>:: Associate the results of an aggregation with geographic
locations.
Timeseries:: Compute and combine data from multiple time series
Timelion:: Compute and combine data from multiple time series
data sets.
. Specify a search query to retrieve the data for your visualization:

View file

@ -109,6 +109,7 @@
"brace": "0.5.1",
"bunyan": "1.7.1",
"check-hash": "1.0.1",
"color": "1.0.3",
"commander": "2.8.1",
"css-loader": "0.17.0",
"d3": "3.5.6",
@ -186,12 +187,14 @@
"classnames": "2.2.5",
"del": "1.2.1",
"elasticdump": "2.1.1",
"enzyme": "2.7.0",
"eslint": "3.11.1",
"eslint-plugin-babel": "4.0.0",
"eslint-plugin-mocha": "4.7.0",
"event-stream": "3.3.2",
"expect.js": "0.3.1",
"faker": "1.1.0",
"flot-charts": "^0.8.3",
"grunt": "1.0.1",
"grunt-aws-s3": "0.14.5",
"grunt-babel": "6.0.0",
@ -211,6 +214,7 @@
"image-diff": "1.6.0",
"intern": "3.2.3",
"istanbul-instrumenter-loader": "0.1.3",
"jsdom": "9.9.1",
"karma": "1.2.0",
"karma-chrome-launcher": "0.2.0",
"karma-coverage": "0.5.1",
@ -231,15 +235,25 @@
"npm": "3.10.10",
"portscanner": "1.0.0",
"proxyquire": "1.7.10",
"pui-react-overlay-trigger": "^7.0.0",
"pui-react-tooltip": "^7.0.0",
"react": "15.2.0",
"react-ace": "3.7.0",
"react-addons-test-utils": "15.2.0",
"react-anything-sortable": "^1.6.1",
"react-color": "^2.2.7",
"react-dom": "15.2.0",
"react-markdown": "^2.4.2",
"react-redux": "4.4.5",
"react-router": "2.0.0",
"react-router-redux": "4.0.4",
"react-select": "^1.0.0-rc.1",
"react-sortable": "^1.1.0",
"reactcss": "^1.0.7",
"redux": "3.0.0",
"redux-thunk": "0.1.0",
"sass-loader": "4.0.0",
"simianhacker-react-resize-aware": "^1.0.11",
"simple-git": "1.37.0",
"sinon": "1.17.2",
"source-map": "0.5.6",

View file

@ -0,0 +1,31 @@
import fieldsRoutes from './server/routes/fields';
import visDataRoutes from './server/routes/vis';
export default function (kibana) {
return new kibana.Plugin({
require: ['kibana','elasticsearch'],
uiExports: {
visTypes: [
'plugins/metrics/kbn_vis_types'
]
},
config(Joi) {
return Joi.object({
enabled: Joi.boolean().default(true),
chartResolution: Joi.number().default(150),
minimumBucketSize: Joi.number().default(10)
}).default();
},
init(server, options) {
const { status } = server.plugins.elasticsearch;
fieldsRoutes(server);
visDataRoutes(server);
}
});
}

View file

@ -0,0 +1,6 @@
{
"author": "Chris Cowan<chris@elastic.co>",
"name": "metrics",
"version": "kibana"
}

View file

@ -0,0 +1,67 @@
// import React from 'react';
// import { expect } from 'chai';
// import { shallow } from 'enzyme';
// import sinon from 'sinon';
// import AddDeleteButtons from '../add_delete_buttons';
// import Tooltip from '../tooltip';
// describe('<AddDeleteButtons />', () => {
// it('calls onAdd={handleAdd}', () => {
// const handleAdd = sinon.spy();
// const wrapper = shallow(
// <AddDeleteButtons onAdd={handleAdd} />
// );
// wrapper.find('a').at(0).simulate('click');
// expect(handleAdd.calledOnce).to.equal(true);
// });
// it('calls onDelete={handleDelete}', () => {
// const handleDelete = sinon.spy();
// const wrapper = shallow(
// <AddDeleteButtons onDelete={handleDelete} />
// );
// wrapper.find('a').at(1).simulate('click');
// expect(handleDelete.calledOnce).to.equal(true);
// });
// it('calls onClone={handleClone}', () => {
// const handleClone = sinon.spy();
// const wrapper = shallow(
// <AddDeleteButtons onClone={handleClone} />
// );
// wrapper.find('a').at(0).simulate('click');
// expect(handleClone.calledOnce).to.equal(true);
// });
// it('disableDelete={true}', () => {
// const wrapper = shallow(
// <AddDeleteButtons disableDelete={true} />
// );
// expect(wrapper.find({ text: 'Delete' })).to.have.length(0);
// });
// it('disableAdd={true}', () => {
// const wrapper = shallow(
// <AddDeleteButtons disableAdd={true} />
// );
// expect(wrapper.find({ text: 'Add' })).to.have.length(0);
// });
// it('should not display clone by default', () => {
// const wrapper = shallow(
// <AddDeleteButtons />
// );
// expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
// });
// it('should not display clone when disableAdd={true}', () => {
// const fn = sinon.spy();
// const wrapper = shallow(
// <AddDeleteButtons onClone={fn} disableAdd={true} />
// );
// expect(wrapper.find({ text: 'Clone' })).to.have.length(0);
// });
// });

View file

@ -0,0 +1,33 @@
// import React from 'react';
// import { expect } from 'chai';
// import { shallow } from 'enzyme';
// import sinon from 'sinon';
// import YesNo from '../yes_no';
// describe('<YesNo />', () => {
// it('call onChange={handleChange} on yes', () => {
// const handleChange = sinon.spy();
// const wrapper = shallow(
// <YesNo name="test" onChange={handleChange} />
// );
// wrapper.find('input').first().simulate('change');
// expect(handleChange.calledOnce).to.equal(true);
// expect(handleChange.firstCall.args[0]).to.eql({
// test: 1
// });
// });
// it('call onChange={handleChange} on no', () => {
// const handleChange = sinon.spy();
// const wrapper = shallow(
// <YesNo name="test" onChange={handleChange} />
// );
// wrapper.find('input').last().simulate('change');
// expect(handleChange.calledOnce).to.equal(true);
// expect(handleChange.firstCall.args[0]).to.eql({
// test: 0
// });
// });
// });

View file

@ -0,0 +1,58 @@
import React, { Component, PropTypes } from 'react';
import Tooltip from './tooltip';
function AddDeleteButtons(props) {
const createDelete = () => {
if (props.disableDelete) {
return null;
}
return (
<Tooltip text="Delete">
<a className="thor__button-outlined-danger sm" onClick={ props.onDelete }>
<i className="fa fa-trash-o"></i>
</a>
</Tooltip>
);
};
const createAdd = () => {
if (props.disableAdd) {
return null;
}
return (
<Tooltip text="Add">
<a className="thor__button-outlined-default sm" onClick={ props.onAdd }>
<i className="fa fa-plus"></i>
</a>
</Tooltip>
);
};
const deleteBtn = createDelete();
const addBtn = createAdd();
let clone;
if (props.onClone && !props.disableAdd) {
clone = (
<Tooltip text="Clone">
<a className="thor__button-outlined-default sm" onClick={ props.onClone }>
<i className="fa fa-files-o"></i>
</a>
</Tooltip>
);
}
return (
<div className="add_delete__buttons">
{ clone }
{ addBtn }
{ deleteBtn }
</div>
);
}
AddDeleteButtons.propTypes = {
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
onClone: PropTypes.func,
onAdd: PropTypes.func,
onDelete: PropTypes.func
};
export default AddDeleteButtons;

View file

@ -0,0 +1,51 @@
import React, { PropTypes } from 'react';
import StdAgg from './std_agg';
import aggToComponent from '../lib/agg_to_component';
import { sortable } from 'react-anything-sortable';
function Agg(props) {
const { model } = props;
let Component = aggToComponent[model.type];
if (!Component) {
Component = StdAgg;
}
const style = Object.assign({ cursor: 'default' }, props.style);
return (
<div
className={props.className}
style={style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
<Component
fields={props.fields}
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onChange={props.onChange}
onDelete={props.onDelete}
panel={props.panel}
series={props.series}
siblings={props.siblings}/>
</div>
);
}
Agg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
sortData: PropTypes.string,
};
export default sortable(Agg);

View file

@ -0,0 +1,53 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import AddDeleteButtons from '../add_delete_buttons';
import Tooltip from '../tooltip';
function AggRow(props) {
let iconClassName = 'fa fa-eye-slash';
let iconRowClassName = 'vis_editor__agg_row-icon';
const last = _.last(props.siblings);
if (last.id === props.model.id) {
iconClassName = 'fa fa-eye';
iconRowClassName += ' last';
}
let dragHandle;
if (!props.disableDelete) {
dragHandle = (
<div>
<Tooltip text="Sort">
<div className="vis_editor__agg_sort thor__button-outlined-default sm">
<i className="fa fa-sort"></i>
</div>
</Tooltip>
</div>
);
}
return (
<div className="vis_editor__agg_row">
<div className="vis_editor__agg_row-item">
<div className={iconRowClassName}>
<i className={iconClassName}></i>
</div>
{props.children}
{ dragHandle }
<AddDeleteButtons
onAdd={props.onAdd}
onDelete={props.onDelete}
disableDelete={props.disableDelete}/>
</div>
</div>
);
}
AggRow.propTypes = {
disableDelete: PropTypes.bool,
model: PropTypes.object,
onAdd: PropTypes.func,
onDelete: PropTypes.func,
siblings: PropTypes.array,
};
export default AggRow;

View file

@ -0,0 +1,26 @@
import React, { PropTypes } from 'react';
import Select from 'react-select';
import { createOptions } from '../lib/agg_lookup';
function AggSelect(props) {
const { siblings, panelType } = props;
const options = createOptions(panelType, siblings);
return (
<div className="vis_editor__row_item">
<Select
clearable={false}
options={options}
value={props.value || 'count'}
onChange={props.onChange}/>
</div>
);
}
AggSelect.propTypes = {
onChange: PropTypes.func,
panelType: PropTypes.string,
siblings: PropTypes.array,
value: PropTypes.string
};
export default AggSelect;

View file

@ -0,0 +1,84 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import uuid from 'node-uuid';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import Vars from './vars';
class CalculationAgg extends Component {
componentWillMount() {
if (!this.props.model.variables) {
this.props.onChange(_.assign({}, this.props.model, {
variables: [{ id: uuid.v1() }]
}));
}
}
render() {
const { panel, siblings } = this.props;
const defaults = { script: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div>
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
<div className="vis_editor__variables">
<div className="vis_editor__label">Variables</div>
<Vars
metrics={siblings}
onChange={handleChange}
name="variables"
model={model}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Script (Painless)</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={handleTextChange('script')}
value={model.script}/>
</div>
</div>
</div>
</AggRow>
);
}
}
CalculationAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default CalculationAgg;

View file

@ -0,0 +1,52 @@
import React, { PropTypes } from 'react';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import MetricSelect from './metric_select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
function CumlativeSumAgg(props) {
const { model, panel, siblings } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
return (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Metric</div>
<MetricSelect
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
value={model.field}/>
</div>
</AggRow>
);
}
CumlativeSumAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default CumlativeSumAgg;

View file

@ -0,0 +1,71 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import MetricSelect from './metric_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
class DerivativeAgg extends Component {
render() {
const { siblings, panel } = this.props;
const defaults = { unit: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Metric</div>
<MetricSelect
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
value={model.field}/>
</div>
<div>
<div className="vis_editor__label">Units (1s, 1m, etc)</div>
<input
className="vis_editor__input"
onChange={handleTextChange('unit')}
value={model.unit}
type="text"/>
</div>
</AggRow>
);
}
}
DerivativeAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default DerivativeAgg;

View file

@ -0,0 +1,44 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import Select from 'react-select';
import AggLookup from '../lib/agg_lookup';
import generateByTypeFilter from '../lib/generate_by_type_filter';
function FieldSelect(props) {
const { type, fields, indexPattern } = props;
if (type === 'count') {
return null;
}
const options = (fields[indexPattern] || [])
.filter(generateByTypeFilter(props.restrict))
.map(field => {
return { label: field.name, value: field.name };
});
return (
<Select
placeholder="Select field..."
disabled={props.disabled}
options={options}
value={props.value}
onChange={props.onChange}/>
);
}
FieldSelect.defaultProps = {
indexPattern: '*',
disabled: false,
restrict: 'none'
};
FieldSelect.propTypes = {
disabled: PropTypes.bool,
fields: PropTypes.object,
indexPattern: PropTypes.string,
onChange: PropTypes.func,
restrict: PropTypes.string,
type: PropTypes.string,
value: PropTypes.string
};
export default FieldSelect;

View file

@ -0,0 +1,103 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import MetricSelect from './metric_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
class FilterRatioAgg extends Component {
render() {
const { series, fields, siblings, panel } = this.props;
const handleChange = createChangeHandler(this.props.onChange, this.props.model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
const defaults = {
numerator: '*',
denominator: '*',
metric_agg: 'count'
};
const model = { ...defaults, ...this.props.model };
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div style={{ flex: '1 0 auto' }}>
<div style={{ flex: '1 0 auto', display: 'flex' }}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Numerator</div>
<input
className="vis_editor__input-grows-100"
onChange={handleTextChange('numerator')}
value={model.numerator}
type="text"/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Denominator</div>
<input
className="vis_editor__input-grows-100"
onChange={handleTextChange('denominator')}
value={model.denominator}
type="text"/>
</div>
</div>
<div style={{ flex: '1 0 auto', display: 'flex', marginTop: '10px' }}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Metric Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType="metrics"
value={model.metric_agg}
onChange={handleSelectChange('metric_agg')}/>
</div>
{ model.metric_agg !== 'count' ? (
<div className="vis_editor__row_item">
<div className="vis_editor__label">Field</div>
<FieldSelect
fields={fields}
type={model.metric_agg}
restrict="numeric"
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}/>
</div>) : null }
</div>
</div>
</AggRow>
);
}
}
FilterRatioAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default FilterRatioAgg;

View file

@ -0,0 +1,64 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import Select from 'react-select';
import calculateSiblings from '../lib/calculate_siblings';
import calculateLabel from '../lib/calculate_label';
import basicAggs from '../lib/basic_aggs';
function createTypeFilter(restrict, exclude) {
return (metric) => {
if (_.includes(exclude, metric.type)) return false;
switch (restrict) {
case 'basic':
return _.includes(basicAggs, metric.type);
default:
return true;
}
};
}
function MetricSelect(props) {
const {
restrict,
metric,
onChange,
value,
exclude
} = props;
const metrics = props.metrics
.filter(createTypeFilter(restrict, exclude));
const options = calculateSiblings(metrics, metric)
.filter(row => !/_bucket$/.test(row.type) && !/^series/.test(row.type))
.map(row => {
const label = calculateLabel(row, metrics);
return { value: row.id, label };
});
return (
<Select
placeholder="Select metric..."
options={options.concat(props.additionalOptions)}
value={value}
onChange={onChange}/>
);
}
MetricSelect.defaultProps = {
additionalOptions: [],
exclude: [],
metric: {},
restrict: 'none',
};
MetricSelect.propTypes = {
additionalOptions: PropTypes.array,
exclude: PropTypes.array,
metric: PropTypes.object,
onChange: PropTypes.func,
restrict: PropTypes.string,
value: PropTypes.string
};
export default MetricSelect;

View file

@ -0,0 +1,118 @@
import React, { Component, PropTypes } from 'react';
import AggRow from './agg_row';
import AggSelect from './agg_select';
import MetricSelect from './metric_select';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import createNumberHandler from '../lib/create_number_handler';
class MovingAverageAgg extends Component {
render() {
const { panel, siblings } = this.props;
const defaults = {
settings: '',
minimize: 0,
window: '',
model: 'simple'
};
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const handleNumberChange = createNumberHandler(handleChange);
const modelOptions = [
{ label: 'Simple', value: 'simple' },
{ label: 'Linear', value: 'linear' },
{ label: 'Exponentially Weighted', value: 'ewma' },
{ label: 'Holt-Linear', value: 'holt' },
{ label: 'Holt-Winters', value: 'holt_winters' }
];
const minimizeOptions = [
{ label: 'True', value: 1 },
{ label: 'False', value: 0 }
];
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Metric</div>
<MetricSelect
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
value={model.field}/>
</div>
</div>
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<div className="vis_editor__label">Model</div>
<Select
clearable={false}
placeholder="Select..."
onChange={ handleSelectChange('model') }
value={this.props.model.model}
options={ modelOptions }/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Window Size</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={handleNumberChange('window')}
value={model.window}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Minimize</div>
<Select
placeholder="Select..."
onChange={ handleSelectChange('minimize') }
value={model.minimize}
options={ minimizeOptions }/>
</div>
</div>
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<div className="vis_editor__label">Settings (<code>Key=Value</code> space seperated)</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={handleTextChange('settings')}
value={model.settings}/>
</div>
</div>
</div>
</AggRow>
);
}
}
MovingAverageAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default MovingAverageAgg;

View file

@ -0,0 +1,190 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import AggRow from './agg_row';
import collectionActions from '../lib/collection_actions';
import calculateSiblings from '../lib/calculate_siblings';
import AddDeleteButtons from '../add_delete_buttons';
import Select from 'react-select';
import uuid from 'node-uuid';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createNumberHandler from '../lib/create_number_handler';
const newPercentile = (opts) => {
return _.assign({ id: uuid.v1(), mode: 'line', shade: 0.2 }, opts);
};
class Percentiles extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleTextChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = _.get(e, 'value', _.get(e, 'target.value'));
handleChange(_.assign({}, item, part));
};
}
handleNumberChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = Number(_.get(e, 'value', _.get(e, 'target.value')));
handleChange(_.assign({}, item, part));
};
}
renderRow(row, i, items) {
const defaults = { value: '', percentile: '', shade: '' };
const model = { ...defaults, ...row };
const handleAdd = collectionActions.handleAdd.bind(null, this.props, newPercentile);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
const modeOptions = [
{ label: 'Line', value: 'line' },
{ label: 'Band', value: 'band' }
];
const optionsStyle = {};
if (model.mode === 'line') {
optionsStyle.display = 'none';
}
return (
<div className="vis_editor__percentiles-row" key={model.id}>
<div className="vis_editor__percentiles-content">
<input
placeholder="Percentile"
className="vis_editor__input-grows"
type="text"
onChange={this.handleNumberChange(model, 'value')}
value={model.value}/>
<div className="vis_editor__label">Mode</div>
<div className="vis_editor__row_item">
<Select
clearable={false}
onChange={this.handleTextChange(model, 'mode')}
options={modeOptions}
value={model.mode}/>
</div>
<div style={optionsStyle} className="vis_editor__label">Fill To</div>
<input
style={optionsStyle}
className="vis_editor__input-grows"
type="text"
onChange={this.handleNumberChange(model, 'percentile')}
value={model.percentile}/>
<div style={optionsStyle} className="vis_editor__label">Shade (0 to 1)</div>
<input
style={optionsStyle}
className="vis_editor__input-grows"
type="text"
onChange={this.handleNumberChange(model, 'shade')}
value={model.shade}/>
</div>
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}/>
</div>
);
}
render() {
const { model, name } = this.props;
if (!model[name]) return (<div/>);
const rows = model[name].map(this.renderRow);
return (
<div className="vis_editor__percentiles">
{ rows }
</div>
);
}
}
Percentiles.defaultProps = {
name: 'percentile'
};
Percentiles.propTypes = {
name: PropTypes.string,
model: PropTypes.object,
onChange: PropTypes.func
};
class PercentileAgg extends Component {
componentWillMount() {
if (!this.props.model.percentiles) {
this.props.onChange(_.assign({}, this.props.model, {
percentiles: [newPercentile({ value: 50 })]
}));
}
}
render() {
const { series, model, panel, fields } = this.props;
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleNumberChange = createNumberHandler(handleChange);
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__agg_row-item">
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Field</div>
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}/>
</div>
</div>
<Percentiles
onChange={handleChange}
name="percentiles"
model={model}/>
</div>
</AggRow>
);
}
}
PercentileAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default PercentileAgg;

View file

@ -0,0 +1,76 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import AggRow from './agg_row';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
class PercentileRankAgg extends Component {
render() {
const { series, panel, fields } = this.props;
const defaults = { value: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Field</div>
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}/>
</div>
<div className="vis_editor__percentile_rank_value">
<div className="vis_editor__label">Value</div>
<input
className="vis_editor__input-grows"
value={model.value}
onChange={handleTextChange('value')}/>
</div>
</AggRow>
);
}
}
PercentileRankAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default PercentileRankAgg;

View file

@ -0,0 +1,71 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import MetricSelect from './metric_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createNumberHandler from '../lib/create_number_handler';
class SerialDiffAgg extends Component {
render() {
const { siblings, panel } = this.props;
const defaults = { lag: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleNumberChange = createNumberHandler(handleChange);
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Metric</div>
<MetricSelect
onChange={handleSelectChange('field')}
metrics={siblings}
metric={model}
value={model.field}/>
</div>
<div>
<div className="vis_editor__label">Lag</div>
<input
className="vis_editor__input"
onChange={handleNumberChange('lag')}
value={model.lag}
type="text"/>
</div>
</AggRow>
);
}
}
SerialDiffAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default SerialDiffAgg;

View file

@ -0,0 +1,66 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import Select from 'react-select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
function SeriesAgg(props) {
const { model, panel, fields } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const functionOptions = [
{ label: 'Sum', value: 'sum' },
{ label: 'Max', value: 'max' },
{ label: 'Min', value: 'min' },
{ label: 'Avg', value: 'mean' },
{ label: 'Overall Sum', value: 'overall_sum' },
{ label: 'Overall Max', value: 'overall_max' },
{ label: 'Overall Min', value: 'overall_min' },
{ label: 'Overall Avg', value: 'overall_avg' },
{ label: 'Cumlative Sum', value: 'cumlative_sum' },
];
return (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}>
<div className="vis_editor__item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__item">
<div className="vis_editor__label">Function</div>
<Select
value={model.function}
options={functionOptions}
onChange={handleSelectChange('function')}/>
</div>
</AggRow>
);
}
SeriesAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default SeriesAgg;

View file

@ -0,0 +1,63 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import AggRow from './agg_row';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
function StandardAgg(props) {
const { model, panel, series, fields } = props;
const handleChange = createChangeHandler(props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
let restrict = 'numeric';
if (model.type === 'cardinality') {
restrict = 'string';
}
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
return (
<AggRow
disableDelete={props.disableDelete}
model={props.model}
onAdd={props.onAdd}
onDelete={props.onDelete}
siblings={props.siblings}>
<div className="vis_editor__item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
{ model.type !== 'count' ? (<div className="vis_editor__item">
<div className="vis_editor__label">Field</div>
<FieldSelect
fields={fields}
type={model.type}
restrict={restrict}
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}/>
</div>) : null }
</AggRow>
);
}
StandardAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default StandardAgg;

View file

@ -0,0 +1,88 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggSelect from './agg_select';
import FieldSelect from './field_select';
import AggRow from './agg_row';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
class StandardDeviationAgg extends Component {
render() {
const { series, panel, fields } = this.props;
const defaults = { sigma: '' };
const model = { ...defaults, ...this.props.model };
const modeOptions = [
{ label: 'Raw', value: 'raw' },
{ label: 'Upper Bound', value: 'upper' },
{ label: 'Lower Bound', value: 'lower' },
{ label: 'Bounds Band', value: 'band' }
];
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const indexPattern = series.override_index_pattern && series.series_index_pattern || panel.index_pattern;
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__std_deviation-field">
<div className="vis_editor__label">Field</div>
<FieldSelect
fields={fields}
type={model.type}
restrict="numeric"
indexPattern={indexPattern}
value={model.field}
onChange={handleSelectChange('field')}/>
</div>
<div className="vis_editor__std_deviation-sigma_item">
<div className="vis_editor__label">Sigma</div>
<input
className="vis_editor__std_deviation-sigma"
value={model.sigma}
onChange={handleTextChange('sigma')}/>
</div>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Mode</div>
<Select
options={modeOptions}
onChange={handleSelectChange('mode')}
value={model.mode}/>
</div>
</AggRow>
);
}
}
StandardDeviationAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default StandardDeviationAgg;

View file

@ -0,0 +1,96 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AggRow from './agg_row';
import MetricSelect from './metric_select';
import AggSelect from './agg_select';
import Select from 'react-select';
import createChangeHandler from '../lib/create_change_handler';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
class StandardSiblingAgg extends Component {
render() {
const { siblings, panel } = this.props;
const defaults = { sigma: '' };
const model = { ...defaults, ...this.props.model };
const handleChange = createChangeHandler(this.props.onChange, model);
const handleSelectChange = createSelectHandler(handleChange);
const handleTextChange = createTextHandler(handleChange);
const stdDev = {};
if (model.type === 'std_deviation_bucket') {
stdDev.sigma = (
<div className="vis_editor__std_deviation-sigma_item">
<div className="vis_editor__label">Sigma</div>
<input
className="vis_editor__std_deviation-sigma"
value={model.sigma}
onChange={handleTextChange('sigma')}/>
</div>
);
const modeOptions = [
{ label: 'Raw', value: 'raw' },
{ label: 'Upper Bound', value: 'upper' },
{ label: 'Lower Bound', value: 'lower' },
{ label: 'Bounds Band', value: 'band' }
];
stdDev.mode = (
<div className="vis_editor__row_item">
<div className="vis_editor__label">Mode</div>
<Select
options={modeOptions}
onChange={handleSelectChange('mode')}
value={model.mode}/>
</div>
);
}
return (
<AggRow
disableDelete={this.props.disableDelete}
model={this.props.model}
onAdd={this.props.onAdd}
onDelete={this.props.onDelete}
siblings={this.props.siblings}>
<div className="vis_editor__row_item">
<div className="vis_editor__label">Aggregation</div>
<AggSelect
siblings={this.props.siblings}
panelType={panel.type}
value={model.type}
onChange={handleSelectChange('type')}/>
</div>
<div className="vis_editor__std_sibling-metric">
<div className="vis_editor__label">Metric</div>
<MetricSelect
onChange={handleSelectChange('field')}
exclude={['percentile']}
metrics={siblings}
metric={model}
value={model.field}/>
</div>
{ stdDev.sigma }
{ stdDev.mode }
</AggRow>
);
}
}
StandardSiblingAgg.propTypes = {
disableDelete: PropTypes.bool,
fields: PropTypes.object,
model: PropTypes.object,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onDelete: PropTypes.func,
panel: PropTypes.object,
series: PropTypes.object,
siblings: PropTypes.array,
};
export default StandardSiblingAgg;

View file

@ -0,0 +1,80 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AddDeleteButtons from '../add_delete_buttons';
import collectionActions from '../lib/collection_actions';
import MetricSelect from './metric_select';
class CalculationVars extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = _.get(e, 'value', _.get(e, 'target.value'));
handleChange(_.assign({}, item, part));
};
}
renderRow(row, i, items) {
const defaults = { name: '' };
const model = { ...defaults, ...row };
const handleAdd = collectionActions.handleAdd.bind(null, this.props);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, row);
return (
<div className="vis_editor__calc_vars-row" key={row.id}>
<div className="vis_editor__calc_vars-name">
<input
placeholder="Variable Name"
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(row, 'name')}
value={row.name} />
</div>
<div className="vis_editor__calc_vars-var">
<MetricSelect
onChange={this.handleChange(row, 'field')}
exclude={['percentile']}
metrics={this.props.metrics}
metric={this.props.model}
value={row.field}/>
</div>
<div className="vis_editor__calc_vars-control">
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}/>
</div>
</div>
);
}
render() {
const { model, name } = this.props;
if (!model[name]) return (<div/>);
const rows = model[name].map(this.renderRow);
return (
<div className="vis_editor__calc_vars">
{ rows }
</div>
);
}
}
CalculationVars.defaultProps = {
name: 'variables'
};
CalculationVars.propTypes = {
metrics: PropTypes.array,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func
};
export default CalculationVars;

View file

@ -0,0 +1,168 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import IndexPattern from './index_pattern';
import collectionActions from './lib/collection_actions';
import AddDeleteButtons from './add_delete_buttons';
import ColorPicker from './color_picker';
import FieldSelect from './aggs/field_select';
import uuid from 'node-uuid';
import IconSelect from './icon_select';
function newAnnotation() {
return {
id: uuid.v1(),
color: '#F00',
index_pattern: '*',
time_field: '@timestamp',
icon: 'fa-tag'
};
}
class AnnotationsEditor extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = _.get(e, 'value', _.get(e, 'target.value'));
handleChange(_.assign({}, item, part));
};
}
renderRow(row, i, items) {
const { fields } = this.props;
const defaults = { fields: '', template: '', index_pattern: '*', query_string: '' };
const model = { ...defaults, ...row };
const handleChange = (part) => {
const fn = collectionActions.handleChange.bind(null, this.props);
fn(_.assign({}, model, part));
};
const handleAdd = collectionActions.handleAdd
.bind(null, this.props, newAnnotation);
const handleDelete = collectionActions.handleDelete
.bind(null, this.props, model);
return (
<div className="vis_editor__annotations-row" key={model.id}>
<div className="vis_editor__annotations-color">
<ColorPicker
disableTrash={true}
onChange={handleChange}
name="color"
value={model.color}/>
</div>
<div className="vis_editor__annotations-content">
<div className="vis_editor__row">
<div className="vis_editor__row-item">
<div className="vis_editor__label">Index Pattern (required)</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'index_pattern')}
value={model.index_pattern} />
</div>
<div className="vis_editor__row-item">
<div className="vis_editor__label">Time Field (required)</div>
<FieldSelect
restrict="date"
value={model.time_field}
onChange={this.handleChange(model, 'time_field')}
indexPattern={model.index_pattern}
fields={this.props.fields}/>
</div>
</div>
<div className="vis_editor__row">
<div className="vis_editor__row-item">
<div className="vis_editor__label">Query String</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'query_string')}
value={model.query_string} />
</div>
</div>
<div className="vis_editor__row">
<div className="vis_editor__row-item">
<div className="vis_editor__label">Icon (required)</div>
<div className="vis_editor__item">
<IconSelect
value={model.icon}
onChange={this.handleChange(model, 'icon')} />
</div>
</div>
<div className="vis_editor__row-item">
<div className="vis_editor__label">Fields (required - comma separated paths)</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'fields')}
value={model.fields} />
</div>
<div className="vis_editor__row-item">
<div className="vis_editor__label">Row Template (required - eg.<code>{'{{field}}'}</code>)</div>
<input
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'template')}
value={model.template} />
</div>
</div>
</div>
<div className="vis_editor__annotations-controls">
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete} />
</div>
</div>
);
}
render() {
const { model } = this.props;
let content;
if (!model.annotations || !model.annotations.length) {
const handleAdd = collectionActions.handleAdd
.bind(null, this.props, newAnnotation);
content = (
<div className="vis_editor__annotations-missing">
<p>Click the button below to create an annotation data source.</p>
<a className="thor__button-outlined-default large"
onClick={handleAdd}>Add Data Source</a>
</div>
);
} else {
const annotations = model.annotations.map(this.renderRow);
content = (
<div className="vis_editor__annotations">
<div className="kbnTabs sm">
<div className="kbnTabs__tab-active">Data Sources</div>
</div>
{ annotations }
</div>
);
}
return(
<div className="vis_editor__container">
{ content }
</div>
);
}
}
AnnotationsEditor.defaultProps = {
name: 'annotations'
};
AnnotationsEditor.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func
};
export default AnnotationsEditor;

View file

@ -0,0 +1,95 @@
import React, { Component, PropTypes } from 'react';
import Tooltip from './tooltip';
import CustomColorPicker from './custom_color_picker';
const Picker = CustomColorPicker;
class ColorPicker extends Component {
constructor(props) {
super(props);
this.state = {
displayPlicker: false,
color: {}
};
this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleClear = this.handleClear.bind(this);
this.handleClose = this.handleClose.bind(this);
}
handleChange(color) {
const { rgb, hex } = color;
const part = {};
part[this.props.name] = `rgba(${rgb.r},${rgb.g},${rgb.b},${rgb.a})`;
if (this.props.onChange) this.props.onChange(part);
}
handleClick() {
this.setState({ displayPicker: !this.state.displayColorPicker });
}
handleClose() {
this.setState({ displayPicker: false });
}
handleClear() {
const part = {};
part[this.props.name] = null;
this.props.onChange(part);
}
renderSwatch() {
if (!this.props.value) {
return (
<div
className="vis_editor__color_picker-swatch-empty"
onClick={this.handleClick}/>
);
}
return (
<div
style={{ backgroundColor: this.props.value }}
className="vis_editor__color_picker-swatch"
onClick={this.handleClick}/>
);
}
render() {
const swatch = this.renderSwatch();
const value = this.props.value || undefined;
let clear;
if (!this.props.disableTrash) {
clear = (
<div className="vis_editor__color_picker-clear" onClick={this.handleClear}>
<Tooltip text="Clear">
<i className="fa fa-ban"/>
</Tooltip>
</div>
);
}
return (
<div className="vis_editor__color_picker">
{ swatch }
{ clear }
{ this.state.displayPicker ? <div className="vis_editor__color_picker-popover">
<div className="vis_editor__color_picker-cover"
onClick={this.handleClose}/>
<Picker
color={ value }
onChangeComplete={this.handleChange} />
</div> : null }
</div>
);
}
}
ColorPicker.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string,
disableTrash: PropTypes.bool,
onChange: PropTypes.func
};
export default ColorPicker;

View file

@ -0,0 +1,115 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import AddDeleteButtons from './add_delete_buttons';
import Select from 'react-select';
import collectionActions from './lib/collection_actions';
import ColorPicker from './color_picker';
class ColorRules extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name, cast = String) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
const part = {};
part[name] = cast(_.get(e, 'value', _.get(e, 'target.value')));
if (part[name] === 'undefined') part[name] = undefined;
handleChange(_.assign({}, item, part));
};
}
renderRow(row, i, items) {
const defaults = { value: '' };
const model = { ...defaults, ...row };
const handleAdd = collectionActions.handleAdd.bind(null, this.props);
const handleDelete = collectionActions.handleDelete.bind(null, this.props, model);
const operatorOptions = [
{ label: '> greater then', value: 'gt' },
{ label: '>= greater then or equal', value: 'gte' },
{ label: '< less then', value: 'lt' },
{ label: '<= less then or equal', value: 'lte' },
];
const handleColorChange = (part) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
handleChange(_.assign({}, model, part));
};
let secondary;
if (!this.props.hideSecondary) {
secondary = (
<div className="color_rules__secondary">
<div className="color_rules__label">and {this.props.secondaryName} to</div>
<ColorPicker
onChange={handleColorChange}
name={this.props.secondaryVarName}
value={model[this.props.secondaryVarName]}/>
</div>
);
}
return (
<div key={model.id} className="color_rules__rule">
<div className="color_rules__label">Set {this.props.primaryName} to</div>
<ColorPicker
onChange={handleColorChange}
name={this.props.primaryVarName}
value={model[this.props.primaryVarName]}/>
{ secondary }
<div className="color_rules__label">if metric is</div>
<div className="color_rules__item">
<Select
onChange={this.handleChange(model, 'opperator')}
value={model.opperator}
options={operatorOptions}/>
</div>
<input
className="color_rules__input"
type="text"
value={model.value}
onChange={this.handleChange(model, 'value', Number)}/>
<div className="color_rules__control">
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}/>
</div>
</div>
);
}
render() {
const { model, name } = this.props;
if (!model[name]) return (<div/>);
const rows = model[name].map(this.renderRow);
return (
<div className="color_rules">
{ rows }
</div>
);
}
}
ColorRules.defaultProps = {
name: 'color_rules',
primaryName: 'background',
primaryVarName: 'background_color',
secondaryName: 'text',
secondaryVarName: 'color',
hideSecondary: false
};
ColorRules.propTypes = {
name: PropTypes.string,
model: PropTypes.object,
onChange: PropTypes.func,
primaryName: PropTypes.string,
primaryVarName: PropTypes.string,
secondaryName: PropTypes.string,
secondaryVarName: PropTypes.string,
hideSecondary: PropTypes.bool
};
export default ColorRules;

View file

@ -0,0 +1,131 @@
import React, { Component, PropTypes } from 'react';
import { ColorWrap as colorWrap, Saturation, Hue, Alpha, Checkboard } from 'react-color/lib/components/common';
import ChromeFields from 'react-color/lib/components/chrome/ChromeFields';
import ChromePointer from 'react-color/lib/components/chrome/ChromePointer';
import ChromePointerCircle from 'react-color/lib/components/chrome/ChromePointerCircle';
import CompactColor from 'react-color/lib/components/compact/CompactColor';
import color from 'react-color/lib/helpers/color';
import shallowCompare from 'react-addons-shallow-compare';
export class CustomColorPicker extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(nextProps, nextState);
}
handleChange(data) {
this.props.onChange(data);
}
render() {
const rgb = this.props.rgb;
const styles = {
active: {
background: `rgba(${ rgb.r }, ${ rgb.g }, ${ rgb.b }, ${ rgb.a })`,
},
Saturation: {
radius: '2px 2px 0 0 '
},
Hue: {
radius: '2px',
},
Alpha: {
radius: '2px',
}
};
const handleSwatchChange = (data) => {
if (data.hex) {
color.isValidHex(data.hex) && this.props.onChange({
hex: data.hex,
source: 'hex',
});
} else {
this.props.onChange(data);
}
};
const swatches = this.props.colors.map((c) => {
return (
<CompactColor
key={c}
color={c}
onClick={handleSwatchChange}/>
);
});
return (
<div className="custom-picker color_picker">
<div className="color_picker__saturation">
<Saturation
style={styles.Saturation}
{ ...this.props }
pointer={ChromePointerCircle}
onChange={this.handleChange}
/>
</div>
<div className="color_picker__body">
<div className="color_picker__controls flexbox-fix">
<div className={ this.props.disableAlpha ? 'color_picker__color-disable_alpha' : 'color_picker__color' }>
<div className={ this.props.disableAlpha ? 'color_picker__swatch-disable_alpha' : 'color_picker__swatch' }>
<div className="color_picker__active" />
<Checkboard />
</div>
</div>
<div className="color_picker__toggles">
<div className={ this.props.disableAlpha ? 'color_picker__hue-disable_alpha' : 'color_picker__hue' }>
<Hue
style={styles.Hue}
{...this.props}
pointer={ChromePointer}
onChange={this.handleChange}
/>
</div>
<div className={ this.props.disableAlpha ? 'color_picker__alpha-disable_alpha' : 'color_picker__alpha'}>
<Alpha
style={styles.Alpha}
{...this.props}
pointer={ChromePointer}
onChange={this.handleChange}
/>
</div>
</div>
</div>
<ChromeFields
{...this.props}
onChange={this.handleChange}
disableAlpha={this.props.disableAlpha}
/>
<div className="color_picker__swatches flexbox-fix">
{swatches}
</div>
</div>
</div>
);
}
}
CustomColorPicker.defaultProps = {
colors: [
'#4D4D4D', '#999999', '#FFFFFF', '#F44E3B', '#FE9200', '#FCDC00',
'#DBDF00', '#A4DD00', '#68CCCA', '#73D8FF', '#AEA1FF', '#FDA1FF',
'#333333', '#808080', '#cccccc', '#D33115', '#E27300', '#FCC400',
'#B0BC00', '#68BC00', '#16A5A5', '#009CE0', '#7B64FF', '#FA28FF',
'#0F1419', '#666666', '#B3B3B3', '#9F0500', '#C45100', '#FB9E00',
'#808900', '#194D33', '#0C797D', '#0062B1', '#653294', '#AB149E',
],
};
CustomColorPicker.propTypes = {
color: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
onChangeComplete: PropTypes.func,
onChange: PropTypes.func
};
export default colorWrap(CustomColorPicker);

View file

@ -0,0 +1,83 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import Select from 'react-select';
class DataFormatPicker extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleCustomChange = this.handleCustomChange.bind(this);
}
handleCustomChange() {
this.props.onChange({ value: this.custom && this.custom.value || '' });
}
handleChange(value) {
if (value.value === 'custom') {
this.handleCustomChange();
} else {
this.props.onChange(value);
}
}
render() {
const value = this.props.value || '';
let defaultValue = value;
if (!_.includes(['bytes', 'number', 'percent'], value)) {
defaultValue = 'custom';
}
const options = [
{ label: 'Bytes', value: 'bytes' },
{ label: 'Number', value: 'number' },
{ label: 'Percent', value: 'percent' },
{ label: 'Custom', value: 'custom' }
];
let custom;
if (defaultValue === 'custom') {
custom = (
<div className="vis_editor__data_format_picker-custom_row">
<div className="vis_editor__label">
Format String (See <a href="http://numeraljs.com/" target="_BLANK">Numeral.js</a>)
</div>
<input
className="vis_editor__input"
defaultValue={value}
ref={(el) => this.custom = el}
onChange={this.handleCustomChange}
type="text"/>
</div>
);
}
return (
<div className="vis_editor__data_format_picker-container">
<div className="vis_editor__label">
{this.props.label}
</div>
<div className="vis_editor__item">
<Select
clearable={false}
value={defaultValue}
options={options}
onChange={this.handleChange}/>
</div>
{custom}
</div>
);
}
}
DataFormatPicker.defaultProps = {
label: 'Data Formatter'
};
DataFormatPicker.propTypes = {
value: PropTypes.string,
label: PropTypes.string,
onChange: PropTypes.func
};
export default DataFormatPicker;

View file

@ -0,0 +1,40 @@
import React, { PropTypes } from 'react';
import reactcss from 'reactcss';
import _ from 'lodash';
function ErrorComponent(props) {
const { error } = props;
let additionalInfo;
const type = _.get(error, 'error.caused_by.type');
if (type === 'script_exception') {
const scriptStack = _.get(error, 'error.caused_by.script_stack');
const reason = _.get(error, 'error.caused_by.caused_by.reason');
additionalInfo = (
<div className="metrics_error__additional">
<div className="metrics_error__reason">{ reason }</div>
<div className="metrics_error__stack">{ scriptStack.join('\n')}</div>
</div>
);
} else {
const reason = _.get(error, 'error.caused_by.reason');
additionalInfo = (
<div className="metrics_error__additional">
<div className="metrics_error__reason">{ reason }</div>
</div>
);
}
return (
<div className="metrics_error">
<div className="merics_error__title">The request for this panel failed.</div>
{ additionalInfo }
</div>
);
}
ErrorComponent.propTypes = {
error: PropTypes.object
};
export default ErrorComponent;

View file

@ -0,0 +1,116 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
class IconOption extends Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
}
handleMouseDown(event) {
event.preventDefault();
event.stopPropagation();
this.props.onSelect(this.props.option, event);
}
handleMouseEnter(event) {
this.props.onFocus(this.props.option, event);
}
handleMouseMove(event) {
if (this.props.isFocused) return;
this.props.onFocus(this.props.option, event);
}
render() {
const icon = this.props.option.value;
const title = this.props.option.label;
return (
<div className={this.props.className}
onMouseEnter={this.handleMouseEnter}
onMouseDown={this.handleMouseDown}
onMouseMove={this.handleMouseMove}
title={title}>
<span className="Select-value-label">
<i className={`vis_editor__icon_select-option fa ${icon}`}></i>
{ this.props.children }
</span>
</div>
);
}
}
IconOption.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
isDisabled: PropTypes.bool,
isFocused: PropTypes.bool,
isSelected: PropTypes.bool,
onFocus: PropTypes.func,
onSelect: PropTypes.func,
option: PropTypes.object.isRequired,
};
function IconValue(props) {
const icon = props.value && props.value.value;
const label = props.value && props.value.label;
return (
<div className="Select-value" title={label}>
<span className="Select-value-label">
<i className={`vis_editor__icon_select-value fa ${icon}`}></i>
{ props.children }
</span>
</div>
);
}
IconValue.propTypes = {
children: PropTypes.node,
placeholder: PropTypes.string,
value: PropTypes.object.isRequired
};
function IconSelect(props) {
return (
<Select
clearable={false}
onChange={props.onChange}
value={props.value}
optionComponent={IconOption}
valueComponent={IconValue}
options={props.icons} />
);
}
IconSelect.defaultProps = {
icons: [
{ value: 'fa-asterisk', label: 'Asterisk' },
{ value: 'fa-bell', label: 'Bell' },
{ value: 'fa-bolt', label: 'Bolt' },
{ value: 'fa-bomb', label: 'Bomb' },
{ value: 'fa-bug', label: 'Bug' },
{ value: 'fa-comment', label: 'Comment' },
{ value: 'fa-exclamation-circle', label: 'Exclamation Circle' },
{ value: 'fa-exclamation-triangle', label: 'Exclamation Triangle' },
{ value: 'fa-fire', label: 'Fire' },
{ value: 'fa-flag', label: 'Flag' },
{ value: 'fa-heart', label: 'Heart' },
{ value: 'fa-map-marker', label: 'Map Marker' },
{ value: 'fa-map-pin', label: 'Map Pin' },
{ value: 'fa-star', label: 'Star' },
{ value: 'fa-tag', label: 'Tag' },
]
};
IconSelect.propTypes = {
icons: PropTypes.array,
onChange: PropTypes.func,
value: PropTypes.string.isRequired
};
export default IconSelect;

View file

@ -0,0 +1,66 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import FieldSelect from './aggs/field_select';
import createSelectHandler from './lib/create_select_handler';
import createTextHandler from './lib/create_text_handler';
class IndexPattern extends Component {
render() {
const { fields, prefix } = this.props;
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const timeFieldName = `${prefix}time_field`;
const indexPatternName = `${prefix}index_pattern`;
const intervalName = `${prefix}interval`;
const defaults = {
[indexPatternName]: '*',
[intervalName]: 'auto'
};
const model = { ...defaults, ...this.props.model };
return (
<div className={this.props.className}>
<div className="vis_editor__label">Index Pattern</div>
<input
className="vis_editor__input"
disabled={this.props.disabled}
onChange={handleTextChange(indexPatternName, '*')}
value={model[indexPatternName]}/>
<div className="vis_editor__label">Time Field</div>
<div className="vis_editor__index_pattern-fields">
<FieldSelect
restrict="date"
value={model[timeFieldName]}
disabled={this.props.disabled}
onChange={handleSelectChange(timeFieldName)}
indexPattern={model[indexPatternName]}
fields={fields}/>
</div>
<div className="vis_editor__label">Interval (auto, 1m, 1d, 1w, 1y)</div>
<input
className="vis_editor__input"
disabled={this.props.disabled}
onChange={handleTextChange(intervalName, 'auto')}
value={model[intervalName]}/>
</div>
);
}
}
IndexPattern.defaultProps = {
prefix: '',
disabled: false,
className: 'vis_editor__row'
};
IndexPattern.propTypes = {
model: PropTypes.object.isRequired,
fields: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
prefix: PropTypes.string,
disabled: PropTypes.bool,
className: PropTypes.string
};
export default IndexPattern;

View file

@ -0,0 +1,45 @@
import { expect } from 'chai';
import { createOptions, isBasicAgg } from '../agg_lookup';
describe('aggLookup', () => {
describe('isBasicAgg(metric)', () => {
it('returns true for a basic metric (count)', () => {
expect(isBasicAgg({ type: 'count' })).to.equal(true);
});
it('returns false for a pipeline metric (derivative)', () => {
expect(isBasicAgg({ type: 'derivative' })).to.equal(false);
});
});
describe('createOptions(type, siblings)', () => {
it('returns options for all aggs', () => {
const options = createOptions();
expect(options).to.have.length(26);
options.forEach((option) => {
expect(option).to.have.property('label');
expect(option).to.have.property('value');
expect(option).to.have.property('disabled');
});
});
it('returns options for basic', () => {
const options = createOptions('basic');
expect(options).to.have.length(13);
expect(options.every(opt => isBasicAgg({ type: opt.value }))).to.equal(true);
});
it('returns options for pipeline', () => {
const options = createOptions('pipeline');
expect(options).to.have.length(13);
expect(options.every(opt => !isBasicAgg({ type: opt.value }))).to.equal(true);
});
it('returns options for all if given unknown key', () => {
const options = createOptions('foo');
expect(options).to.have.length(26);
});
});
});

View file

@ -0,0 +1,62 @@
import { expect } from 'chai';
import calculateLabel from '../calculate_label';
describe('calculateLabel(metric, metrics)', () => {
it('returns "Unkonwn" for empty metric', () => {
expect(calculateLabel()).to.equal('Unknown');
});
it('returns the metric.alias if set', () => {
expect(calculateLabel({ alias: 'Example' })).to.equal('Example');
});
it('returns "Count" for a count metric', () => {
expect(calculateLabel({ type: 'count' })).to.equal('Count');
});
it('returns "Calcuation" for a bucket script metric', () => {
expect(calculateLabel({ type: 'calculation' })).to.equal('Calculation');
});
it('returns formated label for series_agg', () => {
const label = calculateLabel({ type: 'series_agg', function: 'max' });
expect(label).to.equal('Series Agg (max)');
});
it('returns formated label for basic aggs', () => {
const label = calculateLabel({ type: 'avg', field: 'memory' });
expect(label).to.equal('Average of memory');
});
it('returns formated label for pipeline aggs', () => {
const metric = { id: 2, type: 'derivative', field: 1 };
const metrics = [
{ id: 1, type: 'max', field: 'network.out.bytes' },
metric
];
const label = calculateLabel(metric, metrics);
expect(label).to.equal('Derivative of Max of network.out.bytes');
});
it('returns formated label for pipeline aggs (deep)', () => {
const metric = { id: 3, type: 'derivative', field: 2 };
const metrics = [
{ id: 1, type: 'max', field: 'network.out.bytes' },
{ id: 2, type: 'moving_average', field: 1 },
metric
];
const label = calculateLabel(metric, metrics);
expect(label).to.equal('Derivative of Moving Average of Max of network.out.bytes');
});
it('returns formated label for pipeline aggs uses alias for field metric', () => {
const metric = { id: 2, type: 'derivative', field: 1 };
const metrics = [
{ id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
metric
];
const label = calculateLabel(metric, metrics);
expect(label).to.equal('Derivative of Outbound Traffic');
});
});

View file

@ -0,0 +1,19 @@
import calculateSiblings from '../calculate_siblings';
import { expect } from 'chai';
describe('calculateSiblings(metrics, metric)', () => {
it('should return all siblings', () => {
const metrics = [
{ id: 1, type: 'max', field: 'network.bytes' },
{ id: 2, type: 'derivative', field: 1 },
{ id: 3, type: 'derivative', field: 2 },
{ id: 4, type: 'moving_average', field: 2 },
{ id: 5, type: 'count' }
];
const siblings = calculateSiblings(metrics, { id: 2 });
expect(siblings).to.eql([
{ id: 1, type: 'max', field: 'network.bytes' },
{ id: 5, type: 'count' }
]);
});
});

View file

@ -0,0 +1,58 @@
import sinon from 'sinon';
import { expect } from 'chai';
import {
handleChange,
handleAdd,
handleDelete
} from '../collection_actions';
describe('collection actions', () => {
it('handleChange() calls props.onChange() with updated collection', () => {
const fn = sinon.spy();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
name: 'test',
onChange: fn
};
handleChange.call(null, props, { id: 1, title: 'bar' });
expect(fn.calledOnce).to.equal(true);
expect(fn.firstCall.args[0]).to.eql({
test: [{ id:1, title: 'bar' }]
});
});
it('handleAdd() calls props.onChange() with update collection', () => {
const newItemFn = sinon.stub().returns({ id: 2, title: 'example' });
const fn = sinon.spy();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
name: 'test',
onChange: fn
};
handleAdd.call(null, props, newItemFn);
expect(fn.calledOnce).to.equal(true);
expect(newItemFn.calledOnce).to.equal(true);
expect(fn.firstCall.args[0]).to.eql({
test: [{ id:1, title: 'foo' }, { id: 2, title: 'example' }]
});
});
it('handleDelete() calls props.onChange() with update collection', () => {
const fn = sinon.spy();
const props = {
model: { test: [{ id: 1, title: 'foo' }] },
name: 'test',
onChange: fn
};
handleDelete.call(null, props, { id: 1 });
expect(fn.calledOnce).to.equal(true);
expect(fn.firstCall.args[0]).to.eql({
test: []
});
});
});

View file

@ -0,0 +1,8 @@
import convertSeriesToVars from '../convert_series_to_vars';
import { expect } from 'chai';
describe('convertSeriesToVars(series, model)', () => {
it('returns and object', () => {
});
});

View file

@ -0,0 +1,27 @@
import sinon from 'sinon';
import { expect } from 'chai';
import createNumberHandler from '../create_number_handler';
describe('createNumberHandler()', () => {
let handleChange;
let changeHandler;
let event;
beforeEach(() => {
handleChange = sinon.spy();
changeHandler = createNumberHandler(handleChange);
event = { preventDefault: sinon.spy(), target: { value: '1' } };
const fn = changeHandler('test');
fn(event);
});
it('calls handleChange() funciton with partial', () => {
expect(event.preventDefault.calledOnce).to.equal(true);
expect(handleChange.calledOnce).to.equal(true);
expect(handleChange.firstCall.args[0]).to.eql({
test: 1
});
});
});

View file

@ -0,0 +1,26 @@
import sinon from 'sinon';
import { expect } from 'chai';
import createSelectHandler from '../create_select_handler';
describe('createSelectHandler()', () => {
let handleChange;
let changeHandler;
let event;
beforeEach(() => {
handleChange = sinon.spy();
changeHandler = createSelectHandler(handleChange);
const fn = changeHandler('test');
fn({ value: 'foo' });
});
it('calls handleChange() funciton with partial', () => {
expect(handleChange.calledOnce).to.equal(true);
expect(handleChange.firstCall.args[0]).to.eql({
test: 'foo'
});
});
});

View file

@ -0,0 +1,28 @@
import sinon from 'sinon';
import { expect } from 'chai';
import createTextHandler from '../create_text_handler';
describe('createTextHandler()', () => {
let handleChange;
let changeHandler;
let event;
beforeEach(() => {
handleChange = sinon.spy();
changeHandler = createTextHandler(handleChange);
event = { preventDefault: sinon.spy(), target: { value: 'foo' } };
const fn = changeHandler('test');
fn(event);
});
it('calls handleChange() funciton with partial', () => {
expect(event.preventDefault.calledOnce).to.equal(true);
expect(handleChange.calledOnce).to.equal(true);
expect(handleChange.firstCall.args[0]).to.eql({
test: 'foo'
});
});
});

View file

@ -0,0 +1,53 @@
import generateByTypeFilter from '../generate_by_type_filter';
import { expect } from 'chai';
describe('generateByTypeFilter()', () => {
describe('numeric', () => {
const fn = generateByTypeFilter('numeric');
[
'scaled_float',
'half_float',
'integer',
'float',
'long',
'double'
].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('string', () => {
const fn = generateByTypeFilter('string');
['string', 'keyword', 'text'].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('date', () => {
const fn = generateByTypeFilter('date');
['date'].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
describe('all', () => {
const fn = generateByTypeFilter('all');
[
'scaled_float',
'half_float',
'integer',
'float',
'long',
'double',
'string',
'text',
'keyword',
'date',
'whatever'
].forEach((type) => {
it(`should return true for ${type}`, () => expect(fn({ type })).to.equal(true));
});
});
});

View file

@ -0,0 +1,78 @@
import uuid from 'node-uuid';
import { expect } from 'chai';
import reIdSeries from '../re_id_series';
describe('reIdSeries()', () => {
it('reassign ids for series with just basic metrics', () => {
const series = {
id: uuid.v1(),
metrics: [
{ id: uuid.v1() },
{ id: uuid.v1() }
]
};
const newSeries = reIdSeries(series);
expect(newSeries).to.not.equal(series);
expect(newSeries.id).to.not.equal(series.id);
newSeries.metrics.forEach((val, key) => {
expect(val.id).to.not.equal(series.metrics[key].id);
});
});
it('reassign ids for series with just basic metrics and group by', () => {
const firstMetricId = uuid.v1();
const series = {
id: uuid.v1(),
metrics: [
{ id: firstMetricId },
{ id: uuid.v1() }
],
terms_order_by: firstMetricId
};
const newSeries = reIdSeries(series);
expect(newSeries).to.not.equal(series);
expect(newSeries.id).to.not.equal(series.id);
newSeries.metrics.forEach((val, key) => {
expect(val.id).to.not.equal(series.metrics[key].id);
});
expect(newSeries.terms_order_by).to.equal(newSeries.metrics[0].id);
});
it('reassign ids for series with pipeline metrics', () => {
const firstMetricId = uuid.v1();
const series = {
id: uuid.v1(),
metrics: [
{ id: firstMetricId },
{ id: uuid.v1(), field: firstMetricId }
]
};
const newSeries = reIdSeries(series);
expect(newSeries).to.not.equal(series);
expect(newSeries.id).to.not.equal(series.id);
expect(newSeries.metrics[0].id).to.equal(newSeries.metrics[1].field);
});
it('reassign ids for series with calculation vars', () => {
const firstMetricId = uuid.v1();
const series = {
id: uuid.v1(),
metrics: [
{ id: firstMetricId },
{
id: uuid.v1(),
type: 'calculation',
variables: [{ id: uuid.v1(), field: firstMetricId }]
}
]
};
const newSeries = reIdSeries(series);
expect(newSeries).to.not.equal(series);
expect(newSeries.id).to.not.equal(series.id);
expect(newSeries.metrics[1].variables[0].field).to.equal(newSeries.metrics[0].id);
});
});

View file

@ -0,0 +1,23 @@
import { expect } from 'chai';
import replaceVars from '../replace_vars';
describe('replaceVars(str, args, vars)', () => {
it('replaces vars with values', () => {
const vars = { total: 100 };
const args = { host: 'test-01' };
const template = '# {{args.host}} {{total}}';
expect(replaceVars(template, args, vars)).to.equal('# test-01 100');
});
it('replaces args override vars', () => {
const vars = { total: 100, args: { test: 'foo-01' } };
const args = { test: 'bar-01' };
const template = '# {{args.test}} {{total}}';
expect(replaceVars(template, args, vars)).to.equal('# bar-01 100');
});
it('returns original string if error', () => {
const vars = { total: 100 };
const args = { host: 'test-01' };
const template = '# {{args.host}} {{total';
expect(replaceVars(template, args, vars)).to.equal('# {{args.host}} {{total');
});
});

View file

@ -0,0 +1,47 @@
import { expect } from 'chai';
import tickFormatter from '../tick_formatter';
describe('tickFormatter(format, template)', () => {
it('returns a number with two decimal place by default', () => {
const fn = tickFormatter();
expect(fn(1.5556)).to.equal('1.56');
});
it('returns a percent with percent formatter', () => {
const fn = tickFormatter('percent');
expect(fn(0.5556)).to.equal('55.56%');
});
it('returns a byte formatted string with byte formatter', () => {
const fn = tickFormatter('bytes');
expect(fn(1500 ^ 10)).to.equal('1.5KB');
});
it('returns a custom forrmatted string with custom formatter', () => {
const fn = tickFormatter('0.0a');
expect(fn(1500)).to.equal('1.5k');
});
it('returns a custom forrmatted string with custom formatter and template', () => {
const fn = tickFormatter('0.0a', '{{value}}/s');
expect(fn(1500)).to.equal('1.5k/s');
});
it('returns zero if passed a string', () => {
const fn = tickFormatter();
expect(fn('100')).to.equal('0');
});
it('returns value if passed a bad formatter', () => {
const fn = tickFormatter('102');
expect(fn(100)).to.equal('100');
});
it('returns formatted value if passed a bad template', () => {
const fn = tickFormatter('number', '{{value');
expect(fn(1.5556)).to.equal('1.56');
});
});

View file

@ -0,0 +1,78 @@
import _ from 'lodash';
const lookup = {
'count': 'Count',
'calculation': 'Calculation',
'std_deviation': 'Std. Deviation',
'variance': 'Variance',
'sum_of_squares': 'Sum of Sq.',
'avg': 'Average',
'max': 'Max',
'min': 'Min',
'sum': 'Sum',
'percentile': 'Percentile',
'percentile_rank': 'Percentile Rank',
'cardinality': 'Cardinality',
'value_count': 'Value Count',
'derivative': 'Derivative',
'cumulative_sum': 'Cumulative Sum',
'moving_average': 'Moving Average',
'avg_bucket': 'Overall Average',
'min_bucket': 'Overall Min',
'max_bucket': 'Overall Max',
'sum_bucket': 'Overall Sum',
'variance_bucket': 'Overall Variance',
'sum_of_squares_bucket': 'Overall Sum of Sq.',
'std_deviation_bucket': 'Overall Std. Deviation',
'series_agg': 'Series Agg',
'serial_diff': 'Serial Difference',
'filter_ratio': 'Filter Ratio'
};
const pipeline = [
'calculation',
'derivative',
'cumulative_sum',
'moving_average',
'avg_bucket',
'min_bucket',
'max_bucket',
'sum_bucket',
'variance_bucket',
'sum_of_squares_bucket',
'std_deviation_bucket',
'series_agg',
'serial_diff'
];
const byType = {
_all: lookup,
pipeline: pipeline,
basic: _.omit(lookup, pipeline),
metrics: _.pick(lookup, [
'count',
'avg',
'min',
'max',
'sum',
'cardinality',
'value_count'
])
};
export function isBasicAgg(item) {
return _.includes(Object.keys(byType.basic), item.type);
}
export function createOptions(type = '_all', siblings = []) {
let aggs = byType[type];
if (!aggs) aggs = byType._all;
return _(aggs)
.map((label, value) => {
const disabled = false;
return { label, value, disabled };
})
.sortBy('label')
.value();
}
export default lookup;

View file

@ -0,0 +1,42 @@
import MovingAverage from '../aggs/moving_average';
import Derivative from '../aggs/derivative';
import Calculation from '../aggs/calculation';
import StdAgg from '../aggs/std_agg';
import Percentile from '../aggs/percentile';
import CumulativeSum from '../aggs/cumulative_sum';
import StdDeviation from '../aggs/std_deviation';
import StdSibling from '../aggs/std_sibling';
import SeriesAgg from '../aggs/series_agg';
import SerialDiff from '../aggs/serial_diff';
import FilterRatio from '../aggs/filter_ratio';
import PercentileRank from '../aggs/percentile_rank';
export default {
count: StdAgg,
avg: StdAgg,
max: StdAgg,
min: StdAgg,
sum: StdAgg,
std_deviation: StdDeviation,
sum_of_squares: StdAgg,
variance: StdAgg,
avg_bucket: StdSibling,
max_bucket: StdSibling,
min_bucket: StdSibling,
sum_bucket: StdSibling,
variance_bucket: StdSibling,
sum_of_squares_bucket: StdSibling,
std_deviation_bucket: StdSibling,
percentile: Percentile,
percentile_rank: PercentileRank,
cardinality: StdAgg,
value_count: StdAgg,
calculation: Calculation,
cumulative_sum: CumulativeSum,
moving_average: MovingAverage,
derivative: Derivative,
series_agg: SeriesAgg,
serial_diff: SerialDiff,
filter_ratio: FilterRatio
};

View file

@ -0,0 +1,12 @@
export default [
'count',
'avg',
'max',
'min',
'sum',
'std_deviation',
'variance',
'sum_of_squares',
'value_count',
'cardinality'
];

View file

@ -0,0 +1,37 @@
import _ from 'lodash';
import lookup from './agg_lookup';
const paths = [
'cumulative_sum',
'derivative',
'moving_average',
'avg_bucket',
'sum_bucket',
'min_bucket',
'max_bucket',
'std_deviation_bucket',
'variance_bucket',
'sum_of_squares_bucket',
'serial_diff'
];
export default function calculateLabel(metric, metrics) {
if (!metric) return 'Unknown';
if (metric.alias) return metric.alias;
if (metric.type === 'count') return 'Count';
if (metric.type === 'calculation') return 'Calculation';
if (metric.type === 'series_agg') return `Series Agg (${metric.function})`;
if (metric.type === 'filter_ratio') return 'Filter Ratio';
if (metric.type === 'percentile_rank') {
return `${lookup[metric.type]} (${metric.value}) of ${metric.field}`;
}
if (_.includes(paths, metric.type)) {
const targetMetric = _.find(metrics, { id: metric.field });
const targetLabel = calculateLabel(targetMetric, metrics);
return `${lookup[metric.type]} of ${targetLabel}`;
}
return `${lookup[metric.type]} of ${metric.field}`;
}

View file

@ -0,0 +1,17 @@
import _ from 'lodash';
function getAncestors(siblings, item) {
const ancestors = item.id && [item.id] || [];
siblings.forEach((sib) => {
if (_.includes(ancestors, sib.field)) {
ancestors.push(sib.id);
}
});
return ancestors;
}
export default (siblings, model) => {
const ancestors = getAncestors(siblings, model);
return siblings.filter(row => !_.includes(ancestors, row.id));
};

View file

@ -0,0 +1,38 @@
import uuid from 'node-uuid';
import _ from 'lodash';
export function handleChange(props, doc) {
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.map(row => {
if (row.id === doc.id) return doc;
return row;
});
if (_.isFunction(props.onChange)) {
props.onChange(_.assign({}, model, part));
}
}
export function handleDelete(props, doc) {
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.filter(row => row.id !== doc.id);
if (_.isFunction(props.onChange)) {
props.onChange(_.assign({}, model, part));
}
}
const newFn = () => ({ id: uuid.v1() });
export function handleAdd(props, fn = newFn) {
if (!_.isFunction(fn)) fn = newFn;
const { model, name } = props;
const collection = model[name] || [];
const part = {};
part[name] = collection.concat([fn()]);
if (_.isFunction(props.onChange)) {
props.onChange(_.assign({}, model, part));
}
}
export default { handleAdd, handleDelete, handleChange };

View file

@ -0,0 +1,41 @@
import _ from 'lodash';
import getLastValue from '../../visualizations/lib/get_last_value';
import tickFormatter from './tick_formatter';
import moment from 'moment';
import calculateLabel from './calculate_label';
export default (series, model) => {
const variables = {};
model.series.forEach(seriesModel => {
series
.filter(row => _.startsWith(row.id, seriesModel.id))
.forEach(row => {
const metric = _.last(seriesModel.metrics);
const varName = [
_.snakeCase(row.label),
_.snakeCase(seriesModel.var_name)
].filter(v => v).join('.');
const formatter = tickFormatter(seriesModel.formatter, seriesModel.value_template);
const lastValue = getLastValue(row.data, 10);
const data = {
last: {
raw: lastValue,
formatted: formatter(lastValue)
},
data: {
raw: row.data,
formatted: row.data.map(point => {
return [moment(point[0]).format('lll'), formatter(point[1])];
})
}
};
_.set(variables, varName, data);
_.set(variables, `${_.snakeCase(row.label)}.label`, row.label);
});
});
return variables;
};

View file

@ -0,0 +1,26 @@
import React from 'react';
import seriesChangeHandler from './series_change_handler';
import newMetricAggFn from './new_metric_agg_fn';
import { handleAdd, handleDelete } from './collection_actions';
import Agg from '../aggs/agg';
export default function createAggRowRender(props) {
return (row, index, items) => {
const { panel, model, fields } = props;
const changeHandler = seriesChangeHandler(props, items);
return (
<Agg
key={row.id}
disableDelete={items.length < 2}
fields={fields}
model={row}
onAdd={handleAdd.bind(null, props, newMetricAggFn)}
onChange={changeHandler}
onDelete={handleDelete.bind(null, props, row)}
panel={panel}
series={model}
siblings={items}
sortData={row.id} />
);
};
}

View file

@ -0,0 +1,5 @@
import _ from 'lodash';
export default (handleChange, model) => part => {
const doc = _.assign({}, model, part);
handleChange(doc);
};

View file

@ -0,0 +1,10 @@
import _ from 'lodash';
export default (handleChange) => {
return (name, defaultValue) => (e) => {
e.preventDefault();
const value = Number(_.get(e, 'target.value', defaultValue));
if (_.isFunction(handleChange)) {
return handleChange({ [name]: value });
}
};
};

View file

@ -0,0 +1,10 @@
import _ from 'lodash';
export default (handleChange) => {
return (name) => (value) => {
if (_.isFunction(handleChange)) {
return handleChange({
[name]: value && value.value || null
});
}
};
};

View file

@ -0,0 +1,10 @@
import _ from 'lodash';
export default (handleChange) => {
return (name, defaultValue) => (e) => {
e.preventDefault();
const value = _.get(e, 'target.value', defaultValue);
if (_.isFunction(handleChange)) {
return handleChange({ [name]: value });
}
};
};

View file

@ -0,0 +1,27 @@
import _ from 'lodash';
export default function byType(type) {
return (field) => {
switch (type) {
case 'numeric':
return _.includes([
'scaled_float',
'half_float',
'integer',
'float',
'long',
'double'
], field.type);
case 'string':
return _.includes([
'string', 'keyword', 'text'
], field.type);
case 'date':
return _.includes([
'date'
], field.type);
default:
return true;
}
};
}

View file

@ -0,0 +1,7 @@
import uuid from 'node-uuid';
export default () => {
return {
id: uuid.v1(),
type: 'count'
};
};

View file

@ -0,0 +1,19 @@
import uuid from 'node-uuid';
import _ from 'lodash';
import newMetricAggFn from './new_metric_agg_fn';
export default (obj = {}) => {
return _.assign({
id: uuid.v1(),
color: '#68BC00',
split_mode: 'everything',
metrics: [ newMetricAggFn() ],
seperate_axis: 0,
axis_position: 'right',
formatter: 'number',
chart_type: 'line',
line_width: 1,
point_size: 1,
fill: 0,
stacked: 'none'
}, obj);
};

View file

@ -0,0 +1,22 @@
import uuid from 'node-uuid';
import _ from 'lodash';
export default source => {
const series = _.cloneDeep(source);
series.id = uuid.v1();
series.metrics.forEach((metric) => {
const id = uuid.v1();
const metricId = metric.id;
metric.id = id;
if (series.terms_order_by === metricId) series.terms_order_by = id;
series.metrics.filter(r => r.field === metricId).forEach(r => r.field = id);
series.metrics.filter(r => r.type === 'calculation' &&
r.variables.some(v => v.field === metricId))
.forEach(r => {
r.variables.filter(v => v.field === metricId).forEach(v => {
v.id = uuid.v1();
v.field = id;
});
});
});
return series;
};

View file

@ -0,0 +1,10 @@
import _ from 'lodash';
import handlebars from 'handlebars/dist/handlebars';
export default function replaceVars(str, args = {}, vars = {}) {
try {
const template = handlebars.compile(str);
return template(_.assign({}, vars, { args }));
} catch (e) {
return str;
}
}

View file

@ -0,0 +1,23 @@
import _ from 'lodash';
import newMetricAggFn from './new_metric_agg_fn';
import { isBasicAgg } from './agg_lookup';
import {
handleAdd,
handleChange
} from './collection_actions';
export default (props, items) => doc => {
// If we only have one sibling and the user changes to a pipeline
// agg we are going to add the pipeline instead of changing the
// current item.
if (items.length === 1 && !isBasicAgg(doc)) {
handleAdd.call(null, props, () => {
const metric = newMetricAggFn();
metric.type = doc.type;
const incompatPipelines = ['calculation', 'series_agg'];
if (!_.contains(incompatPipelines, doc.type)) metric.field = doc.id;
return metric;
});
} else {
handleChange.call(null, props, doc);
}
};

View file

@ -0,0 +1,32 @@
import numeral from '@spalger/numeral';
import _ from 'lodash';
import handlebars from 'handlebars/dist/handlebars';
const formatLookup = {
'bytes': '0.0b',
'number': '0,0.[00]',
'percent': '0.[00]%'
};
export default (format = '0,0.[00]', template) => {
if (!template) template = '{{value}}';
const render = handlebars.compile(template);
return (val) => {
const formatString = formatLookup[format] || format;
let value;
if (!_.isNumber(val)) {
value = 0;
} else {
try {
value = numeral(val).format(formatString);
} catch (e) {
value = val;
}
}
try {
return render({ value });
} catch (e) {
return String(value);
}
};
};

View file

@ -0,0 +1,144 @@
/* eslint max-len:0 */
import React, { Component, PropTypes } from 'react';
import tickFormatter from './lib/tick_formatter';
import moment from 'moment';
import calculateLabel from './lib/calculate_label';
import convertSeriesToVars from './lib/convert_series_to_vars';
import AceEditor from 'react-ace';
import _ from 'lodash';
import brace from 'brace';
import 'brace/mode/markdown';
import 'brace/theme/github';
import { getLastValue } from 'plugins/metrics/visualizations';
import numeral from 'numeral';
class MarkdownEditor extends Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleOnLoad = this.handleOnLoad.bind(this);
}
handleChange(value) {
this.props.onChange({ markdown: value });
}
handleOnLoad(ace) {
this.ace = ace;
}
handleVarClick(snippet) {
return (e) => {
if (this.ace) this.ace.insert(snippet);
};
}
render() {
const { model, visData } = this.props;
const series = _.get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model);
const rows = [];
const rawFormatter = tickFormatter('0.[0000]');
const createPrimativeRow = key => {
const snippet = `{{ ${key} }}`;
let value = _.get(variables, key);
if (/raw$/.test(key)) value = rawFormatter(value);
rows.push(
<tr key={key}>
<td>
<a onClick={this.handleVarClick(snippet)}>
{ snippet }
</a>
</td>
<td>
<code>"{ value }"</code>
</td>
</tr>
);
};
const createArrayRow = key => {
const snippet = `{{# ${key} }}{{/ ${key} }}`;
const date = _.get(variables, `${key}[0][0]`);
let value = _.get(variables, `${key}[0][1]`);
if (/raw$/.test(key)) value = rawFormatter(value);
rows.push(
<tr key={key}>
<td>
<a onClick={this.handleVarClick(snippet)}>
{ `{{ ${key} }}` }
</a>
</td>
<td>
<code>[ [ "{date}", "{value}" ], ... ]</code>
</td>
</tr>
);
};
function walk(obj, path = []) {
for (const name in obj) {
if (_.isArray(obj[name])) {
createArrayRow(path.concat(name).join('.'));
} else if (_.isObject(obj[name])) {
walk(obj[name], path.concat(name));
} else {
createPrimativeRow(path.concat(name).join('.'));
}
}
}
walk(variables);
return (
<div className="vis_editor__markdown">
<div className="vis_editor__markdown-editor">
<AceEditor
onLoad={this.handleOnLoad}
mode="markdown"
theme="github"
width="100%"
height="100%"
name={`ace-${model.id}`}
setOptions={{ wrap: true, fontSize: '14px' }}
value={model.markdown}
onChange={this.handleChange}/>
</div>
<div className="vis_editor__markdown-variables">
<div>The following variables can be used in the Markdown by using the Handlebar (mustache) syntax. <a href="http://handlebarsjs.com/expressions.html" target="_BLANK">Click here for documentation</a> on the available expressions. HTML is also enabled.</div>
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
<div className="vis_editor__markdown-code-desc">There is also a special variable named <code>_all</code> which you can use to access the entire tree. This is useful for creating lists with data from a group by...</div>
<pre>
<code>{`# All servers:
{{#each _all}}
- {{ label }} {{ last.formatted }}
{{/each}}`}</code>
</pre>
</div>
</div>
);
}
}
MarkdownEditor.propTypes = {
onChange: PropTypes.func,
model: PropTypes.object,
visData: PropTypes.object
};
export default MarkdownEditor;

View file

@ -0,0 +1,32 @@
import React, { PropTypes } from 'react';
import timeseries from './panel_config/timeseries';
import metric from './panel_config/metric';
import topN from './panel_config/top_n';
import gauge from './panel_config/gauge';
import markdown from './panel_config/markdown';
const types = {
timeseries,
metric,
top_n: topN,
gauge,
markdown
};
function PanelConfig(props) {
const { model } = props;
const component = types[model.type];
if (component) {
return React.createElement(component, props);
}
return (<div>Missing panel config for "{model.type}"</div>);
}
PanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default PanelConfig;

View file

@ -0,0 +1,168 @@
import React, { Component, PropTypes } from 'react';
import SeriesEditor from '../series_editor';
import IndexPattern from '../index_pattern';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import createNumberHandler from '../lib/create_number_handler';
import DataFormatPicker from '../data_format_picker';
import ColorRules from '../color_rules';
import ColorPicker from '../color_picker';
import uuid from 'node-uuid';
import YesNo from 'plugins/metrics/components/yes_no';
class GaugePanelConfig extends Component {
constructor(props) {
super(props);
this.state = { selectedTab: 'data' };
}
componentWillMount() {
const { model } = this.props;
const parts = {};
if (!model.gauge_color_rules ||
(model.gauge_color_rules && model.gauge_color_rules.length === 0)) {
parts.gauge_color_rules = [{ id: uuid.v1() }];
}
if (model.gauge_width == null) parts.gauge_width = 10;
if (model.gauge_inner_width == null) parts.gauge_inner_width = 10;
if (model.gauge_style == null) parts.gauge_style = 'half';
this.props.onChange(parts);
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
render() {
const { selectedTab } = this.state;
const defaults = {
gauge_max: '',
filter: '',
gauge_style: 'circle',
gauge_inner_width: '',
gauge_width: ''
};
const model = { ...defaults, ...this.props.model };
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const handleNumberChange = createNumberHandler(this.props.onChange);
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
const styleOptions = [
{ label: 'Circle', value: 'circle' },
{ label: 'Half Circle', value: 'half' }
];
let view;
if (selectedTab === 'data') {
view = (
<SeriesEditor
colorPicker={true}
fields={this.props.fields}
limit={1}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange} />
);
} else {
view = (
<div className="vis_editor__container">
<IndexPattern
fields={this.props.fields}
model={this.props.model}
onChange={this.props.onChange}/>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Panel Filter</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('filter')}
value={model.filter}/>
<div className="vis_editor__label">Ignore Global Filter</div>
<YesNo
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}/>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Background Color</div>
<ColorPicker
onChange={this.props.onChange}
name="background_color"
value={model.background_color}/>
<div className="vis_editor__label">Gauge Max (empty for auto)</div>
<input
className="vis_editor__input-grows"
type="number"
onChange={handleTextChange('gauge_max')}
value={model.gauge_max}/>
<div className="vis_editor__label">Gauge Style</div>
<Select
autosize={false}
clearable={false}
options={styleOptions}
value={model.gauge_style}
onChange={handleSelectChange('gauge_style')}/>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Inner Color</div>
<ColorPicker
onChange={this.props.onChange}
name="gauge_inner_color"
value={model.gauge_inner_color}/>
<div className="vis_editor__label">Inner Line Width</div>
<input
className="vis_editor__input-grows"
type="number"
onChange={handleTextChange('gauge_inner_width')}
value={model.gauge_inner_width}/>
<div className="vis_editor__label">Gauge Line Width</div>
<input
className="vis_editor__input-grows"
type="number"
onChange={handleTextChange('gauge_width')}
value={model.gauge_width} />
</div>
<div>
<div className="vis_editor__label">Color Rules</div>
</div>
<div className="vis_editor__vis_config-row">
<ColorRules
primaryName="gauge color"
primaryVarName="gauge"
secondaryName="text color"
secondaryVarName="text"
model={model}
onChange={this.props.onChange}
name="gauge_color_rules"/>
</div>
</div>
);
}
return (
<div>
<div className="kbnTabs">
<div className={`kbnTabs__tab${selectedTab === 'data' && '-active' || ''}`}
onClick={e => this.switchTab('data')}>Data</div>
<div className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={e => this.switchTab('options')}>Panel Options</div>
</div>
{view}
</div>
);
}
}
GaugePanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default GaugePanelConfig;

View file

@ -0,0 +1,157 @@
import React, { Component, PropTypes } from 'react';
import SeriesEditor from '../series_editor';
import IndexPattern from '../index_pattern';
import AceEditor from 'react-ace';
import brace from 'brace';
import 'brace/mode/less';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import DataFormatPicker from '../data_format_picker';
import ColorPicker from '../color_picker';
import YesNo from '../yes_no';
import MarkdownEditor from '../markdown_editor';
import less from 'less/lib/less-browser';
const lessC = less(window, { env: 'production' });
class MarkdownPanelConfig extends Component {
constructor(props) {
super(props);
this.state = { selectedTab: 'markdown' };
this.handleCSSChange = this.handleCSSChange.bind(this);
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
handleCSSChange(value) {
const { model } = this.props;
const lessSrc = `#markdown-${model.id} {
${value}
}`;
lessC.render(lessSrc, { compress: true }, (e, output) => {
const parts = { markdown_less: value };
if (output) {
parts.markdown_css = output.css;
}
this.props.onChange(parts);
});
}
render() {
const defaults = { filter: '' };
const model = { ...defaults, ...this.props.model };
const { selectedTab } = this.state;
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
const legendPositionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' },
{ label: 'Bottom', value: 'bottom' }
];
const alignOptions = [
{ label: 'Top', value: 'top' },
{ label: 'Middle', value: 'middle' },
{ label: 'Bottom', value: 'bottom' }
];
let view;
if (selectedTab === 'markdown') {
view = (<MarkdownEditor {...this.props}/>);
} else if (selectedTab === 'data') {
view = (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange} />
);
} else {
view = (
<div className="vis_editor__container">
<IndexPattern
fields={this.props.fields}
model={this.props.model}
onChange={this.props.onChange}/>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Background Color</div>
<ColorPicker
onChange={this.props.onChange}
name="background_color"
value={model.background_color}/>
<div className="vis_editor__label">Panel Filter</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('filter')}
value={model.filter} />
<div className="vis_editor__label">Ignore Global Filter</div>
<YesNo
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}/>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Show Scrollbars</div>
<YesNo
value={model.markdown_scrollbars}
name="markdown_scrollbars"
onChange={this.props.onChange}/>
<div className="vis_editor__label">Vertical Alignment</div>
<div className="vis_editor__row_item">
<Select
autosize={true}
clearable={false}
options={alignOptions}
value={model.markdown_vertical_align}
onChange={handleSelectChange('markdown_vertical_align')}/>
</div>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Custom CSS (supports Less)</div>
</div>
<div className="vis_editor__ace-editor">
<AceEditor
mode="less"
theme="github"
width="100%"
name={`ace-css-${model.id}`}
setOptions={{ fontSize: '14px' }}
value={ model.markdown_less}
onChange={this.handleCSSChange}/>
</div>
</div>
);
}
return (
<div>
<div className="kbnTabs">
<div className={`kbnTabs__tab${selectedTab === 'markdown' && '-active' || ''}`}
onClick={e => this.switchTab('markdown')}>Markdown</div>
<div className={`kbnTabs__tab${selectedTab === 'data' && '-active' || ''}`}
onClick={e => this.switchTab('data')}>Data</div>
<div className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={e => this.switchTab('options')}>Panel Options</div>
</div>
{view}
</div>
);
}
}
MarkdownPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default MarkdownPanelConfig;

View file

@ -0,0 +1,106 @@
import React, { Component, PropTypes } from 'react';
import SeriesEditor from '../series_editor';
import IndexPattern from '../index_pattern';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import DataFormatPicker from '../data_format_picker';
import ColorRules from '../color_rules';
import YesNo from '../yes_no';
import uuid from 'node-uuid';
class MetricPanelConfig extends Component {
constructor(props) {
super(props);
this.state = { selectedTab: 'data' };
}
componentWillMount() {
const { model } = this.props;
if (!model.background_color_rules || (model.background_color_rules && model.background_color_rules.length === 0)) {
this.props.onChange({
background_color_rules: [{ id: uuid.v1() }]
});
}
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
render() {
const { selectedTab } = this.state;
const defaults = { filter: '' };
const model = { ...defaults, ...this.props.model };
const handleTextChange = createTextHandler(this.props.onChange);
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
let view;
if (selectedTab === 'data') {
view = (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
limit={2}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange} />
);
} else {
view = (
<div className="vis_editor__container">
<IndexPattern
fields={this.props.fields}
model={this.props.model}
onChange={this.props.onChange}/>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Panel Filter</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('filter')}
value={model.filter}/>
<div className="vis_editor__label">Ignore Global Filter</div>
<YesNo
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}/>
</div>
<div>
<div className="vis_editor__label">Color Rules</div>
</div>
<div className="vis_editor__vis_config-row">
<ColorRules
model={model}
onChange={this.props.onChange}
name="background_color_rules"/>
</div>
</div>
);
}
return (
<div>
<div className="kbnTabs">
<div className={`kbnTabs__tab${selectedTab === 'data' && '-active' || ''}`}
onClick={e => this.switchTab('data')}>Data</div>
<div className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={e => this.switchTab('options')}>Panel Options</div>
</div>
{view}
</div>
);
}
}
MetricPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default MetricPanelConfig;

View file

@ -0,0 +1,151 @@
import React, { Component, PropTypes } from 'react';
import SeriesEditor from '../series_editor';
import AnnotationsEditor from '../annotations_editor';
import IndexPattern from '../index_pattern';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import DataFormatPicker from '../data_format_picker';
import ColorPicker from '../color_picker';
import YesNo from '../yes_no';
class TimeseriesPanelConfig extends Component {
constructor(props) {
super(props);
this.state = { selectedTab: 'data' };
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
render() {
const defaults = {
filter: '',
axis_max: '',
axis_min: '',
legend_position: 'right'
};
const model = { ...defaults, ...this.props.model };
const { selectedTab } = this.state;
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
const legendPositionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' },
{ label: 'Bottom', value: 'bottom' }
];
let view;
if (selectedTab === 'data') {
view = (
<SeriesEditor
fields={this.props.fields}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange} />
);
} else if (selectedTab === 'annotations') {
view = (
<AnnotationsEditor
fields={this.props.fields}
model={this.props.model}
name="annotations"
onChange={this.props.onChange} />
);
} else {
view = (
<div className="vis_editor__container">
<IndexPattern
fields={this.props.fields}
model={this.props.model}
onChange={this.props.onChange}/>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Axis Min</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('axis_min')}
value={model.axis_min}/>
<div className="vis_editor__label">Axis Max</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('axis_max')}
value={model.axis_max}/>
<div className="vis_editor__label">Axis Position</div>
<div className="vis_editor__row_item">
<Select
autosize={false}
clearable={false}
options={positionOptions}
value={model.axis_position}
onChange={handleSelectChange('axis_position')}/>
</div>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Background Color</div>
<ColorPicker
onChange={this.props.onChange}
name="background_color"
value={model.background_color}/>
<div className="vis_editor__label">Show Legend</div>
<YesNo
value={model.show_legend}
name="show_legend"
onChange={this.props.onChange}/>
<div className="vis_editor__label">Legend Position</div>
<div className="vis_editor__row_item">
<Select
clearable={false}
options={legendPositionOptions}
value={model.legend_position}
onChange={handleSelectChange('legend_position')}/>
</div>
</div>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Panel Filter</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('filter')}
value={model.filter}/>
<div className="vis_editor__label">Ignore Global Filter</div>
<YesNo
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}/>
</div>
</div>
);
}
return (
<div>
<div className="kbnTabs">
<div className={`kbnTabs__tab${selectedTab === 'data' && '-active' || ''}`}
onClick={e => this.switchTab('data')}>Data</div>
<div className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={e => this.switchTab('options')}>Panel Options</div>
<div className={`kbnTabs__tab${selectedTab === 'annotations' && '-active' || ''}`}
onClick={e => this.switchTab('annotations')}>Annotations</div>
</div>
{view}
</div>
);
}
}
TimeseriesPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default TimeseriesPanelConfig;

View file

@ -0,0 +1,129 @@
import React, { Component, PropTypes } from 'react';
import SeriesEditor from '../series_editor';
import _ from 'lodash';
import IndexPattern from '../index_pattern';
import Select from 'react-select';
import createSelectHandler from '../lib/create_select_handler';
import createTextHandler from '../lib/create_text_handler';
import DataFormatPicker from '../data_format_picker';
import ColorRules from '../color_rules';
import ColorPicker from '../color_picker';
import uuid from 'node-uuid';
import YesNo from '../yes_no';
class TopNPanelConfig extends Component {
constructor(props) {
super(props);
this.state = { selectedTab: 'data' };
}
componentWillMount() {
const { model } = this.props;
const parts = {};
if (!model.bar_color_rules || (model.bar_color_rules && model.bar_color_rules.length === 0)) {
parts.bar_color_rules = [{ id: uuid.v1() }];
}
if (model.series && model.series.length > 0) {
parts.series = [_.assign({}, model.series[0])];
}
this.props.onChange(parts);
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
render() {
const { selectedTab } = this.state;
const { fields } = this.props;
const defaults = { drilldown_url: '', filter: '' };
const model = { ...defaults, ...this.props.model };
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
let view;
if (selectedTab === 'data') {
view = (
<SeriesEditor
colorPicker={false}
fields={this.props.fields}
limit={1}
model={this.props.model}
name={this.props.name}
onChange={this.props.onChange} />
);
} else {
view = (
<div className="vis_editor__container">
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Item Url (This supports mustache templating.
<code>{'{{key}}'}</code> is set to the term)</div>
<input
className="vis_editor__input-grows"
onChange={handleTextChange('drilldown_url')}
value={model.drilldown_url}/>
</div>
<IndexPattern
fields={this.props.fields}
model={this.props.model}
onChange={this.props.onChange}/>
<div className="vis_editor__vis_config-row">
<div className="vis_editor__label">Background Color</div>
<ColorPicker
onChange={this.props.onChange}
name="background_color"
value={model.background_color}/>
<div className="vis_editor__label">Panel Filter</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('filter')}
value={model.filter}/>
<div className="vis_editor__label">Ignore Global Filter</div>
<YesNo
value={model.ignore_global_filter}
name="ignore_global_filter"
onChange={this.props.onChange}/>
</div>
<div>
<div className="vis_editor__label">Color Rules</div>
</div>
<div className="vis_editor__vis_config-row">
<ColorRules
model={model}
primaryVarName="bar_color"
primaryName="bar"
hideSecondary={true}
onChange={this.props.onChange}
name="bar_color_rules"/>
</div>
</div>
);
}
return (
<div>
<div className="kbnTabs">
<div className={`kbnTabs__tab${selectedTab === 'data' && '-active' || ''}`}
onClick={e => this.switchTab('data')}>Data</div>
<div className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
onClick={e => this.switchTab('options')}>Panel Options</div>
</div>
{view}
</div>
);
}
}
TopNPanelConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
visData: PropTypes.object,
};
export default TopNPanelConfig;

View file

@ -0,0 +1,108 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import timeseries from './vis_types/timeseries/series';
import metric from './vis_types/metric/series';
import topN from './vis_types/top_n/series';
import gauge from './vis_types/gauge/series';
import markdown from './vis_types/markdown/series';
import { sortable } from 'react-anything-sortable';
const lookup = {
top_n: topN,
metric,
timeseries,
gauge,
markdown
};
class Series extends Component {
constructor(props) {
super(props);
this.state = {
visible: true,
selectedTab: 'metrics'
};
this.handleChange = this.handleChange.bind(this);
this.switchTab = this.switchTab.bind(this);
this.toggleVisible = this.toggleVisible.bind(this);
}
switchTab(selectedTab) {
this.setState({ selectedTab });
}
handleChange(part) {
if (this.props.onChange) {
const { model } = this.props;
const doc = _.assign({}, model, part);
this.props.onChange(doc);
}
}
toggleVisible(e) {
e.preventDefault();
this.setState({ visible: !this.state.visible });
}
render() {
const { panel } = this.props;
const Component = lookup[panel.type];
if (Component) {
const params = {
className: this.props.className,
colorPicker: this.props.colorPicker,
disableAdd: this.props.disableAdd,
disableDelete: this.props.disableDelete,
fields: this.props.fields,
name: this.props.name,
onAdd: this.props.onAdd,
onChange: this.handleChange,
onClone: this.props.onClone,
onDelete: this.props.onDelete,
onMouseDown: this.props.onMouseDown,
onTouchStart: this.props.onTouchStart,
onSortableItemMount: this.props.onSortableItemMount,
onSortableItemReadyToMove: this.props.onSortableItemReadyToMove,
model: this.props.model,
panel: this.props.panel,
selectedTab: this.state.selectedTab,
sortData: this.props.sortData,
style: this.props.style,
switchTab: this.switchTab,
toggleVisible: this.toggleVisible,
visible: this.state.visible
};
return (<Component {...params}/>);
}
return (<div>Missing Series component for panel type: {panel.type}</div>);
}
}
Series.defaultProps = {
name: 'metrics'
};
Series.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
sortData: PropTypes.string,
};
export default sortable(Series);

View file

@ -0,0 +1,64 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import DataFormatPicker from './data_format_picker';
import createSelectHandler from './lib/create_select_handler';
import createTextHandler from './lib/create_text_handler';
import YesNo from './yes_no';
import IndexPattern from './index_pattern';
class SeriesConfig extends Component {
render() {
const { fields } = this.props;
const defaults = { offset_time: '', value_template: '' };
const model = { ...defaults, ...this.props.model };
const handleSelectChange = createSelectHandler(this.props.onChange);
const handleTextChange = createTextHandler(this.props.onChange);
return (
<div>
<div className="vis_editor__series_config-container">
<div className="vis_editor__series_config-row">
<DataFormatPicker
onChange={handleSelectChange('formatter')}
value={model.formatter}/>
<div className="vis_editor__label">Template (eg.<code>{'{{value}}/s'}</code>)</div>
<input
className="vis_editor__input-grows"
onChange={handleTextChange('value_template')}
value={model.value_template}/>
<div className="vis_editor__label">Offset series time by (1m, 1h, 1w, 1d)</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('offset_time')}
value={model.offset_time}/>
</div>
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Override Index Pattern</div>
<YesNo
value={model.override_index_pattern}
name="override_index_pattern"
onChange={this.props.onChange}/>
<IndexPattern
onChange={this.props.onChange}
model={this.props.model}
fields={this.props.fields}
prefix="series_"
className="vis_editor__row_item vis_editor__row"
disabled={!model.override_index_pattern} />
</div>
</div>
</div>
);
}
}
SeriesConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func
};
export default SeriesConfig;

View file

@ -0,0 +1,83 @@
import React, { Component, PropTypes } from 'react';
import reIdSeries from './lib/re_id_series';
import _ from 'lodash';
import Series from './series';
import {
handleAdd,
handleDelete,
handleChange
} from './lib/collection_actions';
import newSeriesFn from './lib/new_series_fn';
import Sortable from 'react-anything-sortable';
class SeriesEditor extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleClone(series) {
const newSeries = reIdSeries(series);
handleAdd.call(null, this.props, () => newSeries);
}
renderRow(row, index) {
const { props } = this;
const { fields, model, name, limit, colorPicker } = props;
return (
<Series
colorPicker={colorPicker}
disableAdd={model[name].length >= limit}
disableDelete={model[name].length < 2}
fields={fields}
key={row.id}
onAdd={handleAdd.bind(null, props, newSeriesFn)}
onChange={handleChange.bind(null, props)}
onClone={() => this.handleClone(row)}
onDelete={handleDelete.bind(null, props, row)}
model={row}
panel={model}
sortData={row.id} />
);
}
render() {
const { limit, model, name } = this.props;
const series = model[name]
.filter((val, index) => index < (limit || Infinity))
.map(this.renderRow);
const handleSort = (data) => {
const series = data.map(id => model[name].find(s => s.id === id));
this.props.onChange({ series });
};
return (
<div className="vis_editor__series_editor-container">
<Sortable
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__sort">
{ series }
</Sortable>
</div>
);
}
}
SeriesEditor.defaultProps = {
name: 'series',
limit: Infinity,
colorPicker: true
};
SeriesEditor.propTypes = {
colorPicker: PropTypes.bool,
fields: PropTypes.object,
limit: PropTypes.number,
model: PropTypes.object,
name: PropTypes.string,
onChange: PropTypes.func
};
export default SeriesEditor;

View file

@ -0,0 +1,73 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import _ from 'lodash';
import FieldSelect from './aggs/field_select';
import MetricSelect from './aggs/metric_select';
import calculateLabel from './lib/calculate_label';
import createTextHandler from './lib/create_text_handler';
import createSelectHandler from './lib/create_select_handler';
import uuid from 'node-uuid';
import SplitByTerms from './splits/terms';
import SplitByFilter from './splits/filter';
import SplitByFilters from './splits/filters';
import SplitByEverything from './splits/everything';
class Split extends Component {
componentWillReceiveProps(nextProps) {
const { model } = nextProps;
if (model.split_mode === 'filters' && !model.split_filters) {
this.props.onChange({
split_filters: [
{ color: model.color, id: uuid.v1() }
]
});
}
}
render() {
const { model, panel } = this.props;
const indexPattern = model.override_index_pattern &&
model.series_index_pattern ||
panel.index_pattern;
if (model.split_mode === 'filter') {
return (
<SplitByFilter
model={model}
onChange={this.props.onChange} />
);
}
if (model.split_mode === 'filters') {
return (
<SplitByFilters
model={model}
onChange={this.props.onChange} />
);
}
if (model.split_mode === 'terms') {
return (
<SplitByTerms
model={model}
indexPattern={indexPattern}
fields={this.props.fields}
onChange={this.props.onChange} />
);
}
return (
<SplitByEverything
model={model}
onChange={this.props.onChange} />
);
}
}
Split.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func,
panel: PropTypes.object
};
export default Split;

View file

@ -0,0 +1,28 @@
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import React, { Component, PropTypes } from 'react';
function SplitByEverything(props) {
const { onChange, model } = props;
const handleSelectChange = createSelectHandler(onChange);
return (
<div className="vis_editor__split-container">
<div className="vis_editor__label">Group By</div>
<div className="vis_editor__split-selects">
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')} />
</div>
</div>
);
}
SplitByEverything.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
};
export default SplitByEverything;

View file

@ -0,0 +1,38 @@
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import React, { Component, PropTypes } from 'react';
class SplitByFilter extends Component {
render() {
const { onChange } = this.props;
const defaults = { filter: '' };
const model = { ...defaults, ...this.props.model };
const handleTextChange = createTextHandler(onChange);
const handleSelectChange = createSelectHandler(onChange);
return (
<div className="vis_editor__split-container">
<div className="vis_editor__label">Group By</div>
<div className="vis_editor__split-selects">
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')} />
</div>
<div className="vis_editor__label">Query String</div>
<input
className="vis_editor__split-filter"
value={model.filter}
onChange={handleTextChange('filter')} />
</div>
);
}
}
SplitByFilter.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
};
export default SplitByFilter;

View file

@ -0,0 +1,89 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import collectionActions from '../lib/collection_actions';
import AddDeleteButtons from '../add_delete_buttons';
import ColorPicker from '../color_picker';
import uuid from 'node-uuid';
class FilterItems extends Component {
constructor(props) {
super(props);
this.renderRow = this.renderRow.bind(this);
}
handleChange(item, name) {
return (e) => {
const handleChange = collectionActions.handleChange.bind(null, this.props);
handleChange(_.assign({}, item, {
[name]: _.get(e, 'value', _.get(e, 'target.value'))
}));
};
}
renderRow(row, i, items) {
const defaults = { filter: '', label: '' };
const model = { ...defaults, ...row };
const handleChange = (part) => {
const fn = collectionActions.handleChange.bind(null, this.props);
fn(_.assign({}, model, part));
};
const newFilter = () => ({ color: this.props.model.color, id: uuid.v1() });
const handleAdd = collectionActions.handleAdd
.bind(null, this.props, newFilter);
const handleDelete = collectionActions.handleDelete
.bind(null, this.props, model);
return (
<div className="vis_editor__split-filter-row" key={model.id}>
<div className="vis_editor__split-filter-color">
<ColorPicker
disableTrash={true}
onChange={handleChange}
name="color"
value={model.color}/>
</div>
<div className="vis_editor__split-filter-item">
<input
placeholder="Filter"
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'filter')}
value={model.filter}/>
</div>
<div className="vis_editor__split-filter-item">
<input
placeholder="Label"
className="vis_editor__input-grows-100"
type="text"
onChange={this.handleChange(model, 'label')}
value={model.label}/>
</div>
<div className="vis_editor__split-filter-control">
<AddDeleteButtons
onAdd={handleAdd}
onDelete={handleDelete}
disableDelete={items.length < 2}/>
</div>
</div>
);
}
render() {
const { model, name } = this.props;
if (!model[name]) return (<div/>);
const rows = model[name].map(this.renderRow);
return (
<div className="vis_editor__split-filters">
{ rows }
</div>
);
}
}
FilterItems.propTypes = {
name: PropTypes.string,
model: PropTypes.object,
onChange: PropTypes.func
};
export default FilterItems;

View file

@ -0,0 +1,35 @@
import createSelectHandler from '../lib/create_select_handler';
import GroupBySelect from './group_by_select';
import FilterItems from './filter_items';
import React, { Component, PropTypes } from 'react';
function SplitByFilters(props) {
const { onChange, model } = props;
const handleSelectChange = createSelectHandler(onChange);
return(
<div className="vis_editor__item">
<div className="vis_editor__split-container">
<div className="vis_editor__label">Group By</div>
<div className="vis_editor__split-selects">
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')} />
</div>
</div>
<div className="vis_editor__split-container">
<div className="vis_editor__row vis_editor__item">
<FilterItems
name="split_filters"
model={model}
onChange={onChange} />
</div>
</div>
</div>
);
}
SplitByFilters.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
};
export default SplitByFilters;

View file

@ -0,0 +1,25 @@
import React, { PropTypes } from 'react';
import Select from 'react-select';
function GroupBySelect(props) {
const modeOptions = [
{ label: 'Everything', value: 'everything' },
{ label: 'Filter', value: 'filter' },
{ label: 'Filters', value: 'filters' },
{ label: 'Terms', value: 'terms' }
];
return (
<Select
clearable={false}
value={ props.value || 'everything' }
onChange={props.onChange}
options={ modeOptions }/>
);
}
GroupBySelect.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string
};
export default GroupBySelect;

View file

@ -0,0 +1,65 @@
import React, { Component, PropTypes } from 'react';
import GroupBySelect from './group_by_select';
import createTextHandler from '../lib/create_text_handler';
import createSelectHandler from '../lib/create_select_handler';
import FieldSelect from '../aggs/field_select';
import MetricSelect from '../aggs/metric_select';
class SplitByTerms extends Component {
render() {
const handleTextChange = createTextHandler(this.props.onChange);
const handleSelectChange = createSelectHandler(this.props.onChange);
const { indexPattern } = this.props;
const defaults = { terms_size: 10, terms_order_by: '_count' };
const model = { ...defaults, ...this.props.model };
const { metrics } = model;
const defaultCount = { value: '_count', label: 'Doc Count (default)' };
return (
<div className="vis_editor__split-container">
<div className="vis_editor__label">Group By</div>
<div className="vis_editor__split-selects">
<GroupBySelect
value={model.split_mode}
onChange={handleSelectChange('split_mode')} />
</div>
<div className="vis_editor__label">By</div>
<div className="vis_editor__item">
<FieldSelect
indexPattern={indexPattern}
onChange={handleSelectChange('terms_field')}
value={model.terms_field}
fields={this.props.fields} />
</div>
<div className="vis_editor__label">Top</div>
<input
placeholder="Size..."
type="number"
value={model.terms_size}
className="vis_editor__split-term_count"
onChange={handleTextChange('terms_size')} />
<div className="vis_editor__label">Order By</div>
<div className="vis_editor__split-aggs">
<MetricSelect
metrics={metrics}
clearable={false}
additionalOptions={[defaultCount]}
onChange={handleSelectChange('terms_order_by')}
restrict="basic"
value={model.terms_order_by}/>
</div>
</div>
);
}
}
SplitByTerms.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func,
indexPattern: PropTypes.string,
fields: PropTypes.object
};
export default SplitByTerms;

View file

@ -0,0 +1,26 @@
import React, { Component, PropTypes } from 'react';
import { Tooltip } from 'pui-react-tooltip';
import { OverlayTrigger } from 'pui-react-overlay-trigger';
function TooltipComponent(props) {
const tooltip = (
<Tooltip>{ props.text }</Tooltip>
);
return (
<OverlayTrigger placement={props.placement} overlay={tooltip}>
{ props.children}
</OverlayTrigger>
);
}
TooltipComponent.defaultProps = {
placement: 'top',
text: 'Tip!'
};
TooltipComponent.propTypes = {
placement: PropTypes.string,
text: PropTypes.node
};
export default TooltipComponent;

View file

@ -0,0 +1,67 @@
import React, { Component, PropTypes } from 'react';
import VisEditorVisualization from './vis_editor_visualization';
import Visualization from './visualization';
import VisPicker from './vis_picker';
import PanelConfig from './panel_config';
class VisEditor extends Component {
constructor(props) {
super(props);
this.state = { model: props.model };
}
render() {
const handleChange = (part) => {
const nextModel = { ...this.state.model, ...part };
this.setState({ model: nextModel });
if (this.props.onChange) {
this.props.onChange(nextModel);
}
};
if (this.props.embedded) {
return (
<Visualization
fields={this.props.fields}
model={this.props.model}
visData={this.props.visData} />
);
}
const { model } = this.state;
if (model) {
return (
<div className="vis_editor">
<VisPicker
model={model}
onChange={handleChange} />
<VisEditorVisualization
model={model}
visData={this.props.visData}
onBrush={this.props.onBrush}
onChange={handleChange} />
<PanelConfig
fields={this.props.fields}
model={model}
visData={this.props.visData}
onChange={handleChange} />
</div>
);
}
return null;
}
}
VisEditor.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
visData: PropTypes.object
};
export default VisEditor;

View file

@ -0,0 +1,78 @@
import React, { Component, PropTypes } from 'react';
import Visualization from './visualization';
class VisEditorVisualization extends Component {
constructor(props) {
super(props);
this.state = {
height: 250,
dragging: false
};
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
}
handleMouseDown(e) {
this.setState({ dragging: true });
}
handleMouseUp(e) {
this.setState({ dragging: false });
}
componentWillMount() {
this.handleMouseMove = (event) => {
if (this.state.dragging) {
const height = this.state.height + event.movementY;
if (height > 250) {
this.setState({ height });
}
}
};
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
}
render() {
const style = { height: this.state.height };
if (this.state.dragging) {
style.userSelect = 'none';
}
const visBackgroundColor = '#FFF';
return (
<div>
<div style={style} className="vis_editor__visualization">
<Visualization
backgroundColor={visBackgroundColor}
className="dashboard__visualization"
model={this.props.model}
onBrush={this.props.onBrush}
onChange={this.handleChange}
visData={this.props.visData} />
</div>
<div
className="vis_editor__visualization-draghandle"
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}>
<i className="fa fa-ellipsis-h"></i>
</div>
</div>
);
}
}
VisEditorVisualization.propTypes = {
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
visData: PropTypes.object
};
export default VisEditorVisualization;

View file

@ -0,0 +1,68 @@
import React, { Component, PropTypes } from 'react';
function VisPickerItem(props) {
const { label, icon, type } = props;
let itemClassName = 'vis_editor__vis_picker-item';
let iconClassName = 'vis_editor__vis_picker-icon';
let labelClassName = 'vis_editor__vis_picker-label';
if (props.selected) {
itemClassName += ' selected';
iconClassName += ' selected';
labelClassName += ' selected';
}
return (
<div className={itemClassName} onClick={e => props.onClick(type)}>
<div className={iconClassName}>
<i className={`fa ${icon}`}></i>
</div>
<div className={labelClassName}>
{ label }
</div>
</div>
);
}
VisPickerItem.propTypes = {
icon: PropTypes.string,
label: PropTypes.string,
onClick: PropTypes.func,
type: PropTypes.string,
selected: PropTypes.bool
};
function VisPicker(props) {
const handleChange = (type) => {
props.onChange({ type });
};
const { model } = props;
const icons = [
{ type: 'timeseries', icon: 'fa-line-chart', label: 'Time Series' },
{ type: 'metric', icon: 'fa-superscript', label: 'Metric' },
{ type: 'top_n', icon: 'fa-bar-chart fa-rotate-90', label: 'Top N' },
{ type: 'gauge', icon: 'fa-circle-o-notch', label: 'Gauge' },
{ type: 'markdown', icon: 'fa-paragraph', label: 'Markdown' }
].map((item, i, items) => {
return (
<VisPickerItem
key={item.type}
onClick={handleChange}
selected={ item.type === model.type }
{...item}/>
);
});
return (
<div className="vis_editor__vis_picker-container">
{ icons }
</div>
);
}
VisPicker.propTypes = {
model: PropTypes.object,
onChange: PropTypes.func
};
export default VisPicker;

View file

@ -0,0 +1,167 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import ColorPicker from '../../color_picker';
import AddDeleteButtons from '../../add_delete_buttons';
import SeriesConfig from '../../series_config';
import Sortable from 'react-anything-sortable';
import Split from '../../split';
import Tooltip from '../../tooltip';
import createAggRowRender from '../../lib/create_agg_row_render';
import createTextHandler from '../../lib/create_text_handler';
function GaugeSeries(props) {
const {
panel,
fields,
onAdd,
onChange,
onDelete,
disableDelete,
disableAdd,
selectedTab,
visible
} = props;
const defaults = { label: '' };
const model = { ...defaults, ...props.model };
const handleChange = createTextHandler(onChange);
const aggs = model.metrics.map(createAggRowRender(props));
let caretClassName = 'fa fa-caret-down';
if (!visible) caretClassName = 'fa fa-caret-right';
let body = null;
if (visible) {
let metricsClassName = 'kbnTabs__tab';
let optionsClassname = 'kbnTabs__tab';
if (selectedTab === 'metrics') metricsClassName += '-active';
if (selectedTab === 'options') optionsClassname += '-active';
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
seriesBody = (
<div>
<Sortable
style={{ cursor: 'default' }}
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__agg_sort">
{ aggs }
</Sortable>
<div className="vis_editor__series_row">
<div className="vis_editor__series_row-item">
<Split
onChange={props.onChange}
fields={fields}
panel={panel}
model={model}/>
</div>
</div>
</div>
);
} else {
seriesBody = (
<SeriesConfig
fields={props.fields}
model={props.model}
onChange={props.onChange} />
);
}
body = (
<div className="vis_editor__series-row">
<div className="kbnTabs sm">
<div className={metricsClassName}
onClick={e => props.switchTab('metrics')}>Metrics</div>
<div className={optionsClassname}
onClick={e => props.switchTab('options')}>Options</div>
</div>
{seriesBody}
</div>
);
}
let colorPicker;
if (props.colorPicker) {
colorPicker = (
<ColorPicker
disableTrash={true}
onChange={props.onChange}
name="color"
value={model.color}/>
);
}
let dragHandle;
if (!props.disableDelete) {
dragHandle = (
<Tooltip text="Sort">
<div className="vis_editor__sort thor__button-outlined-default sm">
<i className="fa fa-sort"></i>
</div>
</Tooltip>
);
}
return (
<div
className={`${props.className} vis_editor__series`}
style={props.style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
<div className="vis_editor__container">
<div className="vis_editor__series-details">
<div onClick={ props.toggleVisible }><i className={ caretClassName }/></div>
{ colorPicker }
<div className="vis_editor__row vis_editor__row_item">
<input
className="vis_editor__input-grows"
onChange={handleChange('label')}
placeholder='Label'
value={model.label}/>
</div>
{ dragHandle }
<AddDeleteButtons
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
disableDelete={disableDelete}
disableAdd={disableAdd}/>
</div>
</div>
{ body }
</div>
);
}
GaugeSeries.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
selectedTab: PropTypes.string,
sortData: PropTypes.string,
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
};
export default GaugeSeries;

View file

@ -0,0 +1,78 @@
import React, { PropTypes } from 'react';
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import { Gauge, getLastValue } from 'plugins/metrics/visualizations';
import color from 'color';
function getColors(props) {
const { model, visData } = props;
const series = _.get(visData, `${model.id}.series`, []);
let text;
let gauge;
if (model.gauge_color_rules) {
model.gauge_color_rules.forEach((rule) => {
if (rule.opperator && rule.value != null) {
const value = series[0] && getLastValue(series[0].data) || 0;
if (_[rule.opperator](value, rule.value)) {
gauge = rule.gauge;
text = rule.text;
}
}
});
}
return { text, gauge };
}
function GaugeVisualization(props) {
const { backgroundColor, model, visData } = props;
const colors = getColors(props);
const series = _.get(visData, `${model.id}.series`, [])
.map((row, i) => {
const seriesDef = model.series.find(s => _.includes(row.id, s.id));
const newProps = {};
if (seriesDef) {
newProps.formatter = tickFormatter(seriesDef.formatter, seriesDef.value_template);
}
if (i === 0 && colors.gauge) newProps.color = colors.gauge;
return _.assign({}, row, newProps);
});
const params = {
metric: series[0],
type: model.gauge_style || 'half',
reversed: props.reversed
};
if (colors.text) {
params.valueColor = colors.text;
}
if (model.gauge_width) params.gaugeLine = model.gauge_width;
if (model.gauge_inner_color) params.innerColor = model.gauge_inner_color;
if (model.gauge_inner_width) params.innerLine = model.gauge_inner_width;
if (model.gauge_max != null) params.max = model.gauge_max;
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor && panelBackgroundColor !== 'inherit') {
params.reversed = color(panelBackgroundColor).luminosity() < 0.45;
}
const style = { backgroundColor: panelBackgroundColor };
return (
<div className="dashboard__visualization" style={style}>
<Gauge {...params} />
</div>
);
}
GaugeVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default GaugeVisualization;

View file

@ -0,0 +1,149 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import ColorPicker from '../../color_picker';
import AddDeleteButtons from '../../add_delete_buttons';
import SeriesConfig from '../../series_config';
import Sortable from 'react-anything-sortable';
import Tooltip from '../../tooltip';
import Split from '../../split';
import calculateLabel from '../../lib/calculate_label';
import createAggRowRender from '../../lib/create_agg_row_render';
import createTextHandler from '../../lib/create_text_handler';
function MarkdownSeries(props) {
const {
panel,
fields,
onAdd,
onChange,
onDelete,
disableDelete,
disableAdd,
selectedTab,
visible
} = props;
const defaults = { label: '', var_name: '' };
const model = { ...defaults, ...props.model };
const handleChange = createTextHandler(onChange);
const aggs = model.metrics.map(createAggRowRender(props));
let caretClassName = 'fa fa-caret-down';
if (!visible) caretClassName = 'fa fa-caret-right';
let body = null;
if (visible) {
let metricsClassName = 'kbnTabs__tab';
let optionsClassname = 'kbnTabs__tab';
if (selectedTab === 'metrics') metricsClassName += '-active';
if (selectedTab === 'options') optionsClassname += '-active';
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
seriesBody = (
<div>
<Sortable
style={{ cursor: 'default' }}
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__agg_sort">
{ aggs }
</Sortable>
<div className="vis_editor__series_row">
<div className="vis_editor__series_row-item">
<Split
onChange={props.onChange}
fields={fields}
panel={panel}
model={model}/>
</div>
</div>
</div>
);
} else {
seriesBody = (
<SeriesConfig
fields={props.fields}
model={props.model}
onChange={props.onChange} />
);
}
body = (
<div className="vis_editor__series-row">
<div className="kbnTabs sm">
<div className={metricsClassName}
onClick={e => props.switchTab('metrics')}>Metrics</div>
<div className={optionsClassname}
onClick={e => props.switchTab('options')}>Options</div>
</div>
{seriesBody}
</div>
);
}
return (
<div
className={`${props.className} vis_editor__series`}
style={props.style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
<div className="vis_editor__container">
<div className="vis_editor__series-details">
<div onClick={ props.toggleVisible }><i className={ caretClassName }/></div>
<div className="vis_editor__row vis_editor__row_item">
<input
className="vis_editor__input-grows vis_editor__row_item"
onChange={handleChange('label')}
placeholder='Label'
value={model.label}/>
<input
className="vis_editor__input-grows"
onChange={handleChange('var_name')}
placeholder='Variable Name'
value={model.var_name}/>
</div>
<AddDeleteButtons
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
disableDelete={disableDelete}
disableAdd={disableAdd}/>
</div>
</div>
{ body }
</div>
);
}
MarkdownSeries.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
selectedTab: PropTypes.string,
sortData: PropTypes.string,
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
};
export default MarkdownSeries;

View file

@ -0,0 +1,61 @@
import React, { PropTypes } from 'react';
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import { getLastValue } from 'plugins/metrics/visualizations';
import color from 'color';
import Markdown from 'react-markdown';
import replaceVars from '../../lib/replace_vars';
import convertSeriesToVars from '../../lib/convert_series_to_vars';
function MarkdownVisualization(props) {
const { backgroundColor, model, visData } = props;
const series = _.get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model);
const style = { };
let reversed = props.reversed;
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor) {
style.backgroundColor = panelBackgroundColor;
reversed = color(panelBackgroundColor).luminosity() < 0.45;
}
let markdown;
if (model.markdown) {
const markdownSource = replaceVars(model.markdown, {}, {
_all: variables,
...variables
});
let className = 'thorMarkdown';
let contentClassName = `thorMarkdown__content ${model.markdown_vertical_align}`;
if (model.markdown_scrollbars) contentClassName += ' scrolling';
if (reversed) className += ' reversed';
markdown = (
<div className={className}>
<style type="text/css">
{model.markdown_css}
</style>
<div className={contentClassName}>
<div id={`markdown-${model.id}`}>
<Markdown source={markdownSource}/>
</div>
</div>
</div>
);
}
return (
<div className="dashboard__visualization" style={style}>
{markdown}
</div>
);
}
MarkdownVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default MarkdownVisualization;

View file

@ -0,0 +1,167 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import ColorPicker from '../../color_picker';
import AddDeleteButtons from '../../add_delete_buttons';
import SeriesConfig from '../../series_config';
import Sortable from 'react-anything-sortable';
import Split from '../../split';
import Tooltip from '../../tooltip';
import createAggRowRender from '../../lib/create_agg_row_render';
import createTextHandler from '../../lib/create_text_handler';
function MetricSeries(props) {
const {
panel,
fields,
onAdd,
onChange,
onDelete,
disableDelete,
disableAdd,
selectedTab,
visible
} = props;
const defaults = { label: '' };
const model = { ...defaults, ...props.model };
const handleChange = createTextHandler(onChange);
const aggs = model.metrics.map(createAggRowRender(props));
let caretClassName = 'fa fa-caret-down';
if (!visible) caretClassName = 'fa fa-caret-right';
let body = null;
if (visible) {
let metricsClassName = 'kbnTabs__tab';
let optionsClassname = 'kbnTabs__tab';
if (selectedTab === 'metrics') metricsClassName += '-active';
if (selectedTab === 'options') optionsClassname += '-active';
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
seriesBody = (
<div>
<Sortable
style={{ cursor: 'default' }}
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__agg_sort">
{ aggs }
</Sortable>
<div className="vis_editor__series_row">
<div className="vis_editor__series_row-item">
<Split
onChange={props.onChange}
fields={fields}
panel={panel}
model={model}/>
</div>
</div>
</div>
);
} else {
seriesBody = (
<SeriesConfig
fields={props.fields}
model={props.model}
onChange={props.onChange} />
);
}
body = (
<div className="vis_editor__series-row">
<div className="kbnTabs sm">
<div className={metricsClassName}
onClick={e => props.switchTab('metrics')}>Metrics</div>
<div className={optionsClassname}
onClick={e => props.switchTab('options')}>Options</div>
</div>
{seriesBody}
</div>
);
}
let colorPicker;
if (props.colorPicker) {
colorPicker = (
<ColorPicker
disableTrash={true}
onChange={props.onChange}
name="color"
value={model.color}/>
);
}
let dragHandle;
if (!props.disableDelete) {
dragHandle = (
<Tooltip text="Sort">
<div className="vis_editor__sort thor__button-outlined-default sm">
<i className="fa fa-sort"></i>
</div>
</Tooltip>
);
}
return (
<div
className={`${props.className} vis_editor__series`}
style={props.style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
<div className="vis_editor__container">
<div className="vis_editor__series-details">
<div onClick={ props.toggleVisible }><i className={ caretClassName }/></div>
{ colorPicker }
<div className="vis_editor__row vis_editor__row_item">
<input
className="vis_editor__input-grows"
onChange={handleChange('label')}
placeholder='Label'
value={model.label}/>
</div>
{ dragHandle }
<AddDeleteButtons
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
disableDelete={disableDelete}
disableAdd={disableAdd}/>
</div>
</div>
{ body }
</div>
);
}
MetricSeries.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
selectedTab: PropTypes.string,
sortData: PropTypes.string,
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
};
export default MetricSeries;

View file

@ -0,0 +1,73 @@
import React, { PropTypes } from 'react';
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import { Metric, getLastValue } from 'plugins/metrics/visualizations';
import { findDOMNode } from 'react-dom';
import color from 'color';
function getColors(props) {
const { model, visData } = props;
const series = _.get(visData, `${model.id}.series`, []);
let color;
let background;
if (model.background_color_rules) {
model.background_color_rules.forEach((rule) => {
if (rule.opperator && rule.value != null) {
const value = series[0] && getLastValue(series[0].data) || 0;
if (_[rule.opperator](value, rule.value)) {
background = rule.background_color;
color = rule.color;
}
}
});
}
return { color, background };
}
function MetricVisualization(props) {
const { backgroundColor, model, visData } = props;
const colors = getColors(props);
const series = _.get(visData, `${model.id}.series`, [])
.map((row, i) => {
const seriesDef = model.series.find(s => _.includes(row.id, s.id));
const newProps = {};
if (seriesDef) {
newProps.formatter = tickFormatter(seriesDef.formatter, seriesDef.value_template);
}
if (i === 0 && colors.color) newProps.color = colors.color;
return _.assign({}, _.pick(row, ['label', 'data']), newProps);
});
const params = {
metric: series[0],
reversed: props.reversed
};
if (series[1]) {
params.secondary = series[1];
}
const panelBackgroundColor = colors.background || backgroundColor;
if (panelBackgroundColor && panelBackgroundColor !== 'inherit') {
params.reversed = color(panelBackgroundColor).luminosity() < 0.45;
}
const style = { backgroundColor: panelBackgroundColor };
return (
<div className="dashboard__visualization" style={style}>
<Metric {...params}/>
</div>
);
}
MetricVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default MetricVisualization;

View file

@ -0,0 +1,221 @@
import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import DataFormatPicker from '../../data_format_picker';
import createSelectHandler from '../../lib/create_select_handler';
import YesNo from '../../yes_no';
import createTextHandler from '../../lib/create_text_handler';
import IndexPattern from '../../index_pattern';
function TimeseriesConfig(props) {
const handleSelectChange = createSelectHandler(props.onChange);
const handleTextChange = createTextHandler(props.onChange);
const defaults = {
fill: '',
line_width: '',
point_size: '',
value_template: '{{value}}',
offset_time: '',
split_color_mode: 'gradient',
axis_min: '',
axis_max: '',
stacked: 'none',
steps: 0
};
const model = { ...defaults, ...props.model };
const stackedOptions = [
{ label: 'None', value: 'none' },
{ label: 'Stacked', value: 'stacked' },
{ label: 'Percent', value: 'percent' }
];
const positionOptions = [
{ label: 'Right', value: 'right' },
{ label: 'Left', value: 'left' }
];
const chartTypeOptions = [
{ label: 'Bar', value: 'bar' },
{ label: 'Line', value: 'line' }
];
const splitColorOptions = [
{ label: 'Gradient', value: 'gradient' },
{ label: 'Rainbow', value: 'rainbow' }
];
let type;
if (model.chart_type === 'line') {
type = (
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Chart Type</div>
<div className="vis_editor__item">
<Select
clearable={false}
options={chartTypeOptions}
value={model.chart_type}
onChange={handleSelectChange('chart_type')}/>
</div>
<div className="vis_editor__label">Stacked</div>
<div className="vis_editor__item">
<Select
clearable={false}
options={stackedOptions}
value={model.stacked}
onChange={handleSelectChange('stacked')}/>
</div>
<div className="vis_editor__label">Fill (0 to 1)</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('fill')}
value={model.fill}/>
<div className="vis_editor__label">Line Width</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('line_width')}
value={model.line_width}/>
<div className="vis_editor__label">Point Size</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('point_size')}
value={model.point_size}/>
<div className="vis_editor__label">Steps</div>
<YesNo
value={model.steps}
name="steps"
onChange={props.onChange}/>
</div>
);
}
if (model.chart_type === 'bar') {
type = (
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Chart Type</div>
<div className="vis_editor__item">
<Select
clearable={false}
options={chartTypeOptions}
value={model.chart_type}
onChange={handleSelectChange('chart_type')}/>
</div>
<div className="vis_editor__label">Stacked</div>
<div className="vis_editor__item">
<Select
clearable={false}
options={stackedOptions}
value={model.stacked}
onChange={handleSelectChange('stacked')}/>
</div>
<div className="vis_editor__label">Fill (0 to 1)</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('fill')}
value={model.fill}/>
<div className="vis_editor__label">Line Width</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('line_width')}
value={model.line_width}/>
</div>
);
}
const disableSeperateYaxis = model.seperate_axis ? false : true;
return (
<div>
<div className="vis_editor__series_config-container">
<div className="vis_editor__series_config-row">
<DataFormatPicker
onChange={handleSelectChange('formatter')}
value={model.formatter}/>
<div className="vis_editor__label">Template (eg.<code>{'{{value}}/s'}</code>)</div>
<input
className="vis_editor__input-grows"
onChange={handleTextChange('value_template')}
value={model.value_template}/>
</div>
{ type }
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Offset series time by (1m, 1h, 1w, 1d)</div>
<input
className="vis_editor__input-grows"
type="text"
onChange={handleTextChange('offset_time')}
value={model.offset_time}/>
<div className="vis_editor__label">Hide in Legend</div>
<YesNo
value={model.hide_in_legend}
name="hide_in_legend"
onChange={props.onChange}/>
<div className="vis_editor__label">Split Color Theme</div>
<div className="vis_editor__row_item">
<Select
clearable={false}
options={splitColorOptions}
value={model.split_color_mode}
onChange={handleSelectChange('split_color_mode')}/>
</div>
</div>
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Separate Axis</div>
<YesNo
value={model.seperate_axis}
name="seperate_axis"
onChange={props.onChange}/>
<div className="vis_editor__label">Axis Min</div>
<input
className="vis_editor__input-grows"
type="text"
disabled={disableSeperateYaxis}
onChange={handleTextChange('axis_min')}
value={model.axis_min}/>
<div className="vis_editor__label">Axis Max</div>
<input
className="vis_editor__input-grows"
type="text"
disabled={disableSeperateYaxis}
onChange={handleTextChange('axis_max')}
value={model.axis_max}/>
<div className="vis_editor__label">Axis Position</div>
<div className="vis_editor__row_item">
<Select
clearable={false}
disabled={disableSeperateYaxis}
options={positionOptions}
value={model.axis_position}
onChange={handleSelectChange('axis_position')}/>
</div>
</div>
<div className="vis_editor__series_config-row">
<div className="vis_editor__label">Override Index Pattern</div>
<YesNo
value={model.override_index_pattern}
name="override_index_pattern"
onChange={props.onChange}/>
<IndexPattern
{...props}
prefix="series_"
className="vis_editor__row_item vis_editor__row"
disabled={!model.override_index_pattern}
with-interval={true} />
</div>
</div>
</div>
);
}
TimeseriesConfig.propTypes = {
fields: PropTypes.object,
model: PropTypes.object,
onChange: PropTypes.func
};
export default TimeseriesConfig;

View file

@ -0,0 +1,166 @@
import React, { Component, PropTypes } from 'react';
import ColorPicker from '../../color_picker';
import AddDeleteButtons from '../../add_delete_buttons';
import SeriesConfig from './config';
import Sortable from 'react-anything-sortable';
import Tooltip from '../../tooltip';
import Split from '../../split';
import createAggRowRender from '../../lib/create_agg_row_render';
import createTextHandler from '../../lib/create_text_handler';
function TimeseriesSeries(props) {
const {
panel,
fields,
onAdd,
onDelete,
disableDelete,
disableAdd,
selectedTab,
onChange,
visible
} = props;
const defaults = { label: '' };
const model = { ...defaults, ...props.model };
const handleChange = createTextHandler(onChange);
const aggs = model.metrics.map(createAggRowRender(props));
let caretClassName = 'fa fa-caret-down';
if (!visible) caretClassName = 'fa fa-caret-right';
let body = null;
if (visible) {
let metricsClassName = 'kbnTabs__tab';
let optionsClassname = 'kbnTabs__tab';
if (selectedTab === 'metrics') metricsClassName += '-active';
if (selectedTab === 'options') optionsClassname += '-active';
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
seriesBody = (
<div>
<Sortable
style={{ cursor: 'default' }}
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__agg_sort">
{ aggs }
</Sortable>
<div className="vis_editor__series_row">
<div className="vis_editor__series_row-item">
<Split
onChange={props.onChange}
fields={fields}
panel={panel}
model={model}/>
</div>
</div>
</div>
);
} else {
seriesBody = (
<SeriesConfig
fields={props.fields}
model={props.model}
onChange={props.onChange} />
);
}
body = (
<div className="vis_editor__series-row">
<div className="kbnTabs sm">
<div className={metricsClassName}
onClick={e => props.switchTab('metrics')}>Metrics</div>
<div className={optionsClassname}
onClick={e => props.switchTab('options')}>Options</div>
</div>
{seriesBody}
</div>
);
}
let colorPicker;
if (props.colorPicker) {
colorPicker = (
<ColorPicker
disableTrash={true}
onChange={props.onChange}
name="color"
value={model.color}/>
);
}
let dragHandle;
if (!props.disableDelete) {
dragHandle = (
<Tooltip text="Sort">
<div className="vis_editor__sort thor__button-outlined-default sm">
<i className="fa fa-sort"></i>
</div>
</Tooltip>
);
}
return (
<div
className={`${props.className} vis_editor__series`}
style={props.style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
<div className="vis_editor__container">
<div className="vis_editor__series-details">
<div onClick={ props.toggleVisible }><i className={ caretClassName }/></div>
{ colorPicker }
<div className="vis_editor__row vis_editor__row_item">
<input
className="vis_editor__input-grows"
onChange={handleChange('label')}
placeholder='Label'
value={model.label}/>
</div>
{ dragHandle }
<AddDeleteButtons
onDelete={onDelete}
onClone={props.onClone}
onAdd={onAdd}
disableDelete={disableDelete}
disableAdd={disableAdd}/>
</div>
</div>
{ body }
</div>
);
}
TimeseriesSeries.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
selectedTab: PropTypes.string,
sortData: PropTypes.string,
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
};
export default TimeseriesSeries;

View file

@ -0,0 +1,147 @@
import React, { PropTypes } from 'react';
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import { Timeseries } from 'plugins/metrics/visualizations';
import color from 'color';
import replaceVars from '../../lib/replace_vars';
function hasSeperateAxis(row) {
return row.seperate_axis;
}
function TimeseriesVisualization(props) {
const { backgroundColor, model, visData } = props;
const series = _.get(visData, `${model.id}.series`, []);
let annotations;
if (model.annotations && _.isArray(model.annotations)) {
annotations = model.annotations.map(annotation => {
const data = _.get(visData, `${model.id}.annotations.${annotation.id}`, [])
.map(item => [item.key, item.docs]);
return {
id: annotation.id,
color: annotation.color,
icon: annotation.icon,
series: data.map(s => {
return [s[0], s[1].map(doc => {
return replaceVars(annotation.template, null, doc);
})];
})
};
});
}
const seriesModel = model.series.map(s => _.cloneDeep(s));
const firstSeries = seriesModel.find(s => s.formatter && !s.seperate_axis);
const formatter = tickFormatter(_.get(firstSeries, 'formatter'), _.get(firstSeries, 'value_template'));
const mainAxis = {
position: model.axis_position,
tickFormatter: formatter,
axis_formatter: _.get(firstSeries, 'formatter', 'number'),
};
if (model.axis_min) mainAxis.min = model.axis_min;
if (model.axis_max) mainAxis.max = model.axis_max;
const yaxes = [mainAxis];
seriesModel.forEach(s => {
series
.filter(r => _.startsWith(r.id, s.id))
.forEach(r => r.tickFormatter = tickFormatter(s.formatter, s.value_template));
if (s.hide_in_legend) {
series
.filter(r => _.startsWith(r.id, s.id))
.forEach(r => delete r.label);
}
if (s.stacked === 'percent') {
s.seperate_axis = true;
s.axis_formatter = 'percent';
s.axis_min = 0;
s.axis_max = 1;
s.axis_position = model.axis_position;
const seriesData = series.filter(r => _.startsWith(r.id, s.id));
const first = seriesData[0];
if (first) {
first.data.forEach((row, index) => {
const rowSum = seriesData.reduce((acc, item) => {
return item.data[index][1] + acc;
}, 0);
seriesData.forEach(item => {
item.data[index][1] = rowSum && item.data[index][1] / rowSum || 0;
});
});
}
}
});
let axisCount = 1;
if (seriesModel.some(hasSeperateAxis)) {
seriesModel.forEach((row) => {
if (row.seperate_axis) {
axisCount++;
const formatter = tickFormatter(row.formatter, row.value_template);
const yaxis = {
alignTicksWithAxis: 1,
position: row.axis_position,
tickFormatter: formatter,
axis_formatter: row.axis_formatter
};
if (row.axis_min != null) yaxis.min = row.axis_min;
if (row.axis_max != null) yaxis.max = row.axis_max;
yaxes.push(yaxis);
// Assign axis and formatter to each series
series
.filter(r => _.startsWith(r.id, row.id))
.forEach(r => {
r.yaxis = axisCount;
});
}
});
}
const params = {
crosshair: true,
tickFormatter: formatter,
legendPosition: model.legend_position || 'right',
series,
annotations,
yaxes,
reversed: props.reversed,
legend: Boolean(model.show_legend),
onBrush: (ranges) => {
if (props.onBrush) props.onBrush(ranges);
}
};
const style = { };
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor) {
style.backgroundColor = panelBackgroundColor;
params.reversed = color(panelBackgroundColor || backgroundColor).luminosity() < 0.45;
}
return (
<div className="dashboard__visualization" style={style}>
<Timeseries {...params}/>
</div>
);
}
TimeseriesVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default TimeseriesVisualization;

View file

@ -0,0 +1,122 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
import ColorPicker from '../../color_picker';
import AddDeleteButtons from '../../add_delete_buttons';
import SeriesConfig from '../../series_config';
import Sortable from 'react-anything-sortable';
import Tooltip from '../../tooltip';
import MetricSelect from '../../aggs/metric_select';
import Split from '../../split';
import { handleChange } from '../../lib/collection_actions';
import createAggRowRender from '../../lib/create_agg_row_render';
function TopNSeries(props) {
const {
panel,
model,
fields,
onAdd,
onDelete,
disableDelete,
disableAdd,
selectedTab,
visible,
} = props;
const aggs = model.metrics.map(createAggRowRender(props));
let caretClassName = 'fa fa-caret-down';
if (!visible) caretClassName = 'fa fa-caret-right';
let body = null;
if (visible) {
let metricsClassName = 'kbnTabs__tab';
let optionsClassname = 'kbnTabs__tab';
if (selectedTab === 'metrics') metricsClassName += '-active';
if (selectedTab === 'options') optionsClassname += '-active';
let seriesBody;
if (selectedTab === 'metrics') {
const handleSort = (data) => {
const metrics = data.map(id => model.metrics.find(m => m.id === id));
props.onChange({ metrics });
};
seriesBody = (
<div>
<Sortable
style={{ cursor: 'default' }}
dynamic={true}
direction="vertical"
onSort={handleSort}
sortHandle="vis_editor__agg_sort">
{ aggs }
</Sortable>
<div className="vis_editor__series_row">
<div className="vis_editor__series_row-item">
<Split
onChange={props.onChange}
fields={fields}
panel={panel}
model={model}/>
</div>
</div>
</div>
);
} else {
seriesBody = (
<SeriesConfig
fields={props.fields}
model={props.model}
onChange={props.onChange} />
);
}
body = (
<div className="vis_editor__series-row">
<div className="kbnTabs sm">
<div className={metricsClassName}
onClick={e => props.switchTab('metrics')}>Metrics</div>
<div className={optionsClassname}
onClick={e => props.switchTab('options')}>Options</div>
</div>
{seriesBody}
</div>
);
}
return (
<div
className={`${props.className} vis_editor__series`}
style={props.style}
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}>
{ body }
</div>
);
}
TopNSeries.propTypes = {
className: PropTypes.string,
colorPicker: PropTypes.bool,
disableAdd: PropTypes.bool,
disableDelete: PropTypes.bool,
fields: PropTypes.object,
name: PropTypes.string,
onAdd: PropTypes.func,
onChange: PropTypes.func,
onClone: PropTypes.func,
onDelete: PropTypes.func,
onMouseDown: PropTypes.func,
onSortableItemMount: PropTypes.func,
onSortableItemReadyToMove: PropTypes.func,
onTouchStart: PropTypes.func,
model: PropTypes.object,
panel: PropTypes.object,
selectedTab: PropTypes.string,
sortData: PropTypes.string,
style: PropTypes.object,
switchTab: PropTypes.func,
toggleVisible: PropTypes.func,
visible: PropTypes.bool
};
export default TopNSeries;

View file

@ -0,0 +1,63 @@
import tickFormatter from '../../lib/tick_formatter';
import _ from 'lodash';
import { TopN, getLastValue } from 'plugins/metrics/visualizations';
import color from 'color';
import React, { PropTypes } from 'react';
function TopNVisualization(props) {
const { backgroundColor, model, visData } = props;
const series = _.get(visData, `${model.id}.series`, [])
.map(item => {
const id = _.first(item.id.split(/:/));
const seriesConfig = model.series.find(s => s.id === id);
if (seriesConfig) {
const formatter = tickFormatter(seriesConfig.formatter, seriesConfig.value_template);
const value = getLastValue(item.data, item.data.length);
let color = item.color || seriesConfig.color;
if (model.bar_color_rules) {
model.bar_color_rules.forEach(rule => {
if (rule.opperator && rule.value != null && rule.bar_color) {
if (_[rule.opperator](value, rule.value)) {
color = rule.bar_color;
}
}
});
}
return _.assign({}, item, {
color,
tickFormatter: formatter
});
}
return item;
});
const params = {
series: series,
reversed: props.reversed
};
const panelBackgroundColor = model.background_color || backgroundColor;
if (panelBackgroundColor && panelBackgroundColor !== 'inherit') {
params.reversed = color(panelBackgroundColor).luminosity() < 0.45;
}
const style = { backgroundColor: panelBackgroundColor };
return (
<div className="dashboard__visualization" style={style}>
<TopN {...params}/>
</div>
);
}
TopNVisualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default TopNVisualization;

View file

@ -0,0 +1,58 @@
import React, { PropTypes } from 'react';
import _ from 'lodash';
import timeseries from './vis_types/timeseries/vis';
import metric from './vis_types/metric/vis';
import topN from './vis_types/top_n/vis';
import gauge from './vis_types/gauge/vis';
import markdown from './vis_types/markdown/vis';
import Error from './error';
const types = {
timeseries,
metric,
top_n: topN,
gauge,
markdown
};
function Visualization(props) {
const { visData, model } = props;
// Show the error panel
const error = _.get(visData, `${model.id}.error`);
if (error) {
return (
<div className={props.className}>
<Error error={error}/>
</div>
);
}
const component = types[model.type];
if (component) {
return React.createElement(component, {
reversed: props.reversed,
backgroundColor: props.backgroundColor,
model: props.model,
onBrush: props.onBrush,
onChange: props.onChange,
visData: props.visData
});
}
return (<div className={props.className}></div>);
}
Visualization.defaultProps = {
className: 'thor__visualization'
};
Visualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
reversed: PropTypes.bool,
visData: PropTypes.object
};
export default Visualization;

View file

@ -0,0 +1,41 @@
import React, { Component, PropTypes } from 'react';
import _ from 'lodash';
function YesNo(props) {
const { name, value } = props;
const handleChange = value => {
const { name } = props;
return (e) => {
const parts = { [name]: value };
props.onChange(parts);
};
};
const inputName = name + _.uniqueId();
return (
<div className="thor__yes_no">
<label>
<input
type="radio"
name={inputName}
checked={Boolean(value)}
value="yes"
onChange={handleChange(1)}/>
Yes</label>
<label>
<input
type="radio"
name={inputName}
checked={!Boolean(value)}
value="no"
onChange={handleChange(0)}/>
No</label>
</div>
);
}
YesNo.propTypes = {
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
};
export default YesNo;

View file

@ -0,0 +1,27 @@
import _ from 'lodash';
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import modules from 'ui/modules';
import VisEditor from '../components/vis_editor';
import addScope from '../lib/add_scope';
import angular from 'angular';
import createBrushHandler from '../lib/create_brush_handler';
const app = modules.get('apps/metrics/directives');
app.directive('metricsVisEditor', (timefilter) => {
return {
restrict: 'E',
link: ($scope, $el, $attrs) => {
const addToState = ['embedded', 'fields', 'visData'];
const Component = addScope(VisEditor, $scope, addToState);
const handleBrush = createBrushHandler($scope, timefilter);
const handleChange = part => {
$scope.$evalAsync(() => angular.copy(part, $scope.model));
};
render(<Component model={$scope.model} onChange={handleChange} onBrush={handleBrush} />, $el[0]);
$scope.$on('$destroy', () => {
unmountComponentAtNode($el[0]);
});
}
};
});

Some files were not shown because too many files have changed in this diff Show more