mirror of
https://github.com/elastic/kibana.git
synced 2025-04-25 10:23:14 -04:00
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:
parent
7cbc22b052
commit
4f3e625d7f
247 changed files with 16793 additions and 5 deletions
|
@ -32,7 +32,7 @@ Timelion expression as a Kibana dashboard panel. You can then add it to
|
||||||
a dashboard like any other visualization.
|
a dashboard like any other visualization.
|
||||||
|
|
||||||
TIP: You can also create time series visualizations right from the Visualize
|
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.
|
expression in the expression field.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
<<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
|
<<tilemap,Tile map>>:: Associate the results of an aggregation with geographic
|
||||||
locations.
|
locations.
|
||||||
Timeseries:: Compute and combine data from multiple time series
|
Timelion:: Compute and combine data from multiple time series
|
||||||
data sets.
|
data sets.
|
||||||
|
|
||||||
. Specify a search query to retrieve the data for your visualization:
|
. Specify a search query to retrieve the data for your visualization:
|
||||||
|
|
14
package.json
14
package.json
|
@ -110,6 +110,7 @@
|
||||||
"brace": "0.5.1",
|
"brace": "0.5.1",
|
||||||
"bunyan": "1.7.1",
|
"bunyan": "1.7.1",
|
||||||
"check-hash": "1.0.1",
|
"check-hash": "1.0.1",
|
||||||
|
"color": "1.0.3",
|
||||||
"commander": "2.8.1",
|
"commander": "2.8.1",
|
||||||
"css-loader": "0.17.0",
|
"css-loader": "0.17.0",
|
||||||
"d3": "3.5.6",
|
"d3": "3.5.6",
|
||||||
|
@ -187,12 +188,14 @@
|
||||||
"classnames": "2.2.5",
|
"classnames": "2.2.5",
|
||||||
"del": "1.2.1",
|
"del": "1.2.1",
|
||||||
"elasticdump": "2.1.1",
|
"elasticdump": "2.1.1",
|
||||||
|
"enzyme": "2.7.0",
|
||||||
"eslint": "3.11.1",
|
"eslint": "3.11.1",
|
||||||
"eslint-plugin-babel": "4.0.0",
|
"eslint-plugin-babel": "4.0.0",
|
||||||
"eslint-plugin-mocha": "4.7.0",
|
"eslint-plugin-mocha": "4.7.0",
|
||||||
"event-stream": "3.3.2",
|
"event-stream": "3.3.2",
|
||||||
"expect.js": "0.3.1",
|
"expect.js": "0.3.1",
|
||||||
"faker": "1.1.0",
|
"faker": "1.1.0",
|
||||||
|
"flot-charts": "^0.8.3",
|
||||||
"grunt": "1.0.1",
|
"grunt": "1.0.1",
|
||||||
"grunt-aws-s3": "0.14.5",
|
"grunt-aws-s3": "0.14.5",
|
||||||
"grunt-babel": "6.0.0",
|
"grunt-babel": "6.0.0",
|
||||||
|
@ -212,6 +215,7 @@
|
||||||
"image-diff": "1.6.0",
|
"image-diff": "1.6.0",
|
||||||
"intern": "3.2.3",
|
"intern": "3.2.3",
|
||||||
"istanbul-instrumenter-loader": "0.1.3",
|
"istanbul-instrumenter-loader": "0.1.3",
|
||||||
|
"jsdom": "9.9.1",
|
||||||
"karma": "1.2.0",
|
"karma": "1.2.0",
|
||||||
"karma-chrome-launcher": "0.2.0",
|
"karma-chrome-launcher": "0.2.0",
|
||||||
"karma-coverage": "0.5.1",
|
"karma-coverage": "0.5.1",
|
||||||
|
@ -232,15 +236,25 @@
|
||||||
"npm": "3.10.10",
|
"npm": "3.10.10",
|
||||||
"portscanner": "1.0.0",
|
"portscanner": "1.0.0",
|
||||||
"proxyquire": "1.7.10",
|
"proxyquire": "1.7.10",
|
||||||
|
"pui-react-overlay-trigger": "^7.0.0",
|
||||||
|
"pui-react-tooltip": "^7.0.0",
|
||||||
"react": "15.2.0",
|
"react": "15.2.0",
|
||||||
|
"react-ace": "3.7.0",
|
||||||
"react-addons-test-utils": "15.2.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-dom": "15.2.0",
|
||||||
|
"react-markdown": "^2.4.2",
|
||||||
"react-redux": "4.4.5",
|
"react-redux": "4.4.5",
|
||||||
"react-router": "2.0.0",
|
"react-router": "2.0.0",
|
||||||
"react-router-redux": "4.0.4",
|
"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": "3.0.0",
|
||||||
"redux-thunk": "0.1.0",
|
"redux-thunk": "0.1.0",
|
||||||
"sass-loader": "4.0.0",
|
"sass-loader": "4.0.0",
|
||||||
|
"simianhacker-react-resize-aware": "^1.0.11",
|
||||||
"simple-git": "1.37.0",
|
"simple-git": "1.37.0",
|
||||||
"sinon": "1.17.2",
|
"sinon": "1.17.2",
|
||||||
"source-map": "0.5.6",
|
"source-map": "0.5.6",
|
||||||
|
|
31
src/core_plugins/metrics/index.js
Normal file
31
src/core_plugins/metrics/index.js
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
6
src/core_plugins/metrics/package.json
Normal file
6
src/core_plugins/metrics/package.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"author": "Chris Cowan<chris@elastic.co>",
|
||||||
|
"name": "metrics",
|
||||||
|
"version": "kibana"
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// });
|
||||||
|
|
|
@ -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
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// });
|
|
@ -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;
|
51
src/core_plugins/metrics/public/components/aggs/agg.js
Normal file
51
src/core_plugins/metrics/public/components/aggs/agg.js
Normal 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);
|
53
src/core_plugins/metrics/public/components/aggs/agg_row.js
Normal file
53
src/core_plugins/metrics/public/components/aggs/agg_row.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
103
src/core_plugins/metrics/public/components/aggs/filter_ratio.js
Normal file
103
src/core_plugins/metrics/public/components/aggs/filter_ratio.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
190
src/core_plugins/metrics/public/components/aggs/percentile.js
Normal file
190
src/core_plugins/metrics/public/components/aggs/percentile.js
Normal 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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
63
src/core_plugins/metrics/public/components/aggs/std_agg.js
Normal file
63
src/core_plugins/metrics/public/components/aggs/std_agg.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
80
src/core_plugins/metrics/public/components/aggs/vars.js
Normal file
80
src/core_plugins/metrics/public/components/aggs/vars.js
Normal 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;
|
168
src/core_plugins/metrics/public/components/annotations_editor.js
Normal file
168
src/core_plugins/metrics/public/components/annotations_editor.js
Normal 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;
|
95
src/core_plugins/metrics/public/components/color_picker.js
Normal file
95
src/core_plugins/metrics/public/components/color_picker.js
Normal 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;
|
115
src/core_plugins/metrics/public/components/color_rules.js
Normal file
115
src/core_plugins/metrics/public/components/color_rules.js
Normal 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;
|
|
@ -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);
|
|
@ -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;
|
40
src/core_plugins/metrics/public/components/error.js
Normal file
40
src/core_plugins/metrics/public/components/error.js
Normal 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;
|
116
src/core_plugins/metrics/public/components/icon_select.js
Normal file
116
src/core_plugins/metrics/public/components/icon_select.js
Normal 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;
|
66
src/core_plugins/metrics/public/components/index_pattern.js
Normal file
66
src/core_plugins/metrics/public/components/index_pattern.js
Normal 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;
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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: []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
import convertSeriesToVars from '../convert_series_to_vars';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('convertSeriesToVars(series, model)', () => {
|
||||||
|
it('returns and object', () => {
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
|
@ -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'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
|
@ -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');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
78
src/core_plugins/metrics/public/components/lib/agg_lookup.js
Normal file
78
src/core_plugins/metrics/public/components/lib/agg_lookup.js
Normal 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;
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
|
||||||
|
|
12
src/core_plugins/metrics/public/components/lib/basic_aggs.js
Normal file
12
src/core_plugins/metrics/public/components/lib/basic_aggs.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export default [
|
||||||
|
'count',
|
||||||
|
'avg',
|
||||||
|
'max',
|
||||||
|
'min',
|
||||||
|
'sum',
|
||||||
|
'std_deviation',
|
||||||
|
'variance',
|
||||||
|
'sum_of_squares',
|
||||||
|
'value_count',
|
||||||
|
'cardinality'
|
||||||
|
];
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
};
|
||||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||||
|
|
||||||
|
};
|
|
@ -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} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
export default (handleChange, model) => part => {
|
||||||
|
const doc = _.assign({}, model, part);
|
||||||
|
handleChange(doc);
|
||||||
|
};
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
export default (handleChange) => {
|
||||||
|
return (name) => (value) => {
|
||||||
|
if (_.isFunction(handleChange)) {
|
||||||
|
return handleChange({
|
||||||
|
[name]: value && value.value || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import uuid from 'node-uuid';
|
||||||
|
export default () => {
|
||||||
|
return {
|
||||||
|
id: uuid.v1(),
|
||||||
|
type: 'count'
|
||||||
|
};
|
||||||
|
};
|
|
@ -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);
|
||||||
|
};
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
144
src/core_plugins/metrics/public/components/markdown_editor.js
Normal file
144
src/core_plugins/metrics/public/components/markdown_editor.js
Normal 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;
|
32
src/core_plugins/metrics/public/components/panel_config.js
Normal file
32
src/core_plugins/metrics/public/components/panel_config.js
Normal 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;
|
168
src/core_plugins/metrics/public/components/panel_config/gauge.js
Normal file
168
src/core_plugins/metrics/public/components/panel_config/gauge.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
129
src/core_plugins/metrics/public/components/panel_config/top_n.js
Normal file
129
src/core_plugins/metrics/public/components/panel_config/top_n.js
Normal 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;
|
108
src/core_plugins/metrics/public/components/series.js
Normal file
108
src/core_plugins/metrics/public/components/series.js
Normal 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);
|
64
src/core_plugins/metrics/public/components/series_config.js
Normal file
64
src/core_plugins/metrics/public/components/series_config.js
Normal 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;
|
||||||
|
|
83
src/core_plugins/metrics/public/components/series_editor.js
Normal file
83
src/core_plugins/metrics/public/components/series_editor.js
Normal 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;
|
73
src/core_plugins/metrics/public/components/split.js
Normal file
73
src/core_plugins/metrics/public/components/split.js
Normal 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;
|
|
@ -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;
|
||||||
|
|
38
src/core_plugins/metrics/public/components/splits/filter.js
Normal file
38
src/core_plugins/metrics/public/components/splits/filter.js
Normal 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;
|
|
@ -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;
|
35
src/core_plugins/metrics/public/components/splits/filters.js
Normal file
35
src/core_plugins/metrics/public/components/splits/filters.js
Normal 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;
|
|
@ -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;
|
65
src/core_plugins/metrics/public/components/splits/terms.js
Normal file
65
src/core_plugins/metrics/public/components/splits/terms.js
Normal 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;
|
26
src/core_plugins/metrics/public/components/tooltip.js
Normal file
26
src/core_plugins/metrics/public/components/tooltip.js
Normal 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;
|
67
src/core_plugins/metrics/public/components/vis_editor.js
Normal file
67
src/core_plugins/metrics/public/components/vis_editor.js
Normal 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;
|
|
@ -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;
|
68
src/core_plugins/metrics/public/components/vis_picker.js
Normal file
68
src/core_plugins/metrics/public/components/vis_picker.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
58
src/core_plugins/metrics/public/components/visualization.js
Normal file
58
src/core_plugins/metrics/public/components/visualization.js
Normal 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;
|
41
src/core_plugins/metrics/public/components/yes_no.js
Normal file
41
src/core_plugins/metrics/public/components/yes_no.js
Normal 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;
|
27
src/core_plugins/metrics/public/directives/vis_editor.js
Normal file
27
src/core_plugins/metrics/public/directives/vis_editor.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue