mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Expose vis registration (#122348)
* expose vis registration * add example app * remove file * fix and stabilize * tsconfig fix * fix type problems * handle migrations * fix problems * fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d9d3230b94
commit
2f869baf18
36 changed files with 1023 additions and 99 deletions
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": 0
|
||||
}
|
||||
}
|
23
x-pack/examples/third_party_vis_lens_example/README.md
Normal file
23
x-pack/examples/third_party_vis_lens_example/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Third party Lens visualization
|
||||
|
||||
To run this example plugin, use the command `yarn start --run-examples`.
|
||||
|
||||
This example shows how to register a visualization to Lens which lives along the regular visualizations (xy, table and so on).
|
||||
|
||||
The following parts can be seen in this example:
|
||||
* Registering the visualization type so it shows up in the Lens editor along with custom edit UI and hooks to update state on user interactions (add dimension, delete dimension).
|
||||
* Registering the used expression functions and expression renderers to actually render the expression into a DOM element.
|
||||
* Providing a sample migration on the Kibana server which allows to update existing stored visualizations and change their state on Kibana upgrade / import of old saved objects.
|
||||
|
||||
|
||||
To test the migration, you can import the following ndjson file via saved object import (requires installed logs sample data):
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
```
|
||||
{"attributes":{"fieldFormatMap":"{\"hour_of_day\":{}}","runtimeFieldMap":"{\"hour_of_day\":{\"type\":\"long\",\"script\":{\"source\":\"emit(doc['timestamp'].value.getHour());\"}}}","timeFieldName":"timestamp","title":"kibana_sample_data_logs"},"coreMigrationVersion":"8.0.0","id":"90943e30-9a47-11e8-b64d-95841ca0b247","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2022-01-24T10:54:24.209Z","version":"WzQzMTQ3LDFd"}
|
||||
{"attributes":{"description":"","state":{"datasourceStates":{"indexpattern":{"layers":{"f2700077-50bf-48e4-829c-f695f87e226d":{"columnOrder":["5e704cac-8490-457a-b635-01f3a5a132b7"],"columns":{"5e704cac-8490-457a-b635-01f3a5a132b7":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"column":"5e704cac-8490-457a-b635-01f3a5a132b7","layerId":"f2700077-50bf-48e4-829c-f695f87e226d"}},"title":"Rotating number test","visualizationType":"rotatingNumber"},"coreMigrationVersion":"8.0.0","id":"468f0be0-7e86-11ec-9739-d570ffd3fbe4","migrationVersion":{"lens":"8.0.0"},"references":[{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-f2700077-50bf-48e4-829c-f695f87e226d","type":"index-pattern"}],"type":"lens","updated_at":"2022-01-26T08:59:31.618Z","version":"WzQzNjUzLDFd"}
|
||||
{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]}
|
||||
```
|
||||
|
||||
</details>
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const DEFAULT_COLOR = '#000000';
|
12
x-pack/examples/third_party_vis_lens_example/common/types.ts
Normal file
12
x-pack/examples/third_party_vis_lens_example/common/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface RotatingNumberState {
|
||||
accessor?: string;
|
||||
color: string;
|
||||
layerId: string;
|
||||
}
|
24
x-pack/examples/third_party_vis_lens_example/kibana.json
Normal file
24
x-pack/examples/third_party_vis_lens_example/kibana.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"id": "thirdPartyVisLensExample",
|
||||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["third_part_vis_lens_example"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"lens",
|
||||
"dataViews",
|
||||
"embeddable",
|
||||
"developerExamples",
|
||||
"expressions",
|
||||
"fieldFormats"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": [
|
||||
"kibanaReact"
|
||||
],
|
||||
"owner": {
|
||||
"name": "Vis Editors",
|
||||
"githubTeam": "kibana-vis-editors"
|
||||
}
|
||||
}
|
14
x-pack/examples/third_party_vis_lens_example/package.json
Normal file
14
x-pack/examples/third_party_vis_lens_example/package.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "third_party_vis_lens_example",
|
||||
"version": "1.0.0",
|
||||
"main": "target/examples/third_party_vis_lens_example",
|
||||
"kibana": {
|
||||
"version": "kibana",
|
||||
"templateVersion": "1.0.0"
|
||||
},
|
||||
"license": "Elastic License 2.0",
|
||||
"scripts": {
|
||||
"kbn": "node ../../../scripts/kbn.js",
|
||||
"build": "rm -rf './target' && ../../../node_modules/.bin/tsc"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { css, keyframes } from '@emotion/css';
|
||||
import type {
|
||||
Datatable,
|
||||
ExpressionFunctionDefinition,
|
||||
ExpressionRenderDefinition,
|
||||
IInterpreterRenderHandlers,
|
||||
} from '../../../../src/plugins/expressions/public';
|
||||
import { RotatingNumberState } from '../common/types';
|
||||
import { FormatFactory } from '../../../../src/plugins/field_formats/common';
|
||||
|
||||
export const getRotatingNumberRenderer = (
|
||||
formatFactory: Promise<FormatFactory>
|
||||
): ExpressionRenderDefinition<RotatingNumberChartProps> => ({
|
||||
name: 'rotating_number',
|
||||
displayName: 'Rotating number',
|
||||
help: 'Rotating number renderer',
|
||||
validate: () => undefined,
|
||||
reuseDomNode: true,
|
||||
render: async (
|
||||
domNode: Element,
|
||||
config: RotatingNumberChartProps,
|
||||
handlers: IInterpreterRenderHandlers
|
||||
) => {
|
||||
ReactDOM.render(
|
||||
<RotatingNumberChart {...config} formatFactory={await formatFactory} />,
|
||||
domNode,
|
||||
() => {
|
||||
handlers.done();
|
||||
}
|
||||
);
|
||||
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
|
||||
},
|
||||
});
|
||||
|
||||
const rotating = keyframes`
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
function RotatingNumberChart({
|
||||
data,
|
||||
args,
|
||||
formatFactory,
|
||||
}: RotatingNumberChartProps & { formatFactory: FormatFactory }) {
|
||||
const { accessor, color } = args;
|
||||
const column = data.columns.find((col) => col.id === accessor);
|
||||
const rawValue = accessor && data.rows[0]?.[accessor];
|
||||
|
||||
const value =
|
||||
column && column.meta?.params
|
||||
? formatFactory(column.meta?.params).convert(rawValue)
|
||||
: Number(Number(rawValue).toFixed(3)).toString();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
font-size: 100px;
|
||||
overflow: hidden;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
color: ${color};
|
||||
animation: ${rotating} 5s linear infinite;
|
||||
`}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export interface RotatingNumberChartProps {
|
||||
data: Datatable;
|
||||
args: RotatingNumberState;
|
||||
}
|
||||
|
||||
interface RotatingNumberRender {
|
||||
type: 'render';
|
||||
as: 'rotating_number';
|
||||
value: RotatingNumberChartProps;
|
||||
}
|
||||
|
||||
export const rotatingNumberFunction: ExpressionFunctionDefinition<
|
||||
'rotating_number',
|
||||
Datatable,
|
||||
Omit<RotatingNumberState, 'layerId' | 'layerType'>,
|
||||
RotatingNumberRender
|
||||
> = {
|
||||
name: 'rotating_number',
|
||||
type: 'render',
|
||||
help: 'A rotating number',
|
||||
args: {
|
||||
accessor: {
|
||||
types: ['string'],
|
||||
help: 'The column whose value is being displayed',
|
||||
},
|
||||
color: {
|
||||
types: ['string'],
|
||||
help: 'Color of the number',
|
||||
},
|
||||
},
|
||||
inputTypes: ['datatable'],
|
||||
fn(data, args) {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'rotating_number',
|
||||
value: {
|
||||
data,
|
||||
args,
|
||||
},
|
||||
} as RotatingNumberRender;
|
||||
},
|
||||
};
|
10
x-pack/examples/third_party_vis_lens_example/public/index.ts
Normal file
10
x-pack/examples/third_party_vis_lens_example/public/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EmbeddedLensExamplePlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new EmbeddedLensExamplePlugin();
|
138
x-pack/examples/third_party_vis_lens_example/public/plugin.ts
Normal file
138
x-pack/examples/third_party_vis_lens_example/public/plugin.ts
Normal file
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ExpressionsSetup } from 'src/plugins/expressions/public';
|
||||
import { FieldFormatsStart } from 'src/plugins/field_formats/public';
|
||||
import { Plugin, CoreSetup, AppNavLinkStatus } from '../../../../src/core/public';
|
||||
import { DataViewsPublicPluginStart, DataView } from '../../../../src/plugins/data_views/public';
|
||||
import { LensPublicSetup, LensPublicStart } from '../../../plugins/lens/public';
|
||||
import { DeveloperExamplesSetup } from '../../../../examples/developer_examples/public';
|
||||
import { TypedLensByValueInput, PersistedIndexPatternLayer } from '../../../plugins/lens/public';
|
||||
import { getRotatingNumberRenderer, rotatingNumberFunction } from './expression';
|
||||
import { getRotatingNumberVisualization } from './visualization';
|
||||
import { RotatingNumberState } from '../common/types';
|
||||
|
||||
export interface SetupDependencies {
|
||||
developerExamples: DeveloperExamplesSetup;
|
||||
lens: LensPublicSetup;
|
||||
expressions: ExpressionsSetup;
|
||||
}
|
||||
|
||||
export interface StartDependencies {
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
lens: LensPublicStart;
|
||||
fieldFormats: FieldFormatsStart;
|
||||
}
|
||||
|
||||
function getLensAttributes(defaultDataView: DataView): TypedLensByValueInput['attributes'] {
|
||||
const dataLayer: PersistedIndexPatternLayer = {
|
||||
columnOrder: ['col1'],
|
||||
columns: {
|
||||
col1: {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Count of records',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rotatingNumberConfig: RotatingNumberState = {
|
||||
accessor: 'col1',
|
||||
color: '#ff0000',
|
||||
layerId: 'layer1',
|
||||
};
|
||||
|
||||
return {
|
||||
visualizationType: 'rotatingNumber',
|
||||
title: 'Prefilled from example app',
|
||||
references: [
|
||||
{
|
||||
id: defaultDataView.id!,
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: defaultDataView.id!,
|
||||
name: 'indexpattern-datasource-layer-layer1',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: dataLayer,
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: [],
|
||||
query: { language: 'kuery', query: '' },
|
||||
visualization: rotatingNumberConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class EmbeddedLensExamplePlugin
|
||||
implements Plugin<void, void, SetupDependencies, StartDependencies>
|
||||
{
|
||||
public setup(
|
||||
core: CoreSetup<StartDependencies>,
|
||||
{ developerExamples, lens, expressions }: SetupDependencies
|
||||
) {
|
||||
core.application.register({
|
||||
id: 'third_party_lens_vis_example',
|
||||
title: 'Third party Lens vis example',
|
||||
navLinkStatus: AppNavLinkStatus.hidden,
|
||||
mount: (params) => {
|
||||
(async () => {
|
||||
const [, { lens: lensStart, dataViews }] = await core.getStartServices();
|
||||
const defaultDataView = await dataViews.getDefault();
|
||||
lensStart.navigateToPrefilledEditor({
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: 'now-5d',
|
||||
to: 'now',
|
||||
},
|
||||
attributes: getLensAttributes(defaultDataView!),
|
||||
});
|
||||
})();
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
developerExamples.register({
|
||||
appId: 'third_party_lens_vis_example',
|
||||
title: 'Third party Lens visualization',
|
||||
description: 'Add custom visualization types to the Lens editor',
|
||||
links: [
|
||||
{
|
||||
label: 'README',
|
||||
href: 'https://github.com/elastic/kibana/tree/main/x-pack/examples/third_party_lens_vis_example',
|
||||
iconType: 'logoGithub',
|
||||
size: 's',
|
||||
target: '_blank',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expressions.registerRenderer(() =>
|
||||
getRotatingNumberRenderer(
|
||||
core.getStartServices().then(([, { fieldFormats }]) => fieldFormats.deserialize)
|
||||
)
|
||||
);
|
||||
expressions.registerFunction(() => rotatingNumberFunction);
|
||||
|
||||
lens.registerVisualization(async () => getRotatingNumberVisualization({ theme: core.theme }));
|
||||
}
|
||||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFormRow, EuiColorPicker } from '@elastic/eui';
|
||||
import { render } from 'react-dom';
|
||||
import { Ast } from '@kbn/interpreter';
|
||||
import { ThemeServiceStart } from '../../../../src/core/public';
|
||||
import { KibanaThemeProvider } from '../../../../src/plugins/kibana_react/public';
|
||||
import { Visualization, OperationMetadata } from '../../../plugins/lens/public';
|
||||
import type { RotatingNumberState } from '../common/types';
|
||||
import { DEFAULT_COLOR } from '../common/constants';
|
||||
import { layerTypes } from '../../../plugins/lens/public';
|
||||
|
||||
const toExpression = (state: RotatingNumberState): Ast | null => {
|
||||
if (!state.accessor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'rotating_number',
|
||||
arguments: {
|
||||
accessor: [state.accessor],
|
||||
color: [state?.color || 'black'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
export const getRotatingNumberVisualization = ({
|
||||
theme,
|
||||
}: {
|
||||
theme: ThemeServiceStart;
|
||||
}): Visualization<RotatingNumberState> => ({
|
||||
id: 'rotatingNumber',
|
||||
|
||||
visualizationTypes: [
|
||||
{
|
||||
id: 'rotatingNumber',
|
||||
icon: 'refresh',
|
||||
label: 'Rotating number',
|
||||
groupLabel: 'Goal and single value',
|
||||
sortPriority: 3,
|
||||
},
|
||||
],
|
||||
|
||||
getVisualizationTypeId() {
|
||||
return 'rotatingNumber';
|
||||
},
|
||||
|
||||
clearLayer(state) {
|
||||
return {
|
||||
...state,
|
||||
accessor: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
getLayerIds(state) {
|
||||
return [state.layerId];
|
||||
},
|
||||
|
||||
getDescription() {
|
||||
return {
|
||||
icon: 'refresh',
|
||||
label: 'A number that rotates',
|
||||
};
|
||||
},
|
||||
|
||||
getSuggestions: ({ state, table }) => {
|
||||
if (table.columns.length > 1) {
|
||||
return [];
|
||||
}
|
||||
if (state && table.changeType === 'unchanged') {
|
||||
return [];
|
||||
}
|
||||
const column = table.columns[0];
|
||||
if (column.operation.isBucketed || column.operation.dataType !== 'number') {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
previewIcon: 'refresh',
|
||||
score: 0.5,
|
||||
title: `Rotating ${table.label}` || 'Rotating number',
|
||||
state: {
|
||||
layerId: table.layerId,
|
||||
color: state?.color || DEFAULT_COLOR,
|
||||
accessor: column.columnId,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
layerId: addNewLayer(),
|
||||
accessor: undefined,
|
||||
color: DEFAULT_COLOR,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getConfiguration(props) {
|
||||
return {
|
||||
groups: [
|
||||
{
|
||||
groupId: 'metric',
|
||||
groupLabel: 'Rotating number',
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.accessor
|
||||
? [
|
||||
{
|
||||
columnId: props.state.accessor,
|
||||
triggerIcon: 'color',
|
||||
color: props.state.color,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
supportsMoreColumns: !props.state.accessor,
|
||||
filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number',
|
||||
enableDimensionEditor: true,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
getSupportedLayers() {
|
||||
return [
|
||||
{
|
||||
type: layerTypes.DATA,
|
||||
label: 'Add visualization layer',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getLayerType(layerId, state) {
|
||||
if (state?.layerId === layerId) {
|
||||
return layerTypes.DATA;
|
||||
}
|
||||
},
|
||||
|
||||
toExpression: (state) => toExpression(state),
|
||||
toPreviewExpression: (state) => toExpression(state),
|
||||
|
||||
setDimension({ prevState, columnId }) {
|
||||
return { ...prevState, accessor: columnId };
|
||||
},
|
||||
|
||||
removeDimension({ prevState }) {
|
||||
return { ...prevState, accessor: undefined };
|
||||
},
|
||||
|
||||
renderDimensionEditor(domElement, props) {
|
||||
render(
|
||||
<KibanaThemeProvider theme$={theme.theme$}>
|
||||
<EuiFormRow label="Pick a color">
|
||||
<EuiColorPicker
|
||||
onChange={(newColor) => {
|
||||
props.setState({ ...props.state, color: newColor });
|
||||
}}
|
||||
color={props.state.color}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</KibanaThemeProvider>,
|
||||
domElement
|
||||
);
|
||||
},
|
||||
|
||||
getErrorMessages(state) {
|
||||
// Is it possible to break it?
|
||||
return undefined;
|
||||
},
|
||||
});
|
11
x-pack/examples/third_party_vis_lens_example/server/index.ts
Normal file
11
x-pack/examples/third_party_vis_lens_example/server/index.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializer } from 'kibana/server';
|
||||
import { ThirdPartyVisLensExamplePlugin } from './plugin';
|
||||
|
||||
export const plugin: PluginInitializer<void, void> = () => new ThirdPartyVisLensExamplePlugin();
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { Plugin, CoreSetup } from 'kibana/server';
|
||||
import { LensServerPluginSetup } from '../../../plugins/lens/server';
|
||||
import { DEFAULT_COLOR } from '../common/constants';
|
||||
import { RotatingNumberState as Post81RotatingNumberState } from '../common/types';
|
||||
|
||||
// Old versions of this visualization had a slightly different shape of state
|
||||
interface Pre81RotatingNumberState {
|
||||
column?: string;
|
||||
layerId: string;
|
||||
}
|
||||
|
||||
// this plugin's dependencies
|
||||
export interface Dependencies {
|
||||
lens: LensServerPluginSetup;
|
||||
}
|
||||
|
||||
export class ThirdPartyVisLensExamplePlugin implements Plugin<void, void, Dependencies> {
|
||||
public setup(core: CoreSetup, { lens }: Dependencies) {
|
||||
lens.registerVisualizationMigration('rotatingNumber', () => ({
|
||||
// Example state migration which will be picked by all the places Lens visualizations are stored
|
||||
'8.1.0': (oldState: Pre81RotatingNumberState): Post81RotatingNumberState => {
|
||||
return {
|
||||
// column gets renamed to accessor
|
||||
accessor: oldState.column,
|
||||
// layer id just gets copied over
|
||||
layerId: oldState.layerId,
|
||||
// color gets pre-set with default color
|
||||
color: DEFAULT_COLOR,
|
||||
};
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
public start() {}
|
||||
public stop() {}
|
||||
}
|
24
x-pack/examples/third_party_vis_lens_example/tsconfig.json
Normal file
24
x-pack/examples/third_party_vis_lens_example/tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./target/types"
|
||||
},
|
||||
"include": [
|
||||
"index.ts",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"common/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"exclude": [],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/expressions/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/kibana_react/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data_views/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/field_formats/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/embeddable/tsconfig.json" },
|
||||
{ "path": "../../plugins/lens/tsconfig.json" },
|
||||
{ "path": "../../../examples/developer_examples/tsconfig.json" },
|
||||
]
|
||||
}
|
|
@ -823,7 +823,7 @@ describe('common utils', () => {
|
|||
].join('\n\n');
|
||||
|
||||
const extractedReferences = extractLensReferencesFromCommentString(
|
||||
makeLensEmbeddableFactory(() => ({})),
|
||||
makeLensEmbeddableFactory(() => ({}), {}),
|
||||
commentString
|
||||
);
|
||||
|
||||
|
@ -922,7 +922,7 @@ describe('common utils', () => {
|
|||
].join('\n\n');
|
||||
|
||||
const updatedReferences = getOrUpdateLensReferences(
|
||||
makeLensEmbeddableFactory(() => ({})),
|
||||
makeLensEmbeddableFactory(() => ({}), {}),
|
||||
newCommentString,
|
||||
{
|
||||
references: currentCommentReferences,
|
||||
|
|
|
@ -36,7 +36,7 @@ import { GENERATED_ALERT, SUB_CASE_SAVED_OBJECT } from './constants';
|
|||
|
||||
describe('comments migrations', () => {
|
||||
const migrations = createCommentsMigrations({
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({})),
|
||||
lensEmbeddableFactory: makeLensEmbeddableFactory(() => ({}), {}),
|
||||
});
|
||||
|
||||
const contextMock = savedObjectsServiceMock.createMigrationContext();
|
||||
|
|
|
@ -24,6 +24,7 @@ import { TableDimensionEditor } from './components/dimension_editor';
|
|||
import { CUSTOM_PALETTE } from '../shared_components/coloring/constants';
|
||||
import { LayerType, layerTypes } from '../../common';
|
||||
import { getDefaultSummaryLabel, PagingState } from '../../common/expressions';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
|
||||
import type { ColumnState, SortingState } from '../../common/expressions';
|
||||
import { DataTableToolbar } from './components/toolbar';
|
||||
export interface DatatableVisualizationState {
|
||||
|
@ -84,6 +85,8 @@ export const getDatatableVisualization = ({
|
|||
|
||||
switchVisualizationType: (_, state) => state,
|
||||
|
||||
triggers: [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick],
|
||||
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
|
|
|
@ -47,6 +47,9 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
const visualization = useLensSelector(selectVisualization);
|
||||
const areDatasourcesLoaded = useLensSelector(selectAreDatasourcesLoaded);
|
||||
const isVisualizationLoaded = !!visualization.state;
|
||||
const visualizationTypeIsKnown = Boolean(
|
||||
visualization.activeId && props.visualizationMap[visualization.activeId]
|
||||
);
|
||||
const framePublicAPI: FramePublicAPI = useLensSelector((state) =>
|
||||
selectFramePublicAPI(state, datasourceMap)
|
||||
);
|
||||
|
@ -121,6 +124,7 @@ export function EditorFrame(props: EditorFrameProps) {
|
|||
)
|
||||
}
|
||||
suggestionsPanel={
|
||||
visualizationTypeIsKnown &&
|
||||
areDatasourcesLoaded && (
|
||||
<SuggestionPanelWrapper
|
||||
ExpressionRenderer={props.ExpressionRenderer}
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
getMissingCurrentDatasource,
|
||||
getMissingIndexPatterns,
|
||||
getMissingVisualizationTypeError,
|
||||
getUnknownVisualizationTypeError,
|
||||
} from '../error_helper';
|
||||
import { DatasourceStates } from '../../state_management';
|
||||
|
||||
|
@ -97,6 +98,12 @@ export async function persistedStateToExpression(
|
|||
errors: [{ shortMessage: '', longMessage: getMissingVisualizationTypeError() }],
|
||||
};
|
||||
}
|
||||
if (!visualizations[visualizationType]) {
|
||||
return {
|
||||
ast: null,
|
||||
errors: [getUnknownVisualizationTypeError(visualizationType)],
|
||||
};
|
||||
}
|
||||
const visualization = visualizations[visualizationType!];
|
||||
const datasourceStates = await initializeDatasources(
|
||||
datasourceMap,
|
||||
|
|
|
@ -247,10 +247,13 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) {
|
|||
if (!flyoutOpen) {
|
||||
return { visualizationTypes: [], visualizationsLookup: {} };
|
||||
}
|
||||
const subVisualizationId = getCurrentVisualizationId(
|
||||
props.visualizationMap[visualization.activeId || ''],
|
||||
visualization.state
|
||||
);
|
||||
const subVisualizationId =
|
||||
visualization.activeId && props.visualizationMap[visualization.activeId]
|
||||
? getCurrentVisualizationId(
|
||||
props.visualizationMap[visualization.activeId],
|
||||
visualization.state
|
||||
)
|
||||
: undefined;
|
||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||
// reorganize visualizations in groups
|
||||
const grouped: Record<
|
||||
|
|
|
@ -46,7 +46,10 @@ import { UiActionsStart } from '../../../../../../../src/plugins/ui_actions/publ
|
|||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../../src/plugins/visualizations/public';
|
||||
import { WorkspacePanelWrapper } from './workspace_panel_wrapper';
|
||||
import { DropIllustration } from '../../../assets/drop_illustration';
|
||||
import { getOriginalRequestErrorMessages } from '../../error_helper';
|
||||
import {
|
||||
getOriginalRequestErrorMessages,
|
||||
getUnknownVisualizationTypeError,
|
||||
} from '../../error_helper';
|
||||
import { getMissingIndexPattern, validateDatasourceAndVisualization } from '../state_helpers';
|
||||
import { DefaultInspectorAdapters } from '../../../../../../../src/plugins/expressions/common';
|
||||
import {
|
||||
|
@ -163,6 +166,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
]
|
||||
: [];
|
||||
|
||||
const unknownVisError = visualization.activeId && !activeVisualization;
|
||||
|
||||
// Note: mind to all these eslint disable lines: the frameAPI will change too frequently
|
||||
// and to prevent race conditions it is ok to leave them there.
|
||||
|
||||
|
@ -180,7 +185,7 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
);
|
||||
|
||||
const expression = useMemo(() => {
|
||||
if (!configurationValidationError?.length && !missingRefsErrors.length) {
|
||||
if (!configurationValidationError?.length && !missingRefsErrors.length && !unknownVisError) {
|
||||
try {
|
||||
const ast = buildExpression({
|
||||
visualization: activeVisualization,
|
||||
|
@ -213,6 +218,12 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
}));
|
||||
}
|
||||
}
|
||||
if (unknownVisError) {
|
||||
setLocalState((s) => ({
|
||||
...s,
|
||||
expressionBuildError: [getUnknownVisualizationTypeError(visualization.activeId!)],
|
||||
}));
|
||||
}
|
||||
}, [
|
||||
activeVisualization,
|
||||
visualization.state,
|
||||
|
@ -221,6 +232,8 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({
|
|||
datasourceLayers,
|
||||
configurationValidationError?.length,
|
||||
missingRefsErrors.length,
|
||||
unknownVisError,
|
||||
visualization.activeId,
|
||||
]);
|
||||
|
||||
const expressionExists = Boolean(expression);
|
||||
|
@ -415,6 +428,7 @@ export const VisualizationWrapper = ({
|
|||
fixAction?: DatasourceFixAction<unknown>;
|
||||
}>;
|
||||
missingRefsErrors?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
|
||||
unknownVisError?: Array<{ shortMessage: string; longMessage: React.ReactNode }>;
|
||||
};
|
||||
ExpressionRendererComponent: ReactExpressionRendererType;
|
||||
application: ApplicationStart;
|
||||
|
|
|
@ -165,3 +165,17 @@ export function getMissingIndexPatterns(indexPatternIds: string[]) {
|
|||
values: { count: indexPatternIds.length, ids: indexPatternIds.join(', ') },
|
||||
});
|
||||
}
|
||||
|
||||
export function getUnknownVisualizationTypeError(visType: string) {
|
||||
return {
|
||||
shortMessage: i18n.translate('xpack.lens.unknownVisType.shortMessage', {
|
||||
defaultMessage: `Unknown visualization type`,
|
||||
}),
|
||||
longMessage: i18n.translate('xpack.lens.unknownVisType.longMessage', {
|
||||
defaultMessage: `The visualization type {visType} could not be resolved.`,
|
||||
values: {
|
||||
visType,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -265,22 +265,10 @@ export class Embeddable
|
|||
}
|
||||
|
||||
public supportedTriggers() {
|
||||
if (!this.savedVis) {
|
||||
if (!this.savedVis || !this.savedVis.visualizationType) {
|
||||
return [];
|
||||
}
|
||||
switch (this.savedVis.visualizationType) {
|
||||
case 'lnsXY':
|
||||
case 'lnsHeatmap':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
|
||||
case 'lnsDatatable':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick];
|
||||
case 'lnsPie':
|
||||
return [VIS_EVENT_TO_TRIGGER.filter];
|
||||
case 'lnsGauge':
|
||||
case 'lnsMetric':
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
return this.deps.visualizationMap[this.savedVis.visualizationType]?.triggers || [];
|
||||
}
|
||||
|
||||
public getInspectorAdapters() {
|
||||
|
|
|
@ -52,7 +52,8 @@ export type TypedLensByValueInput = Omit<LensByValueInput, 'attributes'> & {
|
|||
| LensAttributes<'lnsDatatable', DatatableVisualizationState>
|
||||
| LensAttributes<'lnsMetric', MetricState>
|
||||
| LensAttributes<'lnsHeatmap', HeatmapVisualizationState>
|
||||
| LensAttributes<'lnsGauge', GaugeVisualizationState>;
|
||||
| LensAttributes<'lnsGauge', GaugeVisualizationState>
|
||||
| LensAttributes<string, unknown>;
|
||||
};
|
||||
|
||||
export type EmbeddableComponentProps = (TypedLensByValueInput | LensByReferenceInput) & {
|
||||
|
|
|
@ -12,7 +12,7 @@ export type {
|
|||
TypedLensByValueInput,
|
||||
} from './embeddable/embeddable_component';
|
||||
export type { XYState } from './xy_visualization/types';
|
||||
export type { DataType, OperationMetadata } from './types';
|
||||
export type { DataType, OperationMetadata, Visualization } from './types';
|
||||
export type {
|
||||
PieVisualizationState,
|
||||
PieLayerState,
|
||||
|
@ -61,7 +61,8 @@ export type {
|
|||
StaticValueIndexPatternColumn,
|
||||
} from './indexpattern_datasource/types';
|
||||
export type { LensEmbeddableInput } from './embeddable';
|
||||
export { layerTypes } from '../common';
|
||||
|
||||
export type { LensPublicStart } from './plugin';
|
||||
export type { LensPublicStart, LensPublicSetup } from './plugin';
|
||||
|
||||
export const plugin = () => new LensPlugin();
|
||||
|
|
|
@ -12,6 +12,7 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n-react';
|
|||
import type { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { ThemeServiceStart } from 'kibana/public';
|
||||
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
|
||||
import type {
|
||||
Visualization,
|
||||
OperationMetadata,
|
||||
|
@ -102,6 +103,8 @@ export const getPieVisualization = ({
|
|||
shape: visualizationTypeId as PieVisualizationState['shape'],
|
||||
}),
|
||||
|
||||
triggers: [VIS_EVENT_TO_TRIGGER.filter],
|
||||
|
||||
initialize(addNewLayer, state, mainPalette) {
|
||||
return (
|
||||
state || {
|
||||
|
|
|
@ -70,7 +70,7 @@ import {
|
|||
} from '../../../../src/plugins/ui_actions/public';
|
||||
import { APP_ID, getEditPath, NOT_INTERNATIONALIZED_PRODUCT_NAME } from '../common/constants';
|
||||
import type { FormatFactory } from '../common/types';
|
||||
import type { VisualizationType } from './types';
|
||||
import type { Visualization, VisualizationType, EditorFrameSetup } from './types';
|
||||
import { getLensAliasConfig } from './vis_type_alias';
|
||||
import { visualizeFieldAction } from './trigger_actions/visualize_field_actions';
|
||||
|
||||
|
@ -117,6 +117,23 @@ export interface LensPluginStartDependencies {
|
|||
usageCollection?: UsageCollectionStart;
|
||||
}
|
||||
|
||||
export interface LensPublicSetup {
|
||||
/**
|
||||
* Register 3rd party visualization type
|
||||
* See `x-pack/examples/3rd_party_lens_vis` for exemplary usage.
|
||||
*
|
||||
* In case the visualization is a function returning a promise, it will only be called once Lens is actually requiring it.
|
||||
* This can be used to lazy-load parts of the code to keep the initial bundle as small as possible.
|
||||
*
|
||||
* This API might undergo breaking changes even in minor versions.
|
||||
*
|
||||
* @experimental
|
||||
*/
|
||||
registerVisualization: <T>(
|
||||
visualization: Visualization<T> | (() => Promise<Visualization<T>>)
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface LensPublicStart {
|
||||
/**
|
||||
* React component which can be used to embed a Lens visualization into another application.
|
||||
|
@ -173,6 +190,8 @@ export interface LensPublicStart {
|
|||
export class LensPlugin {
|
||||
private datatableVisualization: DatatableVisualizationType | undefined;
|
||||
private editorFrameService: EditorFrameServiceType | undefined;
|
||||
private editorFrameSetup: EditorFrameSetup | undefined;
|
||||
private queuedVisualizations: Array<Visualization | (() => Promise<Visualization>)> = [];
|
||||
private indexpatternDatasource: IndexPatternDatasourceType | undefined;
|
||||
private xyVisualization: XyVisualizationType | undefined;
|
||||
private metricVisualization: MetricVisualizationType | undefined;
|
||||
|
@ -301,6 +320,17 @@ export class LensPlugin {
|
|||
}
|
||||
|
||||
urlForwarding.forwardApp('lens', 'lens');
|
||||
|
||||
return {
|
||||
registerVisualization: (vis: Visualization | (() => Promise<Visualization>)) => {
|
||||
if (this.editorFrameSetup) {
|
||||
this.editorFrameSetup.registerVisualization(vis);
|
||||
} else {
|
||||
// queue visualizations if editor frame is not yet ready as it's loaded async
|
||||
this.queuedVisualizations.push(vis);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async initParts(
|
||||
|
@ -351,6 +381,11 @@ export class LensPlugin {
|
|||
this.pieVisualization.setup(core, dependencies);
|
||||
this.heatmapVisualization.setup(core, dependencies);
|
||||
this.gaugeVisualization.setup(core, dependencies);
|
||||
|
||||
this.queuedVisualizations.forEach((queuedVis) => {
|
||||
editorFrameSetupInterface.registerVisualization(queuedVis);
|
||||
});
|
||||
this.editorFrameSetup = editorFrameSetupInterface;
|
||||
}
|
||||
|
||||
start(core: CoreStart, startDependencies: LensPluginStartDependencies): LensPublicStart {
|
||||
|
|
|
@ -654,6 +654,10 @@ export interface Visualization<T = unknown> {
|
|||
|
||||
getMainPalette?: (state: T) => undefined | PaletteOutput;
|
||||
|
||||
/**
|
||||
* Supported triggers of this visualization type when embedded somewhere
|
||||
*/
|
||||
triggers?: string[];
|
||||
/**
|
||||
* Visualizations must provide at least one type for the chart switcher,
|
||||
* but can register multiple subtypes
|
||||
|
|
|
@ -15,6 +15,7 @@ import { PaletteRegistry } from 'src/plugins/charts/public';
|
|||
import { FieldFormatsStart } from 'src/plugins/field_formats/public';
|
||||
import { ThemeServiceStart } from 'kibana/public';
|
||||
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
|
||||
import { getSuggestions } from './xy_suggestions';
|
||||
import { XyToolbar, DimensionEditor } from './xy_config_panel';
|
||||
import { LayerHeader } from './xy_config_panel/layer_header';
|
||||
|
@ -177,6 +178,8 @@ export const getXyVisualization = ({
|
|||
|
||||
getSuggestions,
|
||||
|
||||
triggers: [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush],
|
||||
|
||||
initialize(addNewLayer, state) {
|
||||
return (
|
||||
state || {
|
||||
|
|
|
@ -13,11 +13,11 @@ import { GetMigrationFunctionObjectFn } from 'src/plugins/kibana_utils/common';
|
|||
|
||||
describe('embeddable migrations', () => {
|
||||
test('should have all saved object migrations versions (>7.13.0)', () => {
|
||||
const savedObjectMigrationVersions = Object.keys(getAllMigrations({})).filter((version) => {
|
||||
const savedObjectMigrationVersions = Object.keys(getAllMigrations({}, {})).filter((version) => {
|
||||
return semverGte(version, '7.13.1');
|
||||
});
|
||||
const embeddableMigrationVersions = (
|
||||
makeLensEmbeddableFactory(() => ({}))()?.migrations as GetMigrationFunctionObjectFn
|
||||
makeLensEmbeddableFactory(() => ({}), {})()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
if (embeddableMigrationVersions) {
|
||||
expect(savedObjectMigrationVersions.sort()).toEqual(
|
||||
|
@ -47,14 +47,17 @@ describe('embeddable migrations', () => {
|
|||
};
|
||||
|
||||
const migrations = (
|
||||
makeLensEmbeddableFactory(() => ({
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
},
|
||||
}))()?.migrations as GetMigrationFunctionObjectFn
|
||||
makeLensEmbeddableFactory(
|
||||
() => ({
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{}
|
||||
)()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
|
||||
const migratedLensDoc = migrations[migrationVersion](lensVisualizationDoc);
|
||||
|
@ -76,4 +79,57 @@ describe('embeddable migrations', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should properly apply a custom visualization migration', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
visualizationType: 'abc',
|
||||
state: {
|
||||
visualization: { oldState: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationFn = jest.fn((oldState: { oldState: boolean }) => ({
|
||||
newState: oldState.oldState,
|
||||
}));
|
||||
|
||||
const embeddableMigrationVersions = (
|
||||
makeLensEmbeddableFactory(() => ({}), {
|
||||
abc: () => ({
|
||||
[migrationVersion]: migrationFn,
|
||||
}),
|
||||
})()?.migrations as GetMigrationFunctionObjectFn
|
||||
)();
|
||||
|
||||
const migratedLensDoc = embeddableMigrationVersions?.[migrationVersion](lensVisualizationDoc);
|
||||
const otherLensDoc = embeddableMigrationVersions?.[migrationVersion]({
|
||||
...lensVisualizationDoc,
|
||||
attributes: {
|
||||
...lensVisualizationDoc.attributes,
|
||||
visualizationType: 'def',
|
||||
},
|
||||
});
|
||||
|
||||
expect(migrationFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
visualizationType: 'abc',
|
||||
state: {
|
||||
visualization: { newState: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(otherLensDoc).toEqual({
|
||||
attributes: {
|
||||
visualizationType: 'def',
|
||||
state: {
|
||||
visualization: { oldState: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,9 +19,11 @@ import {
|
|||
commonRenameOperationsForFormula,
|
||||
commonRenameRecordsField,
|
||||
commonUpdateVisLayerType,
|
||||
getLensCustomVisualizationMigrations,
|
||||
getLensFilterMigrations,
|
||||
} from '../migrations/common_migrations';
|
||||
import {
|
||||
CustomVisualizationMigrations,
|
||||
LensDocShape713,
|
||||
LensDocShape715,
|
||||
LensDocShapePre712,
|
||||
|
@ -31,55 +33,64 @@ import {
|
|||
import { extract, inject } from '../../common/embeddable_factory';
|
||||
|
||||
export const makeLensEmbeddableFactory =
|
||||
(getFilterMigrations: () => MigrateFunctionsObject) => (): EmbeddableRegistryDefinition => {
|
||||
(
|
||||
getFilterMigrations: () => MigrateFunctionsObject,
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
) =>
|
||||
(): EmbeddableRegistryDefinition => {
|
||||
return {
|
||||
id: DOC_TYPE,
|
||||
migrations: () =>
|
||||
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisStatePre715> };
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.1.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715 };
|
||||
const migratedLensState = commonRenameRecordsField(
|
||||
commonRenameFilterReferences(lensState.attributes)
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
}),
|
||||
mergeMigrationFunctionMaps(
|
||||
mergeMigrationFunctionMaps(getLensFilterMigrations(getFilterMigrations()), {
|
||||
// This migration is run in 7.13.1 for `by value` panels because the 7.13 release window was missed.
|
||||
'7.13.1': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShapePre712 };
|
||||
const migratedLensState = commonRenameOperationsForFormula(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.14.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape713 };
|
||||
const migratedLensState = commonRemoveTimezoneDateHistogramParam(
|
||||
lensState.attributes
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.15.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisStatePre715> };
|
||||
const migratedLensState = commonUpdateVisLayerType(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'7.16.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715<VisState716> };
|
||||
const migratedLensState = commonMakeReversePaletteAsCustom(lensState.attributes);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
'8.1.0': (state) => {
|
||||
const lensState = state as unknown as { attributes: LensDocShape715 };
|
||||
const migratedLensState = commonRenameRecordsField(
|
||||
commonRenameFilterReferences(lensState.attributes)
|
||||
);
|
||||
return {
|
||||
...lensState,
|
||||
attributes: migratedLensState,
|
||||
} as unknown as SerializableRecord;
|
||||
},
|
||||
}),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
),
|
||||
extract,
|
||||
inject,
|
||||
};
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
|
||||
import { cloneDeep, mapValues } from 'lodash';
|
||||
import { PaletteOutput } from 'src/plugins/charts/common';
|
||||
import { MigrateFunctionsObject } from '../../../../../src/plugins/kibana_utils/common';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import {
|
||||
mergeMigrationFunctionMaps,
|
||||
MigrateFunction,
|
||||
MigrateFunctionsObject,
|
||||
} from '../../../../../src/plugins/kibana_utils/common';
|
||||
import {
|
||||
LensDocShapePre712,
|
||||
OperationTypePre712,
|
||||
|
@ -18,6 +23,7 @@ import {
|
|||
VisStatePost715,
|
||||
VisStatePre715,
|
||||
VisState716,
|
||||
CustomVisualizationMigrations,
|
||||
LensDocShape810,
|
||||
} from './types';
|
||||
import { CustomPaletteParams, DOCUMENT_FIELD_NAME, layerTypes } from '../../common';
|
||||
|
@ -186,6 +192,51 @@ export const commonRenameFilterReferences = (attributes: LensDocShape715): LensD
|
|||
return newAttributes as LensDocShape810;
|
||||
};
|
||||
|
||||
const getApplyCustomVisualizationMigrationToLens = (id: string, migration: MigrateFunction) => {
|
||||
return (savedObject: { attributes: LensDocShape }) => {
|
||||
if (savedObject.attributes.visualizationType !== id) return savedObject;
|
||||
return {
|
||||
...savedObject,
|
||||
attributes: {
|
||||
...savedObject.attributes,
|
||||
state: {
|
||||
...savedObject.attributes.state,
|
||||
visualization: migration(
|
||||
savedObject.attributes.state.visualization as SerializableRecord
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This creates a migration map that applies custom visualization migrations
|
||||
*/
|
||||
export const getLensCustomVisualizationMigrations = (
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
) => {
|
||||
return Object.entries(customVisualizationMigrations)
|
||||
.map(([id, migrationGetter]) => {
|
||||
const migrationMap: MigrateFunctionsObject = {};
|
||||
const currentMigrations = migrationGetter();
|
||||
for (const version in currentMigrations) {
|
||||
if (currentMigrations.hasOwnProperty(version)) {
|
||||
migrationMap[version] = getApplyCustomVisualizationMigrationToLens(
|
||||
id,
|
||||
currentMigrations[version]
|
||||
);
|
||||
}
|
||||
}
|
||||
return migrationMap;
|
||||
})
|
||||
.reduce(
|
||||
(fullMigrationMap, currentVisualizationTypeMigrationMap) =>
|
||||
mergeMigrationFunctionMaps(fullMigrationMap, currentVisualizationTypeMigrationMap),
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This creates a migration map that applies filter migrations to Lens visualizations
|
||||
*/
|
||||
|
|
|
@ -24,7 +24,7 @@ import { PaletteOutput } from 'src/plugins/charts/common';
|
|||
import { Filter } from '@kbn/es-query';
|
||||
|
||||
describe('Lens migrations', () => {
|
||||
const migrations = getAllMigrations({});
|
||||
const migrations = getAllMigrations({}, {});
|
||||
describe('7.7.0 missing dimensions in XY', () => {
|
||||
const context = {} as SavedObjectMigrationContext;
|
||||
|
||||
|
@ -1611,14 +1611,17 @@ describe('Lens migrations', () => {
|
|||
},
|
||||
};
|
||||
|
||||
const migrationFunctionsObject = getAllMigrations({
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
const migrationFunctionsObject = getAllMigrations(
|
||||
{
|
||||
[migrationVersion]: (filters: Filter[]) => {
|
||||
return filters.map((filterState) => ({
|
||||
...filterState,
|
||||
migrated: true,
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
{}
|
||||
);
|
||||
|
||||
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
|
||||
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
|
||||
|
@ -1642,4 +1645,63 @@ describe('Lens migrations', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should properly apply a custom visualization migration', () => {
|
||||
const migrationVersion = 'some-version';
|
||||
|
||||
const lensVisualizationDoc = {
|
||||
attributes: {
|
||||
visualizationType: 'abc',
|
||||
state: {
|
||||
visualization: { oldState: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const migrationFn = jest.fn((oldState: { oldState: boolean }) => ({
|
||||
newState: oldState.oldState,
|
||||
}));
|
||||
|
||||
const migrationFunctionsObject = getAllMigrations(
|
||||
{},
|
||||
{
|
||||
abc: () => ({
|
||||
[migrationVersion]: migrationFn,
|
||||
}),
|
||||
}
|
||||
);
|
||||
const migratedLensDoc = migrationFunctionsObject[migrationVersion](
|
||||
lensVisualizationDoc as SavedObjectUnsanitizedDoc,
|
||||
{} as SavedObjectMigrationContext
|
||||
);
|
||||
const otherLensDoc = migrationFunctionsObject[migrationVersion](
|
||||
{
|
||||
...lensVisualizationDoc,
|
||||
attributes: {
|
||||
...lensVisualizationDoc.attributes,
|
||||
visualizationType: 'def',
|
||||
},
|
||||
} as SavedObjectUnsanitizedDoc,
|
||||
{} as SavedObjectMigrationContext
|
||||
);
|
||||
|
||||
expect(migrationFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(migratedLensDoc).toEqual({
|
||||
attributes: {
|
||||
visualizationType: 'abc',
|
||||
state: {
|
||||
visualization: { newState: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(otherLensDoc).toEqual({
|
||||
attributes: {
|
||||
visualizationType: 'def',
|
||||
state: {
|
||||
visualization: { oldState: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
VisStatePost715,
|
||||
VisStatePre715,
|
||||
VisState716,
|
||||
CustomVisualizationMigrations,
|
||||
LensDocShape810,
|
||||
} from './types';
|
||||
import {
|
||||
|
@ -36,6 +37,7 @@ import {
|
|||
commonMakeReversePaletteAsCustom,
|
||||
commonRenameFilterReferences,
|
||||
getLensFilterMigrations,
|
||||
getLensCustomVisualizationMigrations,
|
||||
commonRenameRecordsField,
|
||||
} from './common_migrations';
|
||||
|
||||
|
@ -473,9 +475,13 @@ const lensMigrations: SavedObjectMigrationMap = {
|
|||
};
|
||||
|
||||
export const getAllMigrations = (
|
||||
filterMigrations: MigrateFunctionsObject
|
||||
filterMigrations: MigrateFunctionsObject,
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
): SavedObjectMigrationMap =>
|
||||
mergeSavedObjectMigrationMaps(
|
||||
lensMigrations,
|
||||
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
|
||||
mergeSavedObjectMigrationMaps(
|
||||
lensMigrations,
|
||||
getLensFilterMigrations(filterMigrations) as unknown as SavedObjectMigrationMap
|
||||
),
|
||||
getLensCustomVisualizationMigrations(customVisualizationMigrations)
|
||||
);
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
import type { PaletteOutput } from 'src/plugins/charts/common';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { Query } from 'src/plugins/data/public';
|
||||
import type { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common';
|
||||
import type { CustomPaletteParams, LayerType, PersistableFilter } from '../../common';
|
||||
|
||||
export type CustomVisualizationMigrations = Record<string, () => MigrateFunctionsObject>;
|
||||
|
||||
export type OperationTypePre712 =
|
||||
| 'avg'
|
||||
| 'cardinality'
|
||||
|
|
|
@ -14,6 +14,8 @@ import {
|
|||
} from 'src/plugins/data/server';
|
||||
import { ExpressionsServerSetup } from 'src/plugins/expressions/server';
|
||||
import { FieldFormatsStart } from 'src/plugins/field_formats/server';
|
||||
import type { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common';
|
||||
|
||||
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { setupRoutes } from './routes';
|
||||
import { getUiSettings } from './ui_settings';
|
||||
|
@ -26,6 +28,7 @@ import { setupSavedObjects } from './saved_objects';
|
|||
import { EmbeddableSetup } from '../../../../src/plugins/embeddable/server';
|
||||
import { setupExpressions } from './expressions';
|
||||
import { makeLensEmbeddableFactory } from './embeddable/make_lens_embeddable_factory';
|
||||
import type { CustomVisualizationMigrations } from './migrations/types';
|
||||
|
||||
export interface PluginSetupContract {
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
|
@ -43,11 +46,22 @@ export interface PluginStartContract {
|
|||
}
|
||||
|
||||
export interface LensServerPluginSetup {
|
||||
/**
|
||||
* Server side embeddable definition which provides migrations to run if Lens state is embedded into another saved object somewhere
|
||||
*/
|
||||
lensEmbeddableFactory: ReturnType<typeof makeLensEmbeddableFactory>;
|
||||
/**
|
||||
* Register custom migration functions for custom third party Lens visualizations
|
||||
*/
|
||||
registerVisualizationMigration: (
|
||||
id: string,
|
||||
migrationsGetter: () => MigrateFunctionsObject
|
||||
) => void;
|
||||
}
|
||||
|
||||
export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {}> {
|
||||
private readonly telemetryLogger: Logger;
|
||||
private customVisualizationMigrations: CustomVisualizationMigrations = {};
|
||||
|
||||
constructor(private initializerContext: PluginInitializerContext) {
|
||||
this.telemetryLogger = initializerContext.logger.get('usage');
|
||||
|
@ -57,7 +71,7 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
const getFilterMigrations = plugins.data.query.filterManager.getAllMigrations.bind(
|
||||
plugins.data.query.filterManager
|
||||
);
|
||||
setupSavedObjects(core, getFilterMigrations);
|
||||
setupSavedObjects(core, getFilterMigrations, this.customVisualizationMigrations);
|
||||
setupRoutes(core, this.initializerContext.logger.get());
|
||||
setupExpressions(core, plugins.expressions);
|
||||
core.uiSettings.register(getUiSettings());
|
||||
|
@ -72,10 +86,22 @@ export class LensServerPlugin implements Plugin<LensServerPluginSetup, {}, {}, {
|
|||
initializeLensTelemetry(this.telemetryLogger, core, plugins.taskManager);
|
||||
}
|
||||
|
||||
const lensEmbeddableFactory = makeLensEmbeddableFactory(getFilterMigrations);
|
||||
const lensEmbeddableFactory = makeLensEmbeddableFactory(
|
||||
getFilterMigrations,
|
||||
this.customVisualizationMigrations
|
||||
);
|
||||
plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory());
|
||||
return {
|
||||
lensEmbeddableFactory,
|
||||
registerVisualizationMigration: (
|
||||
id: string,
|
||||
migrationsGetter: () => MigrateFunctionsObject
|
||||
) => {
|
||||
if (this.customVisualizationMigrations[id]) {
|
||||
throw new Error(`Migrations object for visualization ${id} registered already`);
|
||||
}
|
||||
this.customVisualizationMigrations[id] = migrationsGetter;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,12 @@ import { CoreSetup } from 'kibana/server';
|
|||
import { MigrateFunctionsObject } from '../../../../src/plugins/kibana_utils/common';
|
||||
import { getEditPath } from '../common';
|
||||
import { getAllMigrations } from './migrations/saved_object_migrations';
|
||||
import { CustomVisualizationMigrations } from './migrations/types';
|
||||
|
||||
export function setupSavedObjects(
|
||||
core: CoreSetup,
|
||||
getFilterMigrations: () => MigrateFunctionsObject
|
||||
getFilterMigrations: () => MigrateFunctionsObject,
|
||||
customVisualizationMigrations: CustomVisualizationMigrations
|
||||
) {
|
||||
core.savedObjects.registerType({
|
||||
name: 'lens',
|
||||
|
@ -29,7 +31,7 @@ export function setupSavedObjects(
|
|||
uiCapabilitiesPath: 'visualize.show',
|
||||
}),
|
||||
},
|
||||
migrations: () => getAllMigrations(getFilterMigrations()),
|
||||
migrations: () => getAllMigrations(getFilterMigrations(), customVisualizationMigrations),
|
||||
mappings: {
|
||||
properties: {
|
||||
title: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue