Skip to content

Commit

Permalink
[TSVB] Enables url drilldowns for range selection (#95296) (#96128)
Browse files Browse the repository at this point in the history
* Temp research code

* Make it work

* Cleanup

* Convert series to datatable

* Remove unecessary log

* Minor

* Fix types problem

* Add unit tests

* Take under consideration the override index pattern setting

* Implement brush event for dual mode

* Move indexpatterns fetch outside the loop

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
stratoula and kibanamachine authored Apr 5, 2021
1 parent f78d202 commit f01d3a8
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 22 deletions.
3 changes: 3 additions & 0 deletions src/plugins/vis_type_timeseries/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export interface PanelData {
id: string;
label: string;
data: Array<[number, number]>;
seriesId: string;
splitByLabel: string;
isSplitByTerms: boolean;
}

export const isVisTableData = (data: TimeseriesVisData): data is TableData =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IndexPattern, IndexPatternField } from 'src/plugins/data/public';
import { PanelData } from '../../../../common/types';
import { TimeseriesVisParams } from '../../../types';
import { convertSeriesToDataTable, addMetaToColumns } from './convert_series_to_datatable';

jest.mock('../../../services', () => {
return {
getDataStart: jest.fn(() => {
return {
indexPatterns: jest.fn(),
};
}),
};
});

