Improve visualization typings (#79128)

* Improve visualization typings

* Fix vis selection dialog

* Fix broken getInfoMessage type

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Tim Roes 2020-10-06 13:04:18 +02:00 committed by GitHub
parent 2ba729f904
commit b8f4ea1d06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 696 additions and 196 deletions

View file

@ -76,7 +76,7 @@ function TileMapOptions(props: TileMapOptionsProps) {
<BasicOptions {...props} />
<SwitchOption
disabled={!vis.type.visConfig.canDesaturate}
disabled={!vis.type.visConfig?.canDesaturate}
label={i18n.translate('tileMap.visParams.desaturateTilesLabel', {
defaultMessage: 'Desaturate tiles',
})}

View file

@ -55,7 +55,6 @@ export function createTileMapTypeDefinition(dependencies) {
wms: uiSettings.get('visualization:tileMap:WMSdefaults'),
},
},
requiresPartialRows: true,
visualization: CoordinateMapsVisualization,
responseHandler: convertToGeoJson,
editorConfig: {

View file

@ -18,7 +18,7 @@
*/
import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react';
import { get, isEqual } from 'lodash';
import { isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
import { keys, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EventEmitter } from 'events';
@ -71,7 +71,7 @@ function DefaultEditorSideBar({
]);
const metricSchemas = (vis.type.schemas.metrics || []).map((s: Schema) => 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'), [

View file

@ -39,7 +39,7 @@ export const toExpressionAst = (vis: Vis, params: any) => {
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('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,
});

View file

@ -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);
});
});

View file

@ -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);
},
};

View file

@ -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();

View file

@ -38,7 +38,7 @@ export const toExpressionAst = (vis: Vis<TagCloudVisParams>, params: BuildPipeli
const esaggs = buildExpressionFunction<EsaggsExpressionFunctionDefinition>('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,
});

View file

@ -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);

View file

