mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Time Series Metric Visualizations (#10662)
Backports PR #9725 **Commit 1:** Initial import * Original sha:f9d527ecad
* Authored by Chris Cowan <chris@chriscowan.us> on 2016-12-21T00:27:11Z **Commit 2:** Merge branch 'master' of github.com:elastic/kibana into metrics * Original sha:c5e936f272
* Authored by Chris Cowan <chris@chriscowan.us> on 2016-12-21T14:26:16Z **Commit 3:** updating the editor width to match the new specs * Original sha:0d0f4c6aa8
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:51:06Z **Commit 4:** Merge branch 'master' of github.com:elastic/kibana into metrics * Original sha:2fb42d0e5d
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:54:40Z **Commit 5:** Adding tribe node support * Original sha:63a736aa21
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T16:57:40Z **Commit 6:** Adding tests for server libs * Original sha:6ede79b8e4
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T22:34:57Z **Commit 7:** removing bluebird * Original sha:1cb28600fb
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-02T22:39:23Z **Commit 8:** removing extra cruft * Original sha:4efe0ecd92
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T17:01:32Z **Commit 9:** Fixing the font sizes * Original sha:33f4d535b8
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T18:09:08Z **Commit 10:** Fixed the updating code * Original sha:51ff9f8fa9
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T18:56:29Z **Commit 11:** Adding brushing * Original sha:2ba0463721
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T20:23:46Z **Commit 12:** Fixing linting issues * Original sha:09b6ada25d
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T20:33:12Z **Commit 13:** Adding global filters * Original sha:985ec1c02c
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-04T21:44:20Z **Commit 14:** Adding missing packages * Original sha:bd87e894cc
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T13:16:15Z **Commit 15:** Default gauge style to half circle * Original sha:5ce7d856fb
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T15:10:51Z **Commit 16:** Fixing the markdown css bug * Original sha:0b72290f6a
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-05T15:26:53Z **Commit 17:** Adding tests for the get_vis_data api * Original sha:a6e91ba86e
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T15:16:05Z **Commit 18:** Adding time offset * Original sha:5ee12ac7e2
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T17:36:56Z **Commit 19:** Adding time offset to each type * Original sha:ee52e34456
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-12T17:42:33Z **Commit 20:** fixing bugs from time offset * Original sha:16b7469d41
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-13T23:12:18Z **Commit 21:** adding index pattern option to series * Original sha:1e64c7cc70
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-17T21:57:13Z **Commit 22:** Merge branch 'master' of github.com:elastic/kibana into metrics * Original sha:b4ce642e68
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-17T23:05:40Z **Commit 23:** Adding index pattern overrides * Original sha:b5ff6c6f46
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:29:25Z **Commit 24:** Adding index pattern overrides * Original sha:6641c8e6e4
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:38:12Z **Commit 25:** Fixing tests * Original sha:b7822ecb5c
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T05:43:56Z **Commit 26:** Fixing brushing in the vis editor * Original sha:74dc57f460
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T17:14:04Z **Commit 27:** Changing the label * Original sha:95e58b0c86
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T17:19:03Z **Commit 28:** Change the behavior of selecting a pipeline agg when only one exists. * Original sha:5d2bf42680
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T18:18:57Z **Commit 29:** Refactoring series a bit * Original sha:f1ff1988ce
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T18:33:15Z **Commit 30:** Changing series options to just options * Original sha:23821b52bd
* Authored by Chris Cowan <chris@chriscowan.us> on 2017-01-18T20:34:59Z
This commit is contained in:
parent
48e0046173
commit
6bb9e1dfcb
247 changed files with 16793 additions and 5 deletions
|
@ -6,7 +6,7 @@
|
|||
Timelion is a time series data visualizer that enables you to combine totally
|
||||
independent data sources within a single visualization. It's driven by a simple
|
||||
expression language you use to retrieve time series data, perform calculations
|
||||
to tease out the answers to complex questions, and visualize the results.
|
||||
to tease out the answers to complex questions, and visualize the results.
|
||||
|
||||
For example, Timelion enables you to easily get the answers to questions like:
|
||||
|
||||
|
@ -32,7 +32,7 @@ Timelion expression as a Kibana dashboard panel. You can then add it to
|
|||
a dashboard like any other visualization.
|
||||
|
||||
TIP: You can also create time series visualizations right from the Visualize
|
||||
app--just select the Timeseries visualization type and enter a Timelion
|
||||
app--just select the Timelion visualization type and enter a Timelion
|
||||
expression in the expression field.
|
||||
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ instructions.
|
|||
<<tagcloud-chart,Tag cloud>>:: Display words as a cloud in which the size of the word correspond to its importance
|
||||
<<tilemap,Tile map>>:: Associate the results of an aggregation with geographic
|
||||
locations.
|
||||
Timeseries:: Compute and combine data from multiple time series
|
||||
Timelion:: Compute and combine data from multiple time series
|
||||
data sets.
|
||||
|
||||
. Specify a search query to retrieve the data for your visualization:
|
||||
|
|
14
package.json
14
package.json
|
@ -109,6 +109,7 @@
|
|||
"brace": "0.5.1",
|
||||
"bunyan": "1.7.1",
|
||||
"check-hash": "1.0.1",
|
||||
"color": "1.0.3",
|
||||
"commander": "2.8.1",
|
||||
"css-loader": "0.17.0",
|
||||
"d3": "3.5.6",
|
||||
|
@ -186,12 +187,14 @@
|
|||
"classnames": "2.2.5",
|
||||
"del": "1.2.1",
|
||||
"elasticdump": "2.1.1",
|
||||
"enzyme": "2.7.0",
|
||||
"eslint": "3.11.1",
|
||||
"eslint-plugin-babel": "4.0.0",
|
||||
"eslint-plugin-mocha": "4.7.0",
|
||||
"event-stream": "3.3.2",
|
||||
"expect.js": "0.3.1",
|
||||
"faker": "1.1.0",
|
||||
"flot-charts": "^0.8.3",
|
||||
"grunt": "1.0.1",
|
||||
"grunt-aws-s3": "0.14.5",
|
||||
"grunt-babel": "6.0.0",
|
||||
|
@ -211,6 +214,7 @@
|
|||
"image-diff": "1.6.0",
|
||||
"intern": "3.2.3",
|
||||
"istanbul-instrumenter-loader": "0.1.3",
|
||||
"jsdom": "9.9.1",
|
||||
"karma": "1.2.0",
|
||||
"karma-chrome-launcher": "0.2.0",
|
||||
"karma-coverage": "0.5.1",
|
||||
|
@ -231,15 +235,25 @@
|
|||
"npm": "3.10.10",
|
||||
"portscanner": "1.0.0",
|
||||
"proxyquire": "1.7.10",
|
||||
"pui-react-overlay-trigger": "^7.0.0",
|
||||
"pui-react-tooltip": "^7.0.0",
|
||||
"react": "15.2.0",
|
||||
"react-ace": "3.7.0",
|
||||
"react-addons-test-utils": "15.2.0",
|
||||
"react-anything-sortable": "^1.6.1",
|
||||
"react-color": "^2.2.7",
|
||||
"react-dom": "15.2.0",
|
||||
"react-markdown": "^2.4.2",
|
||||
"react-redux": "4.4.5",
|
||||
"react-router": "2.0.0",
|
||||
"react-router-redux": "4.0.4",
|
||||
"react-select": "^1.0.0-rc.1",
|
||||
"react-sortable": "^1.1.0",
|
||||
"reactcss": "^1.0.7",
|
||||
"redux": "3.0.0",
|
||||
"redux-thunk": "0.1.0",
|
||||
"sass-loader": "4.0.0",
|
||||
"simianhacker-react-resize-aware": "^1.0.11",
|
||||
"simple-git": "1.37.0",
|
||||
"sinon": "1.17.2",
|
||||
"source-map": "0.5.6",
|
||||
|
|
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