Skip to content

Commit

Permalink
[TSVB] Visualize runtime fields (#95772) (#96787)
Browse files Browse the repository at this point in the history
* [TSVB] Visualize runtime fields

* fix CI

* Update visualization_error.tsx

* Update build_request_body.ts

* fix group by for table view

* fix issue on switching the index pattern mode

Co-authored-by: Kibana Machine <[email protected]>

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
alexwizp and kibanamachine authored Apr 12, 2021
1 parent cbb0ece commit b99ad57
Show file tree
Hide file tree
Showing 65 changed files with 532 additions and 423 deletions.
23 changes: 23 additions & 0 deletions src/plugins/vis_type_timeseries/common/calculate_label.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { calculateLabel } from './calculate_label';
import type { MetricsItemsSchema } from './types';
import { SanitizedFieldType } from './types';

describe('calculateLabel(metric, metrics)', () => {
test('returns the metric.alias if set', () => {
Expand Down Expand Up @@ -82,4 +83,26 @@ describe('calculateLabel(metric, metrics)', () => {

expect(label).toEqual('Derivative of Outbound Traffic');
});

test('should throw an error if field not found', () => {
const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
metric,
] as unknown) as MetricsItemsSchema[];
const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }];

expect(() => calculateLabel(metric, metrics, fields)).toThrowError('Field "3" not found');
});

test('should not throw an error if field not found (isThrowErrorOnFieldNotFound is false)', () => {
const metric = ({ id: 2, type: 'max', field: 3 } as unknown) as MetricsItemsSchema;
const metrics = ([
{ id: 1, type: 'max', field: 'network.out.bytes', alias: 'Outbound Traffic' },
metric,
] as unknown) as MetricsItemsSchema[];
const fields: SanitizedFieldType[] = [{ name: '2', label: '2', type: 'field' }];

expect(calculateLabel(metric, metrics, fields, false)).toBe('Max of 3');
});
});
12 changes: 5 additions & 7 deletions src/plugins/vis_type_timeseries/common/calculate_label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { includes, startsWith } from 'lodash';
import { i18n } from '@kbn/i18n';
import { lookup } from './agg_lookup';
import { MetricsItemsSchema, SanitizedFieldType } from './types';
import { extractFieldLabel } from './fields_utils';

