From a65abc07b16dffea78113b188e1e3ad724fc1341 Mon Sep 17 00:00:00 2001 From: Ashik Meerankutty Date: Wed, 15 Jul 2020 18:53:03 +0530 Subject: [PATCH] Convert vis_type_vega to Typescript (#68915) --- package.json | 3 +- renovate.json5 | 8 + .../public/map/service_settings.d.ts | 1 + .../public/components/vega_vis_editor.tsx | 7 +- ...{ems_file_parser.js => ems_file_parser.ts} | 14 +- ...{es_query_parser.js => es_query_parser.ts} | 62 +++-- .../{time_cache.js => time_cache.ts} | 32 ++- .../vis_type_vega/public/data_model/types.ts | 246 ++++++++++++++++ .../{url_parser.js => url_parser.ts} | 6 +- .../public/data_model/{utils.js => utils.ts} | 16 +- .../{vega_parser.js => vega_parser.ts} | 263 +++++++++++------- src/plugins/vis_type_vega/public/vega_fn.ts | 3 +- .../public/vega_request_handler.ts | 3 - yarn.lock | 5 + 14 files changed, 511 insertions(+), 158 deletions(-) rename src/plugins/vis_type_vega/public/data_model/{ems_file_parser.js => ems_file_parser.ts} (86%) rename src/plugins/vis_type_vega/public/data_model/{es_query_parser.js => es_query_parser.ts} (87%) rename src/plugins/vis_type_vega/public/data_model/{time_cache.js => time_cache.ts} (79%) create mode 100644 src/plugins/vis_type_vega/public/data_model/types.ts rename src/plugins/vis_type_vega/public/data_model/{url_parser.js => url_parser.ts} (92%) rename src/plugins/vis_type_vega/public/data_model/{utils.js => utils.ts} (75%) rename src/plugins/vis_type_vega/public/data_model/{vega_parser.js => vega_parser.ts} (74%) diff --git a/package.json b/package.json index 476ae949e55ac..1944e0a04bb5a 100644 --- a/package.json +++ b/package.json @@ -138,9 +138,9 @@ "@kbn/babel-preset": "1.0.0", "@kbn/config-schema": "1.0.0", "@kbn/i18n": "1.0.0", - "@kbn/telemetry-tools": "1.0.0", "@kbn/interpreter": "1.0.0", "@kbn/pm": "1.0.0", + "@kbn/telemetry-tools": "1.0.0", "@kbn/test-subj-selector": "0.2.1", "@kbn/ui-framework": "1.0.0", "@kbn/ui-shared-deps": "1.0.0", @@ -340,6 +340,7 @@ "@types/hapi-auth-cookie": "^9.1.0", "@types/has-ansi": "^3.0.0", "@types/history": "^4.7.3", + "@types/hjson": "^2.4.2", "@types/hoek": "^4.1.3", "@types/inert": "^5.1.2", "@types/jest": "^25.2.3", diff --git a/renovate.json5 b/renovate.json5 index 5a807b4b090c1..1ba6dc0ff7e1b 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -426,6 +426,14 @@ '@types/history', ], }, + { + groupSlug: 'hjson', + groupName: 'hjson related packages', + packageNames: [ + 'hjson', + '@types/hjson', + ], + }, { groupSlug: 'inquirer', groupName: 'inquirer related packages', diff --git a/src/plugins/maps_legacy/public/map/service_settings.d.ts b/src/plugins/maps_legacy/public/map/service_settings.d.ts index e265accaeb8fd..105836ff25f8b 100644 --- a/src/plugins/maps_legacy/public/map/service_settings.d.ts +++ b/src/plugins/maps_legacy/public/map/service_settings.d.ts @@ -48,4 +48,5 @@ export interface IServiceSettings { getEMSHotLink(layer: FileLayer): Promise; getTMSServices(): Promise; getFileLayers(): Promise; + getUrlForRegionLayer(layer: FileLayer): Promise; } diff --git a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx index 1da5e7544850a..5e770fcff556d 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx +++ b/src/plugins/vis_type_vega/public/components/vega_vis_editor.tsx @@ -20,7 +20,6 @@ import React, { useCallback } from 'react'; import { EuiCodeEditor } from '@elastic/eui'; import compactStringify from 'json-stringify-pretty-compact'; -// @ts-ignore import hjson from 'hjson'; import 'brace/mode/hjson'; import { i18n } from '@kbn/i18n'; @@ -45,7 +44,11 @@ const hjsonStringifyOptions = { keepWsc: true, }; -function format(value: string, stringify: typeof compactStringify, options?: any) { +function format( + value: string, + stringify: typeof hjson.stringify | typeof compactStringify, + options?: any +) { try { const spec = hjson.parse(value, { legacyRoot: false, keepWsc: true }); return stringify(spec, options); diff --git a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts similarity index 86% rename from src/plugins/vis_type_vega/public/data_model/ems_file_parser.js rename to src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts index ecdf6a43d5287..59256d47de97c 100644 --- a/src/plugins/vis_type_vega/public/data_model/ems_file_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/ems_file_parser.ts @@ -18,14 +18,20 @@ */ import { i18n } from '@kbn/i18n'; +// @ts-ignore import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; +import { IServiceSettings, FileLayer } from '../../../maps_legacy/public'; +import { Data, UrlObject, Requests } from './types'; /** * This class processes all Vega spec customizations, * converting url object parameters into query results. */ export class EmsFileParser { - constructor(serviceSettings) { + _serviceSettings: IServiceSettings; + _fileLayersP?: Promise; + + constructor(serviceSettings: IServiceSettings) { this._serviceSettings = serviceSettings; } @@ -33,7 +39,7 @@ export class EmsFileParser { /** * Update request object, expanding any context-aware keywords */ - parseUrl(obj, url) { + parseUrl(obj: Data, url: UrlObject) { if (typeof url.name !== 'string') { throw new Error( i18n.translate('visTypeVega.emsFileParser.missingNameOfFileErrorMessage', { @@ -59,13 +65,13 @@ export class EmsFileParser { * @param {object[]} requests each object is generated by parseUrl() * @returns {Promise} */ - async populateData(requests) { + async populateData(requests: Requests[]) { if (requests.length === 0) return; const layers = await this._fileLayersP; for (const { obj, name } of requests) { - const foundLayer = layers.find((v) => v.name === name); + const foundLayer = layers?.find((v) => v.name === name); if (!foundLayer) { throw new Error( i18n.translate('visTypeVega.emsFileParser.emsFileNameDoesNotExistErrorMessage', { diff --git a/src/plugins/vis_type_vega/public/data_model/es_query_parser.js b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts similarity index 87% rename from src/plugins/vis_type_vega/public/data_model/es_query_parser.js rename to src/plugins/vis_type_vega/public/data_model/es_query_parser.ts index f7772ff888a61..4fdd68f9e9dbe 100644 --- a/src/plugins/vis_type_vega/public/data_model/es_query_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/es_query_parser.ts @@ -19,24 +19,38 @@ import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { isPlainObject, cloneDeep } from 'lodash'; +import { cloneDeep, isPlainObject } from 'lodash'; +import { SearchParams } from 'elasticsearch'; +import { TimeCache } from './time_cache'; +import { SearchAPI } from './search_api'; +import { Opts, Type, Data, UrlObject, Bool, Requests, Query, ContextVarsObject } from './types'; -const TIMEFILTER = '%timefilter%'; -const AUTOINTERVAL = '%autointerval%'; -const MUST_CLAUSE = '%dashboard_context-must_clause%'; -const FILTER_CLAUSE = '%dashboard_context-filter_clause%'; -const MUST_NOT_CLAUSE = '%dashboard_context-must_not_clause%'; +const TIMEFILTER: string = '%timefilter%'; +const AUTOINTERVAL: string = '%autointerval%'; +const MUST_CLAUSE: string = '%dashboard_context-must_clause%'; +const MUST_NOT_CLAUSE: string = '%dashboard_context-must_not_clause%'; +const FILTER_CLAUSE: string = '%dashboard_context-filter_clause%'; // These values may appear in the 'url': { ... } object -const LEGACY_CONTEXT = '%context_query%'; -const CONTEXT = '%context%'; -const TIMEFIELD = '%timefield%'; +const LEGACY_CONTEXT: string = '%context_query%'; +const CONTEXT: string = '%context%'; +const TIMEFIELD: string = '%timefield%'; /** * This class parses ES requests specified in the data.url objects. */ export class EsQueryParser { - constructor(timeCache, searchAPI, filters, onWarning) { + _timeCache: TimeCache; + _searchAPI: SearchAPI; + _filters: Bool; + _onWarning: (...args: string[]) => void; + + constructor( + timeCache: TimeCache, + searchAPI: SearchAPI, + filters: Bool, + onWarning: (...args: string[]) => void + ) { this._timeCache = timeCache; this._searchAPI = searchAPI; this._filters = filters; @@ -47,7 +61,7 @@ export class EsQueryParser { /** * Update request object, expanding any context-aware keywords */ - parseUrl(dataObject, url) { + parseUrl(dataObject: Data, url: UrlObject) { let body = url.body; let context = url[CONTEXT]; delete url[CONTEXT]; @@ -167,13 +181,13 @@ export class EsQueryParser { // Use dashboard context const newQuery = cloneDeep(this._filters); if (timefield) { - newQuery.bool.must.push(body.query); + newQuery.bool!.must!.push(body.query); } body.query = newQuery; } } - this._injectContextVars(body.aggs, false); + this._injectContextVars(body.aggs!, false); return { dataObject, url }; } @@ -182,8 +196,8 @@ export class EsQueryParser { * @param {object[]} requests each object is generated by parseUrl() * @returns {Promise} */ - async populateData(requests) { - const esSearches = requests.map((r) => r.url); + async populateData(requests: Requests[]) { + const esSearches = requests.map((r: Requests) => r.url); const data$ = this._searchAPI.search(esSearches); const results = await data$.toPromise(); @@ -198,7 +212,7 @@ export class EsQueryParser { * @param {*} obj * @param {boolean} isQuery - if true, the `obj` belongs to the req's query portion */ - _injectContextVars(obj, isQuery) { + _injectContextVars(obj: Query | SearchParams['body']['aggs'], isQuery: boolean) { if (obj && typeof obj === 'object') { if (Array.isArray(obj)) { // For arrays, replace MUST_CLAUSE and MUST_NOT_CLAUSE string elements @@ -239,7 +253,7 @@ export class EsQueryParser { } } else { for (const prop of Object.keys(obj)) { - const subObj = obj[prop]; + const subObj = (obj as ContextVarsObject)[prop]; if (!subObj || typeof obj !== 'object') continue; // replace "interval": { "%autointerval%": true|integer } with @@ -260,7 +274,9 @@ export class EsQueryParser { ); } const bounds = this._timeCache.getTimeBounds(); - obj.interval = EsQueryParser._roundInterval((bounds.max - bounds.min) / size); + (obj as ContextVarsObject).interval = EsQueryParser._roundInterval( + (bounds.max - bounds.min) / size + ); continue; } @@ -269,7 +285,7 @@ export class EsQueryParser { case 'min': case 'max': // Replace {"%timefilter%": "min|max", ...} object with a timestamp - obj[prop] = this._getTimeBound(subObj, subObj[TIMEFILTER]); + (obj as ContextVarsObject)[prop] = this._getTimeBound(subObj, subObj[TIMEFILTER]); continue; case true: // Replace {"%timefilter%": true, ...} object with the "range" object @@ -302,7 +318,7 @@ export class EsQueryParser { * @param {object} obj * @return {object} */ - _createRangeFilter(obj) { + _createRangeFilter(obj: Opts) { obj.gte = moment(this._getTimeBound(obj, 'min')).toISOString(); obj.lte = moment(this._getTimeBound(obj, 'max')).toISOString(); obj.format = 'strict_date_optional_time'; @@ -320,9 +336,9 @@ export class EsQueryParser { * @param {'min'|'max'} type * @returns {*} */ - _getTimeBound(opts, type) { + _getTimeBound(opts: Opts, type: Type): number { const bounds = this._timeCache.getTimeBounds(); - let result = bounds[type]; + let result = bounds[type]?.valueOf() || 0; if (opts.shift) { const shift = opts.shift; @@ -380,7 +396,7 @@ export class EsQueryParser { * @param interval (ms) * @returns {string} */ - static _roundInterval(interval) { + static _roundInterval(interval: number): string { switch (true) { case interval <= 500: // <= 0.5s return '100ms'; diff --git a/src/plugins/vis_type_vega/public/data_model/time_cache.js b/src/plugins/vis_type_vega/public/data_model/time_cache.ts similarity index 79% rename from src/plugins/vis_type_vega/public/data_model/time_cache.js rename to src/plugins/vis_type_vega/public/data_model/time_cache.ts index cf241655592f3..27012d3cdc6c2 100644 --- a/src/plugins/vis_type_vega/public/data_model/time_cache.js +++ b/src/plugins/vis_type_vega/public/data_model/time_cache.ts @@ -17,26 +17,36 @@ * under the License. */ +import { TimefilterContract } from '../../../data/public'; +import { TimeRange } from '../../../data/common'; +import { CacheBounds } from './types'; + /** * Optimization caching - always return the same value if queried within this time * @type {number} */ -const AlwaysCacheMaxAge = 40; + +const AlwaysCacheMaxAge: number = 40; /** * This class caches timefilter's bounds to minimize number of server requests */ export class TimeCache { - constructor(timefilter, maxAge) { + _timefilter: TimefilterContract; + _maxAge: number; + _cachedBounds?: CacheBounds; + _cacheTS: number; + _timeRange?: TimeRange; + + constructor(timefilter: TimefilterContract, maxAge: number) { this._timefilter = timefilter; this._maxAge = maxAge; - this._cachedBounds = null; this._cacheTS = 0; } // Simplifies unit testing // noinspection JSMethodCanBeStatic - _now() { + _now(): number { return Date.now(); } @@ -44,10 +54,10 @@ export class TimeCache { * Get cached time range values * @returns {{min: number, max: number}} */ - getTimeBounds() { + getTimeBounds(): CacheBounds { const ts = this._now(); - let bounds; + let bounds: CacheBounds | null = null; if (this._cachedBounds) { const diff = ts - this._cacheTS; @@ -76,7 +86,7 @@ export class TimeCache { return this._cachedBounds; } - setTimeRange(timeRange) { + setTimeRange(timeRange: TimeRange): void { this._timeRange = timeRange; } @@ -85,11 +95,11 @@ export class TimeCache { * @returns {{min: number, max: number}} * @private */ - _getBounds() { - const bounds = this._timefilter.calculateBounds(this._timeRange); + _getBounds(): CacheBounds { + const bounds = this._timefilter.calculateBounds(this._timeRange!); return { - min: bounds.min.valueOf(), - max: bounds.max.valueOf(), + min: bounds.min!.valueOf(), + max: bounds.max!.valueOf(), }; } } diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts new file mode 100644 index 0000000000000..9876faf0fc88f --- /dev/null +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -0,0 +1,246 @@ +/* + * 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 { SearchResponse, SearchParams } from 'elasticsearch'; +import { Filter } from 'src/plugins/data/public'; +import { DslQuery } from 'src/plugins/data/common'; +import { EsQueryParser } from './es_query_parser'; +import { EmsFileParser } from './ems_file_parser'; +import { UrlParser } from './url_parser'; + +interface Body { + aggs?: SearchParams['body']['aggs']; + query?: Query; + timeout?: string; +} + +interface Coordinate { + axis: { + title: string; + }; + field: string; +} + +interface Encoding { + x: Coordinate; + y: Coordinate; +} + +interface AutoSize { + type: string; + contains: string; +} + +interface Padding { + left: number; + right: number; + top: number; + bottom: number; +} + +interface Mark { + color?: string; + fill?: string; +} + +type Renderer = 'svg' | 'canvas'; + +interface VegaSpecConfig extends KibanaConfig { + kibana: KibanaConfig; + padding: Padding; + projection: Projection; + autosize: AutoSize; + tooltips: TooltipConfig; + mark: Mark; +} + +interface Projection { + name: string; +} + +interface RequestDataObject { + values: SearchResponse; +} + +interface RequestObject { + url: string; +} + +type ContextVarsObjectProps = + | string + | { + [CONSTANTS.AUTOINTERVAL]: number; + }; + +type ToolTipPositions = 'top' | 'right' | 'bottom' | 'left'; + +export interface KibanaConfig { + controlsLocation: ControlsLocation; + controlsDirection: ControlsDirection; + hideWarnings: boolean; + type: string; + renderer: Renderer; +} + +export interface VegaSpec { + [index: string]: any; + $schema: string; + data?: Data; + encoding?: Encoding; + mark?: string; + title?: string; + autosize: AutoSize; + projections: Projection[]; + width?: number; + height?: number; + padding?: number | Padding; + _hostConfig?: KibanaConfig; + config: VegaSpecConfig; +} + +export enum CONSTANTS { + TIMEFILTER = '%timefilter%', + CONTEXT = '%context%', + LEGACY_CONTEXT = '%context_query%', + TYPE = '%type%', + SYMBOL = 'Symbol(vega_id)', + AUTOINTERVAL = '%auautointerval%', +} + +export interface Opts { + [index: string]: any; + [CONSTANTS.TIMEFILTER]?: boolean; + gte?: string; + lte?: string; + format?: string; + shift?: number; + unit?: string; +} + +export type Type = 'min' | 'max'; + +export interface TimeBucket { + key_as_string: string; + key: number; + doc_count: number; + [CONSTANTS.SYMBOL]: number; +} + +export interface Bool { + [index: string]: any; + bool?: Bool; + must?: DslQuery[]; + filter?: Filter[]; + should?: never[]; + must_not?: Filter[]; +} + +export interface Query { + range?: { [x: number]: Opts }; + bool?: Bool; +} + +export interface UrlObject { + [index: string]: any; + [CONSTANTS.TIMEFILTER]?: string; + [CONSTANTS.CONTEXT]?: boolean; + [CONSTANTS.LEGACY_CONTEXT]?: string; + [CONSTANTS.TYPE]?: string; + name?: string; + index?: string; + body?: Body; + size?: number; + timeout?: string; +} + +export interface Data { + [index: string]: any; + url?: UrlObject; + values?: unknown; + source?: unknown; +} + +export interface CacheOptions { + max: number; + maxAge: number; +} + +export interface CacheBounds { + min: number; + max: number; +} + +export interface Requests extends RequestObject { + obj: RequestObject; + name: string; + dataObject: RequestDataObject; +} + +export interface ContextVarsObject { + [index: string]: any; + prop: ContextVarsObjectProps; + interval: string; +} + +export interface TooltipConfig { + position?: ToolTipPositions; + padding?: number | Padding; + centerOnMark?: boolean | number; +} + +export interface DstObj { + [index: string]: any; + type?: string; + latitude?: number; + longitude?: number; + zoom?: number; + mapStyle?: string | boolean; + minZoom?: number; + maxZoom?: number; + zoomControl?: boolean; + scrollWheelZoom?: boolean; + delayRepaint?: boolean; +} + +export type ControlsLocation = 'row' | 'column' | 'row-reverse' | 'column-reverse'; + +export type ControlsDirection = 'horizontal' | 'vertical'; + +export interface VegaConfig extends DstObj { + [index: string]: any; + maxBounds?: number; + tooltips?: TooltipConfig | boolean; + controlsLocation?: ControlsLocation; + controlsDirection?: ControlsDirection; +} + +export interface UrlParserConfig { + [index: string]: any; + elasticsearch: EsQueryParser; + emsfile: EmsFileParser; + url: UrlParser; +} + +export interface PendingType { + [index: string]: any; + dataObject?: Data; + obj?: Data; + url?: UrlObject; + name?: string; +} diff --git a/src/plugins/vis_type_vega/public/data_model/url_parser.js b/src/plugins/vis_type_vega/public/data_model/url_parser.ts similarity index 92% rename from src/plugins/vis_type_vega/public/data_model/url_parser.js rename to src/plugins/vis_type_vega/public/data_model/url_parser.ts index 9a30f12e08232..a27376bf25061 100644 --- a/src/plugins/vis_type_vega/public/data_model/url_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/url_parser.ts @@ -19,13 +19,15 @@ import $ from 'jquery'; import { i18n } from '@kbn/i18n'; +import { UrlObject } from './types'; /** * This class processes all Vega spec customizations, * converting url object parameters into query results. */ export class UrlParser { - constructor(onWarning) { + _onWarning: (...args: string[]) => void; + constructor(onWarning: (...args: string[]) => void) { this._onWarning = onWarning; } @@ -33,7 +35,7 @@ export class UrlParser { /** * Update request object */ - parseUrl(obj, urlObj) { + parseUrl(obj: UrlObject, urlObj: UrlObject) { let url = urlObj.url; if (!url) { throw new Error( diff --git a/src/plugins/vis_type_vega/public/data_model/utils.js b/src/plugins/vis_type_vega/public/data_model/utils.ts similarity index 75% rename from src/plugins/vis_type_vega/public/data_model/utils.js rename to src/plugins/vis_type_vega/public/data_model/utils.ts index 9cf5e36b81294..4d24b1237daeb 100644 --- a/src/plugins/vis_type_vega/public/data_model/utils.js +++ b/src/plugins/vis_type_vega/public/data_model/utils.ts @@ -23,13 +23,14 @@ export class Utils { /** * If the 2nd array parameter in args exists, append it to the warning/error string value */ - static formatWarningToStr(value) { - if (arguments.length >= 2) { + static formatWarningToStr(...args: any[]) { + let value = args[0]; + if (args.length >= 2) { try { - if (typeof arguments[1] === 'string') { - value += `\n${arguments[1]}`; + if (typeof args[1] === 'string') { + value += `\n${args[1]}`; } else { - value += '\n' + compactStringify(arguments[1], { maxLength: 70 }); + value += '\n' + compactStringify(args[1], { maxLength: 70 }); } } catch (err) { // ignore @@ -38,12 +39,13 @@ export class Utils { return value; } - static formatErrorToStr(error) { + static formatErrorToStr(...args: any[]) { + let error: Error | string = args[0]; if (!error) { error = 'ERR'; } else if (error instanceof Error) { error = error.message; } - return Utils.formatWarningToStr(error, ...Array.from(arguments).slice(1)); + return Utils.formatWarningToStr(error, ...Array.from(args).slice(1)); } } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts similarity index 74% rename from src/plugins/vis_type_vega/public/data_model/vega_parser.js rename to src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 377567e47ced8..17166e1540755 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -18,34 +18,78 @@ */ import _ from 'lodash'; -import { vega, vegaLite } from '../lib/vega'; import schemaParser from 'vega-schema-url-parser'; import versionCompare from 'compare-versions'; -import { EsQueryParser } from './es_query_parser'; import hjson from 'hjson'; +import { VISUALIZATION_COLORS } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { vega, vegaLite } from '../lib/vega'; +import { EsQueryParser } from './es_query_parser'; import { Utils } from './utils'; import { EmsFileParser } from './ems_file_parser'; import { UrlParser } from './url_parser'; -import { VISUALIZATION_COLORS } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; +import { SearchAPI } from './search_api'; +import { TimeCache } from './time_cache'; +import { IServiceSettings } from '../../../maps_legacy/public'; +import { + Bool, + Data, + VegaSpec, + VegaConfig, + TooltipConfig, + DstObj, + UrlParserConfig, + PendingType, + ControlsLocation, + ControlsDirection, + KibanaConfig, +} from './types'; // Set default single color to match other Kibana visualizations -const defaultColor = VISUALIZATION_COLORS[0]; -const locToDirMap = { +const defaultColor: string = VISUALIZATION_COLORS[0]; + +const locToDirMap: Record = { left: 'row-reverse', right: 'row', top: 'column-reverse', bottom: 'column', }; -const DEFAULT_SCHEMA = 'https://vega.github.io/schema/vega/v5.json'; +const DEFAULT_SCHEMA: string = 'https://vega.github.io/schema/vega/v5.json'; // If there is no "%type%" parameter, use this parser -const DEFAULT_PARSER = 'elasticsearch'; +const DEFAULT_PARSER: string = 'elasticsearch'; export class VegaParser { - constructor(spec, searchAPI, timeCache, filters, serviceSettings) { - this.spec = spec; + spec: VegaSpec; + hideWarnings: boolean; + error?: string; + warnings: string[]; + _urlParsers: UrlParserConfig; + isVegaLite?: boolean; + useHover?: boolean; + _config?: VegaConfig; + useMap?: boolean; + renderer?: string; + tooltips?: boolean | TooltipConfig; + mapConfig?: object; + vlspec?: VegaSpec; + useResize?: boolean; + paddingWidth?: number; + paddingHeight?: number; + containerDir?: ControlsLocation | ControlsDirection; + controlsDir?: ControlsLocation; + + constructor( + spec: VegaSpec | string, + searchAPI: SearchAPI, + timeCache: TimeCache, + filters: Bool, + serviceSettings: IServiceSettings + ) { + this.spec = spec as VegaSpec; this.hideWarnings = false; + this.error = undefined; this.warnings = []; @@ -90,10 +134,10 @@ export class VegaParser { this.tooltips = this._parseTooltips(); this._setDefaultColors(); - this._parseControlPlacement(this._config); + this._parseControlPlacement(); if (this.useMap) { this.mapConfig = this._parseMapConfig(); - } else if (this.spec.autosize === undefined) { + } else if (this.spec && this.spec.autosize === undefined) { // Default autosize should be fit, unless it's a map (leaflet-vega handles that) this.spec.autosize = { type: 'fit', contains: 'padding' }; } @@ -123,6 +167,7 @@ export class VegaParser { // This way we let leaflet-vega library inject a different default projection for tile maps. // Also, VL injects default padding and autosize values, but neither should be set for vega-leaflet. if (this.useMap) { + if (!this.spec || !this.vlspec) return; const hasConfig = _.isPlainObject(this.vlspec.config); if (this.vlspec.config === undefined || (hasConfig && !this.vlspec.config.projection)) { // Assume VL generates spec.projections = an array of exactly one object named 'projection' @@ -168,49 +213,52 @@ export class VegaParser { */ _calcSizing() { this.useResize = false; - if (!this.useMap) { - // when useResize is true, vega's canvas size will be set based on the size of the container, - // and will be automatically updated on resize events. - // We delete width & height if the autosize is set to "fit" - // We also set useResize=true in case autosize=none, and width & height are not set - const autosize = this.spec.autosize.type || this.spec.autosize; - if (autosize === 'fit' || (autosize === 'none' && !this.spec.width && !this.spec.height)) { - this.useResize = true; - } - } // Padding is not included in the width/height by default this.paddingWidth = 0; this.paddingHeight = 0; - if (this.useResize && this.spec.padding && this.spec.autosize.contains !== 'padding') { - if (typeof this.spec.padding === 'object') { - this.paddingWidth += (+this.spec.padding.left || 0) + (+this.spec.padding.right || 0); - this.paddingHeight += (+this.spec.padding.top || 0) + (+this.spec.padding.bottom || 0); - } else { - this.paddingWidth += 2 * (+this.spec.padding || 0); - this.paddingHeight += 2 * (+this.spec.padding || 0); + if (this.spec) { + if (!this.useMap) { + // when useResize is true, vega's canvas size will be set based on the size of the container, + // and will be automatically updated on resize events. + // We delete width & height if the autosize is set to "fit" + // We also set useResize=true in case autosize=none, and width & height are not set + const autosize = this.spec.autosize.type || this.spec.autosize; + if (autosize === 'fit' || (autosize === 'none' && !this.spec.width && !this.spec.height)) { + this.useResize = true; + } } - } - if (this.useResize && (this.spec.width || this.spec.height)) { - if (this.isVegaLite) { - delete this.spec.width; - delete this.spec.height; - } else { - this._onWarning( - i18n.translate( - 'visTypeVega.vegaParser.widthAndHeightParamsAreIgnoredWithAutosizeFitWarningMessage', - { - defaultMessage: - 'The {widthParam} and {heightParam} params are ignored with {autosizeParam}', - values: { - autosizeParam: 'autosize=fit', - widthParam: '"width"', - heightParam: '"height"', - }, - } - ) - ); + if (this.useResize && this.spec.padding && this.spec.autosize.contains !== 'padding') { + if (typeof this.spec.padding === 'object') { + this.paddingWidth += (+this.spec.padding.left || 0) + (+this.spec.padding.right || 0); + this.paddingHeight += (+this.spec.padding.top || 0) + (+this.spec.padding.bottom || 0); + } else { + this.paddingWidth += 2 * (+this.spec.padding || 0); + this.paddingHeight += 2 * (+this.spec.padding || 0); + } + } + + if (this.useResize && (this.spec.width || this.spec.height)) { + if (this.isVegaLite) { + delete this.spec.width; + delete this.spec.height; + } else { + this._onWarning( + i18n.translate( + 'visTypeVega.vegaParser.widthAndHeightParamsAreIgnoredWithAutosizeFitWarningMessage', + { + defaultMessage: + 'The {widthParam} and {heightParam} params are ignored with {autosizeParam}', + values: { + autosizeParam: 'autosize=fit', + widthParam: '"width"', + heightParam: '"height"', + }, + } + ) + ); + } } } } @@ -220,9 +268,11 @@ export class VegaParser { * @private */ _parseControlPlacement() { - this.containerDir = locToDirMap[this._config.controlsLocation]; + this.containerDir = this._config?.controlsLocation + ? locToDirMap[this._config.controlsLocation] + : undefined; if (this.containerDir === undefined) { - if (this._config.controlsLocation === undefined) { + if (this._config && this._config.controlsLocation === undefined) { this.containerDir = 'column'; } else { throw new Error( @@ -230,14 +280,14 @@ export class VegaParser { defaultMessage: 'Unrecognized {controlsLocationParam} value. Expecting one of [{locToDirMap}]', values: { - locToDirMap: `"${locToDirMap.keys().join('", "')}"`, + locToDirMap: `"${Object.keys(locToDirMap).join('", "')}"`, controlsLocationParam: 'controlsLocation', }, }) ); } } - const dir = this._config.controlsDirection; + const dir = this._config?.controlsDirection; if (dir !== undefined && dir !== 'horizontal' && dir !== 'vertical') { throw new Error( i18n.translate('visTypeVega.vegaParser.unrecognizedDirValueErrorMessage', { @@ -254,51 +304,53 @@ export class VegaParser { * @returns {object} kibana config * @private */ - _parseConfig() { - let result; - if (this.spec._hostConfig !== undefined) { - result = this.spec._hostConfig; - delete this.spec._hostConfig; - if (!_.isPlainObject(result)) { - throw new Error( - i18n.translate('visTypeVega.vegaParser.hostConfigValueTypeErrorMessage', { - defaultMessage: 'If present, {configName} must be an object', - values: { configName: '"_hostConfig"' }, + _parseConfig(): KibanaConfig | {} { + let result: KibanaConfig | null = null; + if (this.spec) { + if (this.spec._hostConfig !== undefined) { + result = this.spec._hostConfig; + delete this.spec._hostConfig; + if (!_.isPlainObject(result)) { + throw new Error( + i18n.translate('visTypeVega.vegaParser.hostConfigValueTypeErrorMessage', { + defaultMessage: 'If present, {configName} must be an object', + values: { configName: '"_hostConfig"' }, + }) + ); + } + this._onWarning( + i18n.translate('visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage', { + defaultMessage: + '{deprecatedConfigName} has been deprecated. Use {newConfigName} instead.', + values: { + deprecatedConfigName: '"_hostConfig"', + newConfigName: 'config.kibana', + }, }) ); } - this._onWarning( - i18n.translate('visTypeVega.vegaParser.hostConfigIsDeprecatedWarningMessage', { - defaultMessage: - '{deprecatedConfigName} has been deprecated. Use {newConfigName} instead.', - values: { - deprecatedConfigName: '"_hostConfig"', - newConfigName: 'config.kibana', - }, - }) - ); - } - if (_.isPlainObject(this.spec.config) && this.spec.config.kibana !== undefined) { - result = this.spec.config.kibana; - delete this.spec.config.kibana; - if (!_.isPlainObject(result)) { - throw new Error( - i18n.translate('visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage', { - defaultMessage: 'If present, {configName} must be an object', - values: { configName: 'config.kibana' }, - }) - ); + if (_.isPlainObject(this.spec.config) && this.spec.config.kibana !== undefined) { + result = this.spec.config.kibana; + delete this.spec.config.kibana; + if (!_.isPlainObject(result)) { + throw new Error( + i18n.translate('visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage', { + defaultMessage: 'If present, {configName} must be an object', + values: { configName: 'config.kibana' }, + }) + ); + } } } return result || {}; } _parseTooltips() { - if (this._config.tooltips === false) { + if (this._config && this._config.tooltips === false) { return false; } - const result = this._config.tooltips || {}; + const result: TooltipConfig = (this._config?.tooltips as TooltipConfig) || {}; if (result.position === undefined) { result.position = 'top'; @@ -352,12 +404,12 @@ export class VegaParser { * @private */ _parseMapConfig() { - const res = { - delayRepaint: this._config.delayRepaint === undefined ? true : this._config.delayRepaint, + const res: VegaConfig = { + delayRepaint: this._config?.delayRepaint === undefined ? true : this._config.delayRepaint, }; - const validate = (name, isZoom) => { - const val = this._config[name]; + const validate = (name: string, isZoom: boolean) => { + const val = this._config ? this._config[name] : undefined; if (val !== undefined) { const parsed = parseFloat(val); if (Number.isFinite(parsed) && (!isZoom || (parsed >= 0 && parsed <= 30))) { @@ -381,7 +433,7 @@ export class VegaParser { validate(`maxZoom`, true); // `false` is a valid value - res.mapStyle = this._config.mapStyle === undefined ? `default` : this._config.mapStyle; + res.mapStyle = this._config?.mapStyle === undefined ? `default` : this._config.mapStyle; if (res.mapStyle !== `default` && res.mapStyle !== false) { this._onWarning( i18n.translate('visTypeVega.vegaParser.mapStyleValueTypeWarningMessage', { @@ -400,7 +452,7 @@ export class VegaParser { this._parseBool('zoomControl', res, true); this._parseBool('scrollWheelZoom', res, false); - const maxBounds = this._config.maxBounds; + const maxBounds = this._config?.maxBounds; if (maxBounds !== undefined) { if ( !Array.isArray(maxBounds) || @@ -423,8 +475,8 @@ export class VegaParser { return res; } - _parseBool(paramName, dstObj, dflt) { - const val = this._config[paramName]; + _parseBool(paramName: string, dstObj: DstObj, dflt: boolean | string | number) { + const val = this._config ? this._config[paramName] : undefined; if (val === undefined) { dstObj[paramName] = dflt; } else if (typeof val !== 'boolean') { @@ -448,6 +500,7 @@ export class VegaParser { * @private */ _parseSchema() { + if (!this.spec) return false; if (!this.spec.$schema) { this._onWarning( i18n.translate('visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaWarningMessage', { @@ -486,13 +539,13 @@ export class VegaParser { * @private */ async _resolveDataUrls() { - const pending = {}; + const pending: PendingType = {}; - this._findObjectDataUrls(this.spec, (obj) => { + this._findObjectDataUrls(this.spec!, (obj: Data) => { const url = obj.url; delete obj.url; - let type = url['%type%']; - delete url['%type%']; + let type = url!['%type%']; + delete url!['%type%']; if (type === undefined) { type = DEFAULT_PARSER; } @@ -533,7 +586,8 @@ export class VegaParser { * @param {string} [key] field name of the current object * @private */ - _findObjectDataUrls(obj, onFind, key) { + + _findObjectDataUrls(obj: VegaSpec | Data, onFind: (data: Data) => void, key?: unknown) { if (Array.isArray(obj)) { for (const elem of obj) { this._findObjectDataUrls(elem, onFind, key); @@ -557,7 +611,7 @@ export class VegaParser { ) ); } - onFind(obj); + onFind(obj as Data); } else { for (const k of Object.keys(obj)) { this._findObjectDataUrls(obj[k], onFind, k); @@ -582,7 +636,7 @@ export class VegaParser { // https://github.com/vega/vega/issues/1083 // Don't set defaults if spec.config.mark.color or fill are set if ( - !this.spec.config.mark || + !this.spec?.config.mark || (this.spec.config.mark.color === undefined && this.spec.config.mark.fill === undefined) ) { this._setDefaultValue(defaultColor, 'config', 'arc', 'fill'); @@ -605,7 +659,7 @@ export class VegaParser { * @param {string} fields * @private */ - _setDefaultValue(value, ...fields) { + _setDefaultValue(value: unknown, ...fields: string[]) { let o = this.spec; for (let i = 0; i < fields.length - 1; i++) { const field = fields[i]; @@ -627,9 +681,10 @@ export class VegaParser { * Add a warning to the warnings array * @private */ - _onWarning() { + _onWarning(...args: any[]) { if (!this.hideWarnings) { - this.warnings.push(Utils.formatWarningToStr(...arguments)); + this.warnings.push(Utils.formatWarningToStr(args)); + return Utils.formatWarningToStr(args); } } } diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index 6b1af6044a2c4..d077aa7aee004 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -23,6 +23,7 @@ import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expre import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; import { TimeRange, Query } from '../../data/public'; +import { VegaParser } from './data_model/vega_parser'; type Input = KibanaContext | null; type Output = Promise>; @@ -34,7 +35,7 @@ interface Arguments { export type VisParams = Required; interface RenderValue { - visData: Input; + visData: VegaParser; visType: 'vega'; visConfig: VisParams; } diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index ac28f0b3782b2..997b1982d749a 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -20,8 +20,6 @@ import { Filter, esQuery, TimeRange, Query } from '../../data/public'; import { SearchAPI } from './data_model/search_api'; - -// @ts-ignore import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; @@ -64,7 +62,6 @@ export function createVegaRequestHandler( const esQueryConfigs = esQuery.getEsQueryConfig(uiSettings); const filtersDsl = esQuery.buildEsQuery(undefined, query, filters, esQueryConfigs); - // @ts-ignore const { VegaParser } = await import('./data_model/vega_parser'); const vp = new VegaParser(visParams.spec, searchAPI, timeCache, filtersDsl, serviceSettings); diff --git a/yarn.lock b/yarn.lock index 11212c385de90..9dde840d18326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5245,6 +5245,11 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.3.tgz#856c99cdc1551d22c22b18b5402719affec9839a" integrity sha512-cS5owqtwzLN5kY+l+KgKdRJ/Cee8tlmQoGQuIE9tWnSmS3JMKzmxo2HIAk2wODMifGwO20d62xZQLYz+RLfXmw== +"@types/hjson@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/hjson/-/hjson-2.4.2.tgz#fd0288a5b6778cda993c978e43cc978ddc8f22e9" + integrity sha512-MSKTfEyR8DbzJTOAY47BIJBD72ol4cu6BOw5inda0q1eEtEmurVHL4OmYB3Lxa4/DwXbWidkddvtoygbGQEDIw== + "@types/hoek@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@types/hoek/-/hoek-4.1.3.tgz#d1982d48fb0d2a0e5d7e9d91838264d8e428d337"