[TSVB] Custom renderer (#83554)

* Implement custom renderer

* Remove legacy code

* Use custom expression

* Convert to typescript

* Remove savedObjectId extra param

* Other updates

* Fix types

* Cleanup

* Fix functional tests

* Bind uiSettings

* Update snapshot

* Update types

* Remove extra params

* Move common types

* Return back validation error message

* Use panel types enum

* Fix types

* Lazy load visualizations
This commit is contained in:
Daniil 2020-11-23 19:41:26 +03:00 committed by GitHub
parent 378d89b5cd
commit bb023c5c1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 446 additions and 402 deletions

View file

@ -19,3 +19,7 @@
export const MAX_BUCKETS_SETTING = 'metrics:max_buckets';
export const INDEXES_SEPARATOR = ',';
export const ROUTES = {
VIS_DATA: '/api/metrics/vis/data',
};

View file

@ -17,11 +17,11 @@
* under the License.
*/
export const PANEL_TYPES = {
TABLE: 'table',
GAUGE: 'gauge',
MARKDOWN: 'markdown',
TOP_N: 'top_n',
TIMESERIES: 'timeseries',
METRIC: 'metric',
};
export enum PANEL_TYPES {
TABLE = 'table',
GAUGE = 'gauge',
MARKDOWN = 'markdown',
TOP_N = 'top_n',
TIMESERIES = 'timeseries',
METRIC = 'metric',
}

View file

@ -19,8 +19,37 @@
import { TypeOf } from '@kbn/config-schema';
import { metricsItems, panel, seriesItems, visPayloadSchema } from './vis_schema';
import { PANEL_TYPES } from './panel_types';
import { TimeseriesUIRestrictions } from './ui_restrictions';
export type SeriesItemsSchema = TypeOf<typeof seriesItems>;
export type MetricsItemsSchema = TypeOf<typeof metricsItems>;
export type PanelSchema = TypeOf<typeof panel>;
export type VisPayload = TypeOf<typeof visPayloadSchema>;
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 type TimeseriesVisData = SeriesData & {
type: PANEL_TYPES;
uiRestrictions: TimeseriesUIRestrictions;
/**
* series array is responsible only for "table" vis type
*/
series?: unknown[];
};

View file

@ -63,7 +63,7 @@ export const DEFAULT_UI_RESTRICTION: UIRestrictions = {
* @constant
* @public
*/
export const limitOfSeries = {
export const limitOfSeries: Partial<Record<PANEL_TYPES, number>> = {
[PANEL_TYPES.GAUGE]: 1,
[PANEL_TYPES.METRIC]: 2,
};

View file

@ -251,7 +251,14 @@ export const panel = schema.object({
),
time_field: stringOptionalNullable,
time_range_mode: stringOptionalNullable,
type: stringRequired,
type: schema.oneOf([
schema.literal('table'),
schema.literal('gauge'),
schema.literal('markdown'),
schema.literal('top_n'),
schema.literal('timeseries'),
schema.literal('metric'),
]),
});
export const visPayloadSchema = schema.object({
@ -267,7 +274,6 @@ export const visPayloadSchema = schema.object({
})
),
}),
savedObjectId: schema.maybe(schema.string()),
timerange: schema.object({
timezone: stringRequired,
min: stringRequired,

View file

@ -1,41 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
export function NoDataComponent() {
return (
<div className="visError" data-test-subj="noTSVBDataMessage">
<EuiText size="xs" color="subdued">
<EuiIcon type="visualizeApp" size="m" color="subdued" aria-hidden="true" />
<EuiSpacer size="s" />
<p>
<FormattedMessage
id="visTypeTimeseries.noDataDescription"
defaultMessage="No data to display for the selected metrics"
/>
</p>
</EuiText>
</div>
);
}

View file

@ -0,0 +1,115 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useEffect } from 'react';
import { IUiSettingsClient } from 'src/core/public';
import { IInterpreterRenderHandlers } from 'src/plugins/expressions';
import { PersistedState } from 'src/plugins/visualizations/public';
// @ts-expect-error
import { ErrorComponent } from './error';
import { TimeseriesVisTypes } from './vis_types';
import { TimeseriesVisParams } from '../../metrics_fn';
import { TimeseriesVisData } from '../../../common/types';
interface TimeseriesVisualizationProps {
className?: string;
getConfig: IUiSettingsClient['get'];
handlers: IInterpreterRenderHandlers;
model: TimeseriesVisParams;
visData: TimeseriesVisData;
uiState: PersistedState;
}
function TimeseriesVisualization({
className = 'tvbVis',
visData,
model,
handlers,
uiState,
getConfig,
}: TimeseriesVisualizationProps) {
const onBrush = useCallback(
(gte: string, lte: string) => {
handlers.event({
name: 'applyFilter',
data: {
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte,
lte,
},
},
},
],
},
});
},
[handlers]
);
const handleUiState = useCallback(
(field: string, value: { column: string; order: string }) => {
uiState.set(field, value);
// reload visualization because data might need to be re-fetched
uiState.emit('reload');
},
[uiState]
);
useEffect(() => {
handlers.done();
});
// Show the error panel
const error = visData[model.id]?.error;
if (error) {
return (
<div className={className}>
<ErrorComponent error={error} />
</div>
);
}
const VisComponent = TimeseriesVisTypes[model.type];
if (VisComponent) {
return (
<VisComponent
dateFormat={getConfig('dateFormat')}
getConfig={getConfig}
model={model}
visData={visData}
uiState={uiState}
onBrush={onBrush}
onUiState={handleUiState}
/>
);
}
return <div className={className} />;
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TimeseriesVisualization as default };