describe('convert series to datatables', () => {
let indexPattern: IndexPattern;

beforeEach(() => {
const fieldMap: Record<string, IndexPatternField> = {
test1: { name: 'test1', spec: { type: 'date' } } as IndexPatternField,
test2: { name: 'test2' } as IndexPatternField,
test3: { name: 'test3', spec: { type: 'boolean' } } as IndexPatternField,
};

const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap[name];
indexPattern = {
id: 'index1',
title: 'index1',
timeFieldName: 'timestamp',
getFieldByName,
} as IndexPattern;
});

describe('addMetaColumns()', () => {
test('adds the correct meta to a date column', () => {
const columns = [{ id: 0, name: 'test1', isSplit: false }];
const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'count');
expect(columnsWithMeta).toEqual([
{
id: '0',
meta: {
field: 'test1',
index: 'index1',
source: 'esaggs',
sourceParams: {
enabled: true,
indexPatternId: 'index1',
type: 'date_histogram',
},
type: 'date',
},
name: 'test1',
},
]);
});

test('adds the correct meta to a non date column', () => {
const columns = [{ id: 1, name: 'Average of test2', isSplit: false }];
const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg');
expect(columnsWithMeta).toEqual([
{
id: '1',
meta: {
field: 'Average of test2',
index: 'index1',
source: 'esaggs',
sourceParams: {
enabled: true,
indexPatternId: 'index1',
type: 'avg',
},
type: 'number',
},
name: 'Average of test2',
},
]);
});

test('adds the correct meta for a split column', () => {
const columns = [{ id: 2, name: 'test3', isSplit: true }];
const columnsWithMeta = addMetaToColumns(columns, indexPattern, 'avg');
expect(columnsWithMeta).toEqual([
{
id: '2',
meta: {
field: 'test3',
index: 'index1',
source: 'esaggs',
sourceParams: {
enabled: true,
indexPatternId: 'index1',
type: 'terms',
},
type: 'boolean',
},
name: 'test3',
},
]);
});
});

describe('convertSeriesToDataTable()', () => {
const model = {
series: [
{
formatter: 'number',
id: 'series1',
label: '',
line_width: 1,
metrics: [
{
field: 'test2',
id: 'series1',
type: 'avg',
},
],
split_mode: 'terms',
terms_field: 'Cancelled',
type: 'timeseries',
},
],
} as TimeseriesVisParams;
const series = ([
{
id: 'series1:0',
label: 0,
splitByLabel: 'Average of test2',
labelFormatted: 'false',
data: [
[1616454000000, 0],
[1616457600000, 5],
[1616461200000, 7],
[1616464800000, 8],
],
seriesId: 'series1',
isSplitByTerms: true,
},
{
id: 'series1:1',
label: 1,
splitByLabel: 'Average of test2',
labelFormatted: 'true',
data: [
[1616454000000, 10],
[1616457600000, 12],
[1616461200000, 1],
[1616464800000, 14],
],
seriesId: 'series1',
isSplitByTerms: true,
},
] as unknown) as PanelData[];
test('creates one table for one layer series with the correct columns', async () => {
const tables = await convertSeriesToDataTable(model, series, indexPattern);
expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort());

expect(tables.series1.columns.length).toEqual(3);
expect(tables.series1.rows.length).toEqual(8);
});

test('the table rows for a series with term aggregation should be a combination of the different terms', async () => {
const tables = await convertSeriesToDataTable(model, series, indexPattern);
expect(Object.keys(tables).sort()).toEqual([model.series[0].id].sort());

expect(tables.series1.rows.length).toEqual(8);
const expected1 = series[0].data.map((d) => {
d.push(parseInt(series[0].label, 10));
return d;
});
const expected2 = series[1].data.map((d) => {
d.push(parseInt(series[1].label, 10));
return d;
});
expect(tables.series1.rows).toEqual([...expected1, ...expected2]);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IndexPattern } from 'src/plugins/data/public';
import {
Datatable,
DatatableRow,
DatatableColumn,
DatatableColumnType,
} from 'src/plugins/expressions/public';
import { TimeseriesVisParams } from '../../../types';
import { PanelData } from '../../../../common/types';
import { fetchIndexPattern } from '../../../../common/index_patterns_utils';
import { getDataStart } from '../../../services';
import { X_ACCESSOR_INDEX } from '../../visualizations/constants';

interface TSVBTables {
[key: string]: Datatable;
}

interface TSVBColumns {
id: number;
name: string;
isSplit: boolean;
}

export const addMetaToColumns = (
columns: TSVBColumns[],
indexPattern: IndexPattern,
metricsType: string
): DatatableColumn[] => {
return columns.map((column) => {
const field = indexPattern.getFieldByName(column.name);
const type = (field?.spec.type as DatatableColumnType) || 'number';
const cleanedColumn = {
id: column.id.toString(),
name: column.name,
meta: {
type,
field: column.name,
index: indexPattern.title,
source: 'esaggs',
sourceParams: {
enabled: true,
indexPatternId: indexPattern?.id,
type: type === 'date' ? 'date_histogram' : column.isSplit ? 'terms' : metricsType,
},
},
};
return cleanedColumn;
});
};

export const convertSeriesToDataTable = async (
model: TimeseriesVisParams,
series: PanelData[],
initialIndexPattern: IndexPattern
) => {
const tables: TSVBTables = {};
const { indexPatterns } = getDataStart();
for (let layerIdx = 0; layerIdx < model.series.length; layerIdx++) {
const layer = model.series[layerIdx];
let usedIndexPattern = initialIndexPattern;
// The user can overwrite the index pattern of a layer.
// In that case, the index pattern should be fetched again.
if (layer.override_index_pattern) {
const { indexPattern } = await fetchIndexPattern(layer.series_index_pattern, indexPatterns);
if (indexPattern) {
usedIndexPattern = indexPattern;
}
}
const isGroupedByTerms = layer.split_mode === 'terms';
const seriesPerLayer = series.filter((s) => s.seriesId === layer.id);
let id = X_ACCESSOR_INDEX;

const columns: TSVBColumns[] = [
{ id, name: usedIndexPattern.timeFieldName || '', isSplit: false },
];
if (seriesPerLayer.length) {
id++;
columns.push({ id, name: seriesPerLayer[0].splitByLabel, isSplit: false });
// Adds an extra column, if the layer is split by terms aggregation
if (isGroupedByTerms) {
id++;
columns.push({ id, name: layer.terms_field || '', isSplit: true });
}
}
const columnsWithMeta = addMetaToColumns(columns, usedIndexPattern, layer.metrics[0].type);

let rows: DatatableRow[] = [];
for (let j = 0; j < seriesPerLayer.length; j++) {
const data = seriesPerLayer[j].data.map((rowData) => {
const row: DatatableRow = [rowData[0], rowData[1]];
// If the layer is split by terms aggregation, the data array should also contain the split value.
if (isGroupedByTerms) {
row.push(seriesPerLayer[j].label);
}
return row;
});
rows = [...rows, ...data];
}
tables[layer.id] = {
type: 'datatable',
rows,
columns: columnsWithMeta,
};
}
return tables;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
// @ts-expect-error
import { ErrorComponent } from './error';
import { TimeseriesVisTypes } from './vis_types';
import { TimeseriesVisData, PanelData, isVisSeriesData } from '../../../common/types';
import { fetchIndexPattern } from '../../../common/index_patterns_utils';
import { TimeseriesVisParams } from '../../types';
import { isVisSeriesData, TimeseriesVisData } from '../../../common/types';
import { getDataStart } from '../../services';
import { convertSeriesToDataTable } from './lib/convert_series_to_datatable';
import { X_ACCESSOR_INDEX } from '../visualizations/constants';
import { LastValueModeIndicator } from './last_value_mode_indicator';
import { getInterval } from './lib/get_interval';
import { AUTO_INTERVAL } from '../../../common/constants';
Expand Down Expand Up @@ -51,25 +55,29 @@ function TimeseriesVisualization({
palettesService,
}: TimeseriesVisualizationProps) {
const onBrush = useCallback(
(gte: string, lte: string) => {
handlers.event({
name: 'applyFilter',
async (gte: string, lte: string, series: PanelData[]) => {
const indexPatternValue = model.index_pattern || '';
const { indexPatterns } = getDataStart();
const { indexPattern } = await fetchIndexPattern(indexPatternValue, indexPatterns);

const tables = indexPattern
? await convertSeriesToDataTable(model, series, indexPattern)
: null;
const table = tables?.[model.series[0].id];

const range: [number, number] = [parseInt(gte, 10), parseInt(lte, 10)];
const event = {
data: {
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte,
lte,
},
},
},
],
table,
column: X_ACCESSOR_INDEX,
range,
timeFieldName: indexPattern?.timeFieldName,
},
});
name: 'brush',
};
handlers.event(event);
},
[handlers]
[handlers, model]
);

const handleUiState = useCallback(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { PersistedState } from 'src/plugins/visualizations/public';
import { PaletteRegistry } from 'src/plugins/charts/public';

import { TimeseriesVisParams } from '../../../types';
import { TimeseriesVisData } from '../../../../common/types';
import { TimeseriesVisData, PanelData } from '../../../../common/types';

/**
* Lazy load each visualization type, since the only one is presented on the screen at the same time.
Expand Down Expand Up @@ -44,7 +44,7 @@ export const TimeseriesVisTypes: Record<string, React.ComponentType<TimeseriesVi

export interface TimeseriesVisProps {
model: TimeseriesVisParams;
onBrush: (gte: string, lte: string) => void;
onBrush: (gte: string, lte: string, series: PanelData[]) => Promise<void>;
onUiState: (
field: string,
value: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

// @ts-expect-error
import { bombIcon } from '../../components/svg/bomb_icon';
// @ts-expect-error
import { fireIcon } from '../../components/svg/fire_icon';

export const ICON_NAMES = {
Expand Down
Loading

0 comments on commit f01d3a8

Please sign in to comment.