Ensure extra columns are not shown in table vis when showPartialRows:true (#27154)

This commit is contained in:
Luke Elmers 2019-03-11 21:10:48 -06:00 committed by GitHub
parent 3dadece427
commit 08d90d037f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 125 additions and 43 deletions

View file

@ -83,7 +83,7 @@ export const esaggs = () => ({
query: get(context, 'query', null), query: get(context, 'query', null),
filters: get(context, 'filters', null), filters: get(context, 'filters', null),
forceFetch: true, forceFetch: true,
isHierarchical: args.metricsAtAllLevels, metricsAtAllLevels: args.metricsAtAllLevels,
partialRows: args.partialRows, partialRows: args.partialRows,
inspectorAdapters: handlers.inspectorAdapters, inspectorAdapters: handlers.inspectorAdapters,
queryFilter, queryFilter,

View file

@ -21,21 +21,18 @@
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" ng-model="editorState.params.showPartialRows"> <input type="checkbox" ng-model="editorState.params.showPartialRows" data-test-subj="showPartialRows">
<span <span
i18n-id="tableVis.params.showPartialRowsLabel" i18n-id="tableVis.params.showPartialRowsLabel"
i18n-default-message="Show partial rows" i18n-default-message="Show partial rows"
></span> ></span>
</label> &nbsp;
</div> <icon-tip
content="::'tableVis.params.showPartialRowsTip' | i18n: {
<div class="checkbox"> defaultMessage: 'Show rows that have partial data. This will still calculate metrics for every bucket/level, even if they are not displayed.'
<label> }"
<input type="checkbox" ng-model="metricsAtAllLevels" disabled> position="'right'"
<span ></icon-tip>
i18n-id="tableVis.params.calculateMetricsLabel"
i18n-default-message="Calculate metrics for every bucket/level"
></span>
</label> </label>
</div> </div>

View file

@ -138,7 +138,7 @@ describe('tabifyAggResponse Integration', function () {
// the default for a non-hierarchical vis is to display // the default for a non-hierarchical vis is to display
// only complete rows, and only put the metrics at the end. // only complete rows, and only put the metrics at the end.
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { minimalColumns: true }); const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: false });
expectColumns(tabbed, [ext, src, os, avg]); expectColumns(tabbed, [ext, src, os, avg]);

View file

@ -27,10 +27,16 @@ const getColumn = (agg, i) => {
}; };
}; };
export function tabifyGetColumns(aggs, minimal) { /**
* Builds tabify columns.
*
* @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates
* @param {boolean} minimalColumns - setting to true will only return a column for the last bucket/metric instead of one for each level
*/
export function tabifyGetColumns(aggs, minimalColumns) {
// pick the columns // pick the columns
if (minimal) { if (minimalColumns) {
return aggs.map((agg, i) => getColumn(agg, i)); return aggs.map((agg, i) => getColumn(agg, i));
} }

View file

@ -27,20 +27,25 @@ import { tabifyGetColumns } from './_get_columns';
* @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates * @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates
* @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket * @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket
* @param {boolean} partialRows - setting to true will not remove rows with missing values * @param {boolean} partialRows - setting to true will not remove rows with missing values
* @param {Object} timeRange - time range object, if provided
*/ */
function TabbedAggResponseWriter(aggs, { metricsAtAllLevels = false, partialRows = false, timeRange } = {}) { function TabbedAggResponseWriter(aggs, {
metricsAtAllLevels = false,
partialRows = false,
timeRange
} = {}) {
// Private
this._removePartialRows = !partialRows;
// Public
this.rowBuffer = {}; this.rowBuffer = {};
this.bucketBuffer = []; this.bucketBuffer = [];
this.metricBuffer = []; this.metricBuffer = [];
this.metricsForAllBuckets = metricsAtAllLevels;
this.partialRows = partialRows;
this.aggs = aggs; this.aggs = aggs;
this.partialRows = partialRows;
this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels); this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels);
this.aggStack = [...this.columns]; this.aggStack = [...this.columns];
this.rows = []; this.rows = [];
// Extract the time range object if provided // Extract the time range object if provided
if (timeRange) { if (timeRange) {
const timeRangeKey = Object.keys(timeRange)[0]; const timeRangeKey = Object.keys(timeRange)[0];
@ -67,7 +72,7 @@ TabbedAggResponseWriter.prototype.row = function () {
this.rowBuffer[metric.id] = metric.value; this.rowBuffer[metric.id] = metric.value;
}); });
if (!toArray(this.rowBuffer).length || (!this.partialRows && this.isPartialRow(this.rowBuffer))) { if (!toArray(this.rowBuffer).length || (this._removePartialRows && this.isPartialRow(this.rowBuffer))) {
return; return;
} }

View file

@ -21,6 +21,16 @@ import _ from 'lodash';
import { TabbedAggResponseWriter } from './_response_writer'; import { TabbedAggResponseWriter } from './_response_writer';
import { TabifyBuckets } from './_buckets'; import { TabifyBuckets } from './_buckets';
/**
* Sets up the ResponseWriter and kicks off bucket collection.
*
* @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates
* @param {Object} esResponse - response that came back from Elasticsearch
* @param {Object} respOpts - options object for the ResponseWriter with params set by Courier
* @param {boolean} respOpts.metricsAtAllLevels - setting to true will produce metrics for every bucket
* @param {boolean} respOpts.partialRows - setting to true will not remove rows with missing values
* @param {Object} respOpts.timeRange - time range object, if provided
*/
export function tabifyAggResponse(aggs, esResponse, respOpts = {}) { export function tabifyAggResponse(aggs, esResponse, respOpts = {}) {
const write = new TabbedAggResponseWriter(aggs, respOpts); const write = new TabbedAggResponseWriter(aggs, respOpts);
@ -34,7 +44,7 @@ export function tabifyAggResponse(aggs, esResponse, respOpts = {}) {
} }
/** /**
* read an aggregation from a bucket, which is *might* be found at key (if * read an aggregation from a bucket, which *might* be found at key (if
* the response came in object form), and will recurse down the aggregation * the response came in object form), and will recurse down the aggregation
* tree and will pass the read values to the ResponseWriter. * tree and will pass the read values to the ResponseWriter.
* *

View file

@ -18,7 +18,7 @@
*/ */
export { AggConfig } from './agg_config'; export { AggConfig } from './agg_config';
export { Vis, VisProvider, VisState } from './vis'; export { Vis, VisProvider, VisParams, VisState } from './vis';
export { VisualizationController, VisType } from './vis_types/vis_type'; export { VisualizationController, VisType } from './vis_types/vis_type';
export * from './request_handlers'; export * from './request_handlers';
export * from './response_handlers'; export * from './response_handlers';

View file

@ -38,7 +38,7 @@ const CourierRequestHandlerProvider = function () {
filters, filters,
forceFetch, forceFetch,
partialRows, partialRows,
isHierarchical, metricsAtAllLevels,
inspectorAdapters, inspectorAdapters,
queryFilter queryFilter
}) { }) {
@ -67,7 +67,7 @@ const CourierRequestHandlerProvider = function () {
}); });
requestSearchSource.setField('aggs', function () { requestSearchSource.setField('aggs', function () {
return aggs.toDsl(isHierarchical); return aggs.toDsl(metricsAtAllLevels);
}); });
requestSearchSource.onRequestStart((searchSource, searchRequest) => { requestSearchSource.onRequestStart((searchSource, searchRequest) => {
@ -143,7 +143,7 @@ const CourierRequestHandlerProvider = function () {
const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null; const parsedTimeRange = timeRange ? getTime(aggs.indexPattern, timeRange) : null;
const tabifyParams = { const tabifyParams = {
metricsAtAllLevels: isHierarchical, metricsAtAllLevels,
partialRows, partialRows,
timeRange: parsedTimeRange ? parsedTimeRange.range : undefined, timeRange: parsedTimeRange ? parsedTimeRange.range : undefined,
}; };

View file

@ -36,7 +36,7 @@ export interface RequestHandlerParams {
uiState?: PersistedState; uiState?: PersistedState;
partialRows?: boolean; partialRows?: boolean;
inspectorAdapters?: Adapters; inspectorAdapters?: Adapters;
isHierarchical?: boolean; metricsAtAllLevels?: boolean;
visParams?: any; visParams?: any;
} }

View file

@ -31,9 +31,13 @@ export interface Vis {
export type VisProvider = (...dependencies: any[]) => Vis; export type VisProvider = (...dependencies: any[]) => Vis;
export interface VisParams {
[key: string]: any;
}
export interface VisState { export interface VisState {
title: string; title: string;
type: VisType; type: VisType;
params: any; params: VisParams;
aggs: any[]; aggs: any[];
} }

View file

@ -16,6 +16,10 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":0}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":0}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=false 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":false,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[4,5],\\"buckets\\":[0,3]}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with showPartialRows=true and showMetricsAtAllLevels=true 1`] = `"kibana_table visConfig='{\\"showMetricsAtAllLevels\\":true,\\"showPartialRows\\":true,\\"dimensions\\":{\\"metrics\\":[1,2,4,5],\\"buckets\\":[0,3]}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0],\\"buckets\\":[],\\"splitRow\\":[1,2]}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0,1],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`; exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles table function with splits and buckets 1`] = `"kibana_table visConfig='{\\"foo\\":\\"bar\\",\\"dimensions\\":{\\"metrics\\":[0,1],\\"buckets\\":[3],\\"splitRow\\":[2,4]}}' "`;

View file

@ -93,14 +93,15 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
}); });
describe('handles table function', () => { describe('handles table function', () => {
const params = { foo: 'bar' };
it('without splits or buckets', () => { it('without splits or buckets', () => {
const params = { foo: 'bar' };
const schemas = { metric: [0, 1] }; const schemas = { metric: [0, 1] };
const actual = buildPipelineVisFunction.table({ params }, schemas); const actual = buildPipelineVisFunction.table({ params }, schemas);
expect(actual).toMatchSnapshot(); expect(actual).toMatchSnapshot();
}); });
it('with splits', () => { it('with splits', () => {
const params = { foo: 'bar' };
const schemas = { const schemas = {
metric: [0], metric: [0],
split_row: [1, 2], split_row: [1, 2],
@ -110,10 +111,37 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
}); });
it('with splits and buckets', () => { it('with splits and buckets', () => {
const params = { foo: 'bar' };
const schemas = { const schemas = {
metric: [0, 1], metric: [0, 1],
split_row: [2, 4], split_row: [2, 4],
bucket: [3] bucket: [3],
};
const actual = buildPipelineVisFunction.table({ params }, schemas);
expect(actual).toMatchSnapshot();
});
it('with showPartialRows=true and showMetricsAtAllLevels=true', () => {
const params = {
showMetricsAtAllLevels: true,
showPartialRows: true,
};
const schemas = {
metric: [1, 2, 4, 5],
bucket: [0, 3],
};
const actual = buildPipelineVisFunction.table({ params }, schemas);
expect(actual).toMatchSnapshot();
});
it('with showPartialRows=true and showMetricsAtAllLevels=false', () => {
const params = {
showMetricsAtAllLevels: false,
showPartialRows: true,
};
const schemas = {
metric: [1, 2, 4, 5],
bucket: [0, 3],
}; };
const actual = buildPipelineVisFunction.table({ params }, schemas); const actual = buildPipelineVisFunction.table({ params }, schemas);
expect(actual).toMatchSnapshot(); expect(actual).toMatchSnapshot();

View file

@ -21,7 +21,7 @@ import { cloneDeep } from 'lodash';
// @ts-ignore // @ts-ignore
import { setBounds } from 'ui/agg_types/buckets/date_histogram'; import { setBounds } from 'ui/agg_types/buckets/date_histogram';
import { SearchSource } from 'ui/courier'; import { SearchSource } from 'ui/courier';
import { AggConfig, Vis, VisState } from 'ui/vis'; import { AggConfig, Vis, VisParams, VisState } from 'ui/vis';
interface SchemaFormat { interface SchemaFormat {
id: string; id: string;
@ -51,7 +51,7 @@ interface Schemas {
} }
type buildVisFunction = (visState: VisState, schemas: Schemas, uiState: any) => string; type buildVisFunction = (visState: VisState, schemas: Schemas, uiState: any) => string;
type buildVisConfigFunction = (schemas: Schemas) => VisState; type buildVisConfigFunction = (schemas: Schemas, visParams?: VisParams) => VisParams;
interface BuildPipelineVisFunction { interface BuildPipelineVisFunction {
[key: string]: buildVisFunction; [key: string]: buildVisFunction;
@ -226,7 +226,7 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
table: (visState, schemas) => { table: (visState, schemas) => {
const visConfig = { const visConfig = {
...visState.params, ...visState.params,
...buildVisConfig.table(schemas), ...buildVisConfig.table(schemas, visState.params),
}; };
return `kibana_table ${prepareJson('visConfig', visConfig)}`; return `kibana_table ${prepareJson('visConfig', visConfig)}`;
}, },
@ -268,14 +268,24 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
}; };
const buildVisConfig: BuildVisConfigFunction = { const buildVisConfig: BuildVisConfigFunction = {
table: schemas => { table: (schemas, visParams = {}) => {
const visConfig = {} as any; const visConfig = {} as any;
const metrics = schemas.metric;
const buckets = schemas.bucket || [];
visConfig.dimensions = { visConfig.dimensions = {
metrics: schemas.metric, metrics,
buckets: schemas.bucket || [], buckets,
splitRow: schemas.split_row, splitRow: schemas.split_row,
splitColumn: schemas.split_column, splitColumn: schemas.split_column,
}; };
if (visParams.showMetricsAtAllLevels === false && visParams.showPartialRows === true) {
// Handle case where user wants to see partial rows but not metrics at all levels.
// This requires calculating how many metrics will come back in the tabified response,
// and removing all metrics from the dimensions except the last set.
const metricsPerBucket = metrics.length / buckets.length;
visConfig.dimensions.metrics.splice(0, metricsPerBucket * buckets.length - metricsPerBucket);
}
return visConfig; return visConfig;
}, },
metric: schemas => { metric: schemas => {
@ -355,7 +365,7 @@ export const getVisParams = (vis: Vis, params: { timeRange?: any }) => {
if (buildVisConfig[vis.type.name]) { if (buildVisConfig[vis.type.name]) {
visConfig = { visConfig = {
...visConfig, ...visConfig,
...buildVisConfig[vis.type.name](schemas), ...buildVisConfig[vis.type.name](schemas, visConfig),
}; };
} else if (vislibCharts.includes(vis.type.name)) { } else if (vislibCharts.includes(vis.type.name)) {
visConfig.dimensions = buildVislibDimensions(vis, params.timeRange); visConfig.dimensions = buildVislibDimensions(vis, params.timeRange);
@ -392,7 +402,7 @@ export const buildPipeline = (
pipeline += `esaggs pipeline += `esaggs
${prepareString('index', indexPattern.id)} ${prepareString('index', indexPattern.id)}
metricsAtAllLevels=${vis.isHierarchical()} metricsAtAllLevels=${vis.isHierarchical()}
partialRows=${vis.params.showPartialRows || vis.type.requiresPartialRows || false} partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false}
${prepareJson('aggConfigs', visState.aggs)} | `; ${prepareJson('aggConfigs', visState.aggs)} | `;
} }
@ -408,7 +418,7 @@ export const buildPipeline = (
pipeline += `visualization type='${vis.type.name}' pipeline += `visualization type='${vis.type.name}'
${prepareJson('visConfig', visState.params)} ${prepareJson('visConfig', visState.params)}
metricsAtAllLevels=${vis.isHierarchical()} metricsAtAllLevels=${vis.isHierarchical()}
partialRows=${vis.params.showPartialRows || vis.type.name === 'tile_map'} `; partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `;
if (indexPattern) { if (indexPattern) {
pipeline += `${prepareString('index', indexPattern.id)}`; pipeline += `${prepareString('index', indexPattern.id)}`;
} }

View file

@ -73,7 +73,7 @@ export class VisualizeDataLoader {
// searchSource is only there for courier request handler // searchSource is only there for courier request handler
const requestHandlerResponse = await this.requestHandler({ const requestHandlerResponse = await this.requestHandler({
partialRows: this.vis.params.partialRows || this.vis.type.requiresPartialRows, partialRows: this.vis.params.partialRows || this.vis.type.requiresPartialRows,
isHierarchical: this.vis.isHierarchical(), metricsAtAllLevels: this.vis.isHierarchical(),
visParams, visParams,
...params, ...params,
filters: params.filters ? params.filters.filter(filter => !filter.meta.disabled) : undefined, filters: params.filters ? params.filters.filter(filter => !filter.meta.disabled) : undefined,

View file

@ -277,6 +277,25 @@ export default function ({ getService, getPageObjects }) {
]); ]);
}); });
it('should show correct data without showMetricsAtAllLevels even if showPartialRows is selected', async () => {
await PageObjects.visualize.clickOptionsTab();
await PageObjects.visualize.checkCheckbox('showPartialRows');
await PageObjects.visualize.clickGo();
const data = await PageObjects.visualize.getTableVisContent();
expect(data).to.be.eql([
[ 'jpg', 'CN', '1,718' ],
[ 'jpg', 'IN', '1,511' ],
[ 'jpg', 'US', '770' ],
[ 'jpg', 'ID', '314' ],
[ 'jpg', 'PK', '244' ],
[ 'css', 'CN', '422' ],
[ 'css', 'IN', '346' ],
[ 'css', 'US', '189' ],
[ 'css', 'ID', '68' ],
[ 'css', 'BR', '58' ],
]);
});
it('should show metrics on each level', async () => { it('should show metrics on each level', async () => {
await PageObjects.visualize.clickOptionsTab(); await PageObjects.visualize.clickOptionsTab();
await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels'); await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels');
@ -374,7 +393,7 @@ export default function ({ getService, getPageObjects }) {
]); ]);
}); });
it('should not show metrics for split bucket when using showMetricsAtAllLevels', async () => { it('should show metrics for split bucket when using showMetricsAtAllLevels', async () => {
await PageObjects.visualize.clickOptionsTab(); await PageObjects.visualize.clickOptionsTab();
await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels'); await PageObjects.visualize.checkCheckbox('showMetricsAtAllLevels');
await PageObjects.visualize.clickGo(); await PageObjects.visualize.clickGo();

View file

@ -2334,7 +2334,6 @@
"statusPage.statusApp.statusTitle": "插件状态", "statusPage.statusApp.statusTitle": "插件状态",
"statusPage.statusTable.columns.idHeader": "ID", "statusPage.statusTable.columns.idHeader": "ID",
"statusPage.statusTable.columns.statusHeader": "状态", "statusPage.statusTable.columns.statusHeader": "状态",
"tableVis.params.calculateMetricsLabel": "计算每个桶/级别的指标",
"tableVis.params.perPageLabel": "每页", "tableVis.params.perPageLabel": "每页",
"tableVis.params.showMetricsLabel": "显示每个桶/级别的指标", "tableVis.params.showMetricsLabel": "显示每个桶/级别的指标",
"tableVis.params.showPartialRowsLabel": "显示部分行", "tableVis.params.showPartialRowsLabel": "显示部分行",