@ -90,6 +90,12 @@ class VisualizationChart extends React.Component<VisualizationChartProps> {
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

View file

@ -36,7 +36,7 @@ import { VisType } from '../vis_types';
export interface ExprVisState {
title?: string;
type: VisType | string;
type: VisType<unknown> | string;
params?: VisParams;
}
@ -52,7 +52,7 @@ export interface ExprVisAPI {
export class ExprVis extends EventEmitter {
public title: string = '';
public type: VisType;
public type: VisType<unknown>;
public params: VisParams = {};
public sessionState: Record<string, any> = {};
public API: ExprVisAPI;
@ -92,7 +92,7 @@ export class ExprVis extends EventEmitter {
};
}
private getType(type: string | VisType) {
private getType(type: string | VisType<unknown>) {
if (_.isString(type)) {
const newType = getTypes().get(type);
if (!newType) {

View file

@ -86,7 +86,10 @@ const vislibCharts: string[] = [
'line',
];
export const getSchemas = (vis: Vis, { timeRange, timefilter }: BuildPipelineParams): Schemas => {
export const getSchemas = <TVisParams>(
vis: Vis<TVisParams>,
{ 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) {

View file

@ -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);
});
});

View file

@ -84,7 +84,7 @@ const getSearchSource = async (inputSearchSource: ISearchSource, savedSearchId?:
type PartialVisState = Assign<SerializedVis, { data: Partial<SerializedVisData> }>;
export class Vis<TVisParams = VisParams> {
public readonly type: VisType;
public readonly type: VisType<TVisParams>;
public readonly id?: string;
public title: string = '';
public description: string = '';
@ -97,14 +97,14 @@ export class Vis<TVisParams = VisParams> {
public readonly uiState: PersistedState;
constructor(visType: string, visState: SerializedVis = {} as any) {
this.type = this.getType(visType);
this.type = this.getType<TVisParams>(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<TVisParams>(visType: string) {
const type = getTypes().get<TVisParams>(visType);
if (!type) {
const errorMessage = i18n.translate('visualizations.visualizationTypeInvalidMessage', {
defaultMessage: 'Invalid visualization type "{visType}"',
@ -118,7 +118,7 @@ export class Vis<TVisParams = VisParams> {
}
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<TVisParams = VisParams> {
};
}
toExpressionAst() {
return this.type.toExpressionAst(this.params);
}
// deprecated
isHierarchical() {
if (isFunction(this.type.hierarchicalData)) {

View file

@ -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<keyof TriggerContextMapping>;
icon?: string;
image?: string;
stage?: 'experimental' | 'beta' | 'production';
options?: Record<string, any>;
visConfig?: Record<string, any>;
editor?: any;
editorConfig?: Record<string, any>;
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<TVisParams>
extends Pick<
VisType<TVisParams>,
| 'description'
| 'editor'
| 'getInfoMessage'
| 'getSupportedTriggers'
| 'hierarchicalData'
| 'icon'
| 'image'
| 'inspectorAdapters'
| 'name'
| 'requestHandler'
| 'responseHandler'
| 'setup'
| 'title'
>,
Pick<
Partial<VisType<TVisParams>>,
'editorConfig' | 'hidden' | 'stage' | 'useCustomNoDataScreen' | 'visConfig'
> {
options?: Partial<VisType<TVisParams>['options']>;
}
interface ExpressionBaseVisTypeOptions<TVisParams> extends CommonBaseVisTypeOptions {
toExpressionAst: VisToExpressionAst<TVisParams>;
interface ExpressionBaseVisTypeOptions<TVisParams> extends CommonBaseVisTypeOptions<TVisParams> {
toExpressionAst: VisType<TVisParams>['toExpressionAst'];
visualization?: undefined;
}
interface VisualizationBaseVisTypeOptions extends CommonBaseVisTypeOptions {
interface VisualizationBaseVisTypeOptions<TVisParams> extends CommonBaseVisTypeOptions<TVisParams> {
toExpressionAst?: undefined;
visualization: VisualizationControllerConstructor | undefined;
visualization: VisType<TVisParams>['visualization'];
}
export type BaseVisTypeOptions<TVisParams = VisParams> =
| ExpressionBaseVisTypeOptions<TVisParams>
| VisualizationBaseVisTypeOptions;
| VisualizationBaseVisTypeOptions<TVisParams>;
export class BaseVisType<TVisParams = VisParams> {
name: string;
title: string;
description: string;
getSupportedTriggers?: () => Array<keyof TriggerContextMapping>;
icon?: string;
image?: string;
stage: 'experimental' | 'beta' | 'production';
isExperimental: boolean;
options: Record<string, any>;
visualization: VisualizationControllerConstructor | undefined;
visConfig: Record<string, any>;
editor: any;
editorConfig: Record<string, any>;
hidden: boolean;
requiresSearch: boolean;
requestHandler: string | unknown;
responseHandler: string | unknown;
hierarchicalData: boolean | unknown;
setup?: unknown;
useCustomNoDataScreen: boolean;
inspectorAdapters?: Adapters | (() => Adapters);
toExpressionAst?: VisToExpressionAst<TVisParams>;
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<TVisParams = VisParams> implements VisType<TVisParams> {
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<TVisParams>) {
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';
}
}

View file

@ -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';

View file

@ -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<BaseVisTypeOptions, 'visualization' | 'toExpressionAst'>;
export type ReactVisTypeOptions<TVisParams> = Omit<
BaseVisTypeOptions<TVisParams>,
'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<TVisParams>
extends BaseVisType<TVisParams>
implements VisType<TVisParams> {
constructor(opts: ReactVisTypeOptions<TVisParams>) {
super({
...opts,
visualization: ReactVisController,

View file

@ -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<TVisParams = unknown> {
readonly name: string;
readonly title: string;
readonly description?: string;
readonly getSupportedTriggers?: () => Array<keyof TriggerContextMapping>;
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<TVisParams>;
readonly visualization?: VisualizationControllerConstructor;
readonly setup?: (vis: Vis<TVisParams>) => Promise<Vis<TVisParams>>;
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<string, any>;
readonly visConfig: Record<string, any>;
}

View file

@ -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<keyof TriggerContextMapping>;
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<string, VisType> = {};
private types: Record<string, VisType<any>> = {};
private unregisteredHiddenTypes: string[] = [];
public setup() {
const registerVisualization = (registerFn: () => VisType) => {
const visDefinition = registerFn();
if (this.unregisteredHiddenTypes.includes(visDefinition.name)) {
visDefinition.hidden = true;
}
private registerVisualization<TVisParam>(visDefinition: VisType<TVisParam>) {
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: <TVisParams>(config: BaseVisTypeOptions<TVisParams>): 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: <TVisParams>(config: ReactVisTypeOptions<TVisParams>): 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: <TVisParams>(visualization: string): VisType<TVisParams> => {
return this.types[visualization];
},
/**
* returns all registered visualization types
*/
all: () => {
all: (): VisType[] => {
return [...Object.values(this.types)];
},
/**

View file

@ -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
</span>
<ul
class="euiKeyPadMenu visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
<li>
<button
aria-describedby="visTypeDescription-visAliasWithPromotion"
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
type="button"
>
<div
class="euiKeyPadMenuItem__inner"
>
<div
class="euiKeyPadMenuItem__icon"
>
<div
color="secondary"
data-euiicon-type="empty"
/>
</div>
<p
class="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
</p>
</div>
</button>
</li>
<li>
<button
aria-describedby="visTypeDescription-visWithAliasUrl"
@ -359,6 +390,29 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
<p>
Start creating your visualization by selecting a type for that visualization.
</p>
<p>
<strong>
promotion description
</strong>
</p>
<button
class="euiButton euiButton--primary euiButton--small euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<div
class="euiButtonContent__icon"
data-euiicon-type="popout"
/>
<span
class="euiButton__text"
>
another app
</span>
</span>
</button>
</div>
</div>
</div>
@ -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
</FormattedMessage>
</span>
</EuiScreenReaderOnly>
@ -621,6 +675,75 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
className="euiKeyPadMenu visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
<li
key=".$visAliasWithPromotion"
>
<EuiKeyPadMenuItem
aria-describedby="visTypeDescription-visAliasWithPromotion"
className="visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
disabled={false}
key="visAliasWithPromotion"
label={
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-describedby="visTypeDescription-visAliasWithPromotion"
className="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
type="button"
>
<div
className="euiKeyPadMenuItem__inner"
>
<div
className="euiKeyPadMenuItem__icon"
>
<VisTypeIcon>
<EuiIcon
color="secondary"
size="l"
type="empty"
>
<div
color="secondary"
data-euiicon-type="empty"
size="l"
/>
</EuiIcon>
</VisTypeIcon>
</div>
<p
className="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
</p>
</div>
</button>
</EuiKeyPadMenuItem>
</li>
<li
key=".$visWithAliasUrl"
>
@ -867,7 +990,21 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
</EuiSpacer>
<NewVisHelp
onPromotionClicked={[Function]}
promotedTypes={Array []}
promotedTypes={
Array [
Object {
"aliasApp": "anotherApp",
"aliasPath": "#/anotherUrl",
"name": "visAliasWithPromotion",
"promotion": Object {
"buttonText": "another app",
"description": "promotion description",
},
"stage": "production",
"title": "Vis alias with promotion",
},
]
}
>
<EuiText>
<div
@ -882,6 +1019,66 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
Start creating your visualization by selecting a type for that visualization.
</FormattedMessage>
</p>
<p>
<strong>
promotion description
</strong>
</p>
<EuiButton
fill={true}
iconSide="right"
iconType="popout"
onClick={[Function]}
size="s"
>
<EuiButtonDisplay
element="button"
fill={true}
iconSide="right"
iconType="popout"
onClick={[Function]}
size="s"
type="button"
>
<button
className="euiButton euiButton--primary euiButton--small euiButton--fill"
onClick={[Function]}
type="button"
>
<EuiButtonContent
className="euiButton__content"
iconSide="right"
iconType="popout"
textProps={
Object {
"className": "euiButton__text",
}
}
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<EuiIcon
className="euiButtonContent__icon"
size="m"
type="popout"
>
<div
className="euiButtonContent__icon"
data-euiicon-type="popout"
size="m"
/>
</EuiIcon>
<span
className="euiButton__text"
>
another app
</span>
</span>
</EuiButtonContent>
</button>
</EuiButtonDisplay>
</EuiButton>
</div>
</EuiText>
</NewVisHelp>
@ -1129,6 +1326,37 @@ exports[`NewVisModal should render as expected 1`] = `
class="euiKeyPadMenu visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
<li>
<button
aria-describedby="visTypeDescription-visAliasWithPromotion"
class="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
type="button"
>
<div
class="euiKeyPadMenuItem__inner"
>
<div
class="euiKeyPadMenuItem__icon"
>
<div
color="secondary"
data-euiicon-type="empty"
/>
</div>
<p
class="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
</p>
</div>
</button>
</li>
<li>
<button
aria-describedby="visTypeDescription-vis"
@ -1243,6 +1471,29 @@ exports[`NewVisModal should render as expected 1`] = `
<p>
Start creating your visualization by selecting a type for that visualization.
</p>
<p>
<strong>
promotion description
</strong>
</p>
<button
class="euiButton euiButton--primary euiButton--small euiButton--fill"
type="button"
>
<span
class="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<div
class="euiButtonContent__icon"
data-euiicon-type="popout"
/>
<span
class="euiButton__text"
>
another app
</span>
</span>
</button>
</div>
</div>
</div>
@ -1454,6 +1705,75 @@ exports[`NewVisModal should render as expected 1`] = `
className="euiKeyPadMenu visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
<li
key=".$visAliasWithPromotion"
>
<EuiKeyPadMenuItem
aria-describedby="visTypeDescription-visAliasWithPromotion"
className="visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
disabled={false}
key="visAliasWithPromotion"
label={
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<button
aria-describedby="visTypeDescription-visAliasWithPromotion"
className="euiKeyPadMenuItem visNewVisDialog__type"
data-test-subj="visType-visAliasWithPromotion"
data-vis-stage="alias"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
type="button"
>
<div
className="euiKeyPadMenuItem__inner"
>
<div
className="euiKeyPadMenuItem__icon"
>
<VisTypeIcon>
<EuiIcon
color="secondary"
size="l"
type="empty"
>
<div
color="secondary"
data-euiicon-type="empty"
size="l"
/>
</EuiIcon>
</VisTypeIcon>
</div>
<p
className="euiKeyPadMenuItem__label"
>
<span
data-test-subj="visTypeTitle"
>
Vis alias with promotion
</span>
</p>
</div>
</button>
</EuiKeyPadMenuItem>
</li>
<li
key=".$vis"
>
@ -1700,7 +2020,21 @@ exports[`NewVisModal should render as expected 1`] = `
</EuiSpacer>
<NewVisHelp
onPromotionClicked={[Function]}
promotedTypes={Array []}
promotedTypes={
Array [
Object {
"aliasApp": "anotherApp",
"aliasPath": "#/anotherUrl",
"name": "visAliasWithPromotion",
"promotion": Object {
"buttonText": "another app",
"description": "promotion description",
},
"stage": "production",
"title": "Vis alias with promotion",
},
]
}
>
<EuiText>
<div
@ -1715,6 +2049,66 @@ exports[`NewVisModal should render as expected 1`] = `
Start creating your visualization by selecting a type for that visualization.
</FormattedMessage>
</p>
<p>
<strong>
promotion description
</strong>
</p>
<EuiButton
fill={true}
iconSide="right"
iconType="popout"
onClick={[Function]}
size="s"
>
<EuiButtonDisplay
element="button"
fill={true}
iconSide="right"
iconType="popout"
onClick={[Function]}
size="s"
type="button"
>
<button
className="euiButton euiButton--primary euiButton--small euiButton--fill"
onClick={[Function]}
type="button"
>
<EuiButtonContent
className="euiButton__content"
iconSide="right"
iconType="popout"
textProps={
Object {
"className": "euiButton__text",
}
}
>
<span
className="euiButtonContent euiButtonContent--iconRight euiButton__content"
>
<EuiIcon
className="euiButtonContent__icon"
size="m"
type="popout"
>
<div
className="euiButtonContent__icon"
data-euiicon-type="popout"
size="m"
/>
</EuiIcon>
<span
className="euiButton__text"
>
another app
</span>
</span>
</EuiButtonContent>
</button>
</EuiButtonDisplay>
</EuiButton>
</div>
</EuiText>
</NewVisHelp>

View file

@ -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<T>(id: string): VisType<T> {
return (_visTypes.find((vis) => vis.name === id) as unknown) as VisType<T>;
},
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(
<NewVisModal
isOpen={true}
onClose={() => 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(

View file

@ -31,7 +31,6 @@ describe('NewVisHelp', () => {
aliasApp: 'myApp',
aliasPath: '/my/fancy/new/thing',
description: 'Some desc',
highlighted: false,
icon: 'whatever',
name: 'whatever',
promotion: {

View file

@ -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;
}

View file

@ -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<TypeSelectionProps, TypeSelectionState> {
public state: TypeSelectionState = {
highlightedType: null,
@ -155,7 +156,9 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta
</EuiTitle>
<EuiSpacer size="m" />
<NewVisHelp
promotedTypes={(visTypes as VisTypeAliasListEntry[]).filter((t) => t.promotion)}
promotedTypes={visTypes
.map((t) => t.type)
.filter((t): t is VisTypeAlias => isVisTypeAlias(t) && Boolean(t.promotion))}
onPromotionClicked={this.props.onVisTypeSelected}
/>
</React.Fragment>
@ -167,10 +170,7 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta
);
}
private filteredVisTypes(
visTypes: TypesStart,
query: string
): Array<VisTypeListEntry | VisTypeAliasListEntry> {
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<TypeSelectionProps, TypeSelectionSta
const allTypes = [...types, ...visTypes.getAliases()];
let entries: Array<VisTypeListEntry | VisTypeAliasListEntry>;
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<TypeSelectionProps, TypeSelectionSta
type.name.toLowerCase().includes(q) ||
type.title.toLowerCase().includes(q) ||
(typeof type.description === 'string' && type.description.toLowerCase().includes(q));
return { ...type, highlighted: matchesQuery };
return { type, highlighted: matchesQuery };
});
}
return orderBy(entries, ['highlighted', 'promotion', 'title'], ['desc', 'asc', 'asc']);
return orderBy(
entries,
['highlighted', 'type.promotion', 'type.title'],
['desc', 'asc', 'asc']
);
}
private renderVisType = (visType: VisTypeListEntry | VisTypeAliasListEntry) => {
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<TypeSelectionProps, TypeSelectionSta
defaultMessage:
'This visualization is experimental. The design and implementation are less mature than stable visualizations and might be subject to change.',
});
} else if ('aliasPath' in visType && visType.stage === 'beta') {
} else if (isVisTypeAlias(visType.type) && visType.type.stage === 'beta') {
const aliasDescription = i18n.translate('visualizations.newVisWizard.betaDescription', {
defaultMessage:
'This visualization is in beta and is subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features',
@ -236,34 +240,34 @@ class TypeSelection extends React.Component<TypeSelectionProps, TypeSelectionSta
}
const isDisabled = this.state.query !== '' && !visType.highlighted;
const onClick = () => 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 (
<EuiKeyPadMenuItem
key={visType.name}
label={<span data-test-subj="visTypeTitle">{visType.title}</span>}
key={visType.type.name}
label={<span data-test-subj="visTypeTitle">{visType.type.title}</span>}
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}
>
<VisTypeIcon
icon={visType.icon}
image={!('aliasPath' in visType) ? visType.image : undefined}
icon={visType.type.icon}
image={'image' in visType.type ? visType.type.image : undefined}
/>
</EuiKeyPadMenuItem>
);

View file

@ -78,7 +78,7 @@ export const VisualizeEditorCommon = ({
embeddableId={embeddableId}
/>
)}
{visInstance?.vis?.type?.isExperimental && <ExperimentalVisInfo />}
{visInstance?.vis?.type?.stage === 'experimental' && <ExperimentalVisInfo />}
{visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)}
{visInstance && (
<EuiScreenReaderOnly>