* [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:
parent
8c4f2a7809
commit
4fe26b8198
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
.tvbLastValueModePopover {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.tvbLastValueModePopoverBody {
|
||||
width: 360px;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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) {
|
|
@ -0,0 +1,3 @@
|
|||
.tvbLastValueIndicator {
|
||||
align-self: flex-end;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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": "表",
|
||||
|
|
|
@ -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": "表",
|
||||
|
|
Loading…
Reference in a new issue