[TSVB] Show an indicator when using Last Value mode (#91977) (#95817)

* [TSVB] Show an indicator when using Last Value mode

* Extended some TSVB types, remove unused translations and do some refactoring

* Fix some functional tests and label displaying for Last value

* Fix some functional tests and label displaying for Last value

* Refactor data_time_range_mode_label and change some types

* fix CI

* Refactor timeseries_visualization seriesData

* Remove unused re export

* Replace "href" prop with "onClick" in EuiLink and refactor tooltip content

* Change link to text and add pointer style to it

* FIx import in kibana_framework_adapter

* Remove label for entire time range mode and add an icon for last value mode label

* Add action to show last value label for TSVB embeddables

* Fix TimeseriesVisParams import

* Revert "Add action to show last value label for TSVB embeddables"

This reverts commit 15f16d6f72.

* Put the "Last value" badge on the top of visualization and add an option to hide it

* Fix failing _tsvb_markdown test and refactor timeseries_visualization

* Move I18nProvider frim timeseries_visualization to timeseries_vis_renderer

* Add condition to hide gear button when entire time range mode is enabled, fix gauge scroll issue

* Change text in the popover, add condition to indicator if series data is empty, create migration script to hide last value label for previously created visualizations and a test for that

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

Co-authored-by: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
This commit is contained in:
Stratoula Kalafateli 2021-03-30 21:49:18 +03:00 committed by GitHub
parent 8c4f2a7809
commit 4fe26b8198
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 370 additions and 153 deletions

View file

@ -33,32 +33,43 @@ export interface FetchedIndexPattern {
indexPatternString: string | undefined;
}
export type TimeseriesVisData = SeriesData | TableData;
interface TableData {
type: PANEL_TYPES.TABLE;
uiRestrictions: TimeseriesUIRestrictions;
series?: PanelData[];
pivot_label?: string;
}
// series data is not fully typed yet
export type SeriesData = {
type: Exclude<PANEL_TYPES, PANEL_TYPES.TABLE>;
uiRestrictions: TimeseriesUIRestrictions;
} & {
[key: string]: PanelSeries;
};
interface PanelSeries {
annotations: {
[key: string]: unknown[];
};
id: string;
series: PanelData[];
error?: unknown;
}
export interface PanelData {
id: string;
label: string;
data: Array<[number, number]>;
}
// series data is not fully typed yet
interface SeriesData {
[key: string]: {
annotations: {
[key: string]: unknown[];
};
id: string;
series: PanelData[];
error?: unknown;
};
}
export const isVisTableData = (data: TimeseriesVisData): data is TableData =>
data.type === PANEL_TYPES.TABLE;
export type TimeseriesVisData = SeriesData & {
type: PANEL_TYPES;
uiRestrictions: TimeseriesUIRestrictions;
/**
* series array is responsible only for "table" vis type
*/
series?: unknown[];
};
export const isVisSeriesData = (data: TimeseriesVisData): data is SeriesData =>
data.type !== PANEL_TYPES.TABLE;
export interface SanitizedFieldType {
name: string;

View file

@ -224,6 +224,7 @@ export const panel = schema.object({
gauge_inner_width: stringOrNumberOptionalNullable,
gauge_style: stringOptionalNullable,
gauge_max: numberOptionalOrEmptyString,
hide_last_value_indicator: schema.boolean(),
id: stringRequired,
ignore_global_filters: numberOptional,
ignore_global_filter: numberOptional,

View file

@ -26,7 +26,8 @@ import { createSelectHandler } from './lib/create_select_handler';
import { createTextHandler } from './lib/create_text_handler';
import { IndexPatternSelect } from './lib/index_pattern_select';
import { YesNo } from './yes_no';
import { KBN_FIELD_TYPES } from '../../../../../plugins/data/public';
import { LastValueModePopover } from './last_value_mode_popover';
import { KBN_FIELD_TYPES } from '../../../../data/public';
import { FormValidationContext } from '../contexts/form_validation_context';
import { isGteInterval, validateReInterval, isAutoInterval } from './lib/get_interval';
import { i18n } from '@kbn/i18n';
@ -42,6 +43,7 @@ import { UI_SETTINGS } from '../../../../data/common';
const RESTRICT_FIELDS = [KBN_FIELD_TYPES.DATE];
const LEVEL_OF_DETAIL_STEPS = 10;
const LEVEL_OF_DETAIL_MIN_BUCKETS = 1;
const HIDE_LAST_VALUE_INDICATOR = 'hide_last_value_indicator';
const validateIntervalValue = (intervalValue) => {
const isAutoOrGteInterval = isGteInterval(intervalValue) || isAutoInterval(intervalValue);
@ -129,6 +131,11 @@ export const IndexPattern = ({
updateControlValidity(intervalName, intervalValidation.isValid);
}, [intervalName, intervalValidation.isValid, updateControlValidity]);
const toggleIndicatorDisplay = useCallback(
() => onChange({ [HIDE_LAST_VALUE_INDICATOR]: !model.hide_last_value_indicator }),
[model.hide_last_value_indicator, onChange]
);
return (
<div className="index-pattern">
{!isTimeSeries && (
@ -154,6 +161,14 @@ export const IndexPattern = ({
onChange={handleSelectChange(TIME_RANGE_MODE_KEY)}
singleSelection={{ asPlainText: true }}
isDisabled={disabled}
{...(!isEntireTimeRangeActive(model, isTimeSeries) && {
append: (
<LastValueModePopover
isIndicatorDisplayed={!model.hide_last_value_indicator}
toggleIndicatorDisplay={toggleIndicatorDisplay}
/>
),
})}
/>
</EuiFormRow>
<EuiText size="xs" style={{ margin: 0 }}>

View file

@ -0,0 +1,86 @@
/*
* 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 React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiFlexItem, EuiToolTip, EuiFlexGroup, EuiBadge } from '@elastic/eui';
import { getUISettings } from '../../services';
import { convertIntervalIntoUnit, isAutoInterval, isGteInterval } from './lib/get_interval';
import { createIntervalBasedFormatter } from './lib/create_interval_based_formatter';
import { PanelData } from '../../../common/types';
interface LastValueModeIndicatorProps {
seriesData?: PanelData['data'];
panelInterval: number;
modelInterval: string;
}
const lastValueLabel = i18n.translate('visTypeTimeseries.lastValueModeIndicator.lastValue', {
defaultMessage: 'Last value',
});
export const LastValueModeIndicator = ({
seriesData,
panelInterval,
modelInterval,
}: LastValueModeIndicatorProps) => {
if (!seriesData?.length) return <EuiBadge>{lastValueLabel}</EuiBadge>;
const dateFormat = getUISettings().get('dateFormat');
const scaledDataFormat = getUISettings().get('dateFormat:scaled');
const getFormattedPanelInterval = () => {
const interval = convertIntervalIntoUnit(panelInterval, false);
return interval && `${interval.unitValue}${interval.unitString}`;
};
const formatter = createIntervalBasedFormatter(panelInterval, scaledDataFormat, dateFormat);
const lastBucketDate = formatter(seriesData[seriesData.length - 1][0]);
const formattedPanelInterval =
(isAutoInterval(modelInterval) || isGteInterval(modelInterval)) && getFormattedPanelInterval();
const tooltipContent = (
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexItem grow={false}>
<FormattedMessage
id="visTypeTimeseries.lastValueModeIndicator.lastBucketDate"
defaultMessage="Bucket: {lastBucketDate}"
values={{ lastBucketDate }}
/>
</EuiFlexItem>
{formattedPanelInterval && (
<EuiFlexItem grow={false}>
<FormattedMessage
id="visTypeTimeseries.lastValueModeIndicator.panelInterval"
defaultMessage="Interval: {formattedPanelInterval}"
values={{ formattedPanelInterval }}
/>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
return (
<EuiToolTip position="top" display="inlineBlock" content={tooltipContent}>
<EuiBadge
iconType="iInCircle"
iconSide="right"
onClick={() => {}}
onClickAriaLabel={i18n.translate(
'visTypeTimeseries.lastValueModeIndicator.lastValueModeBadgeAriaLabel',
{
defaultMessage: 'View last value details',
}
)}
>
{lastValueLabel}
</EuiBadge>
</EuiToolTip>
);
};

View file

@ -0,0 +1,7 @@
.tvbLastValueModePopover {
height: auto;
}
.tvbLastValueModePopoverBody {
width: 360px;
}

View file

@ -0,0 +1,59 @@
/*
* 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 './last_value_mode_popover.scss';
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiButtonIcon, EuiPopover, EuiPopoverTitle, EuiSwitch } from '@elastic/eui';
interface LastValueModePopoverProps {
isIndicatorDisplayed: boolean;
toggleIndicatorDisplay: () => void;
}
export const LastValueModePopover = ({
isIndicatorDisplayed,
toggleIndicatorDisplay,
}: LastValueModePopoverProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const onButtonClick = useCallback(() => setIsPopoverOpen((isOpen) => !isOpen), []);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
return (
<EuiPopover
className="tvbLastValueModePopover"
button={
<EuiButtonIcon
iconType={'gear'}
onClick={onButtonClick}
aria-label={i18n.translate('visTypeTimeseries.lastValueModePopover.gearButton', {
defaultMessage: 'Change last value indicator display option',
})}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
>
<div className="tvbLastValueModePopoverBody">
<EuiPopoverTitle>
{i18n.translate('visTypeTimeseries.lastValueModePopover.title', {
defaultMessage: 'Last value options',
})}
</EuiPopoverTitle>
<EuiSwitch
checked={isIndicatorDisplayed}
label={i18n.translate('visTypeTimeseries.lastValueModePopover.switch', {
defaultMessage: 'Show label when using Last value mode',
})}
onChange={toggleIndicatorDisplay}
/>
</div>
</EuiPopover>
);
};

View file

@ -8,17 +8,21 @@
import moment from 'moment';
function getFormat(interval, rules = []) {
function getFormat(interval: number, rules: string[][] = []) {
for (let i = rules.length - 1; i >= 0; i--) {
const rule = rules[i];
if (!rule[0] || interval >= moment.duration(rule[0])) {
if (!rule[0] || interval >= Number(moment.duration(rule[0]))) {
return rule[1];
}
}
}
export function createXaxisFormatter(interval, rules, dateFormat) {
return (val) => {
export function createIntervalBasedFormatter(
interval: number,
rules: string[][],
dateFormat: string
) {
return (val: moment.MomentInput): string => {
return moment(val).format(getFormat(interval, rules) ?? dateFormat);
};
}

View file

@ -13,6 +13,8 @@ import { search } from '../../../../../../plugins/data/public';
const { parseEsInterval } = search.aggs;
import { GTE_INTERVAL_RE } from '../../../../common/interval_regexp';
import { AUTO_INTERVAL } from '../../../../common/constants';
import { isVisTableData, PanelData, TimeseriesVisData } from '../../../../common/types';
import { TimeseriesVisParams } from '../../../types';
export const unitLookup = {
s: i18n.translate('visTypeTimeseries.getInterval.secondsLabel', { defaultMessage: 'seconds' }),
@ -24,9 +26,11 @@ export const unitLookup = {
y: i18n.translate('visTypeTimeseries.getInterval.yearsLabel', { defaultMessage: 'years' }),
};
export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true) => {
type TimeUnit = keyof typeof unitLookup;
export const convertIntervalIntoUnit = (interval: number, hasTranslateUnitString = true) => {
// Iterate units from biggest to smallest
const units = Object.keys(unitLookup).reverse();
const units = Object.keys(unitLookup).reverse() as TimeUnit[];
const duration = moment.duration(interval, 'ms');
for (let i = 0; i < units.length; i++) {
@ -41,11 +45,16 @@ export const convertIntervalIntoUnit = (interval, hasTranslateUnitString = true)
}
};
export const isGteInterval = (interval) => GTE_INTERVAL_RE.test(interval);
export const isAutoInterval = (interval) => !interval || interval === AUTO_INTERVAL;
export const isGteInterval = (interval: string) => GTE_INTERVAL_RE.test(interval);
export const isAutoInterval = (interval: string) => !interval || interval === AUTO_INTERVAL;
export const validateReInterval = (intervalValue) => {
const validationResult = {};
interface ValidationResult {
isValid: boolean;
errorMessage?: string;
}
export const validateReInterval = (intervalValue: string) => {
const validationResult = {} as ValidationResult;
try {
parseEsInterval(intervalValue);
@ -58,14 +67,12 @@ export const validateReInterval = (intervalValue) => {
return validationResult;
};
export const getInterval = (visData, model) => {
let series;
if (model && model.type === 'table') {
series = get(visData, `series[0].series`, []);
} else {
series = get(visData, `${model.id}.series`, []);
}
export const getInterval = (visData: TimeseriesVisData, model: TimeseriesVisParams) => {
const series = get(
visData,
isVisTableData(visData) ? `series[0].series` : `${model.id}.series`,
[]
) as PanelData[];
return series.reduce((currentInterval, item) => {
if (item.data.length > 1) {

View file

@ -0,0 +1,3 @@
.tvbLastValueIndicator {
align-self: flex-end;
}

View file

@ -6,8 +6,13 @@
* Side Public License, v 1.
*/
import './timeseries_visualization.scss';
import React, { useCallback, useEffect } from 'react';
import { get } from 'lodash';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { IUiSettingsClient } from 'src/core/public';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
@ -17,7 +22,12 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
import { ErrorComponent } from './error';
import { TimeseriesVisTypes } from './vis_types';
import { TimeseriesVisParams } from '../../types';
import { TimeseriesVisData } from '../../../common/types';
import { isVisSeriesData, TimeseriesVisData } from '../../../common/types';
import { LastValueModeIndicator } from './last_value_mode_indicator';
import { getInterval } from './lib/get_interval';
import { AUTO_INTERVAL } from '../../../common/constants';
import { TIME_RANGE_DATA_MODES } from '../../../common/timerange_data_modes';
import { PANEL_TYPES } from '../../../common/panel_types';
interface TimeseriesVisualizationProps {
className?: string;
@ -76,7 +86,7 @@ function TimeseriesVisualization({
});
// Show the error panel
const error = visData[model.id]?.error;
const error = isVisSeriesData(visData) && visData[model.id]?.error;
if (error) {
return (
<div className={className}>
@ -87,18 +97,40 @@ function TimeseriesVisualization({
const VisComponent = TimeseriesVisTypes[model.type];
const isLastValueMode =
!model.time_range_mode || model.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE;
const shouldDisplayLastValueIndicator =
isLastValueMode && !model.hide_last_value_indicator && model.type !== PANEL_TYPES.TIMESERIES;
if (VisComponent) {
return (
<VisComponent
getConfig={getConfig}
model={model}
visData={visData}
uiState={uiState}
onBrush={onBrush}
onUiState={handleUiState}
syncColors={syncColors}
palettesService={palettesService}
/>
<EuiFlexGroup direction="column" gutterSize="none" responsive={false}>
{shouldDisplayLastValueIndicator && (
<EuiFlexItem className="tvbLastValueIndicator" grow={false}>
<LastValueModeIndicator
seriesData={get(
visData,
`${isVisSeriesData(visData) ? model.id : 'series[0]'}.series[0].data`,
undefined
)}
panelInterval={getInterval(visData, model)}
modelInterval={model.interval ?? AUTO_INTERVAL}
/>
</EuiFlexItem>
)}
<EuiFlexItem>
<VisComponent
getConfig={getConfig}
model={model}
visData={visData}
uiState={uiState}
onBrush={onBrush}
onUiState={handleUiState}
syncColors={syncColors}
palettesService={palettesService}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -8,17 +8,8 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { get } from 'lodash';
import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import {
getInterval,
convertIntervalIntoUnit,
isAutoInterval,
isGteInterval,
} from './lib/get_interval';
import { AUTO_INTERVAL } from '../../../common/constants';
import { PANEL_TYPES } from '../../../common/panel_types';
const MIN_CHART_HEIGHT = 300;
@ -28,7 +19,6 @@ class VisEditorVisualizationUI extends Component {
this.state = {
height: MIN_CHART_HEIGHT,
dragging: false,
panelInterval: 0,
};
this._visEl = React.createRef();
@ -65,18 +55,7 @@ class VisEditorVisualizationUI extends Component {
await this._handler.render(this._visEl.current);
this.props.eventEmitter.emit('embeddableRendered');
this._subscription = this._handler.handler.data$.subscribe((data) => {
this.setPanelInterval(data.value.visData);
onDataChange(data.value);
});
}
setPanelInterval(visData) {
const panelInterval = getInterval(visData, this.props.model);
if (this.state.panelInterval !== panelInterval) {
this.setState({ panelInterval });
}
this._subscription = this._handler.handler.data$.subscribe((data) => onDataChange(data.value));
}
/**
@ -98,28 +77,6 @@ class VisEditorVisualizationUI extends Component {
}
};
hasShowPanelIntervalValue() {
const type = get(this.props, 'model.type', '');
const interval = get(this.props, 'model.interval', AUTO_INTERVAL);
return (
[
PANEL_TYPES.METRIC,
PANEL_TYPES.TOP_N,
PANEL_TYPES.GAUGE,
PANEL_TYPES.MARKDOWN,
PANEL_TYPES.TABLE,
].includes(type) &&
(isAutoInterval(interval) || isGteInterval(interval))
);
}
getFormattedPanelInterval() {
const interval = convertIntervalIntoUnit(this.state.panelInterval, false);
return interval ? `${interval.unitValue}${interval.unitString}` : null;
}
componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
@ -154,8 +111,6 @@ class VisEditorVisualizationUI extends Component {
style.userSelect = 'none';
}
const panelInterval = this.hasShowPanelIntervalValue() && this.getFormattedPanelInterval();
let applyMessage = (
<FormattedMessage
id="visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage"
@ -194,20 +149,6 @@ class VisEditorVisualizationUI extends Component {
/>
</EuiFlexItem>
{panelInterval && (
<EuiFlexItem grow={false}>
<EuiText color="default" size="xs">
<p>
<FormattedMessage
id="visTypeTimeseries.visEditorVisualization.panelInterval"
defaultMessage="Interval: {panelInterval}"
values={{ panelInterval }}
/>
</p>
</EuiText>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiText color={dirty ? 'default' : 'subdued'} size="xs">
<p>{applyMessage}</p>

View file

@ -19,7 +19,7 @@ import { MarkdownSimple } from '../../../../../../../plugins/kibana_react/public
import { replaceVars } from '../../lib/replace_vars';
import { getAxisLabelString } from '../../lib/get_axis_label_string';
import { getInterval } from '../../lib/get_interval';
import { createXaxisFormatter } from '../../lib/create_xaxis_formatter';
import { createIntervalBasedFormatter } from '../../lib/create_interval_based_formatter';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
import { getCoreStart } from '../../../../services';
@ -35,7 +35,11 @@ class TimeseriesVisualization extends Component {
dateFormat = this.props.getConfig('dateFormat');
xAxisFormatter = (interval) => (val) => {
const formatter = createXaxisFormatter(interval, this.scaledDataFormat, this.dateFormat);
const formatter = createIntervalBasedFormatter(
interval,
this.scaledDataFormat,
this.dateFormat
);
return formatter(val);
};

View file

@ -9,12 +9,13 @@
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { IUiSettingsClient } from 'kibana/public';
import type { PersistedState } from '../../visualizations/public';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { TimeseriesRenderValue } from './metrics_fn';
import { TimeseriesVisData } from '../common/types';
import { isVisTableData, TimeseriesVisData } from '../common/types';
import { TimeseriesVisParams } from './types';
import { getChartsSetup } from './services';
@ -24,7 +25,7 @@ const TimeseriesVisualization = lazy(
const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVisParams) => {
if ('type' in visData) {
const data = visData.type === 'table' ? visData.series : visData?.[model.id]?.series;
const data = isVisTableData(visData) ? visData.series : visData?.[model.id]?.series;
return Boolean(data?.length);
}
@ -46,22 +47,24 @@ export const getTimeseriesVisRenderer: (deps: {
const palettesService = await palettes.getPalettes();
render(
<VisualizationContainer
data-test-subj="timeseriesVis"
handlers={handlers}
showNoResult={showNoResult}
>
<TimeseriesVisualization
// it is mandatory to bind uiSettings because of "this" usage inside "get" method
getConfig={uiSettings.get.bind(uiSettings)}
<I18nProvider>
<VisualizationContainer
data-test-subj="timeseriesVis"
handlers={handlers}
model={config.visParams}
visData={config.visData as TimeseriesVisData}
syncColors={config.syncColors}
uiState={handlers.uiState! as PersistedState}
palettesService={palettesService}
/>
</VisualizationContainer>,
showNoResult={showNoResult}
>
<TimeseriesVisualization
// it is mandatory to bind uiSettings because of "this" usage inside "get" method
getConfig={uiSettings.get.bind(uiSettings)}
handlers={handlers}
model={config.visParams}
visData={config.visData as TimeseriesVisData}
syncColors={config.syncColors}
uiState={handlers.uiState! as PersistedState}
palettesService={palettesService}
/>
</VisualizationContainer>
</I18nProvider>,
domNode
);
},

View file

@ -29,3 +29,5 @@ export const config: PluginConfigDescriptor<VisTypeTimeseriesConfig> = {
export function plugin(initializerContext: PluginInitializerContext) {
return new VisTypeTimeseriesPlugin(initializerContext);
}
export { TimeseriesVisData, isVisSeriesData, isVisTableData } from '../common/types';

View file

@ -1921,4 +1921,36 @@ describe('migration visualization', () => {
expect(migratedTestDoc).toEqual(expectedDoc);
});
});
describe('7.13.0 tsvb hide Last value indicator by default', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.13.0'](
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const createTestDocWithType = (type: string) => ({
attributes: {
title: 'My Vis',
description: 'This is my super cool vis.',
visState: `{"type":"metrics","params":{"type":"${type}"}}`,
},
});
it('should set hide_last_value_indicator param to true', () => {
const migratedTestDoc = migrate(createTestDocWithType('markdown'));
const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params
.hide_last_value_indicator;
expect(hideLastValueIndicator).toBeTruthy();
});
it('should ignore timeseries type', () => {
const migratedTestDoc = migrate(createTestDocWithType('timeseries'));
const hideLastValueIndicator = JSON.parse(migratedTestDoc.attributes.visState).params
.hide_last_value_indicator;
expect(hideLastValueIndicator).toBeUndefined();
});
});
});

View file

@ -923,6 +923,34 @@ const migrateVislibAreaLineBarTypes: SavedObjectMigrationFn<any, any> = (doc) =>
return doc;
};
/**
* [TSVB] Hide Last value indicator by default for all TSVB types except timeseries
*/
const hideTSVBLastValueIndicator: SavedObjectMigrationFn<any, any> = (doc) => {
try {
const visState = JSON.parse(doc.attributes.visState);
if (visState && visState.type === 'metrics' && visState.params.type !== 'timeseries')
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify({
...visState,
params: {
...visState.params,
hide_last_value_indicator: true,
},
}),
},
};
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
return doc;
};
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@ -958,5 +986,5 @@ export const visualizationSavedObjectTypeMigrations = {
'7.10.0': flow(migrateFilterRatioQuery, removeTSVBSearchSource),
'7.11.0': flow(enableDataTableVisToolbar),
'7.12.0': flow(migrateVislibAreaLineBarTypes, migrateSchema),
'7.13.0': flow(addSupportOfDualIndexSelectionModeInTSVB),
'7.13.0': flow(addSupportOfDualIndexSelectionModeInTSVB, hideTSVBLastValueIndicator),
};

View file

@ -154,7 +154,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
}
public async getMarkdownText(): Promise<string> {
const el = await find.byCssSelector('.tvbEditorVisualization');
const el = await find.byCssSelector('.tvbVis');
const text = await el.getVisibleText();
return text;
}

View file

@ -163,23 +163,6 @@ export interface InfraFieldDef {
[type: string]: InfraFieldDetails;
}
export interface InfraTSVBResponse {
[key: string]: InfraTSVBPanel;
}
export interface InfraTSVBPanel {
id: string;
series: InfraTSVBSeries[];
}
export interface InfraTSVBSeries {
id: string;
label: string;
data: InfraTSVBDataPoint[];
}
export type InfraTSVBDataPoint = [number, number];
export type InfraRouteConfig<Params, Query, Body, Method extends RouteMethod> = {
method: RouteMethod;
} & RouteConfig<Params, Query, Body, Method>;

View file

@ -14,7 +14,6 @@ import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport';
import { estypes } from '@elastic/elasticsearch';
import {
InfraRouteConfig,
InfraTSVBResponse,
InfraServerPluginSetupDeps,
CallWithRequestParams,
InfraDatabaseSearchResponse,
@ -34,6 +33,7 @@ import { RequestHandler } from '../../../../../../../src/core/server';
import { InfraConfig } from '../../../plugin';
import type { InfraPluginRequestHandlerContext } from '../../../types';
import { IndexPatternsFetcher, UI_SETTINGS } from '../../../../../../../src/plugins/data/server';
import { TimeseriesVisData } from '../../../../../../../src/plugins/vis_type_timeseries/server';
export class KibanaFramework {
public router: IRouter<InfraPluginRequestHandlerContext>;
@ -221,7 +221,7 @@ export class KibanaFramework {
model: TSVBMetricModel,
timerange: { min: number; max: number },
filters: any[]
): Promise<InfraTSVBResponse> {
): Promise<TimeseriesVisData> {
const { getVisData } = this.plugins.visTypeTimeseries;
if (typeof getVisData !== 'function') {
throw new Error('TSVB is not available');

View file

@ -21,6 +21,7 @@ import {
import { calculateMetricInterval } from '../../../utils/calculate_metric_interval';
import { CallWithRequestParams, InfraDatabaseSearchResponse } from '../framework';
import type { InfraPluginRequestHandlerContext } from '../../../types';
import { isVisSeriesData } from '../../../../../../../src/plugins/vis_type_timeseries/server';
export class KibanaMetricsAdapter implements InfraMetricsAdapter {
private framework: KibanaFramework;
@ -59,7 +60,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter {
return Promise.all(requests)
.then((results) => {
return results.map((result) => {
return results.filter(isVisSeriesData).map((result) => {
const metricIds = Object.keys(result).filter(
(k) => !['type', 'uiRestrictions'].includes(k)
);

View file

@ -4549,7 +4549,6 @@
"visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "ビジュアライゼーションへの変更が適用されました。",
"visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "最新の変更が適用されました。",
"visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "変更が自動的に適用されます。",
"visTypeTimeseries.visEditorVisualization.panelInterval": "間隔:{panelInterval}",
"visTypeTimeseries.visPicker.gaugeLabel": "ゲージ",
"visTypeTimeseries.visPicker.metricLabel": "メトリック",
"visTypeTimeseries.visPicker.tableLabel": "表",

View file

@ -4575,7 +4575,6 @@
"visTypeTimeseries.visEditorVisualization.changesHaveNotBeenAppliedMessage": "尚未应用对此可视化的更改。",
"visTypeTimeseries.visEditorVisualization.changesSuccessfullyAppliedMessage": "已应用最新更改。",
"visTypeTimeseries.visEditorVisualization.changesWillBeAutomaticallyAppliedMessage": "将自动应用更改。",
"visTypeTimeseries.visEditorVisualization.panelInterval": "时间间隔:{panelInterval}",
"visTypeTimeseries.visPicker.gaugeLabel": "仪表盘",
"visTypeTimeseries.visPicker.metricLabel": "指标",
"visTypeTimeseries.visPicker.tableLabel": "表",