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:
Chris Cowan 2017-10-24 15:45:57 -07:00
parent cb29f3b6f4
commit fe7e8a59df
64 changed files with 1463 additions and 65 deletions

View file

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

View file

@ -70,6 +70,11 @@
}
}
.vis-editor-content-fullEditor {
.flex-parent();
z-index: 0;
}
.vis-editor-sidebar {
.flex-parent(1, 0, auto);

View 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);
});
});

View 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;
};

View file

@ -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 }))
];

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

@ -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')}

View file

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

View 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,4 @@
import basicAggs from '../../../../common/basic_aggs';
export function isSortable(metric) {
return basicAggs.includes(metric.type);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
};
}

View file

@ -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));
}

View file

@ -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) };
}
}
}

View file

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

View file

@ -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);
};
}

View file

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

View file

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

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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];
}
};

View file

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

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

@ -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);
};
}

View file

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

View file

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

View file

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

View file

@ -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));
}

View file

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

View file

@ -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([]));
});
};
}

View file

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