const paths = [
'cumulative_sum',
Expand All @@ -26,14 +27,11 @@ const paths = [
'positive_only',
];

export const extractFieldLabel = (fields: SanitizedFieldType[], name: string) => {
return fields.find((f) => f.name === name)?.label ?? name;
};

export const calculateLabel = (
metric: MetricsItemsSchema,
metrics: MetricsItemsSchema[] = [],
fields: SanitizedFieldType[] = []
fields: SanitizedFieldType[] = [],
isThrowErrorOnFieldNotFound: boolean = true
): string => {
if (!metric) {
return i18n.translate('visTypeTimeseries.calculateLabel.unknownLabel', {
Expand Down Expand Up @@ -71,7 +69,7 @@ export const calculateLabel = (
if (metric.type === 'positive_rate') {
return i18n.translate('visTypeTimeseries.calculateLabel.positiveRateLabel', {
defaultMessage: 'Counter Rate of {field}',
values: { field: extractFieldLabel(fields, metric.field!) },
values: { field: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound) },
});
}
if (metric.type === 'static') {
Expand Down Expand Up @@ -115,7 +113,7 @@ export const calculateLabel = (
defaultMessage: '{lookupMetricType} of {metricField}',
values: {
lookupMetricType: lookup[metric.type],
metricField: extractFieldLabel(fields, metric.field!),
metricField: extractFieldLabel(fields, metric.field!, isThrowErrorOnFieldNotFound),
},
});
};
1 change: 1 addition & 0 deletions src/plugins/vis_type_timeseries/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export const ROUTES = {
VIS_DATA: '/api/metrics/vis/data',
FIELDS: '/api/metrics/fields',
};
export const USE_KIBANA_INDEXES_KEY = 'use_kibana_indexes';
13 changes: 1 addition & 12 deletions src/plugins/vis_type_timeseries/common/fields_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import { toSanitizedFieldType } from './fields_utils';
import type { FieldSpec, RuntimeField } from '../../data/common';
import type { FieldSpec } from '../../data/common';

describe('fields_utils', () => {
describe('toSanitizedFieldType', () => {
Expand All @@ -34,17 +34,6 @@ describe('fields_utils', () => {
`);
});

test('should filter runtime fields', async () => {
const fields: FieldSpec[] = [
{
...mockedField,
runtimeField: {} as RuntimeField,
},
];

expect(toSanitizedFieldType(fields)).toMatchInlineSnapshot(`Array []`);
});

test('should filter non-aggregatable fields', async () => {
const fields: FieldSpec[] = [
{
Expand Down
60 changes: 51 additions & 9 deletions src/plugins/vis_type_timeseries/common/fields_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,60 @@
* Side Public License, v 1.
*/

import { i18n } from '@kbn/i18n';
import { FieldSpec } from '../../data/common';
import { isNestedField } from '../../data/common';
import { SanitizedFieldType } from './types';
import { FetchedIndexPattern, SanitizedFieldType } from './types';

export const toSanitizedFieldType = (fields: FieldSpec[]) => {
return fields
.filter(
(field) =>
// Make sure to only include mapped fields, e.g. no index pattern runtime fields
!field.runtimeField && field.aggregatable && !isNestedField(field)
)
export class FieldNotFoundError extends Error {
constructor(name: string) {
super(
i18n.translate('visTypeTimeseries.fields.fieldNotFound', {
defaultMessage: `Field "{field}" not found`,
values: { field: name },
})
);
}

public get name() {
return this.constructor.name;
}

public get body() {
return this.message;
}
}

export const extractFieldLabel = (
fields: SanitizedFieldType[],
name: string,
isThrowErrorOnFieldNotFound: boolean = true
) => {
if (fields.length && name) {
const field = fields.find((f) => f.name === name);

if (field) {
return field.label || field.name;
}
if (isThrowErrorOnFieldNotFound) {
throw new FieldNotFoundError(name);
}
}
return name;
};

export function validateField(name: string, index: FetchedIndexPattern) {
if (name && index.indexPattern) {
const field = index.indexPattern.fields.find((f) => f.name === name);
if (!field) {
throw new FieldNotFoundError(name);
}
}
}

export const toSanitizedFieldType = (fields: FieldSpec[]) =>
fields
.filter((field) => field.aggregatable && !isNestedField(field))
.map(
(field) =>
({
Expand All @@ -25,4 +68,3 @@ export const toSanitizedFieldType = (fields: FieldSpec[]) => {
type: field.type,
} as SanitizedFieldType)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,17 @@ describe('fetchIndexPattern', () => {
});

describe('text-based index', () => {
test('should return the Kibana index if it exists', async () => {
test('should return the Kibana index if it exists (fetchKibabaIndexForStringIndexes is true)', async () => {
mockedIndices = [
{
id: 'indexId',
title: 'indexTitle',
},
] as IndexPattern[];

const value = await fetchIndexPattern('indexTitle', indexPatternsService);
const value = await fetchIndexPattern('indexTitle', indexPatternsService, {
fetchKibabaIndexForStringIndexes: true,
});

expect(value).toMatchInlineSnapshot(`
Object {
Expand All @@ -102,8 +104,10 @@ describe('fetchIndexPattern', () => {
`);
});

test('should return only indexPatternString if Kibana index does not exist', async () => {
const value = await fetchIndexPattern('indexTitle', indexPatternsService);
test('should return only indexPatternString if Kibana index does not exist (fetchKibabaIndexForStringIndexes is true)', async () => {
const value = await fetchIndexPattern('indexTitle', indexPatternsService, {
fetchKibabaIndexForStringIndexes: true,
});

expect(value).toMatchInlineSnapshot(`
Object {
Expand Down
18 changes: 13 additions & 5 deletions src/plugins/vis_type_timeseries/common/index_patterns_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ export const extractIndexPatternValues = (

export const fetchIndexPattern = async (
indexPatternValue: IndexPatternValue | undefined,
indexPatternsService: Pick<IndexPatternsService, 'getDefault' | 'get' | 'find'>
indexPatternsService: Pick<IndexPatternsService, 'getDefault' | 'get' | 'find'>,
options: {
fetchKibabaIndexForStringIndexes: boolean;
} = {
fetchKibabaIndexForStringIndexes: false,
}
): Promise<FetchedIndexPattern> => {
let indexPattern: FetchedIndexPattern['indexPattern'];
let indexPatternString: string = '';
Expand All @@ -61,13 +66,16 @@ export const fetchIndexPattern = async (
indexPattern = await indexPatternsService.getDefault();
} else {
if (isStringTypeIndexPattern(indexPatternValue)) {
indexPattern = (await indexPatternsService.find(indexPatternValue)).find(
(index) => index.title === indexPatternValue
);

if (options.fetchKibabaIndexForStringIndexes) {
indexPattern = (await indexPatternsService.find(indexPatternValue)).find(
(index) => index.title === indexPatternValue
);
}
if (!indexPattern) {
indexPatternString = indexPatternValue;
}

indexPatternString = indexPatternValue;
} else if (indexPatternValue.id) {
indexPattern = await indexPatternsService.get(indexPatternValue.id);
}
Expand Down
4 changes: 3 additions & 1 deletion src/plugins/vis_type_timeseries/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface TableData {
export type SeriesData = {
type: Exclude<PANEL_TYPES, PANEL_TYPES.TABLE>;
uiRestrictions: TimeseriesUIRestrictions;
error?: string;
} & {
[key: string]: PanelSeries;
};
Expand All @@ -56,7 +57,7 @@ interface PanelSeries {
};
id: string;
series: PanelData[];
error?: unknown;
error?: string;
}

export interface PanelData {
Expand All @@ -66,6 +67,7 @@ export interface PanelData {
seriesId: string;
splitByLabel: string;
isSplitByTerms: boolean;
error?: string;
}

export const isVisTableData = (data: TimeseriesVisData): data is TableData =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiComboBox, EuiComboBoxProps, EuiComboBoxOptionOption } from '@elastic/eui';
import { METRIC_TYPES } from '../../../../common/metric_types';
import React, { ReactNode, useContext } from 'react';
import {
EuiComboBox,
EuiComboBoxProps,
EuiComboBoxOptionOption,
EuiFormRow,
htmlIdGenerator,
} from '@elastic/eui';
import { getIndexPatternKey } from '../../../../common/index_patterns_utils';
import type { SanitizedFieldType, IndexPatternValue } from '../../../../common/types';
import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions';

// @ts-ignore
import { isFieldEnabled } from '../../lib/check_ui_restrictions';
import { PanelModelContext } from '../../contexts/panel_model_context';
import { USE_KIBANA_INDEXES_KEY } from '../../../../common/constants';

interface FieldSelectProps {
label: string | ReactNode;
type: string;
fields: Record<string, SanitizedFieldType[]>;
indexPattern: IndexPatternValue;
Expand Down Expand Up @@ -45,6 +52,7 @@ const sortByLabel = (a: EuiComboBoxOptionOption<string>, b: EuiComboBoxOptionOpt
};

export function FieldSelect({
label,
type,
fields,
indexPattern = '',
Expand All @@ -56,11 +64,10 @@ export function FieldSelect({
uiRestrictions,
'data-test-subj': dataTestSubj = 'metricsIndexPatternFieldsSelect',
}: FieldSelectProps) {
if (type === METRIC_TYPES.COUNT) {
return null;
}
const panelModel = useContext(PanelModelContext);
const htmlId = htmlIdGenerator();

const selectedOptions: Array<EuiComboBoxOptionOption<string>> = [];
let selectedOptions: Array<EuiComboBoxOptionOption<string>> = [];
let newPlaceholder = placeholder;
const fieldsSelector = getIndexPatternKey(indexPattern);

Expand Down Expand Up @@ -112,19 +119,43 @@ export function FieldSelect({
}
});

if (value && !selectedOptions.length) {
onChange([]);
let isInvalid;

if (Boolean(panelModel?.[USE_KIBANA_INDEXES_KEY])) {
isInvalid = Boolean(value && fields[fieldsSelector] && !selectedOptions.length);

if (value && !selectedOptions.length) {
selectedOptions = [{ label: value!, id: 'INVALID_FIELD' }];
}
} else {
if (value && !selectedOptions.length) {
onChange([]);
}
}

return (
<EuiComboBox
data-test-subj={dataTestSubj}
placeholder={newPlaceholder}
isDisabled={disabled}
options={groupedOptions}
selectedOptions={selectedOptions}
onChange={onChange}
singleSelection={{ asPlainText: true }}
/>
<EuiFormRow
id={htmlId('timeField')}
label={label}
isInvalid={isInvalid}
error={i18n.translate('visTypeTimeseries.fieldSelect.fieldIsNotValid', {
defaultMessage:
'The "{fieldParameter}" field is not valid for use with the current index. Please select a new field.',
values: {
fieldParameter: value,
},
})}
>
<EuiComboBox
data-test-subj={dataTestSubj}
placeholder={newPlaceholder}
isDisabled={disabled}
options={groupedOptions}
selectedOptions={selectedOptions}
onChange={onChange}
singleSelection={{ asPlainText: true }}
isInvalid={isInvalid}
/>
</EuiFormRow>
);
}
Loading

0 comments on commit b99ad57

Please sign in to comment.