mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Table Visualization for TSVB (#12813)
* Adding table vis * Making linter happy * Getting the data api inline * Fixing aggs for table vis * Fixing aggs for table vis * Adding table vis * Adding uiState and sorting * Adding sorting * Adding sorting and removing display fields * fixing color picker in timeseries and gauge; thresholds for trend arrows * Removing thresholds from trends * removing background color * remvoing obsolete tests * Fixing terminology... pivot doesn't make sense * updating error message * making the sort icons match the rest of the app * Fixing eslint bullshit * Fixing a few bugs from merges * Fixing linting issues * Adding a falsy check * Adding aria labels * Changing toggle to use a button * Adding focus-ring back in * Adding check for model and visData; they should never be null * Changing ids to use new htmlIdGenerator function * Switching to htmlIdGenerator * Fixing the way sorting works; fixing the error handling * making no data compatible with vis * Fixing defaults bug; Adding missing css rule * Fixing sorting bug
This commit is contained in:
parent
cb29f3b6f4
commit
fe7e8a59df
64 changed files with 1463 additions and 65 deletions
|
@ -187,6 +187,7 @@
|
|||
"redux": "3.7.2",
|
||||
"redux-actions": "2.2.1",
|
||||
"redux-thunk": "2.2.0",
|
||||
"regression": "2.0.0",
|
||||
"request": "2.61.0",
|
||||
"resize-observer-polyfill": "1.2.1",
|
||||
"rimraf": "2.4.3",
|
||||
|
|
|
@ -70,6 +70,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.vis-editor-content-fullEditor {
|
||||
.flex-parent();
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.vis-editor-sidebar {
|
||||
.flex-parent(1, 0, auto);
|
||||
|
||||
|
|
33
src/core_plugins/metrics/common/__tests__/get_last_value.js
Normal file
33
src/core_plugins/metrics/common/__tests__/get_last_value.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { expect } from 'chai';
|
||||
import getLastValue from '../get_last_value';
|
||||
|
||||
describe('getLastValue(data)', () => {
|
||||
|
||||
it('returns zero if data is not array', () => {
|
||||
expect(getLastValue('foo')).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns the last value', () => {
|
||||
const data = [[1,1]];
|
||||
expect(getLastValue(data)).to.equal(1);
|
||||
});
|
||||
|
||||
it('returns the second to last value if the last value is null (default)', () => {
|
||||
const data = [[1,4], [2, null]];
|
||||
expect(getLastValue(data)).to.equal(4);
|
||||
});
|
||||
|
||||
it('returns the zero if second to last is null (default)', () => {
|
||||
const data = [[1, null], [2, null]];
|
||||
expect(getLastValue(data)).to.equal(0);
|
||||
});
|
||||
|
||||
it('returns the N to last value if the last N-1 values are null (default)', () => {
|
||||
const data = [[1,4], [2, null], [3, null]];
|
||||
expect(getLastValue(data, 3)).to.equal(4);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
21
src/core_plugins/metrics/common/get_last_value.js
Normal file
21
src/core_plugins/metrics/common/get_last_value.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import _ from 'lodash';
|
||||
export default (data, lookback = 2) => {
|
||||
if (_.isNumber(data)) return data;
|
||||
if (!Array.isArray(data)) return 0;
|
||||
// First try the last value
|
||||
const last = data[data.length - 1];
|
||||
const lastValue = Array.isArray(last) && last[1];
|
||||
if (lastValue) return lastValue;
|
||||
|
||||
// If the last value is zero or null because of a partial bucket or
|
||||
// some kind of timeshift weirdness we will show the second to last.
|
||||
let lookbackCounter = 1;
|
||||
let value;
|
||||
while (lookback > lookbackCounter && !value) {
|
||||
const next = data[data.length - ++lookbackCounter];
|
||||
value = _.isArray(next) && next[1] || 0;
|
||||
}
|
||||
return value || 0;
|
||||
};
|
||||
|
||||
|
|
@ -121,6 +121,13 @@ AggSelectOption.props = {
|
|||
option: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function filterByPanelType(panelType) {
|
||||
return agg => {
|
||||
if (panelType === 'table') return agg.value !== 'series_agg';
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
function AggSelect(props) {
|
||||
const { siblings, panelType } = props;
|
||||
|
||||
|
@ -135,7 +142,7 @@ function AggSelect(props) {
|
|||
{ label: 'Metric Aggregations', value: null, heading: true, disabled: true },
|
||||
...metricAggs,
|
||||
{ label: 'Parent Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
|
||||
...pipelineAggs.map(agg => ({ ...agg, disabled: !enablePipelines })),
|
||||
...pipelineAggs.filter(filterByPanelType(panelType)).map(agg => ({ ...agg, disabled: !enablePipelines })),
|
||||
{ label: 'Sibling Pipeline Aggregations', value: null, pipeline: true, heading: true, disabled: true },
|
||||
...siblingAggs.map(agg => ({ ...agg, disabled: !enablePipelines }))
|
||||
];
|
||||
|
|
|
@ -46,6 +46,7 @@ class CalculationAgg extends Component {
|
|||
<div>
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={this.props.panel.type}
|
||||
siblings={this.props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -21,6 +21,7 @@ function CumlativeSumAgg(props) {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const DerivativeAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -44,6 +44,7 @@ export const FilterRatioAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -48,6 +48,7 @@ export const MovingAverageAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -155,6 +155,7 @@ class PercentileAgg extends Component { // eslint-disable-line react/no-multi-co
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={this.props.panel.type}
|
||||
siblings={this.props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -31,6 +31,7 @@ export const PercentileRankAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -26,6 +26,7 @@ export const PositiveOnlyAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -30,6 +30,7 @@ export const SerialDiffAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -8,7 +8,7 @@ import createSelectHandler from '../lib/create_select_handler';
|
|||
import { htmlIdGenerator } from 'ui_framework/services';
|
||||
|
||||
function SeriesAgg(props) {
|
||||
const { model } = props;
|
||||
const { panel, model } = props;
|
||||
|
||||
const handleChange = createChangeHandler(props.onChange, model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
|
@ -27,6 +27,24 @@ function SeriesAgg(props) {
|
|||
{ label: 'Cumlative Sum', value: 'cumlative_sum' },
|
||||
];
|
||||
|
||||
if (panel.type === 'table') {
|
||||
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">
|
||||
Series Agg is not compatible with the table visualization.
|
||||
</div>
|
||||
</div>
|
||||
</AggRow>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AggRow
|
||||
disableDelete={props.disableDelete}
|
||||
|
@ -38,6 +56,7 @@ function SeriesAgg(props) {
|
|||
<div className="vis_editor__item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -34,6 +34,7 @@ export const Static = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -31,6 +31,7 @@ function StandardAgg(props) {
|
|||
<div className="vis_editor__item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -18,9 +18,12 @@ export const StandardDeviationAgg = props => {
|
|||
{ label: 'Raw', value: 'raw' },
|
||||
{ label: 'Upper Bound', value: 'upper' },
|
||||
{ label: 'Lower Bound', value: 'lower' },
|
||||
{ label: 'Bounds Band', value: 'band' }
|
||||
];
|
||||
|
||||
if (panel.type !== 'table') {
|
||||
modeOptions.push({ label: 'Bounds Band', value: 'band' });
|
||||
}
|
||||
|
||||
const handleChange = createChangeHandler(props.onChange, model);
|
||||
const handleSelectChange = createSelectHandler(handleChange);
|
||||
const handleTextChange = createTextHandler(handleChange);
|
||||
|
@ -39,6 +42,7 @@ export const StandardDeviationAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -64,6 +64,7 @@ export const StandardSiblingAgg = props => {
|
|||
<div className="vis_editor__row_item">
|
||||
<div className="vis_editor__label">Aggregation</div>
|
||||
<AggSelect
|
||||
panelType={props.panel.type}
|
||||
siblings={props.siblings}
|
||||
value={model.type}
|
||||
onChange={handleSelectChange('type')}
|
||||
|
|
|
@ -3,11 +3,13 @@ import React from 'react';
|
|||
import timeseries from './panel_config/timeseries';
|
||||
import metric from './panel_config/metric';
|
||||
import topN from './panel_config/top_n';
|
||||
import table from './panel_config/table';
|
||||
import gauge from './panel_config/gauge';
|
||||
import markdown from './panel_config/markdown';
|
||||
|
||||
const types = {
|
||||
timeseries,
|
||||
table,
|
||||
metric,
|
||||
top_n: topN,
|
||||
gauge,
|
||||
|
|
158
src/core_plugins/metrics/public/components/panel_config/table.js
Normal file
158
src/core_plugins/metrics/public/components/panel_config/table.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import FieldSelect from '../aggs/field_select';
|
||||
import SeriesEditor from '../series_editor';
|
||||
import { IndexPattern } from '../index_pattern';
|
||||
import createTextHandler from '../lib/create_text_handler';
|
||||
import createSelectHandler from '../lib/create_select_handler';
|
||||
import uuid from 'uuid';
|
||||
import YesNo from '../yes_no';
|
||||
import { htmlIdGenerator } from 'ui_framework/services';
|
||||
|
||||
class TablePanelConfig 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() }];
|
||||
}
|
||||
this.props.onChange(parts);
|
||||
}
|
||||
|
||||
switchTab(selectedTab) {
|
||||
this.setState({ selectedTab });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedTab } = this.state;
|
||||
const defaults = { drilldown_url: '', filter: '', pivot_label: '', pivot_rows: 10 };
|
||||
const model = { ...defaults, ...this.props.model };
|
||||
const handleSelectChange = createSelectHandler(this.props.onChange);
|
||||
const handleTextChange = createTextHandler(this.props.onChange);
|
||||
const htmlId = htmlIdGenerator();
|
||||
let view;
|
||||
if (selectedTab === 'data') {
|
||||
view = (
|
||||
<div>
|
||||
<div className="vis_editor__table-pivot-fields">
|
||||
<div className="vis_editor__container">
|
||||
<div className="vis_ediotr__vis_config-row">
|
||||
<p>
|
||||
For the table visualization you need to define a field to
|
||||
group by using a terms aggregation.
|
||||
</p>
|
||||
</div>
|
||||
<div className="vis_editor__vis_config-row">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('field')}>Group By Field</label>
|
||||
<div className="vis_editor__row_item">
|
||||
<FieldSelect
|
||||
id={htmlId('field')}
|
||||
fields={this.props.fields}
|
||||
value={model.pivot_id}
|
||||
indexPattern={model.index_pattern}
|
||||
onChange={handleSelectChange('pivot_id')}
|
||||
/>
|
||||
</div>
|
||||
<label className="vis_editor__label" htmlFor={htmlId('pivotLabelInput')}>Column Label</label>
|
||||
<input
|
||||
id={htmlId('pivotLabelInput')}
|
||||
className="vis_editor__input-grows"
|
||||
type="text"
|
||||
onChange={handleTextChange('pivot_label')}
|
||||
value={model.pivot_label}
|
||||
/>
|
||||
<label className="vis_editor__label" htmlFor={htmlId('pivotRowsInput')}>Rows</label>
|
||||
<input
|
||||
id={htmlId('pivotRowsInput')}
|
||||
className="vis_editor__input-number"
|
||||
type="number"
|
||||
onChange={handleTextChange('pivot_rows')}
|
||||
value={model.pivot_rows}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SeriesEditor
|
||||
fields={this.props.fields}
|
||||
model={this.props.model}
|
||||
name={this.props.name}
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
view = (
|
||||
<div className="vis_editor__container">
|
||||
<div className="vis_editor__vis_config-row">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('drilldownInput')}>Item Url (This supports mustache templating.
|
||||
<code>{'{{key}}'}</code> is set to the term)
|
||||
</label>
|
||||
<input
|
||||
id={htmlId('drilldownInput')}
|
||||
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">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('panelFilterInput')}>Panel Filter</label>
|
||||
<input
|
||||
id={htmlId('panelFilterInput')}
|
||||
className="vis_editor__input-grows"
|
||||
type="text"
|
||||
onChange={handleTextChange('filter')}
|
||||
value={model.filter}
|
||||
/>
|
||||
<label className="vis_editor__label" htmlFor={htmlId('globalFilterOption')}>Ignore Global Filter</label>
|
||||
<YesNo
|
||||
id={htmlId('globalFilterOption')}
|
||||
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={() => this.switchTab('data')}
|
||||
>
|
||||
Columns
|
||||
</div>
|
||||
<div
|
||||
className={`kbnTabs__tab${selectedTab === 'options' && '-active' || ''}`}
|
||||
onClick={() => this.switchTab('options')}
|
||||
>
|
||||
Panel Options
|
||||
</div>
|
||||
</div>
|
||||
{view}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TablePanelConfig.propTypes = {
|
||||
fields: PropTypes.object,
|
||||
model: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
visData: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TablePanelConfig;
|
||||
|
|
@ -5,12 +5,14 @@ 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 table from './vis_types/table/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,
|
||||
table,
|
||||
metric,
|
||||
timeseries,
|
||||
gauge,
|
||||
|
@ -53,7 +55,6 @@ class Series extends Component {
|
|||
if (Component) {
|
||||
const params = {
|
||||
className: this.props.className,
|
||||
colorPicker: this.props.colorPicker,
|
||||
disableAdd: this.props.disableAdd,
|
||||
disableDelete: this.props.disableDelete,
|
||||
fields: this.props.fields,
|
||||
|
@ -88,7 +89,6 @@ Series.defaultProps = {
|
|||
|
||||
Series.propTypes = {
|
||||
className: PropTypes.string,
|
||||
colorPicker: PropTypes.bool,
|
||||
disableAdd: PropTypes.bool,
|
||||
disableDelete: PropTypes.bool,
|
||||
fields: PropTypes.object,
|
||||
|
|
|
@ -15,6 +15,11 @@ class VisEditor extends Component {
|
|||
const reversed = get(appState, 'options.darkTheme', false);
|
||||
this.state = { model: props.vis.params, dirty: false, autoApply: true, reversed };
|
||||
this.onBrush = brushHandler(props.vis.API.timeFilter);
|
||||
this.handleUiState = this.handleUiState.bind(this, props.vis);
|
||||
}
|
||||
|
||||
handleUiState(vis, ...args) {
|
||||
vis.uiStateVal(...args);
|
||||
this.handleAppStateChange = this.handleAppStateChange.bind(this);
|
||||
}
|
||||
|
||||
|
@ -66,6 +71,8 @@ class VisEditor extends Component {
|
|||
dateFormat={this.props.config.get('dateFormat')}
|
||||
reversed={reversed}
|
||||
onBrush={this.onBrush}
|
||||
onUiState={this.handleUiState}
|
||||
uiState={this.props.vis.getUiState()}
|
||||
fields={this.props.vis.fields}
|
||||
model={this.props.vis.params}
|
||||
visData={this.props.visData}
|
||||
|
@ -88,6 +95,8 @@ class VisEditor extends Component {
|
|||
autoApply={this.state.autoApply}
|
||||
model={model}
|
||||
visData={this.props.visData}
|
||||
onUiState={this.handleUiState}
|
||||
uiState={this.props.vis.getUiState()}
|
||||
onBrush={this.onBrush}
|
||||
onCommit={handleCommit}
|
||||
onToggleAutoApply={handleAutoApplyToggle}
|
||||
|
|
|
@ -108,6 +108,8 @@ class VisEditorVisualization extends Component {
|
|||
model={this.props.model}
|
||||
onBrush={this.props.onBrush}
|
||||
onChange={this.handleChange}
|
||||
onUiState={this.props.onUiState}
|
||||
uiState={this.props.uiState}
|
||||
visData={this.props.visData}
|
||||
/>
|
||||
</div>
|
||||
|
@ -130,6 +132,8 @@ VisEditorVisualization.propTypes = {
|
|||
onBrush: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onCommit: PropTypes.func,
|
||||
onUiState: PropTypes.func,
|
||||
uiState: PropTypes.object,
|
||||
onToggleAutoApply: PropTypes.func,
|
||||
visData: PropTypes.object,
|
||||
dirty: PropTypes.bool,
|
||||
|
|
|
@ -42,7 +42,8 @@ function VisPicker(props) {
|
|||
{ 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' }
|
||||
{ type: 'markdown', icon: 'fa-paragraph', label: 'Markdown' },
|
||||
{ type: 'table', icon: 'fa-paragraph', label: 'Table' }
|
||||
].map(item => {
|
||||
return (
|
||||
<VisPickerItem
|
||||
|
|
|
@ -98,17 +98,14 @@ function GaugeSeries(props) {
|
|||
);
|
||||
}
|
||||
|
||||
let colorPicker;
|
||||
if (props.colorPicker) {
|
||||
colorPicker = (
|
||||
<ColorPicker
|
||||
disableTrash={true}
|
||||
onChange={props.onChange}
|
||||
name="color"
|
||||
value={model.color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const colorPicker = (
|
||||
<ColorPicker
|
||||
disableTrash={true}
|
||||
onChange={props.onChange}
|
||||
name="color"
|
||||
value={model.color}
|
||||
/>
|
||||
);
|
||||
|
||||
let dragHandle;
|
||||
if (!props.disableDelete) {
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import uuid from 'uuid';
|
||||
import DataFormatPicker from '../../data_format_picker';
|
||||
import createSelectHandler from '../../lib/create_select_handler';
|
||||
import createTextHandler from '../../lib/create_text_handler';
|
||||
import FieldSelect from '../../aggs/field_select';
|
||||
import Select from 'react-select';
|
||||
import YesNo from '../../yes_no';
|
||||
import ColorRules from '../../color_rules';
|
||||
import { htmlIdGenerator } from 'ui_framework/services';
|
||||
|
||||
class TableSeriesConfig extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
const { model } = this.props;
|
||||
if (!model.color_rules || (model.color_rules && model.color_rules.length === 0)) {
|
||||
this.props.onChange({
|
||||
color_rules: [{ id: uuid.v1() }]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const defaults = { offset_time: '', value_template: '' };
|
||||
const model = { ...defaults, ...this.props.model };
|
||||
const handleSelectChange = createSelectHandler(this.props.onChange);
|
||||
const handleTextChange = createTextHandler(this.props.onChange);
|
||||
const htmlId = htmlIdGenerator();
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<div className="vis_editor__series_config-container">
|
||||
<div className="vis_editor__series_config-row">
|
||||
<DataFormatPicker
|
||||
onChange={handleSelectChange('formatter')}
|
||||
value={model.formatter}
|
||||
/>
|
||||
<label className="vis_editor__label" htmlFor={htmlId('valueTemplateInput')}>Template (eg.<code>{'{{value}}/s'}</code>)</label>
|
||||
<input
|
||||
id={htmlId('valueTemplateInput')}
|
||||
className="vis_editor__input-grows"
|
||||
onChange={handleTextChange('value_template')}
|
||||
value={model.value_template}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__series_config-row">
|
||||
<label className="vis_editor__label" htmlFor={htmlId('filterInput')}>Filter</label>
|
||||
<input
|
||||
id={htmlId('filterInput')}
|
||||
className="vis_editor__input-grows"
|
||||
onChange={handleTextChange('filter')}
|
||||
value={model.filter}
|
||||
/>
|
||||
<label className="vis_editor__label">Show Trend Arrows</label>
|
||||
<YesNo
|
||||
value={model.trend_arrows}
|
||||
name="trend_arrows"
|
||||
onChange={this.props.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="vis_editor__series_config-row">
|
||||
<div className="vis_editor__row_item">
|
||||
<FieldSelect
|
||||
fields={this.props.fields}
|
||||
indexPattern={this.props.panel.index_pattern}
|
||||
value={model.aggregate_by}
|
||||
onChange={handleSelectChange('aggregate_by')}
|
||||
/>
|
||||
</div>
|
||||
<label className="vis_editor__label" htmlFor={htmlId('aggregateFunctionInput')}>Aggregate Function</label>
|
||||
<div className="vis_editor__row_item">
|
||||
<Select
|
||||
inputProps={{ id: htmlId('aggregateFunctionInput') }}
|
||||
value={model.aggregate_function}
|
||||
options={functionOptions}
|
||||
onChange={handleSelectChange('aggregate_function')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="vis_editor__series_config-row summarize__colorRules">
|
||||
<ColorRules
|
||||
primaryName="text"
|
||||
primaryVarName="text"
|
||||
hideSecondary={true}
|
||||
model={model}
|
||||
onChange={this.props.onChange}
|
||||
name="color_rules"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TableSeriesConfig.propTypes = {
|
||||
fields: PropTypes.object,
|
||||
model: PropTypes.object,
|
||||
onChange: PropTypes.func
|
||||
};
|
||||
|
||||
export default TableSeriesConfig;
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import basicAggs from '../../../../common/basic_aggs';
|
||||
export function isSortable(metric) {
|
||||
return basicAggs.includes(metric.type);
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import AddDeleteButtons from '../../add_delete_buttons';
|
||||
import SeriesConfig from './config';
|
||||
import Sortable from 'react-anything-sortable';
|
||||
import Tooltip from '../../tooltip';
|
||||
import createTextHandler from '../../lib/create_text_handler';
|
||||
import createAggRowRender from '../../lib/create_agg_row_render';
|
||||
|
||||
function TopNSeries(props) {
|
||||
const {
|
||||
model,
|
||||
onAdd,
|
||||
onChange,
|
||||
onDelete,
|
||||
disableDelete,
|
||||
disableAdd,
|
||||
selectedTab,
|
||||
visible
|
||||
} = props;
|
||||
|
||||
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>
|
||||
);
|
||||
} else {
|
||||
seriesBody = (
|
||||
<SeriesConfig
|
||||
panel={props.panel}
|
||||
fields={props.fields}
|
||||
model={props.model}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
body = (
|
||||
<div className="vis_editor__series-row">
|
||||
<div className="kbnTabs sm">
|
||||
<div
|
||||
className={metricsClassName}
|
||||
onClick={() => props.switchTab('metrics')}
|
||||
>
|
||||
Metrics
|
||||
</div>
|
||||
<div
|
||||
className={optionsClassname}
|
||||
onClick={() => props.switchTab('options')}
|
||||
>
|
||||
Options
|
||||
</div>
|
||||
</div>
|
||||
{seriesBody}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let dragHandle;
|
||||
if (!props.disableDelete) {
|
||||
dragHandle = (
|
||||
<Tooltip text="Sort">
|
||||
<div className="vis_editor__sort thor__button-outlined-default sm">
|
||||
<i className="fa fa-sort" />
|
||||
</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">
|
||||
<button className="vis_editor__series-visibility-toggle" onClick={props.toggleVisible}>
|
||||
<i className={caretClassName}/>
|
||||
</button>
|
||||
<div className="vis_editor__row vis_editor__row_item">
|
||||
<input
|
||||
aria-label="Label"
|
||||
className="vis_editor__input-grows"
|
||||
onChange={handleChange('label')}
|
||||
placeholder="Label"
|
||||
value={model.label}
|
||||
/>
|
||||
</div>
|
||||
{ dragHandle }
|
||||
<AddDeleteButtons
|
||||
addTooltip="Add Series"
|
||||
deleteTooltip="Delete Series"
|
||||
cloneTooltip="Clone Series"
|
||||
onDelete={onDelete}
|
||||
onClone={props.onClone}
|
||||
onAdd={onAdd}
|
||||
disableDelete={disableDelete}
|
||||
disableAdd={disableAdd}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{ body }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TopNSeries.propTypes = {
|
||||
className: PropTypes.string,
|
||||
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,205 @@
|
|||
import _ from 'lodash';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import ticFormatter from '../../lib/tick_formatter';
|
||||
import calculateLabel from '../../../../common/calculate_label';
|
||||
import { isSortable } from './is_sortable';
|
||||
import Tooltip from '../../tooltip';
|
||||
import replaceVars from '../../lib/replace_vars';
|
||||
|
||||
function getColor(rules, colorKey, value) {
|
||||
let color;
|
||||
if (rules) {
|
||||
rules.forEach((rule) => {
|
||||
if (rule.opperator && rule.value != null) {
|
||||
if (_[rule.opperator](value, rule.value)) {
|
||||
color = rule[colorKey];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
class TableVis extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.renderRow = this.renderRow.bind(this);
|
||||
}
|
||||
|
||||
renderRow(row) {
|
||||
const { model } = this.props;
|
||||
const rowId = row.key;
|
||||
let rowDisplay = rowId;
|
||||
if (model.drilldown_url) {
|
||||
const url = replaceVars(model.drilldown_url, {}, { key: row.key });
|
||||
rowDisplay = (<a href={url}>{rowDisplay}</a>);
|
||||
}
|
||||
const columns = row.series.filter(item => item).map(item => {
|
||||
const column = model.series.find(c => c.id === item.id);
|
||||
if (!column) return null;
|
||||
const formatter = ticFormatter(column.formatter, column.value_template);
|
||||
const value = formatter(item.last);
|
||||
let trend;
|
||||
if (column.trend_arrows) {
|
||||
const trendClass = item.slope > 0 ? 'fa-long-arrow-up' : 'fa-long-arrow-down';
|
||||
trend = (
|
||||
<span className="tsvb-table__trend">
|
||||
<i className={`fa ${trendClass}`}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const style = { color: getColor(column.color_rules, 'text', item.last) };
|
||||
return (
|
||||
<td key={`${rowId}-${item.id}`} className="tsvb-table__value" style={style}>
|
||||
<span className="tsvb-table__value-display">{ value }</span>
|
||||
{trend}
|
||||
</td>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<tr key={rowId}>
|
||||
<td className="tsvb-table__fieldName">{rowDisplay}</td>
|
||||
{columns}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const { model, uiState, onUiState } = this.props;
|
||||
const stateKey = `${model.type}.sort`;
|
||||
const sort = uiState.get(stateKey, {
|
||||
column: '_default_',
|
||||
order: 'asc'
|
||||
});
|
||||
const columns = model.series.map(item => {
|
||||
const metric = _.last(item.metrics);
|
||||
const label = item.label || calculateLabel(metric, item.metrics);
|
||||
const handleClick = () => {
|
||||
if (!isSortable(metric)) return;
|
||||
let order;
|
||||
if (sort.column === item.id) {
|
||||
order = sort.order === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
order = 'asc';
|
||||
}
|
||||
onUiState(stateKey, { column: item.id, order });
|
||||
};
|
||||
let sortComponent;
|
||||
if (isSortable(metric)) {
|
||||
let sortIcon;
|
||||
if (sort.column === item.id) {
|
||||
sortIcon = sort.order === 'asc' ? 'sort-asc' : 'sort-desc';
|
||||
} else {
|
||||
sortIcon = 'sort';
|
||||
}
|
||||
sortComponent = (
|
||||
<i className={`fa fa-${sortIcon}`} />
|
||||
);
|
||||
}
|
||||
let headerContent = (
|
||||
<span>{label} {sortComponent}</span>
|
||||
);
|
||||
if (!isSortable(metric)) {
|
||||
headerContent = (
|
||||
<Tooltip text="This Column is Not Sortable">{headerContent}</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<th
|
||||
className="tsvb-table__columnName"
|
||||
onClick={handleClick}
|
||||
key={item.id}
|
||||
>
|
||||
{headerContent}
|
||||
</th>
|
||||
);
|
||||
});
|
||||
const label = model.pivot_label || model.pivot_field || model.pivot_id;
|
||||
let sortIcon;
|
||||
if (sort.column === '_default_') {
|
||||
sortIcon = sort.order === 'asc' ? 'sort-asc' : 'sort-desc';
|
||||
} else {
|
||||
sortIcon = 'sort';
|
||||
}
|
||||
const sortComponent = (
|
||||
<i className={`fa fa-${sortIcon}`} />
|
||||
);
|
||||
const handleSortClick = () => {
|
||||
let order;
|
||||
if (sort.column === '_default_') {
|
||||
order = sort.order === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
order = 'asc';
|
||||
}
|
||||
onUiState(stateKey, { column: '_default_', order });
|
||||
};
|
||||
return (
|
||||
<tr>
|
||||
<th onClick={handleSortClick}>{label} {sortComponent}</th>
|
||||
{ columns }
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visData, model } = this.props;
|
||||
const header = this.renderHeader();
|
||||
let rows;
|
||||
let reversedClass = '';
|
||||
|
||||
if (this.props.reversed) {
|
||||
reversedClass = 'reversed';
|
||||
}
|
||||
|
||||
if (_.isArray(visData.series) && visData.series.length) {
|
||||
rows = visData.series.map(this.renderRow);
|
||||
} else {
|
||||
let message = 'No results available.';
|
||||
if (!model.pivot_id) {
|
||||
message += ' You must choose a group by field for this visualization.';
|
||||
}
|
||||
rows = (
|
||||
<tr>
|
||||
<td
|
||||
className="tsvb-table__noResults"
|
||||
colSpan={model.series.length + 1}
|
||||
>
|
||||
{message}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
return(
|
||||
<div className={`dashboard__visualization ${reversedClass}`}>
|
||||
<table className="table">
|
||||
<thead>
|
||||
{header}
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TableVis.defaultProps = {
|
||||
sort: {}
|
||||
};
|
||||
|
||||
TableVis.propTypes = {
|
||||
visData: PropTypes.object,
|
||||
model: PropTypes.object,
|
||||
backgroundColor: PropTypes.string,
|
||||
onPaginate: PropTypes.func,
|
||||
onUiState: PropTypes.func,
|
||||
uiState: PropTypes.object,
|
||||
pageNumber: PropTypes.number,
|
||||
reversed: PropTypes.bool
|
||||
};
|
||||
|
||||
export default TableVis;
|
|
@ -98,17 +98,14 @@ function TimeseriesSeries(props) {
|
|||
);
|
||||
}
|
||||
|
||||
let colorPicker;
|
||||
if (props.colorPicker) {
|
||||
colorPicker = (
|
||||
<ColorPicker
|
||||
disableTrash={true}
|
||||
onChange={props.onChange}
|
||||
name="color"
|
||||
value={model.color}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const colorPicker = (
|
||||
<ColorPicker
|
||||
disableTrash={true}
|
||||
onChange={props.onChange}
|
||||
name="color"
|
||||
value={model.color}
|
||||
/>
|
||||
);
|
||||
|
||||
let dragHandle;
|
||||
if (!props.disableDelete) {
|
||||
|
|
|
@ -5,6 +5,7 @@ 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 table from './vis_types/table/vis';
|
||||
import gauge from './vis_types/gauge/vis';
|
||||
import markdown from './vis_types/markdown/vis';
|
||||
import Error from './error';
|
||||
|
@ -14,6 +15,7 @@ const types = {
|
|||
timeseries,
|
||||
metric,
|
||||
top_n: topN,
|
||||
table,
|
||||
gauge,
|
||||
markdown
|
||||
};
|
||||
|
@ -29,7 +31,9 @@ function Visualization(props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
const noData = _.get(visData, `${model.id}.series`).length === 0;
|
||||
|
||||
const path = visData.type === 'table' ? 'series' : `${model.id}.series`;
|
||||
const noData = _.get(visData, path, []).length === 0;
|
||||
if (noData) {
|
||||
return (
|
||||
<div className={props.className}>
|
||||
|
@ -47,7 +51,9 @@ function Visualization(props) {
|
|||
model: props.model,
|
||||
onBrush: props.onBrush,
|
||||
onChange: props.onChange,
|
||||
visData: props.visData
|
||||
onUiState: props.onUiState,
|
||||
uiState: props.uiState,
|
||||
visData: visData.type === model.type ? visData : {}
|
||||
});
|
||||
}
|
||||
return <div className={props.className} />;
|
||||
|
@ -63,6 +69,8 @@ Visualization.propTypes = {
|
|||
model: PropTypes.object,
|
||||
onBrush: PropTypes.func,
|
||||
onChange: PropTypes.func,
|
||||
onUiState: PropTypes.func,
|
||||
uiState: PropTypes.object,
|
||||
reversed: PropTypes.bool,
|
||||
visData: PropTypes.object,
|
||||
dateFormat: PropTypes.string
|
||||
|
|
|
@ -8,16 +8,18 @@ const MetricsRequestHandlerProvider = function (Private, Notifier, config, timef
|
|||
|
||||
return {
|
||||
name: 'metrics',
|
||||
handler: function (vis /*, appState, uiState, queryFilter*/) {
|
||||
handler: function (vis , appState, uiState) {
|
||||
const timezone = Private(timezoneProvider)();
|
||||
return new Promise((resolve) => {
|
||||
const panel = vis.params;
|
||||
const uiStateObj = uiState.get(panel.type, {});
|
||||
const timeRange = vis.params.timeRange || timefilter.getBounds();
|
||||
if (panel && panel.id) {
|
||||
const params = {
|
||||
timerange: { timezone, ...timeRange },
|
||||
filters: [dashboardContext()],
|
||||
panels: [panel]
|
||||
panels: [panel],
|
||||
state: uiStateObj
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
.vis_editor__input-grows;
|
||||
width: 100%;
|
||||
}
|
||||
.vis_editor__input-number {
|
||||
.vis_editor__input;
|
||||
width: 60px;
|
||||
}
|
||||
.vis_editor__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -282,6 +286,7 @@
|
|||
height: 250px;
|
||||
line-height: normal;
|
||||
background-color: @white;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.vis_editor__visualization-draghandle {
|
||||
|
@ -580,3 +585,7 @@
|
|||
.vis_editor__dirty_controls-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.vis_editor__table-pivot-fields {
|
||||
border-bottom: 2px solid @lineColor;
|
||||
}
|
||||
|
|
|
@ -104,3 +104,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tsvb-table__value {
|
||||
font-size: 1.1em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tsvb-table__trend {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.tsvb-table__columnName {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default function getIntervalAndTimefield(panel, series) {
|
||||
export default function getIntervalAndTimefield(panel, series = {}) {
|
||||
const timeField = series.override_index_pattern && series.series_time_field || panel.time_field;
|
||||
const interval = series.override_index_pattern && series.series_interval || panel.interval;
|
||||
return { timeField, interval };
|
||||
|
|
|
@ -1,32 +1,8 @@
|
|||
import getRequestParams from './get_request_params';
|
||||
import handleResponseBody from './handle_response_body';
|
||||
import handleErrorResponse from './handle_error_response';
|
||||
import getAnnotations from './get_annotations';
|
||||
import { getTableData } from './get_table_data';
|
||||
import { getSeriesData } from './get_series_data';
|
||||
export default function getPanelData(req) {
|
||||
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data');
|
||||
return panel => {
|
||||
|
||||
const bodies = panel.series.map(series => getRequestParams(req, panel, series));
|
||||
const params = {
|
||||
body: bodies.reduce((acc, items) => acc.concat(items), [])
|
||||
};
|
||||
return callWithRequest(req, 'msearch', params)
|
||||
.then(resp => {
|
||||
const series = resp.responses.map(handleResponseBody(panel));
|
||||
return {
|
||||
[panel.id]: {
|
||||
id: panel.id,
|
||||
series: series.reduce((acc, series) => acc.concat(series), [])
|
||||
}
|
||||
};
|
||||
})
|
||||
.then(resp => {
|
||||
if (!panel.annotations || panel.annotations.length === 0) return resp;
|
||||
return getAnnotations(req, panel).then(annotations => {
|
||||
resp[panel.id].annotations = annotations;
|
||||
return resp;
|
||||
});
|
||||
})
|
||||
.catch(handleErrorResponse(panel));
|
||||
if (panel.type === 'table') return getTableData(req, panel);
|
||||
return getSeriesData(req, panel);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import getRequestParams from './series/get_request_params';
|
||||
import handleResponseBody from './series/handle_response_body';
|
||||
import handleErrorResponse from './handle_error_response';
|
||||
import getAnnotations from './get_annotations';
|
||||
export function getSeriesData(req, panel) {
|
||||
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data');
|
||||
const bodies = panel.series.map(series => getRequestParams(req, panel, series));
|
||||
const params = {
|
||||
body: bodies.reduce((acc, items) => acc.concat(items), [])
|
||||
};
|
||||
return callWithRequest(req, 'msearch', params)
|
||||
.then(resp => {
|
||||
const series = resp.responses.map(handleResponseBody(panel));
|
||||
return {
|
||||
[panel.id]: {
|
||||
id: panel.id,
|
||||
series: series.reduce((acc, series) => acc.concat(series), [])
|
||||
}
|
||||
};
|
||||
})
|
||||
.then(resp => {
|
||||
if (!panel.annotations || panel.annotations.length === 0) return resp;
|
||||
return getAnnotations(req, panel).then(annotations => {
|
||||
resp[panel.id].annotations = annotations;
|
||||
return resp;
|
||||
});
|
||||
})
|
||||
.then(resp => {
|
||||
resp.type = panel.type;
|
||||
return resp;
|
||||
})
|
||||
.catch(handleErrorResponse(panel));
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import buildRequestBody from './table/build_request_body';
|
||||
import handleErrorResponse from './handle_error_response';
|
||||
import { get } from 'lodash';
|
||||
import processBucket from './table/process_bucket';
|
||||
export async function getTableData(req, panel) {
|
||||
const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data');
|
||||
const params = {
|
||||
index: panel.index_pattern,
|
||||
body: buildRequestBody(req, panel)
|
||||
};
|
||||
try {
|
||||
const resp = await callWithRequest(req, 'search', params);
|
||||
const buckets = get(resp, 'aggregations.pivot.buckets', []);
|
||||
return { type: 'table', series: buckets.map(processBucket(panel)) };
|
||||
} catch (err) {
|
||||
if (err.body) {
|
||||
err.response = err.body;
|
||||
return { type: 'table', ...handleErrorResponse(panel)(err) };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import _ from 'lodash';
|
||||
export function calculateAggRoot(doc, column) {
|
||||
let aggRoot = `aggs.pivot.aggs.${column.id}.aggs`;
|
||||
if (_.has(doc, `aggs.pivot.aggs.${column.id}.aggs.column_filter`)) {
|
||||
aggRoot = `aggs.pivot.aggs.${column.id}.aggs.column_filter.aggs`;
|
||||
}
|
||||
return aggRoot;
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import _ from 'lodash';
|
||||
import getBucketSize from '../../helpers/get_bucket_size';
|
||||
import getIntervalAndTimefield from '../../get_interval_and_timefield';
|
||||
import getTimerange from '../../helpers/get_timerange';
|
||||
import { calculateAggRoot } from './calculate_agg_root';
|
||||
|
||||
export default function dateHistogram(req, panel) {
|
||||
return next => doc => {
|
||||
const { timeField, interval } = getIntervalAndTimefield(panel);
|
||||
const { bucketSize, intervalString } = getBucketSize(req, interval);
|
||||
const { from, to } = getTimerange(req);
|
||||
panel.series.forEach(column => {
|
||||
const aggRoot = calculateAggRoot(doc, column);
|
||||
_.set(doc, `${aggRoot}.timeseries.date_histogram`, {
|
||||
field: timeField,
|
||||
interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: {
|
||||
min: from.valueOf(),
|
||||
max: to.valueOf() - (bucketSize * 1000)
|
||||
}
|
||||
});
|
||||
});
|
||||
return next(doc);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/* eslint max-len:0 */
|
||||
const filter = metric => metric.type === 'filter_ratio';
|
||||
import bucketTransform from '../../helpers/bucket_transform';
|
||||
import _ from 'lodash';
|
||||
import { calculateAggRoot } from './calculate_agg_root';
|
||||
export default function ratios(req, panel) {
|
||||
return () => doc => {
|
||||
panel.series.forEach(column => {
|
||||
const aggRoot = calculateAggRoot(doc, column);
|
||||
if (column.metrics.some(filter)) {
|
||||
column.metrics.filter(filter).forEach(metric => {
|
||||
_.set(doc, `${aggRoot}.timecolumn.aggs.${metric.id}-numerator.filter`, {
|
||||
query_string: { query: metric.numerator || '*', analyze_wildcard: true }
|
||||
});
|
||||
_.set(doc, `${aggRoot}.timecolumn.aggs.${metric.id}-denominator.filter`, {
|
||||
query_string: { query: metric.denominator || '*', analyze_wildcard: true }
|
||||
});
|
||||
|
||||
let numeratorPath = `${metric.id}-numerator>_count`;
|
||||
let denominatorPath = `${metric.id}-denominator>_count`;
|
||||
|
||||
if (metric.metric_agg !== 'count' && bucketTransform[metric.metric_agg]) {
|
||||
const aggBody = {
|
||||
metric: bucketTransform[metric.metric_agg]({
|
||||
type: metric.metric_agg,
|
||||
field: metric.field
|
||||
})
|
||||
};
|
||||
_.set(doc, `${aggRoot}.timecolumn.aggs.${metric.id}-numerator.aggs`, aggBody);
|
||||
_.set(doc, `${aggBody}.timecolumn.aggs.${metric.id}-denominator.aggs`, aggBody);
|
||||
numeratorPath = `${metric.id}-numerator>metric`;
|
||||
denominatorPath = `${metric.id}-denominator>metric`;
|
||||
}
|
||||
|
||||
_.set(doc, `${aggRoot}.timecolumn.aggs.${metric.id}`, {
|
||||
bucket_script: {
|
||||
buckets_path: {
|
||||
numerator: numeratorPath,
|
||||
denominator: denominatorPath
|
||||
},
|
||||
script: 'params.numerator != null && params.denominator != null && params.denominator > 0 ? params.numerator / params.denominator : 0'
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
return doc;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import pivot from './pivot';
|
||||
import query from './query';
|
||||
import splitByEverything from './split_by_everything';
|
||||
import splitByTerms from './split_by_terms';
|
||||
import dateHistogram from './date_histogram';
|
||||
import metricBuckets from './metric_buckets';
|
||||
import siblingBuckets from './sibling_buckets';
|
||||
import filterRatios from './filter_ratios';
|
||||
|
||||
export default [
|
||||
query,
|
||||
pivot,
|
||||
splitByTerms,
|
||||
splitByEverything,
|
||||
dateHistogram,
|
||||
metricBuckets,
|
||||
siblingBuckets,
|
||||
filterRatios
|
||||
];
|
|
@ -0,0 +1,28 @@
|
|||
import _ from 'lodash';
|
||||
import getBucketSize from '../../helpers/get_bucket_size';
|
||||
import bucketTransform from '../../helpers/bucket_transform';
|
||||
import getIntervalAndTimefield from '../../get_interval_and_timefield';
|
||||
import { calculateAggRoot } from './calculate_agg_root';
|
||||
export default function metricBuckets(req, panel) {
|
||||
return next => doc => {
|
||||
const { interval } = getIntervalAndTimefield(panel);
|
||||
const { intervalString } = getBucketSize(req, interval);
|
||||
panel.series.forEach(column => {
|
||||
const aggRoot = calculateAggRoot(doc, column);
|
||||
column.metrics
|
||||
.filter(row => !/_bucket$/.test(row.type) && !/^series/.test(row.type))
|
||||
.forEach(metric => {
|
||||
const fn = bucketTransform[metric.type];
|
||||
if (fn) {
|
||||
try {
|
||||
const bucket = fn(metric, column.metrics, intervalString);
|
||||
_.set(doc, `${aggRoot}.timeseries.aggs.${metric.id}`, bucket);
|
||||
} catch (e) {
|
||||
// meh
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return next(doc);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { get, set, last } from 'lodash';
|
||||
|
||||
import basicAggs from '../../../../../common/basic_aggs';
|
||||
import getBucketSize from '../../helpers/get_bucket_size';
|
||||
import getTimerange from '../../helpers/get_timerange';
|
||||
import getIntervalAndTimefield from '../../get_interval_and_timefield';
|
||||
import getBucketsPath from '../../helpers/get_buckets_path';
|
||||
import bucketTransform from '../../helpers/bucket_transform';
|
||||
|
||||
export default function pivot(req, panel) {
|
||||
return next => doc => {
|
||||
const { sort } = req.payload.state;
|
||||
if (panel.pivot_id) {
|
||||
set(doc, 'aggs.pivot.terms.field', panel.pivot_id);
|
||||
set(doc, 'aggs.pivot.terms.size', panel.pivot_rows);
|
||||
if (sort) {
|
||||
const series = panel.series.find(item => item.id === sort.column);
|
||||
const { timeField, interval } = getIntervalAndTimefield(panel, series);
|
||||
const { bucketSize } = getBucketSize(req, interval);
|
||||
const { to } = getTimerange(req);
|
||||
const metric = series && last(series.metrics);
|
||||
if (metric && metric.type === 'count') {
|
||||
set(doc, 'aggs.pivot.terms.order', { _count: sort.order });
|
||||
} else if (metric && basicAggs.includes(metric.type)) {
|
||||
const sortAggKey = `${metric.id}-SORT`;
|
||||
const fn = bucketTransform[metric.type];
|
||||
const bucketPath = getBucketsPath(metric.id, series.metrics)
|
||||
.replace(metric.id, `${sortAggKey} > SORT`);
|
||||
set(doc, `aggs.pivot.terms.order`, { [bucketPath]: sort.order });
|
||||
set(doc, `aggs.pivot.aggs`, {
|
||||
[sortAggKey]: {
|
||||
filter: {
|
||||
range: {
|
||||
[timeField]: {
|
||||
gte: to.valueOf() - (bucketSize * 1500),
|
||||
lte: to.valueOf(),
|
||||
format: 'epoch_millis'
|
||||
}
|
||||
}
|
||||
},
|
||||
aggs: { SORT: fn(metric) }
|
||||
}
|
||||
});
|
||||
} else {
|
||||
set(doc, 'aggs.pivot.terms.order', { _term: get(sort, 'order', 'asc') });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
set(doc, 'aggs.pivot.filter.match_all', {});
|
||||
}
|
||||
return next(doc);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import getBucketSize from '../../helpers/get_bucket_size';
|
||||
import getTimerange from '../../helpers/get_timerange';
|
||||
import getIntervalAndTimefield from '../../get_interval_and_timefield';
|
||||
export default function query(req, panel) {
|
||||
return next => doc => {
|
||||
const { timeField, interval } = getIntervalAndTimefield(panel);
|
||||
const { bucketSize } = getBucketSize(req, interval);
|
||||
const { from, to } = getTimerange(req);
|
||||
|
||||
doc.size = 0;
|
||||
doc.query = {
|
||||
bool: {
|
||||
must: []
|
||||
}
|
||||
};
|
||||
|
||||
const timerange = {
|
||||
range: {
|
||||
[timeField]: {
|
||||
gte: from.valueOf(),
|
||||
lte: to.valueOf() - (bucketSize * 1000),
|
||||
format: 'epoch_millis',
|
||||
}
|
||||
}
|
||||
};
|
||||
doc.query.bool.must.push(timerange);
|
||||
|
||||
const globalFilters = req.payload.filters;
|
||||
if (globalFilters && !panel.ignore_global_filter) {
|
||||
doc.query.bool.must = doc.query.bool.must.concat(globalFilters);
|
||||
}
|
||||
|
||||
if (panel.filter) {
|
||||
doc.query.bool.must.push({
|
||||
query_string: {
|
||||
query: panel.filter,
|
||||
analyze_wildcard: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(doc);
|
||||
|
||||
};
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import _ from 'lodash';
|
||||
import getBucketSize from '../../helpers/get_bucket_size';
|
||||
import bucketTransform from '../../helpers/bucket_transform';
|
||||
import getIntervalAndTimefield from '../../get_interval_and_timefield';
|
||||
import { calculateAggRoot } from './calculate_agg_root';
|
||||
export default function siblingBuckets(req, panel) {
|
||||
return next => doc => {
|
||||
const { interval } = getIntervalAndTimefield(panel);
|
||||
const { bucketSize } = getBucketSize(req, interval);
|
||||
panel.series.forEach(column => {
|
||||
const aggRoot = calculateAggRoot(doc, column);
|
||||
column.metrics
|
||||
.filter(row => /_bucket$/.test(row.type))
|
||||
.forEach(metric => {
|
||||
const fn = bucketTransform[metric.type];
|
||||
if (fn) {
|
||||
try {
|
||||
const bucket = fn(metric, column.metrics, bucketSize);
|
||||
_.set(doc, `${aggRoot}.${metric.id}`, bucket);
|
||||
} catch (e) {
|
||||
// meh
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return next(doc);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import _ from 'lodash';
|
||||
export default function splitByEverything(req, panel) {
|
||||
return next => doc => {
|
||||
panel.series.filter(c => !(c.aggregate_by && c.aggregate_function)).forEach(column => {
|
||||
if (column.filter) {
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.filter.query_string.query`, column.filter);
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.filter.query_string.analyze_wildcard`, true);
|
||||
} else {
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.filter.match_all`, {});
|
||||
}
|
||||
});
|
||||
return next(doc);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
export default function splitByTerm(req, panel) {
|
||||
return next => doc => {
|
||||
panel.series.filter(c => c.aggregate_by && c.aggregate_function).forEach(column => {
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.terms.field`, column.aggregate_by);
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.terms.size`, 100);
|
||||
if (column.filter) {
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.column_filter.filter.query_string.query`, column.filter);
|
||||
_.set(doc, `aggs.pivot.aggs.${column.id}.column_filter.filter.query_string.analyze_wildcard`, true);
|
||||
}
|
||||
});
|
||||
return next(doc);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
function mean(values) {
|
||||
return _.sum(values) / values.length;
|
||||
}
|
||||
|
||||
const basic = fnName => targetSeries => {
|
||||
const data = [];
|
||||
_.zip(...targetSeries).forEach(row => {
|
||||
const key = row[0][0];
|
||||
const values = row.map(r => r[1]);
|
||||
const fn = _[fnName] || (() => null);
|
||||
data.push([key, fn(values)]);
|
||||
});
|
||||
return [data];
|
||||
};
|
||||
|
||||
const overall = fnName => targetSeries => {
|
||||
const fn = _[fnName];
|
||||
const keys = [];
|
||||
const values = [];
|
||||
_.zip(...targetSeries).forEach(row => {
|
||||
keys.push(row[0][0]);
|
||||
values.push(fn(row.map(r => r[1])));
|
||||
});
|
||||
return [keys.map(k => [k, fn(values)])];
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
sum: basic('sum'),
|
||||
max: basic('max'),
|
||||
min: basic('min'),
|
||||
mean(targetSeries) {
|
||||
const data = [];
|
||||
_.zip(...targetSeries).forEach(row => {
|
||||
const key = row[0][0];
|
||||
const values = row.map(r => r[1]);
|
||||
data.push([key, mean(values)]);
|
||||
});
|
||||
return [data];
|
||||
},
|
||||
|
||||
|
||||
overall_max: overall('max'),
|
||||
overall_min: overall('min'),
|
||||
overall_sum: overall('sum'),
|
||||
|
||||
overall_avg(targetSeries) {
|
||||
const fn = mean;
|
||||
const keys = [];
|
||||
const values = [];
|
||||
_.zip(...targetSeries).forEach(row => {
|
||||
keys.push(row[0][0]);
|
||||
values.push(_.sum(row.map(r => r[1])));
|
||||
});
|
||||
return [keys.map(k => [k, fn(values)])];
|
||||
},
|
||||
|
||||
cumlative_sum(targetSeries) {
|
||||
const data = [];
|
||||
let sum = 0;
|
||||
_.zip(...targetSeries).forEach(row => {
|
||||
const key = row[0][0];
|
||||
sum += _.sum(row.map(r => r[1]));
|
||||
data.push([key, sum]);
|
||||
});
|
||||
return [data];
|
||||
}
|
||||
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// import percentile from './percentile';
|
||||
import stdMetric from './std_metric';
|
||||
import stdSibling from './std_sibling';
|
||||
import seriesAgg from './series_agg';
|
||||
|
||||
export default [
|
||||
// percentile,
|
||||
stdMetric,
|
||||
stdSibling,
|
||||
seriesAgg
|
||||
];
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import _ from 'lodash';
|
||||
import getAggValue from '../../helpers/get_agg_value';
|
||||
import getSplits from '../../helpers/get_splits';
|
||||
import getLastMetric from '../../helpers/get_last_metric';
|
||||
export default function percentile(resp, panel, series) {
|
||||
return next => results => {
|
||||
const metric = getLastMetric(series);
|
||||
if (metric.type !== 'percentile') return next(results);
|
||||
|
||||
getSplits(resp, panel, series).forEach((split) => {
|
||||
const label = (split.label) + ` (${series.value})`;
|
||||
const data = split.timeseries.buckets.map(bucket => {
|
||||
const m = _.assign({}, metric, { percent: series.value });
|
||||
return [bucket.key, getAggValue(bucket, m)];
|
||||
});
|
||||
results.push({
|
||||
id: `${percentile.id}:${split.id}`,
|
||||
label,
|
||||
data,
|
||||
});
|
||||
|
||||
});
|
||||
return next(results);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import SeriesAgg from './_series_agg';
|
||||
import _ from 'lodash';
|
||||
import calculateLabel from '../../../../../common/calculate_label';
|
||||
export default function seriesAgg(resp, panel, series) {
|
||||
return next => results => {
|
||||
if (series.aggregate_by && series.aggregate_function) {
|
||||
|
||||
const targetSeries = [];
|
||||
// Filter out the seires with the matching metric and store them
|
||||
// in targetSeries
|
||||
results = results.filter(s => {
|
||||
if (s.id.split(/:/)[0] === series.id) {
|
||||
targetSeries.push(s.data);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const fn = SeriesAgg[series.aggregate_function];
|
||||
const data = fn(targetSeries);
|
||||
results.push({
|
||||
id: `${series.id}`,
|
||||
label: series.label || calculateLabel(_.last(series.metrics), series.metrics),
|
||||
data: _.first(data),
|
||||
});
|
||||
}
|
||||
return next(results);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import getSplits from '../../helpers/get_splits';
|
||||
import getLastMetric from '../../helpers/get_last_metric';
|
||||
import mapBucket from '../../helpers/map_bucket';
|
||||
export default function stdMetric(bucket, panel, series) {
|
||||
return next => results => {
|
||||
const metric = getLastMetric(series);
|
||||
if (metric.type === 'std_deviation' && metric.mode === 'band') {
|
||||
return next(results);
|
||||
}
|
||||
if (metric.type === 'percentile') {
|
||||
return next(results);
|
||||
}
|
||||
if (/_bucket$/.test(metric.type)) return next(results);
|
||||
|
||||
const fakeResp = { aggregations: bucket };
|
||||
getSplits(fakeResp, panel, series).forEach(split => {
|
||||
const data = split.timeseries.buckets.map(mapBucket(metric));
|
||||
results.push({
|
||||
id: split.id,
|
||||
label: split.label,
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
return next(results);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import getSplits from '../../helpers/get_splits';
|
||||
import getLastMetric from '../../helpers/get_last_metric';
|
||||
import getSiblingAggValue from '../../helpers/get_sibling_agg_value';
|
||||
export default function stdSibling(bucket, panel, series) {
|
||||
return next => results => {
|
||||
const metric = getLastMetric(series);
|
||||
|
||||
if (!/_bucket$/.test(metric.type)) return next(results);
|
||||
if (metric.type === 'std_deviation_bucket' && metric.mode === 'band') return next(results);
|
||||
|
||||
const fakeResp = { aggregations: bucket };
|
||||
getSplits(fakeResp, panel, series).forEach(split => {
|
||||
const data = split.timeseries.buckets.map(b => {
|
||||
return [b.key, getSiblingAggValue(split, metric)];
|
||||
});
|
||||
results.push({
|
||||
id: split.id,
|
||||
label: split.label,
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
return next(results);
|
||||
};
|
||||
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import buildProcessorFunction from './build_processor_function';
|
||||
import processors from './request_processors/series';
|
||||
import buildProcessorFunction from '../build_processor_function';
|
||||
import processors from '../request_processors/series';
|
||||
|
||||
function buildRequestBody(req, panel, series) {
|
||||
const processor = buildProcessorFunction(processors, req, panel, series);
|
|
@ -1,5 +1,5 @@
|
|||
import buildProcessorFunction from './build_processor_function';
|
||||
import processors from './response_processors/series';
|
||||
import buildProcessorFunction from '../build_processor_function';
|
||||
import processors from '../response_processors/series';
|
||||
import { get } from 'lodash';
|
||||
|
||||
export default function handleResponseBody(panel) {
|
|
@ -0,0 +1,10 @@
|
|||
import buildProcessorFunction from '../build_processor_function';
|
||||
import processors from '../request_processors/table';
|
||||
|
||||
function buildRequestBody(req, panel) {
|
||||
const processor = buildProcessorFunction(processors, req, panel);
|
||||
const doc = processor({});
|
||||
return doc;
|
||||
}
|
||||
|
||||
export default buildRequestBody;
|
|
@ -0,0 +1,37 @@
|
|||
import getRequestParams from './get_request_params';
|
||||
import handleResponseBody from './handle_response_body';
|
||||
import handleErrorResponse from '../handle_error_response';
|
||||
import getLastValue from '../../../../common/get_last_value';
|
||||
import _ from 'lodash';
|
||||
import regression from 'regression';
|
||||
export function getColumnData(req, panel, entities, client) {
|
||||
const elasticsearch = _.get(req, 'server.plugins.elasticsearch');
|
||||
if (elasticsearch) {
|
||||
const { callWithRequest } = elasticsearch.getCluster('data');
|
||||
if (!client) {
|
||||
client = callWithRequest.bind(null, req);
|
||||
}
|
||||
}
|
||||
const params = {
|
||||
body: getRequestParams(req, panel, entities)
|
||||
};
|
||||
return client('msearch', params)
|
||||
.then(resp => {
|
||||
const handler = handleResponseBody(panel);
|
||||
return entities.map((entity, index) => {
|
||||
entity.data = {};
|
||||
handler(resp.responses[index]).forEach(row => {
|
||||
const linearRegression = regression('linear', row.data);
|
||||
entity.data[row.id] = {
|
||||
last: getLastValue(row.data),
|
||||
slope: linearRegression.equation[0],
|
||||
yIntercept: linearRegression.equation[1],
|
||||
label: row.label
|
||||
};
|
||||
});
|
||||
return entity;
|
||||
});
|
||||
})
|
||||
.catch(handleErrorResponse(panel));
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import buildRequestBody from './build_request_body';
|
||||
export default (req, panel, entities) => {
|
||||
const bodies = [];
|
||||
entities.forEach(entity => {
|
||||
bodies.push({
|
||||
index: panel.index_pattern,
|
||||
ignore: [404],
|
||||
timeout: '90s',
|
||||
requestTimeout: 90000,
|
||||
ignoreUnavailable: true,
|
||||
});
|
||||
bodies.push(buildRequestBody(req, panel, entity));
|
||||
});
|
||||
return bodies;
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import buildProcessorFunction from '../build_processor_function';
|
||||
import _ from 'lodash';
|
||||
import processors from '../response_processors/table';
|
||||
|
||||
export default function handleResponseBody(panel) {
|
||||
return resp => {
|
||||
if (resp.error) {
|
||||
const err = new Error(resp.error.type);
|
||||
err.response = JSON.stringify(resp);
|
||||
throw err;
|
||||
}
|
||||
return panel.columns.map(column => {
|
||||
const processor = buildProcessorFunction(processors, resp, panel, column);
|
||||
return _.first(processor([]));
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import buildProcessorFunction from '../build_processor_function';
|
||||
import processors from '../response_processors/table';
|
||||
import getLastValue from '../../../../common/get_last_value';
|
||||
import regression from 'regression';
|
||||
import { first, get } from 'lodash';
|
||||
export default function processBucket(panel) {
|
||||
return bucket => {
|
||||
const series = panel.series.map(series => {
|
||||
const processor = buildProcessorFunction(processors, bucket, panel, series);
|
||||
const result = first(processor([]));
|
||||
if (!result) return null;
|
||||
const data = get(result, 'data', []);
|
||||
const linearRegression = regression.linear(data);
|
||||
result.last = getLastValue(data);
|
||||
result.slope = linearRegression.equation[0];
|
||||
return result;
|
||||
});
|
||||
return { key: bucket.key, series };
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue