Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [TSVB] Enables url drilldowns for range selection (#95296) #96128

Merged
merged 3 commits into from
Apr 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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