From e754fb7691fdfd2d7cf92ebf69310184eb730bff Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 6 Oct 2020 14:43:19 +0200 Subject: [PATCH] Improve visualization typings (#79128) (#79636) * Improve visualization typings * Fix vis selection dialog * Fix broken getInfoMessage type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/tile_map_options.tsx | 2 +- src/plugins/tile_map/public/tile_map_type.js | 1 - .../public/components/sidebar/sidebar.tsx | 4 +- src/plugins/vis_type_metric/public/to_ast.ts | 2 +- .../public/table_vis_controller.test.ts | 4 +- .../vis_type_table/public/table_vis_type.ts | 4 +- .../vis_type_table/public/vis_controller.ts | 4 +- .../vis_type_tagcloud/public/to_ast.ts | 2 +- src/plugins/vis_type_vega/public/vega_type.ts | 5 +- .../public/components/visualization_chart.tsx | 6 + .../visualizations/public/expressions/vis.ts | 6 +- .../public/legacy/build_pipeline.ts | 12 +- src/plugins/visualizations/public/vis.test.ts | 2 +- src/plugins/visualizations/public/vis.ts | 14 +- .../public/vis_types/base_vis_type.ts | 159 ++++--- .../visualizations/public/vis_types/index.ts | 1 + .../public/vis_types/react_vis_type.ts | 12 +- .../visualizations/public/vis_types/types.ts | 80 ++++ .../public/vis_types/types_service.ts | 61 +-- .../__snapshots__/new_vis_modal.test.tsx.snap | 404 +++++++++++++++++- .../public/wizard/new_vis_modal.test.tsx | 41 +- .../type_selection/new_vis_help.test.tsx | 1 - .../wizard/type_selection/new_vis_help.tsx | 3 +- .../wizard/type_selection/type_selection.tsx | 60 +-- .../components/visualize_editor_common.tsx | 2 +- 25 files changed, 696 insertions(+), 196 deletions(-) create mode 100644 src/plugins/visualizations/public/vis_types/types.ts diff --git a/src/plugins/tile_map/public/components/tile_map_options.tsx b/src/plugins/tile_map/public/components/tile_map_options.tsx index f7fb4daff63f01..1a7b11ccf6e208 100644 --- a/src/plugins/tile_map/public/components/tile_map_options.tsx +++ b/src/plugins/tile_map/public/components/tile_map_options.tsx @@ -76,7 +76,7 @@ function TileMapOptions(props: TileMapOptionsProps) { s.name); const metricAggs = useMemo( - () => responseAggs.filter((agg) => metricSchemas.includes(get(agg, 'schema'))), + () => responseAggs.filter((agg) => agg.schema && metricSchemas.includes(agg.schema)), [responseAggs, metricSchemas] ); const hasHistogramAgg = useMemo(() => responseAggs.some((agg) => agg.type.name === 'histogram'), [ diff --git a/src/plugins/vis_type_metric/public/to_ast.ts b/src/plugins/vis_type_metric/public/to_ast.ts index 7eefd8328ab767..23e4664b82414f 100644 --- a/src/plugins/vis_type_metric/public/to_ast.ts +++ b/src/plugins/vis_type_metric/public/to_ast.ts @@ -39,7 +39,7 @@ export const toExpressionAst = (vis: Vis, params: any) => { const esaggs = buildExpressionFunction('esaggs', { index: vis.data.indexPattern!.id!, metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.type.requiresPartialRows || vis.params.showPartialRows || false, + partialRows: vis.params.showPartialRows || false, aggConfigs: JSON.stringify(vis.data.aggs!.aggs), includeFormatHints: false, }); diff --git a/src/plugins/vis_type_table/public/table_vis_controller.test.ts b/src/plugins/vis_type_table/public/table_vis_controller.test.ts index 2b4017ae0ee817..035ca044137e90 100644 --- a/src/plugins/vis_type_table/public/table_vis_controller.test.ts +++ b/src/plugins/vis_type_table/public/table_vis_controller.test.ts @@ -249,13 +249,13 @@ describe('Table Vis - Controller', () => { const vis = getRangeVis({ showPartialRows: true }); initController(vis); - expect(vis.type.hierarchicalData(vis)).toEqual(true); + expect((vis.type.hierarchicalData as Function)(vis)).toEqual(true); }); test('passes partialRows:false to tabify based on the vis params', () => { const vis = getRangeVis({ showPartialRows: false }); initController(vis); - expect(vis.type.hierarchicalData(vis)).toEqual(false); + expect((vis.type.hierarchicalData as Function)(vis)).toEqual(false); }); }); diff --git a/src/plugins/vis_type_table/public/table_vis_type.ts b/src/plugins/vis_type_table/public/table_vis_type.ts index c1419a48474580..95f4f06ee6111c 100644 --- a/src/plugins/vis_type_table/public/table_vis_type.ts +++ b/src/plugins/vis_type_table/public/table_vis_type.ts @@ -20,7 +20,7 @@ import { CoreSetup, PluginInitializerContext } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { AggGroupNames } from '../../data/public'; import { Schemas } from '../../vis_default_editor/public'; -import { BaseVisTypeOptions, Vis } from '../../visualizations/public'; +import { BaseVisTypeOptions } from '../../visualizations/public'; import { tableVisResponseHandler } from './table_vis_response_handler'; // @ts-ignore import tableVisTemplate from './table_vis.html'; @@ -99,7 +99,7 @@ export function getTableVisTypeDefinition( ]), }, responseHandler: tableVisResponseHandler, - hierarchicalData: (vis: Vis) => { + hierarchicalData: (vis) => { return Boolean(vis.params.showPartialRows || vis.params.showMetricsAtAllLevels); }, }; diff --git a/src/plugins/vis_type_table/public/vis_controller.ts b/src/plugins/vis_type_table/public/vis_controller.ts index 5e82796e663393..1781808660260e 100644 --- a/src/plugins/vis_type_table/public/vis_controller.ts +++ b/src/plugins/vis_type_table/public/vis_controller.ts @@ -103,7 +103,9 @@ export function getTableVisualizationControllerClass( this.$scope = this.$rootScope.$new(); this.$scope.uiState = this.vis.getUiState(); updateScope(); - this.el.find('div').append(this.$compile(this.vis.type!.visConfig.template)(this.$scope)); + this.el + .find('div') + .append(this.$compile(this.vis.type.visConfig?.template ?? '')(this.$scope)); this.$scope.$apply(); } else { updateScope(); diff --git a/src/plugins/vis_type_tagcloud/public/to_ast.ts b/src/plugins/vis_type_tagcloud/public/to_ast.ts index a284bba3073487..876784cc101405 100644 --- a/src/plugins/vis_type_tagcloud/public/to_ast.ts +++ b/src/plugins/vis_type_tagcloud/public/to_ast.ts @@ -38,7 +38,7 @@ export const toExpressionAst = (vis: Vis, params: BuildPipeli const esaggs = buildExpressionFunction('esaggs', { index: vis.data.indexPattern!.id!, metricsAtAllLevels: vis.isHierarchical(), - partialRows: vis.type.requiresPartialRows || false, + partialRows: false, aggConfigs: JSON.stringify(vis.data.aggs!.aggs), includeFormatHints: false, }); diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index a9651c1f5eb33c..0496f765e5e99b 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { BaseVisTypeOptions } from 'src/plugins/visualizations/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; @@ -31,7 +32,9 @@ import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; import { getInfoMessage } from './components/experimental_map_vis_info'; -export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { +export const createVegaTypeDefinition = ( + dependencies: VegaVisualizationDependencies +): BaseVisTypeOptions => { const requestHandler = createVegaRequestHandler(dependencies); const visualization = createVegaVisualization(dependencies); diff --git a/src/plugins/visualizations/public/components/visualization_chart.tsx b/src/plugins/visualizations/public/components/visualization_chart.tsx index fba0afa89ca539..b8a0412db0a4d9 100644 --- a/src/plugins/visualizations/public/components/visualization_chart.tsx +++ b/src/plugins/visualizations/public/components/visualization_chart.tsx @@ -90,6 +90,12 @@ class VisualizationChart extends React.Component { const { vis } = this.props; const Visualization = vis.type.visualization; + if (!Visualization) { + throw new Error( + 'Tried to use VisualizationChart component with a vis without visualization property.' + ); + } + this.visualization = new Visualization(this.chartDiv.current, vis); // We know that containerDiv.current will never be null, since we will always diff --git a/src/plugins/visualizations/public/expressions/vis.ts b/src/plugins/visualizations/public/expressions/vis.ts index 5a99dceda20bd8..87f77e589c9caa 100644 --- a/src/plugins/visualizations/public/expressions/vis.ts +++ b/src/plugins/visualizations/public/expressions/vis.ts @@ -36,7 +36,7 @@ import { VisType } from '../vis_types'; export interface ExprVisState { title?: string; - type: VisType | string; + type: VisType | string; params?: VisParams; } @@ -52,7 +52,7 @@ export interface ExprVisAPI { export class ExprVis extends EventEmitter { public title: string = ''; - public type: VisType; + public type: VisType; public params: VisParams = {}; public sessionState: Record = {}; public API: ExprVisAPI; @@ -92,7 +92,7 @@ export class ExprVis extends EventEmitter { }; } - private getType(type: string | VisType) { + private getType(type: string | VisType) { if (_.isString(type)) { const newType = getTypes().get(type); if (!newType) { diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 79e1c1cca2155a..9f6a4d55532928 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -86,7 +86,10 @@ const vislibCharts: string[] = [ 'line', ]; -export const getSchemas = (vis: Vis, { timeRange, timefilter }: BuildPipelineParams): Schemas => { +export const getSchemas = ( + vis: Vis, + { timeRange, timefilter }: BuildPipelineParams +): Schemas => { const createSchemaConfig = (accessor: number, agg: IAggConfig): SchemaConfig => { if (isDateHistogramBucketAggConfig(agg)) { agg.params.timeRange = timeRange; @@ -155,7 +158,8 @@ export const getSchemas = (vis: Vis, { timeRange, timefilter }: BuildPipelinePar } } if (schemaName === 'split') { - schemaName = `split_${vis.params.row ? 'row' : 'column'}`; + // TODO: We should check if there's a better way then casting to `any` here + schemaName = `split_${(vis.params as any).row ? 'row' : 'column'}`; skipMetrics = responseAggs.length - metrics.length > 1; } if (!schemas[schemaName]) { @@ -410,7 +414,7 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { pipeline += `esaggs ${prepareString('index', indexPattern!.id)} metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} + partialRows=${vis.params.showPartialRows || false} ${prepareJson('aggConfigs', vis.data.aggs!.aggs)} | `; } @@ -433,7 +437,7 @@ export const buildPipeline = async (vis: Vis, params: BuildPipelineParams) => { pipeline += `visualization type='${vis.type.name}' ${prepareJson('visConfig', visConfig)} metricsAtAllLevels=${vis.isHierarchical()} - partialRows=${vis.type.requiresPartialRows || vis.params.showPartialRows || false} `; + partialRows=${vis.params.showPartialRows || false} `; if (indexPattern) { pipeline += `${prepareString('index', indexPattern.id)} `; if (vis.data.aggs) { diff --git a/src/plugins/visualizations/public/vis.test.ts b/src/plugins/visualizations/public/vis.test.ts index c271888b7c7a4b..e1b188f2e460bd 100644 --- a/src/plugins/visualizations/public/vis.test.ts +++ b/src/plugins/visualizations/public/vis.test.ts @@ -121,7 +121,7 @@ describe('Vis Class', function () { }); it('should return true for hierarchical vis (like pie)', function () { - vis.type.hierarchicalData = true; + (vis.type as any).hierarchicalData = true; expect(vis.isHierarchical()).toBe(true); }); }); diff --git a/src/plugins/visualizations/public/vis.ts b/src/plugins/visualizations/public/vis.ts index c6773e5a1bee3a..5c3233a8de896d 100644 --- a/src/plugins/visualizations/public/vis.ts +++ b/src/plugins/visualizations/public/vis.ts @@ -84,7 +84,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?: type PartialVisState = Assign }>; export class Vis { - public readonly type: VisType; + public readonly type: VisType; public readonly id?: string; public title: string = ''; public description: string = ''; @@ -97,14 +97,14 @@ export class Vis { public readonly uiState: PersistedState; constructor(visType: string, visState: SerializedVis = {} as any) { - this.type = this.getType(visType); + this.type = this.getType(visType); this.params = this.getParams(visState.params); this.uiState = new PersistedState(visState.uiState); this.id = visState.id; } - private getType(visType: string) { - const type = getTypes().get(visType); + private getType(visType: string) { + const type = getTypes().get(visType); if (!type) { const errorMessage = i18n.translate('visualizations.visualizationTypeInvalidMessage', { defaultMessage: 'Invalid visualization type "{visType}"', @@ -118,7 +118,7 @@ export class Vis { } private getParams(params: VisParams) { - return defaults({}, cloneDeep(params || {}), cloneDeep(this.type.visConfig.defaults || {})); + return defaults({}, cloneDeep(params ?? {}), cloneDeep(this.type.visConfig?.defaults ?? {})); } async setState(state: PartialVisState) { @@ -202,10 +202,6 @@ export class Vis { }; } - toExpressionAst() { - return this.type.toExpressionAst(this.params); - } - // deprecated isHierarchical() { if (isFunction(this.type.hierarchicalData)) { diff --git a/src/plugins/visualizations/public/vis_types/base_vis_type.ts b/src/plugins/visualizations/public/vis_types/base_vis_type.ts index 4763bc9de9d273..f2933de723a393 100644 --- a/src/plugins/visualizations/public/vis_types/base_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/base_vis_type.ts @@ -17,118 +17,113 @@ * under the License. */ -import _ from 'lodash'; -import { ReactElement } from 'react'; -import { VisParams, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; -import { TriggerContextMapping } from '../../../ui_actions/public'; -import { Adapters } from '../../../inspector/public'; -import { Vis } from '../vis'; +import { defaultsDeep } from 'lodash'; +import { ISchemas } from 'src/plugins/vis_default_editor/public'; +import { VisParams } from '../types'; +import { VisType, VisTypeOptions } from './types'; -interface CommonBaseVisTypeOptions { - name: string; - title: string; - description?: string; - getSupportedTriggers?: () => Array; - icon?: string; - image?: string; - stage?: 'experimental' | 'beta' | 'production'; - options?: Record; - visConfig?: Record; - editor?: any; - editorConfig?: Record; - hidden?: boolean; - requestHandler?: string | unknown; - responseHandler?: string | unknown; - hierarchicalData?: boolean | unknown; - setup?: unknown; - useCustomNoDataScreen?: boolean; - inspectorAdapters?: Adapters | (() => Adapters); - isDeprecated?: boolean; - getInfoMessage?: (vis: Vis) => ReactElement<{}> | null; +interface CommonBaseVisTypeOptions + extends Pick< + VisType, + | 'description' + | 'editor' + | 'getInfoMessage' + | 'getSupportedTriggers' + | 'hierarchicalData' + | 'icon' + | 'image' + | 'inspectorAdapters' + | 'name' + | 'requestHandler' + | 'responseHandler' + | 'setup' + | 'title' + >, + Pick< + Partial>, + 'editorConfig' | 'hidden' | 'stage' | 'useCustomNoDataScreen' | 'visConfig' + > { + options?: Partial['options']>; } -interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { - toExpressionAst: VisToExpressionAst; +interface ExpressionBaseVisTypeOptions extends CommonBaseVisTypeOptions { + toExpressionAst: VisType['toExpressionAst']; visualization?: undefined; } -interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { +interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions { toExpressionAst?: undefined; - visualization: VisualizationControllerConstructor | undefined; + visualization: VisType['visualization']; } export type BaseVisTypeOptions = | ExpressionBaseVisTypeOptions - | VisualizationBaseVisTypeOptions; + | VisualizationBaseVisTypeOptions; -export class BaseVisType { - name: string; - title: string; - description: string; - getSupportedTriggers?: () => Array; - icon?: string; - image?: string; - stage: 'experimental' | 'beta' | 'production'; - isExperimental: boolean; - options: Record; - visualization: VisualizationControllerConstructor | undefined; - visConfig: Record; - editor: any; - editorConfig: Record; - hidden: boolean; - requiresSearch: boolean; - requestHandler: string | unknown; - responseHandler: string | unknown; - hierarchicalData: boolean | unknown; - setup?: unknown; - useCustomNoDataScreen: boolean; - inspectorAdapters?: Adapters | (() => Adapters); - toExpressionAst?: VisToExpressionAst; - getInfoMessage?: (vis: Vis) => ReactElement<{}> | null; +const defaultOptions: VisTypeOptions = { + showTimePicker: true, + showQueryBar: true, + showFilterBar: true, + showIndexSelection: true, + hierarchicalData: false, // we should get rid of this i guess ? +}; + +export class BaseVisType implements VisType { + public readonly name; + public readonly title; + public readonly description; + public readonly getSupportedTriggers; + public readonly icon; + public readonly image; + public readonly stage; + public readonly options; + public readonly visualization; + public readonly visConfig; + public readonly editor; + public readonly editorConfig; + public hidden; + public readonly requestHandler; + public readonly responseHandler; + public readonly hierarchicalData; + public readonly setup; + public readonly useCustomNoDataScreen; + public readonly inspectorAdapters; + public readonly toExpressionAst; + public readonly getInfoMessage; constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { throw new Error('vis_type must define its icon or image'); } - const defaultOptions = { - // controls the visualize editor - showTimePicker: true, - showQueryBar: true, - showFilterBar: true, - showIndexSelection: true, - hierarchicalData: false, // we should get rid of this i guess ? - }; - this.name = opts.name; - this.description = opts.description || ''; + this.description = opts.description ?? ''; this.getSupportedTriggers = opts.getSupportedTriggers; this.title = opts.title; this.icon = opts.icon; this.image = opts.image; this.visualization = opts.visualization; - this.visConfig = _.defaultsDeep({}, opts.visConfig, { defaults: {} }); + this.visConfig = defaultsDeep({}, opts.visConfig, { defaults: {} }); this.editor = opts.editor; - this.editorConfig = _.defaultsDeep({}, opts.editorConfig, { collections: {} }); - this.options = _.defaultsDeep({}, opts.options, defaultOptions); - this.stage = opts.stage || 'production'; - this.isExperimental = opts.stage === 'experimental'; - this.hidden = opts.hidden || false; - this.requestHandler = opts.requestHandler || 'courier'; - this.responseHandler = opts.responseHandler || 'none'; + this.editorConfig = defaultsDeep({}, opts.editorConfig, { collections: {} }); + this.options = defaultsDeep({}, opts.options, defaultOptions); + this.stage = opts.stage ?? 'production'; + this.hidden = opts.hidden ?? false; + this.requestHandler = opts.requestHandler ?? 'courier'; + this.responseHandler = opts.responseHandler ?? 'none'; this.setup = opts.setup; - this.requiresSearch = this.requestHandler !== 'none'; - this.hierarchicalData = opts.hierarchicalData || false; - this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false; + this.hierarchicalData = opts.hierarchicalData ?? false; + this.useCustomNoDataScreen = opts.useCustomNoDataScreen ?? false; this.inspectorAdapters = opts.inspectorAdapters; this.toExpressionAst = opts.toExpressionAst; this.getInfoMessage = opts.getInfoMessage; } - public get schemas() { - if (this.editorConfig && this.editorConfig.schemas) { - return this.editorConfig.schemas; - } - return []; + public get schemas(): ISchemas { + return this.editorConfig?.schemas ?? []; + } + + public get requiresSearch(): boolean { + return this.requestHandler !== 'none'; } } diff --git a/src/plugins/visualizations/public/vis_types/index.ts b/src/plugins/visualizations/public/vis_types/index.ts index 8f38e335691620..22561decabea48 100644 --- a/src/plugins/visualizations/public/vis_types/index.ts +++ b/src/plugins/visualizations/public/vis_types/index.ts @@ -18,5 +18,6 @@ */ export * from './types_service'; +export { VisType } from './types'; export type { BaseVisTypeOptions } from './base_vis_type'; export type { ReactVisTypeOptions } from './react_vis_type'; diff --git a/src/plugins/visualizations/public/vis_types/react_vis_type.ts b/src/plugins/visualizations/public/vis_types/react_vis_type.ts index 047d36d8041116..f6bd51df266955 100644 --- a/src/plugins/visualizations/public/vis_types/react_vis_type.ts +++ b/src/plugins/visualizations/public/vis_types/react_vis_type.ts @@ -19,15 +19,21 @@ import { BaseVisType, BaseVisTypeOptions } from './base_vis_type'; import { ReactVisController } from './react_vis_controller'; +import { VisType } from './types'; -export type ReactVisTypeOptions = Omit; +export type ReactVisTypeOptions = Omit< + BaseVisTypeOptions, + 'visualization' | 'toExpressionAst' +>; /** * This class should only be used for visualizations not using the `toExpressionAst` with a custom renderer. * If you implement a custom renderer you should just mount a react component inside this. */ -export class ReactVisType extends BaseVisType { - constructor(opts: ReactVisTypeOptions) { +export class ReactVisType + extends BaseVisType + implements VisType { + constructor(opts: ReactVisTypeOptions) { super({ ...opts, visualization: ReactVisController, diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts new file mode 100644 index 00000000000000..0cf345bf07be66 --- /dev/null +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -0,0 +1,80 @@ +/* + * 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 { IconType } from '@elastic/eui'; +import React from 'react'; +import { Adapters } from 'src/plugins/inspector'; +import { ISchemas } from 'src/plugins/vis_default_editor/public'; +import { TriggerContextMapping } from '../../../ui_actions/public'; +import { Vis, VisToExpressionAst, VisualizationControllerConstructor } from '../types'; + +export interface VisTypeOptions { + showTimePicker: boolean; + showQueryBar: boolean; + showFilterBar: boolean; + showIndexSelection: boolean; + hierarchicalData: boolean; +} + +/** + * A visualization type representing one specific type of "classical" + * visualizations (i.e. not Lens visualizations). + */ +export interface VisType { + readonly name: string; + readonly title: string; + readonly description?: string; + readonly getSupportedTriggers?: () => Array; + readonly isAccessible?: boolean; + readonly requestHandler?: string | unknown; + readonly responseHandler?: string | unknown; + readonly icon?: IconType; + readonly image?: string; + readonly stage: 'experimental' | 'beta' | 'production'; + readonly requiresSearch: boolean; + readonly useCustomNoDataScreen: boolean; + readonly hierarchicalData?: boolean | ((vis: { params: TVisParams }) => boolean); + readonly inspectorAdapters?: Adapters | (() => Adapters); + /** + * When specified this visualization is deprecated. This function + * should return a ReactElement that will render a deprecation warning. + * It will be shown in the editor when editing/creating visualizations + * of this type. + */ + readonly getInfoMessage?: (vis: Vis) => React.ReactNode; + + readonly toExpressionAst?: VisToExpressionAst; + readonly visualization?: VisualizationControllerConstructor; + + readonly setup?: (vis: Vis) => Promise>; + hidden: boolean; + + readonly schemas: ISchemas; + + readonly options: VisTypeOptions; + + // TODO: The following types still need to be refined properly. + + /** + * The editor that should be used to edit visualizations of this type. + */ + readonly editor?: any; + readonly editorConfig: Record; + readonly visConfig: Record; +} diff --git a/src/plugins/visualizations/public/vis_types/types_service.ts b/src/plugins/visualizations/public/vis_types/types_service.ts index 1afbd6901a1959..5d619064c240e9 100644 --- a/src/plugins/visualizations/public/vis_types/types_service.ts +++ b/src/plugins/visualizations/public/vis_types/types_service.ts @@ -17,33 +17,10 @@ * under the License. */ -import { IconType } from '@elastic/eui'; import { visTypeAliasRegistry, VisTypeAlias } from './vis_type_alias_registry'; import { BaseVisType, BaseVisTypeOptions } from './base_vis_type'; import { ReactVisType, ReactVisTypeOptions } from './react_vis_type'; -import { TriggerContextMapping } from '../../../ui_actions/public'; - -export interface VisType { - name: string; - title: string; - description?: string; - getSupportedTriggers?: () => Array; - visualization: any; - isAccessible?: boolean; - requestHandler: string | unknown; - responseHandler: string | unknown; - icon?: IconType; - image?: string; - stage: 'experimental' | 'beta' | 'production'; - requiresSearch: boolean; - hidden: boolean; - - // Since we haven't typed everything here yet, we basically "any" the rest - // of that interface. This should be removed as soon as this type definition - // has been completed. But that way we at least have typing for a couple of - // properties on that type. - [key: string]: any; -} +import { VisType } from './types'; /** * Vis Types Service @@ -51,21 +28,21 @@ export interface VisType { * @internal */ export class TypesService { - private types: Record = {}; + private types: Record> = {}; private unregisteredHiddenTypes: string[] = []; - public setup() { - const registerVisualization = (registerFn: () => VisType) => { - const visDefinition = registerFn(); - if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { - visDefinition.hidden = true; - } + private registerVisualization(visDefinition: VisType) { + if (this.unregisteredHiddenTypes.includes(visDefinition.name)) { + visDefinition.hidden = true; + } - if (this.types[visDefinition.name]) { - throw new Error('type already exists!'); - } - this.types[visDefinition.name] = visDefinition; - }; + if (this.types[visDefinition.name]) { + throw new Error('type already exists!'); + } + this.types[visDefinition.name] = visDefinition; + } + + public setup() { return { /** * registers a visualization type @@ -73,15 +50,15 @@ export class TypesService { */ createBaseVisualization: (config: BaseVisTypeOptions): void => { const vis = new BaseVisType(config); - registerVisualization(() => vis); + this.registerVisualization(vis); }, /** * registers a visualization which uses react for rendering * @param config - visualization type definition */ - createReactVisualization: (config: ReactVisTypeOptions): void => { + createReactVisualization: (config: ReactVisTypeOptions): void => { const vis = new ReactVisType(config); - registerVisualization(() => vis); + this.registerVisualization(vis); }, /** * registers a visualization alias @@ -93,7 +70,7 @@ export class TypesService { * allows to hide specific visualization types from create visualization dialog * @param {string[]} typeNames - list of type ids to hide */ - hideTypes: (typeNames: string[]) => { + hideTypes: (typeNames: string[]): void => { typeNames.forEach((name: string) => { if (this.types[name]) { this.types[name].hidden = true; @@ -111,13 +88,13 @@ export class TypesService { * returns specific visualization or undefined if not found * @param {string} visualization - id of visualization to return */ - get: (visualization: string) => { + get: (visualization: string): VisType => { return this.types[visualization]; }, /** * returns all registered visualization types */ - all: () => { + all: (): VisType[] => { return [...Object.values(this.types)]; }, /** diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap index c2a2b27457f8d1..2d55059efb5bbd 100644 --- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap +++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap @@ -238,12 +238,43 @@ exports[`NewVisModal filter for visualization types should render as expected 1` aria-live="polite" class="euiScreenReaderOnly" > - 2 types found + 3 types found
    +
  • + +
  • @@ -605,11 +659,11 @@ exports[`NewVisModal filter for visualization types should render as expected 1` id="visualizations.newVisWizard.resultsFound" values={ Object { - "resultCount": 2, + "resultCount": 3, } } > - 2 types found + 3 types found @@ -621,6 +675,75 @@ exports[`NewVisModal filter for visualization types should render as expected 1` className="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + + Vis alias with promotion + + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + > + + +
  • @@ -867,7 +990,21 @@ exports[`NewVisModal filter for visualization types should render as expected 1`

    +

    + + promotion description + +

    + + + + +
    @@ -1129,6 +1326,37 @@ exports[`NewVisModal should render as expected 1`] = ` class="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + +
  • @@ -1454,6 +1705,75 @@ exports[`NewVisModal should render as expected 1`] = ` className="euiKeyPadMenu visNewVisDialog__types" data-test-subj="visNewDialogTypes" > +
  • + + Vis alias with promotion + + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onMouseEnter={[Function]} + onMouseLeave={[Function]} + > + + +
  • @@ -1700,7 +2020,21 @@ exports[`NewVisModal should render as expected 1`] = `

    +

    + + promotion description + +

    + + + + +
    diff --git a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx index f48febfef5b437..51bcfed2016874 100644 --- a/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx +++ b/src/plugins/visualizations/public/wizard/new_vis_modal.test.tsx @@ -51,13 +51,24 @@ describe('NewVisModal', () => { aliasApp: 'otherApp', aliasPath: '#/aliasUrl', }, + { + name: 'visAliasWithPromotion', + title: 'Vis alias with promotion', + stage: 'production', + aliasApp: 'anotherApp', + aliasPath: '#/anotherUrl', + promotion: { + description: 'promotion description', + buttonText: 'another app', + }, + }, ]; const visTypes: TypesStart = { - get: (id: string) => { - return _visTypes.find((vis) => vis.name === id) as VisType; + get(id: string): VisType { + return (_visTypes.find((vis) => vis.name === id) as unknown) as VisType; }, all: () => { - return _visTypes as VisType[]; + return (_visTypes as unknown) as VisType[]; }, getAliases: () => [], }; @@ -107,6 +118,30 @@ describe('NewVisModal', () => { expect(wrapper.find('[data-test-subj="visType-vis"]').exists()).toBe(true); }); + it('should sort promoted visualizations first', () => { + const wrapper = mountWithIntl( + null} + visTypesRegistry={visTypes} + addBasePath={addBasePath} + uiSettings={uiSettings} + application={{} as ApplicationStart} + savedObjects={{} as SavedObjectsStart} + /> + ); + expect( + wrapper + .find('button[data-test-subj^="visType-"]') + .map((button) => button.prop('data-test-subj')) + ).toEqual([ + 'visType-visAliasWithPromotion', + 'visType-vis', + 'visType-visWithAliasUrl', + 'visType-visWithSearch', + ]); + }); + describe('open editor', () => { it('should open the editor for visualizations without search', () => { const wrapper = mountWithIntl( diff --git a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx index fa15a6c9ba02bc..a5b6e8039ba6d5 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.test.tsx @@ -31,7 +31,6 @@ describe('NewVisHelp', () => { aliasApp: 'myApp', aliasPath: '/my/fancy/new/thing', description: 'Some desc', - highlighted: false, icon: 'whatever', name: 'whatever', promotion: { diff --git a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx index fc484389045892..5b226a889408f0 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/new_vis_help.tsx @@ -20,11 +20,10 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { Fragment } from 'react'; import { EuiText, EuiButton } from '@elastic/eui'; -import { VisTypeAliasListEntry } from './type_selection'; import { VisTypeAlias } from '../../vis_types'; interface Props { - promotedTypes: VisTypeAliasListEntry[]; + promotedTypes: VisTypeAlias[]; onPromotionClicked: (visType: VisTypeAlias) => void; } diff --git a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx index f507635093f7ff..8c086ed132ae48 100644 --- a/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx +++ b/src/plugins/visualizations/public/wizard/type_selection/type_selection.tsx @@ -42,11 +42,8 @@ import { VisHelpText } from './vis_help_text'; import { VisTypeIcon } from './vis_type_icon'; import { VisType, TypesStart } from '../../vis_types'; -export interface VisTypeListEntry extends VisType { - highlighted: boolean; -} - -export interface VisTypeAliasListEntry extends VisTypeAlias { +interface VisTypeListEntry { + type: VisType | VisTypeAlias; highlighted: boolean; } @@ -69,6 +66,10 @@ interface TypeSelectionState { query: string; } +function isVisTypeAlias(type: VisType | VisTypeAlias): type is VisTypeAlias { + return 'aliasPath' in type; +} + class TypeSelection extends React.Component { public state: TypeSelectionState = { highlightedType: null, @@ -155,7 +156,9 @@ class TypeSelection extends React.Component t.promotion)} + promotedTypes={visTypes + .map((t) => t.type) + .filter((t): t is VisTypeAlias => isVisTypeAlias(t) && Boolean(t.promotion))} onPromotionClicked={this.props.onVisTypeSelected} /> @@ -167,10 +170,7 @@ class TypeSelection extends React.Component { + private filteredVisTypes(visTypes: TypesStart, query: string): VisTypeListEntry[] { const types = visTypes.all().filter((type) => { // Filter out all lab visualizations if lab mode is not enabled if (!this.props.showExperimental && type.stage === 'experimental') { @@ -187,9 +187,9 @@ class TypeSelection extends React.Component; + let entries: VisTypeListEntry[]; if (!query) { - entries = allTypes.map((type) => ({ ...type, highlighted: false })); + entries = allTypes.map((type) => ({ type, highlighted: false })); } else { const q = query.toLowerCase(); entries = allTypes.map((type) => { @@ -197,17 +197,21 @@ class TypeSelection extends React.Component { + private renderVisType = (visType: VisTypeListEntry) => { let stage = {}; let highlightMsg; - if (!('aliasPath' in visType) && visType.stage === 'experimental') { + if (!isVisTypeAlias(visType.type) && visType.type.stage === 'experimental') { stage = { betaBadgeLabel: i18n.translate('visualizations.newVisWizard.experimentalTitle', { defaultMessage: 'Experimental', @@ -221,7 +225,7 @@ class TypeSelection extends React.Component this.props.onVisTypeSelected(visType); + const onClick = () => this.props.onVisTypeSelected(visType.type); const highlightedType: HighlightedType = { - title: visType.title, - name: visType.name, - description: visType.description, + title: visType.type.title, + name: visType.type.name, + description: visType.type.description, highlightMsg, }; return ( {visType.title}} + key={visType.type.name} + label={{visType.type.title}} onClick={onClick} onFocus={() => this.setHighlightType(highlightedType)} onMouseEnter={() => this.setHighlightType(highlightedType)} onMouseLeave={() => this.setHighlightType(null)} onBlur={() => this.setHighlightType(null)} className="visNewVisDialog__type" - data-test-subj={`visType-${visType.name}`} - data-vis-stage={!('aliasPath' in visType) ? visType.stage : 'alias'} + data-test-subj={`visType-${visType.type.name}`} + data-vis-stage={!isVisTypeAlias(visType.type) ? visType.type.stage : 'alias'} disabled={isDisabled} - aria-describedby={`visTypeDescription-${visType.name}`} + aria-describedby={`visTypeDescription-${visType.type.name}`} {...stage} > ); diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index 37f564aaa3a18a..545552b9055534 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -78,7 +78,7 @@ export const VisualizeEditorCommon = ({ embeddableId={embeddableId} /> )} - {visInstance?.vis?.type?.isExperimental && } + {visInstance?.vis?.type?.stage === 'experimental' && } {visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)} {visInstance && (