Time Series Metric Visualizations (#9725)

* Initial import

* updating the editor width to match the new specs

* Adding tribe node support

* Adding tests for server libs

* removing bluebird

* removing extra cruft

* Fixing the font sizes

* Fixed the updating code

* Adding brushing

* Fixing linting issues

* Adding global filters

* Adding missing packages

* Default gauge style to half circle

* Fixing the markdown css bug

* Adding tests for the get_vis_data api

* Adding time offset

* Adding time offset to each type

* fixing bugs from time offset

* adding index pattern option to series

* Adding index pattern overrides

* Adding index pattern overrides

* Fixing tests

* Fixing brushing in the vis editor

* Changing the label

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

* Refactoring series a bit

* Changing series options to just options

* Making sure we honor the toaster container height

* Adding first tests

* renaming vis_config to panel_config

* renaming vis_config to panel_config

* Adding more tests

* adding more tests

* removing api subdirectory

* refactoring get_vis_data (breaking it up and removing unused imports)

* reorganizing the visualization directory

* Re-organizing directory layouts and moving things to more logical places

* Refactoring React compontents to use ES6 syntax and adding propTypes for each. Also refactored out splats as much as possible.

* Adding serial differencing

* Refactored gauge to use 2 components instead of 4

* Finishing react refactor on visualizations. Consolidated legned funtionality

* Refactoring series config and removing a bunch of duplicate code

* fixing series config name

* Fixing numbers and strings (doesnt matter which it is); Fixing classname

* Changing the way the dark theme works

* Adding new vis into list for test

* Adding empty bucket check

* Fixing the index patterns in the aggs

* Fixing typo

* Refactoring vis_data

* Fixing std_metric

* Fixing refresh-hack

* Adding tests for get_splits, get_last_metric, map_bucket

* Fixing the error handing

* removing restrictions

* Sometimes values are strings or numbers... it doesn't matter

* Adding new color options for splits

* Fixing colors

* fixing size

* Adding support for fitlers agg

* Fixing tests

* Fixing splits for filters

* Fixing Top N to work better with fitlers

* Adding annotation editor

* initial work for annotations

* Finalizing annotations

* Fixing label

* making it expandable

* Fixing hacks fixed by #10175

* Fixing bars to use the same stacking options as lines

* Getting rid of align by colons

* removing unused depends

* removing unused depends

* Changing to readable lodash function

* Adding missing parens

* refactoring custom color picker

* Removing string refs and converting uncontrolled components

* Fixing the controlled components where value maybe null; converting error to css

* refactoring styles from components

* fixing the refresh behavoir borked by fullEditor

* Adding the executor service

* Fixing the test directories

* fixing save

* Adding filter ratios

* Fixing controlled components

* Trying to fix the weird typing

* Fixing offset bug with days

* Adding percentile rank

* Fixing yaxis updates; fixing percentile rank layout; adding steps to line chart

* removing unused depends

* Fixed a bug with the index patterns updating; fixed bug with charts rendering too much

* Fixing tests

* Commenting out React tests because the ENV must have change and they are no longer working

* Moving bucket transform

* moving calculate auto

* Moving calculate_indices

* moving extended_stats_types && get_agg_value

* moving get_buckets_path

* moving get_sibling_agg_value

* moving parse_settings

* moving series_agg

* Moving unit_to_seconds

* Fixing tests

* Fixing per PR

* Renaming vars to make it more clear what's happening

* Changing the way testible functions are exported

* fixing tests

* removing unused imports; fixing typos; fixing package name

* Name has to match the plugin path

* Fixing typos; removing unused imports

* fixing tests

* rearanging and removing unused imports

* Fixing a bug with unque names for radio buttons on the same form

* Fixing filter ratio to use a metric instead of just count

* fixing a bug with the new filter ratios

* Fixing the file path from the #8

* Fixing renderComplete trigger; Fixing embedded mode; Changing names for Timelion and Time Series Visual Builder

* Fixing name

* Fixing docs

* Fixing a typo for the field select for terms splits

* Fixing tests
This commit is contained in:
Chris Cowan 2017-03-02 13:07:28 -07:00 committed by GitHub
parent 7cbc22b052
commit 4f3e625d7f
247 changed files with 16793 additions and 5 deletions

View file

@ -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

@ -110,6 +110,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",
@ -187,12 +188,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",
@ -212,6 +215,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",
@ -232,15 +236,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