View file

@ -23,10 +23,8 @@ import * as Rx from 'rxjs';
import { share } from 'rxjs/operators';
import { isEqual, isEmpty, debounce } from 'lodash';
import { VisEditorVisualization } from './vis_editor_visualization';
import { Visualization } from './visualization';
import { VisPicker } from './vis_picker';
import { PanelConfig } from './panel_config';
import { createBrushHandler } from '../lib/create_brush_handler';
import { fetchFields } from '../lib/fetch_fields';
import { extractIndexPatterns } from '../../../common/extract_index_patterns';
import { getSavedObjectsClient, getUISettings, getDataStart, getCoreStart } from '../../services';
@ -49,7 +47,6 @@ export class VisEditor extends Component {
visFields: props.visFields,
extractedIndexPatterns: [''],
};
this.onBrush = createBrushHandler((data) => props.vis.API.events.applyFilter(data));
this.visDataSubject = new Rx.BehaviorSubject(this.props.visData);
this.visData$ = this.visDataSubject.asObservable().pipe(share());
@ -71,12 +68,6 @@ export class VisEditor extends Component {
return this.props.config.get(...args);
};
handleUiState = (field, value) => {
this.props.vis.uiState.set(field, value);
// reload visualization because data might need to be re-fetched
this.props.vis.uiState.emit('reload');
};
updateVisState = debounce(() => {
this.props.vis.params = this.state.model;
this.props.embeddableHandler.reload();
@ -101,16 +92,14 @@ export class VisEditor extends Component {
dirty = false;
}
if (this.props.isEditorMode) {
const extractedIndexPatterns = extractIndexPatterns(nextModel);
if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
fetchFields(extractedIndexPatterns).then((visFields) =>
this.setState({
visFields,
extractedIndexPatterns,
})
);
}
const extractedIndexPatterns = extractIndexPatterns(nextModel);
if (!isEqual(this.state.extractedIndexPatterns, extractedIndexPatterns)) {
fetchFields(extractedIndexPatterns).then((visFields) =>
this.setState({
visFields,
extractedIndexPatterns,
})
);
}
this.setState({
@ -141,23 +130,6 @@ export class VisEditor extends Component {
};
render() {
if (!this.props.isEditorMode) {
if (!this.props.visParams || !this.props.visData) {
return null;
}
return (
<Visualization
dateFormat={this.props.config.get('dateFormat')}
onBrush={this.onBrush}
onUiState={this.handleUiState}
uiState={this.uiState}
model={this.props.visParams}
visData={this.props.visData}
getConfig={this.getConfig}
/>
);
}
const { model } = this.state;
if (model) {
@ -211,23 +183,12 @@ export class VisEditor extends Component {
}
componentDidMount() {
this.props.renderComplete();
if (this.props.isEditorMode && this.props.eventEmitter) {
this.props.eventEmitter.on('updateEditor', this.updateModel);
}
}
componentDidUpdate() {
this.props.renderComplete();
this.props.eventEmitter.on('updateEditor', this.updateModel);
}
componentWillUnmount() {
this.updateVisState.cancel();
if (this.props.isEditorMode && this.props.eventEmitter) {
this.props.eventEmitter.off('updateEditor', this.updateModel);
}
this.props.eventEmitter.off('updateEditor', this.updateModel);
}
}
@ -241,7 +202,6 @@ VisEditor.propTypes = {
visFields: PropTypes.object,
renderComplete: PropTypes.func,
config: PropTypes.object,
isEditorMode: PropTypes.bool,
savedObj: PropTypes.object,
timeRange: PropTypes.object,
appState: PropTypes.object,

View file

@ -101,4 +101,8 @@ GaugeVisualization.propTypes = {
getConfig: PropTypes.func,
};
export const gauge = visWithSplits(GaugeVisualization);
const gauge = visWithSplits(GaugeVisualization);
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { gauge as default };

View file

@ -0,0 +1,69 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import { IUiSettingsClient } from 'src/core/public';
import { PersistedState } from 'src/plugins/visualizations/public';
import { TimeseriesVisParams } from '../../../metrics_fn';
import { TimeseriesVisData } from '../../../../common/types';
/**
* Lazy load each visualization type, since the only one is presented on the screen at the same time.
* Disable typescript errors since the components are not typed yet.
*/
// @ts-expect-error
const timeseries = lazy(() => import('./timeseries/vis'));
// @ts-expect-error
const metric = lazy(() => import('./metric/vis'));
// @ts-expect-error
const topN = lazy(() => import('./top_n/vis'));
// @ts-expect-error
const table = lazy(() => import('./table/vis'));
// @ts-expect-error
const gauge = lazy(() => import('./gauge/vis'));
// @ts-expect-error
const markdown = lazy(() => import('./markdown/vis'));
export const TimeseriesVisTypes: Record<string, React.ComponentType<TimeseriesVisProps>> = {
timeseries,
metric,
top_n: topN,
table,
gauge,
markdown,
};
export interface TimeseriesVisProps {
model: TimeseriesVisParams;
onBrush: (gte: string, lte: string) => void;
onUiState: (
field: string,
value: {
column: string;
order: string;
}
) => void;
uiState: PersistedState;
visData: TimeseriesVisData;
dateFormat: string;
getConfig: IUiSettingsClient['get'];
}

View file

@ -30,7 +30,7 @@ import { isBackgroundInverted } from '../../../lib/set_is_reversed';
const getMarkdownId = (id) => `markdown-${id}`;
export function MarkdownVisualization(props) {
function MarkdownVisualization(props) {
const { backgroundColor, model, visData, dateFormat } = props;
const series = get(visData, `${model.id}.series`, []);
const variables = convertSeriesToVars(series, model, dateFormat, props.getConfig);
@ -106,3 +106,7 @@ MarkdownVisualization.propTypes = {
dateFormat: PropTypes.string,
getConfig: PropTypes.func,
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { MarkdownVisualization as default };

View file

@ -95,4 +95,8 @@ MetricVisualization.propTypes = {
getConfig: PropTypes.func,
};
export const metric = visWithSplits(MetricVisualization);
const metric = visWithSplits(MetricVisualization);
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { metric as default };

View file

@ -46,7 +46,7 @@ function getColor(rules, colorKey, value) {
return color;
}
export class TableVis extends Component {
class TableVis extends Component {
constructor(props) {
super(props);
@ -260,3 +260,7 @@ TableVis.propTypes = {
pageNumber: PropTypes.number,
getConfig: PropTypes.func,
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TableVis as default };

View file

@ -34,7 +34,7 @@ import { createXaxisFormatter } from '../../lib/create_xaxis_formatter';
import { STACKED_OPTIONS } from '../../../visualizations/constants';
import { getCoreStart } from '../../../../services';
export class TimeseriesVisualization extends Component {
class TimeseriesVisualization extends Component {
static propTypes = {
model: PropTypes.object,
onBrush: PropTypes.func,
@ -44,7 +44,8 @@ export class TimeseriesVisualization extends Component {
};
xAxisFormatter = (interval) => (val) => {
const { scaledDataFormat, dateFormat } = this.props.visData;
const scaledDataFormat = this.props.getConfig('dateFormat:scaled');
const { dateFormat } = this.props;
if (!scaledDataFormat || !dateFormat) {
return val;
@ -245,3 +246,7 @@ export class TimeseriesVisualization extends Component {
);
}
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TimeseriesVisualization as default };

View file

@ -48,7 +48,7 @@ function sortSeries(visData, model) {
}, []);
}
export function TopNVisualization(props) {
function TopNVisualization(props) {
const { backgroundColor, model, visData } = props;
const series = sortSeries(visData, model).map((item) => {
@ -111,3 +111,7 @@ TopNVisualization.propTypes = {
visData: PropTypes.object,
getConfig: PropTypes.func,
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { TopNVisualization as default };

View file

@ -1,96 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';
import { TimeseriesVisualization } from './vis_types/timeseries/vis';
import { metric } from './vis_types/metric/vis';
import { TopNVisualization as topN } from './vis_types/top_n/vis';
import { TableVis as table } from './vis_types/table/vis';
import { gauge } from './vis_types/gauge/vis';
import { MarkdownVisualization as markdown } from './vis_types/markdown/vis';
import { ErrorComponent } from './error';
import { NoDataComponent } from './no_data';
const types = {
timeseries: TimeseriesVisualization,
metric,
top_n: topN,
table,
gauge,
markdown,
};
export function Visualization(props) {
const { visData, model } = props;
// Show the error panel
const error = _.get(visData, `${model.id}.error`);
if (error) {
return (
<div className={props.className}>
<ErrorComponent error={error} />
</div>
);
}
const path = visData.type === 'table' ? 'series' : `${model.id}.series`;
const noData = _.get(visData, path, []).length === 0;
if (noData) {
return (
<div className={props.className}>
<NoDataComponent />
</div>
);
}
const component = types[model.type];
if (component) {
return React.createElement(component, {
dateFormat: props.dateFormat,
backgroundColor: props.backgroundColor,
model: props.model,
onBrush: props.onBrush,
onChange: props.onChange,
onUiState: props.onUiState,
uiState: props.uiState,
visData: visData.type === model.type ? visData : {},
getConfig: props.getConfig,
});
}
return <div className={props.className} />;
}
Visualization.propTypes = {
backgroundColor: PropTypes.string,
className: PropTypes.string,
model: PropTypes.object,
onBrush: PropTypes.func,
onChange: PropTypes.func,
onUiState: PropTypes.func,
uiState: PropTypes.object,
visData: PropTypes.object,
dateFormat: PropTypes.string,
getConfig: PropTypes.func,
};
Visualization.defaultProps = {
className: 'tvbVis',
};

View file

@ -70,7 +70,6 @@ export class EditorController {
visParams={this.state.vis.params}
timeRange={params.timeRange}
renderComplete={() => {}}
isEditorMode={true}
appState={params.appState}
embeddableHandler={this.embeddableHandler}
eventEmitter={this.eventEmitter}

View file

@ -1,48 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { createBrushHandler } from './create_brush_handler';
import { ExprVisAPIEvents } from '../../../../visualizations/public';
describe('brushHandler', () => {
let onBrush: ReturnType<typeof createBrushHandler>;
let applyFilter: ExprVisAPIEvents['applyFilter'];
beforeEach(() => {
applyFilter = jest.fn();
onBrush = createBrushHandler(applyFilter);
});
test('returns brushHandler() should updates timefilter through vis.API.events.applyFilter', () => {
const gte = '2017-01-01T00:00:00Z';
const lte = '2017-01-01T00:10:00Z';
onBrush(gte, lte);
expect(applyFilter).toHaveBeenCalledWith({
timeFieldName: '*',
filters: [
{
range: { '*': { gte: '2017-01-01T00:00:00Z', lte: '2017-01-01T00:10:00Z' } },
},
],
});
});
});

View file

@ -17,38 +17,36 @@
* under the License.
*/
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { KibanaContext } from '../../data/public';
import { ExpressionFunctionDefinition, Render } from '../../expressions/public';
// @ts-ignore
import { PanelSchema, TimeseriesVisData } from '../common/types';
import { metricsRequestHandler } from './request_handler';
type Input = KibanaContext | null;
type Output = Promise<Render<RenderValue>>;
type Output = Promise<Render<TimeseriesRenderValue>>;
interface Arguments {
params: string;
uiState: string;
savedObjectId: string | null;
}
type VisParams = Required<Arguments>;
export type TimeseriesVisParams = PanelSchema;
interface RenderValue {
visType: 'metrics';
visData: Input;
visConfig: VisParams;
uiState: any;
export interface TimeseriesRenderValue {
visData: TimeseriesVisData | {};
visParams: TimeseriesVisParams;
}
export const createMetricsFn = (): ExpressionFunctionDefinition<
export type TimeseriesExpressionFunctionDefinition = ExpressionFunctionDefinition<
'tsvb',
Input,
Arguments,
Output
> => ({
>;
export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({
name: 'tsvb',
type: 'render',
inputTypes: ['kibana_context', 'null'],
@ -66,37 +64,22 @@ export const createMetricsFn = (): ExpressionFunctionDefinition<
default: '"{}"',
help: '',
},
savedObjectId: {
types: ['null', 'string'],
default: null,
help: '',
},
},
async fn(input, args) {
const params = JSON.parse(args.params);
const uiStateParams = JSON.parse(args.uiState);
const savedObjectId = args.savedObjectId;
const { PersistedState } = await import('../../visualizations/public');
const uiState = new PersistedState(uiStateParams);
const visParams: TimeseriesVisParams = JSON.parse(args.params);
const uiState = JSON.parse(args.uiState);
const response = await metricsRequestHandler({
timeRange: get(input, 'timeRange', null),
query: get(input, 'query', null),
filters: get(input, 'filters', null),
visParams: params,
input,
visParams,
uiState,
savedObjectId,
});
response.visType = 'metrics';
return {
type: 'render',
as: 'visualization',
as: 'timeseries_vis',
value: {
uiState,
visType: 'metrics',
visConfig: params,
visParams,
visData: response,
},
};

View file

@ -19,12 +19,9 @@
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { metricsRequestHandler } from './request_handler';
import { EditorController } from './application';
// @ts-ignore
import { PANEL_TYPES } from '../common/panel_types';
import { VisEditor } from './application/components/vis_editor_lazy';
import { toExpressionAst } from './to_ast';
import { VIS_EVENT_TO_TRIGGER, VisGroups, VisParams } from '../../visualizations/public';
import { getDataStart } from './services';
import { INDEXES_SEPARATOR } from '../common/constants';
@ -73,7 +70,6 @@ export const metricsVisDefinition = {
show_grid: 1,
tooltip_mode: 'show_all',
},
component: VisEditor,
},
editor: EditorController,
options: {
@ -81,7 +77,7 @@ export const metricsVisDefinition = {
showFilterBar: false,
showIndexSelection: false,
},
requestHandler: metricsRequestHandler,
toExpressionAst,
getSupportedTriggers: () => {
return [VIS_EVENT_TO_TRIGGER.applyFilter];
},
@ -102,5 +98,4 @@ export const metricsVisDefinition = {
return [];
},
responseHandler: 'none',
};

View file

@ -36,6 +36,7 @@ import {
} from './services';
import { DataPublicPluginStart } from '../../data/public';
import { ChartsPluginSetup } from '../../charts/public';
import { getTimeseriesVisRenderer } from './timeseries_vis_renderer';
/** @internal */
export interface MetricsPluginSetupDependencies {
@ -62,9 +63,14 @@ export class MetricsPlugin implements Plugin<Promise<void>, void> {
{ expressions, visualizations, charts }: MetricsPluginSetupDependencies
) {
expressions.registerFunction(createMetricsFn);
expressions.registerRenderer(
getTimeseriesVisRenderer({
uiSettings: core.uiSettings,
})
);
setUISettings(core.uiSettings);
setChartsSetup(charts);
visualizations.createReactVisualization(metricsVisDefinition);
visualizations.createBaseVisualization(metricsVisDefinition);
}
public start(core: CoreStart, { data }: MetricsPluginStartDependencies) {

View file

@ -17,57 +17,52 @@
* under the License.
*/
import { KibanaContext } from '../../data/public';
import { getTimezone, validateInterval } from './application';
import { getUISettings, getDataStart, getCoreStart } from './services';
import { MAX_BUCKETS_SETTING } from '../common/constants';
import { MAX_BUCKETS_SETTING, ROUTES } from '../common/constants';
import { TimeseriesVisParams } from './metrics_fn';
import { TimeseriesVisData } from '../common/types';
interface MetricsRequestHandlerParams {
input: KibanaContext | null;
uiState: Record<string, any>;
visParams: TimeseriesVisParams;
}
export const metricsRequestHandler = async ({
input,
uiState,
timeRange,
filters,
query,
visParams,
savedObjectId,
}) => {
}: MetricsRequestHandlerParams): Promise<TimeseriesVisData | {}> => {
const config = getUISettings();
const timezone = getTimezone(config);
const uiStateObj = uiState.get(visParams.type, {});
const uiStateObj = uiState[visParams.type] ?? {};
const dataSearch = getDataStart();
const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(timeRange);
const scaledDataFormat = config.get('dateFormat:scaled');
const dateFormat = config.get('dateFormat');
const parsedTimeRange = dataSearch.query.timefilter.timefilter.calculateBounds(input?.timeRange!);
if (visParams && visParams.id && !visParams.isModelInvalid) {
try {
const maxBuckets = config.get(MAX_BUCKETS_SETTING);
const maxBuckets = config.get(MAX_BUCKETS_SETTING);
validateInterval(parsedTimeRange, visParams, maxBuckets);
validateInterval(parsedTimeRange, visParams, maxBuckets);
const resp = await getCoreStart().http.post('/api/metrics/vis/data', {
body: JSON.stringify({
timerange: {
timezone,
...parsedTimeRange,
},
query,
filters,
panels: [visParams],
state: uiStateObj,
savedObjectId: savedObjectId || 'unsaved',
sessionId: dataSearch.search.session.getSessionId(),
}),
});
const resp = await getCoreStart().http.post(ROUTES.VIS_DATA, {
body: JSON.stringify({
timerange: {
timezone,
...parsedTimeRange,
},
query: input?.query,
filters: input?.filters,
panels: [visParams],
state: uiStateObj,
sessionId: dataSearch.search.session.getSessionId(),
}),
});
return {
dateFormat,
scaledDataFormat,
timezone,
...resp,
};
} catch (error) {
return Promise.reject(error);
}
return resp;
}
return Promise.resolve({});
return {};
};

View file

@ -0,0 +1,72 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { IUiSettingsClient } from 'kibana/public';
import { VisualizationContainer } from '../../visualizations/public';
import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers';
import { TimeseriesRenderValue, TimeseriesVisParams } from './metrics_fn';
import { TimeseriesVisData } from '../common/types';
const TimeseriesVisualization = lazy(
() => import('./application/components/timeseries_visualization')
);
const checkIfDataExists = (visData: TimeseriesVisData | {}, model: TimeseriesVisParams) => {
if ('type' in visData) {
const data = visData.type === 'table' ? visData.series : visData?.[model.id]?.series;
return Boolean(data?.length);
}
return false;
};
export const getTimeseriesVisRenderer: (deps: {
uiSettings: IUiSettingsClient;
}) => ExpressionRenderDefinition<TimeseriesRenderValue> = ({ uiSettings }) => ({
name: 'timeseries_vis',
reuseDomNode: true,
render: async (domNode, config, handlers) => {
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
const showNoResult = !checkIfDataExists(config.visData, config.visParams);
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)}
handlers={handlers}
model={config.visParams}
visData={config.visData as TimeseriesVisData}
uiState={handlers.uiState!}
/>
</VisualizationContainer>,
domNode
);
},
});

View file

@ -17,23 +17,17 @@
* under the License.
*/
import { ExprVisAPIEvents } from '../../../../visualizations/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { Vis } from '../../visualizations/public';
import { TimeseriesExpressionFunctionDefinition, TimeseriesVisParams } from './metrics_fn';
export const createBrushHandler = (applyFilter: ExprVisAPIEvents['applyFilter']) => (
gte: string,
lte: string
) => {
return applyFilter({
timeFieldName: '*',
filters: [
{
range: {
'*': {
gte,
lte,
},
},
},
],
export const toExpressionAst = (vis: Vis<TimeseriesVisParams>) => {
const timeseries = buildExpressionFunction<TimeseriesExpressionFunctionDefinition>('tsvb', {
params: JSON.stringify(vis.params),
uiState: JSON.stringify(vis.uiState),
});
const ast = buildExpression([timeseries]);
return ast.toAst();
};

View file

@ -20,46 +20,37 @@
import { FakeRequest, RequestHandlerContext } from 'kibana/server';
import _ from 'lodash';
import { first, map } from 'rxjs/operators';
import { Filter, Query } from 'src/plugins/data/common';
import { getPanelData } from './vis_data/get_panel_data';
import { Framework } from '../plugin';
import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy';
interface GetVisDataResponse {
[key: string]: GetVisDataPanel;
}
interface GetVisDataPanel {
id: string;
series: GetVisDataSeries[];
}
interface GetVisDataSeries {
id: string;
label: string;
data: GetVisDataDataPoint[];
}
type GetVisDataDataPoint = [number, number];
import { TimeseriesVisData } from '../../common/types';
export interface GetVisDataOptions {
timerange?: any;
panels?: any;
filters?: any;
state?: any;
query?: any;
timerange: {
min: number | string;
max: number | string;
timezone?: string;
};
panels: unknown[];
filters?: Filter[];
state?: Record<string, unknown>;
query?: Query | Query[];
sessionId?: string;
}
export type GetVisData = (
requestContext: RequestHandlerContext,
options: GetVisDataOptions,
framework: Framework
) => Promise<GetVisDataResponse>;
) => Promise<TimeseriesVisData>;
export function getVisData(
requestContext: RequestHandlerContext,
request: FakeRequest & { body: GetVisDataOptions },
framework: Framework
): Promise<GetVisDataResponse> {
): Promise<TimeseriesVisData> {
// NOTE / TODO: This facade has been put in place to make migrating to the New Platform easier. It
// removes the need to refactor many layers of dependencies on "req", and instead just augments the top
// level object passed from here. The layers should be refactored fully at some point, but for now
@ -81,10 +72,10 @@ export function getVisData(
.toPromise();
},
};
const promises = (reqFacade.payload as GetVisDataOptions).panels.map(getPanelData(reqFacade));
const promises = reqFacade.payload.panels.map(getPanelData(reqFacade));
return Promise.all(promises).then((res) => {
return res.reduce((acc, data) => {
return _.assign(acc as any, data);
}, {});
}) as Promise<GetVisDataResponse>;
}) as Promise<TimeseriesVisData>;
}

View file

@ -29,7 +29,8 @@ export const getActiveSeries = (panel: PanelSchema) => {
}
// Toogle visibility functionality for 'gauge', 'markdown' is not accessible
const shouldNotApplyFilter = [PANEL_TYPES.GAUGE, PANEL_TYPES.MARKDOWN].includes(panel.type);
const shouldNotApplyFilter =
PANEL_TYPES.GAUGE === panel.type || PANEL_TYPES.MARKDOWN === panel.type;
return visibleSeries.filter((series) => !series.hidden || shouldNotApplyFilter);
};

View file

@ -21,6 +21,7 @@ import { IRouter, KibanaRequest } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { getVisData, GetVisDataOptions } from '../lib/get_vis_data';
import { visPayloadSchema } from '../../common/vis_schema';
import { ROUTES } from '../../common/constants';
import { ValidationTelemetryServiceSetup } from '../index';
import { Framework } from '../plugin';
@ -33,7 +34,7 @@ export const visDataRoutes = (
) => {
router.post(
{
path: '/api/metrics/vis/data',
path: ROUTES.VIS_DATA,
validate: {
body: escapeHatch,
},
@ -43,11 +44,9 @@ export const visDataRoutes = (
visPayloadSchema.validate(request.body);
} catch (error) {
logFailedValidation();
const savedObjectId =
(typeof request.body === 'object' && (request.body as any).savedObjectId) ||
'unavailable';
framework.logger.warn(
`Request validation error: ${error.message} (saved object id: ${savedObjectId}). This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md`
`Request validation error: ${error.message}. This most likely means your TSVB visualization contains outdated configuration. You can report this problem under https://github.com/elastic/kibana/issues/new?template=Bug_report.md`
);
}

View file

@ -3,6 +3,7 @@
exports[`VisualizationNoResults should render according to snapshot 1`] = `
<div
class="visError"
data-test-subj="visNoResult"
>
<div
class="item top"

View file

@ -24,6 +24,7 @@ import { VisualizationNoResults } from './visualization_noresults';
import { IInterpreterRenderHandlers } from '../../../expressions/common';
interface VisualizationContainerProps {
'data-test-subj'?: string;
className?: string;
children: ReactNode;
handlers: IInterpreterRenderHandlers;
@ -31,6 +32,7 @@ interface VisualizationContainerProps {
}
export const VisualizationContainer = ({
'data-test-subj': dataTestSubj = '',
className,
children,
handlers,
@ -45,7 +47,7 @@ export const VisualizationContainer = ({
);
return (
<div className={classes}>
<div data-test-subj={dataTestSubj} className={classes}>
<Suspense fallback={fallBack}>
{showNoResult ? <VisualizationNoResults onInit={() => handlers.done()} /> : children}
</Suspense>

View file

@ -30,7 +30,7 @@ export class VisualizationNoResults extends React.Component<VisualizationNoResul
public render() {
return (
<div className="visError" ref={this.containerDiv}>
<div data-test-subj="visNoResult" className="visError" ref={this.containerDiv}>
<div className="item top" />
<div className="item">
<EuiText size="xs" color="subdued">

View file

@ -4,8 +4,6 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipeline calls t
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles input_control_vis function 1`] = `"input_control_vis visConfig='{\\"some\\":\\"nested\\",\\"data\\":{\\"here\\":true}}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles metrics/tsvb function 1`] = `"tsvb params='{\\"foo\\":\\"bar\\"}' uiState='{}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function with buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"bucket\\":1}' "`;
exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles region_map function without buckets 1`] = `"regionmap visConfig='{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"}}' "`;

View file

@ -101,12 +101,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => {
expect(actual).toMatchSnapshot();
});
it('handles metrics/tsvb function', () => {
const params = { foo: 'bar' };
const actual = buildPipelineVisFunction.metrics(params, schemasDef, uiState);
expect(actual).toMatchSnapshot();
});
describe('handles region_map function', () => {
it('without buckets', () => {
const params = { metric: {} };

View file

@ -222,13 +222,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = {
input_control_vis: (params) => {
return `input_control_vis ${prepareJson('visConfig', params)}`;
},
metrics: ({ title, ...params }, schemas, uiState = {}) => {
const paramsJson = prepareJson('params', params);
const uiStateJson = prepareJson('uiState', uiState);
const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param));
return `tsvb ${paramsArray.join(' ')}`;
},
region_map: (params, schemas) => {
const visConfig = {
...params,

View file

@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }) {
});
it('tsvb time series shows no data message', async () => {
expect(await testSubjects.exists('noTSVBDataMessage')).to.be(true);
expect(await testSubjects.exists('timeseriesVis > visNoResult')).to.be(true);
});
it('metric value shows no data', async () => {

View file

@ -549,7 +549,7 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
public async checkPreviewIsDisabled(): Promise<void> {
log.debug(`Check no data message is present`);
await testSubjects.existOrFail('noTSVBDataMessage', { timeout: 5000 });
await testSubjects.existOrFail('timeseriesVis > visNoResult', { timeout: 5000 });
}
public async cloneSeries(nth: number = 0): Promise<void> {

View file

@ -4028,7 +4028,6 @@
"visTypeTimeseries.movingAverage.windowSizeHint": "ウィンドウは、必ず、期間のサイズの 2 倍以上でなければなりません",
"visTypeTimeseries.movingAverage.windowSizeLabel": "ウィンドウサイズ",
"visTypeTimeseries.noButtonLabel": "いいえ",
"visTypeTimeseries.noDataDescription": "選択されたメトリックに表示するデータがありません",
"visTypeTimeseries.percentile.aggregationLabel": "集約",
"visTypeTimeseries.percentile.fieldLabel": "フィールド",
"visTypeTimeseries.percentile.fillToLabel": "次の基準に合わせる:",

View file

@ -4029,7 +4029,6 @@
"visTypeTimeseries.movingAverage.windowSizeHint": "窗口必须始终至少是期间大小的两倍",
"visTypeTimeseries.movingAverage.windowSizeLabel": "窗口大小",
"visTypeTimeseries.noButtonLabel": "否",
"visTypeTimeseries.noDataDescription": "所选指标没有可显示的数据",
"visTypeTimeseries.percentile.aggregationLabel": "聚合",
"visTypeTimeseries.percentile.fieldLabel": "字段",
"visTypeTimeseries.percentile.fillToLabel": "填充到:",