From 1438607f62fc8aa9e96c54ae4025365b0a1b1964 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Mon, 25 Apr 2022 13:10:56 -0500 Subject: [PATCH 01/34] script-based visualization plugin --- src/plugins/vis_type_script/README.md | 1 + src/plugins/vis_type_script/config.ts | 15 ++++ src/plugins/vis_type_script/jest.config.js | 16 ++++ src/plugins/vis_type_script/kibana.json | 13 ++++ .../vis_type_script/public/expression/fn.ts | 50 +++++++++++++ .../public/expression/renderer.tsx | 36 +++++++++ .../public/expression/to_ast.ts | 30 ++++++++ src/plugins/vis_type_script/public/index.ts | 14 ++++ src/plugins/vis_type_script/public/plugin.ts | 41 ++++++++++ src/plugins/vis_type_script/public/types.ts | 15 ++++ .../vis_definition/editor_options/script.tsx | 75 +++++++++++++++++++ .../editor_options/settings.tsx | 21 ++++++ .../editor_options/settings_lazy.tsx | 19 +++++ .../public/vis_definition/index.ts | 59 +++++++++++++++ src/plugins/vis_type_script/tsconfig.json | 21 ++++++ 15 files changed, 426 insertions(+) create mode 100644 src/plugins/vis_type_script/README.md create mode 100644 src/plugins/vis_type_script/config.ts create mode 100644 src/plugins/vis_type_script/jest.config.js create mode 100644 src/plugins/vis_type_script/kibana.json create mode 100644 src/plugins/vis_type_script/public/expression/fn.ts create mode 100644 src/plugins/vis_type_script/public/expression/renderer.tsx create mode 100644 src/plugins/vis_type_script/public/expression/to_ast.ts create mode 100644 src/plugins/vis_type_script/public/index.ts create mode 100644 src/plugins/vis_type_script/public/plugin.ts create mode 100644 src/plugins/vis_type_script/public/types.ts create mode 100644 src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx create mode 100644 src/plugins/vis_type_script/public/vis_definition/editor_options/settings.tsx create mode 100644 src/plugins/vis_type_script/public/vis_definition/editor_options/settings_lazy.tsx create mode 100644 src/plugins/vis_type_script/public/vis_definition/index.ts create mode 100644 src/plugins/vis_type_script/tsconfig.json diff --git a/src/plugins/vis_type_script/README.md b/src/plugins/vis_type_script/README.md new file mode 100644 index 00000000000000..cebeaf11d02ccd --- /dev/null +++ b/src/plugins/vis_type_script/README.md @@ -0,0 +1 @@ +The script-based visualization that can be used to write custom visualizations. diff --git a/src/plugins/vis_type_script/config.ts b/src/plugins/vis_type_script/config.ts new file mode 100644 index 00000000000000..b831d26854c304 --- /dev/null +++ b/src/plugins/vis_type_script/config.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), +}); + +export type ConfigSchema = TypeOf; diff --git a/src/plugins/vis_type_script/jest.config.js b/src/plugins/vis_type_script/jest.config.js new file mode 100644 index 00000000000000..835d37312eadd1 --- /dev/null +++ b/src/plugins/vis_type_script/jest.config.js @@ -0,0 +1,16 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/src/plugins/vis_type_markdown'], + coverageDirectory: '/target/kibana-coverage/jest/src/plugins/vis_type_markdown', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/src/plugins/vis_type_markdown/{public,server}/**/*.{ts,tsx}'], +}; diff --git a/src/plugins/vis_type_script/kibana.json b/src/plugins/vis_type_script/kibana.json new file mode 100644 index 00000000000000..d4a395baceb452 --- /dev/null +++ b/src/plugins/vis_type_script/kibana.json @@ -0,0 +1,13 @@ +{ + "id": "visTypeScript", + "owner": { + "name": "Kibana Presentation", + "githubTeam": "kibana-presentation" + }, + "description": "Adds a script-based visualization type", + "version": "kibana", + "ui": true, + "server": false, + "requiredPlugins": ["expressions", "visualizations"], + "requiredBundles": ["expressions", "visualizations", "visDefaultEditor"] +} diff --git a/src/plugins/vis_type_script/public/expression/fn.ts b/src/plugins/vis_type_script/public/expression/fn.ts new file mode 100644 index 00000000000000..6a81ef8dcc539b --- /dev/null +++ b/src/plugins/vis_type_script/public/expression/fn.ts @@ -0,0 +1,50 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import type { ExpressionFunctionDefinition, Render } from '../../../expressions/public'; +import type { Arguments } from '../types'; +import type { RenderValue } from './renderer'; + +type ScriptVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'scriptVis', + unknown, + Arguments, + Render +>; + +export const createScriptVisFn = (): ScriptVisExpressionFunctionDefinition => ({ + name: 'scriptVis', + type: 'render', + inputTypes: [], + help: i18n.translate('visTypeMarkdown.function.help', { + defaultMessage: 'Script-based visualization', + }), + args: { + script: { + types: ['string'], + aliases: ['_'], + required: true, + help: i18n.translate('visTypeMarkdown.function.markdown.help', { + defaultMessage: 'Visualization script', + }), + }, + }, + fn(input, args) { + return { + type: 'render', + as: 'script_vis', + value: { + visType: 'script', + visParams: { + script: args.script, + }, + }, + }; + }, +}); diff --git a/src/plugins/vis_type_script/public/expression/renderer.tsx b/src/plugins/vis_type_script/public/expression/renderer.tsx new file mode 100644 index 00000000000000..176acd07b9fd5d --- /dev/null +++ b/src/plugins/vis_type_script/public/expression/renderer.tsx @@ -0,0 +1,36 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import type { ExpressionRenderDefinition } from '../../../expressions'; +import { VisualizationContainer } from '../../../visualizations/public'; +import { VisParams } from '../types'; + +export interface RenderValue { + visType: 'script'; + visParams: VisParams; +} + +export const scriptVisRenderer: ExpressionRenderDefinition = { + name: 'script_vis', + displayName: 'script-based visualization', + reuseDomNode: true, + render: async (domNode, { visParams }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + render( + +
{visParams.script}
+
, + domNode + ); + }, +}; diff --git a/src/plugins/vis_type_script/public/expression/to_ast.ts b/src/plugins/vis_type_script/public/expression/to_ast.ts new file mode 100644 index 00000000000000..067d6474ba7060 --- /dev/null +++ b/src/plugins/vis_type_script/public/expression/to_ast.ts @@ -0,0 +1,30 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VisToExpressionAst } from '../../../visualizations/public'; +import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; +import type { ExpressionFunctionDefinition, Render } from '../../../expressions/public'; +import type { RenderValue } from './renderer'; +import { VisParams } from '../types'; + +type ScriptVisExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'scriptVis', + unknown, + {}, + Render +>; + +export const toExpressionAst: VisToExpressionAst = (vis) => { + const scriptVis = buildExpressionFunction('scriptVis', { + script: vis.params.script, + }); + + const ast = buildExpression([scriptVis]); + + return ast.toAst(); +}; diff --git a/src/plugins/vis_type_script/public/index.ts b/src/plugins/vis_type_script/public/index.ts new file mode 100644 index 00000000000000..fa0722133aba10 --- /dev/null +++ b/src/plugins/vis_type_script/public/index.ts @@ -0,0 +1,14 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext } from '../../../core/public'; +import { ScriptVisPlugin as Plugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new Plugin(initializerContext); +} diff --git a/src/plugins/vis_type_script/public/plugin.ts b/src/plugins/vis_type_script/public/plugin.ts new file mode 100644 index 00000000000000..e1dabdca79e816 --- /dev/null +++ b/src/plugins/vis_type_script/public/plugin.ts @@ -0,0 +1,41 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; +import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; +import { VisualizationsSetup } from '../../visualizations/public'; + +import { scriptVisDefinition } from './vis_definition'; +import { ConfigSchema } from '../config'; +import { scriptVisRenderer } from './expression/renderer'; +import { createScriptVisFn } from './expression/fn'; + +/** @internal */ +export interface ScriptVisPluginDependencies { + expressions: ReturnType; + visualizations: VisualizationsSetup; +} + +/** @internal */ +export class ScriptVisPlugin implements Plugin { + initializerContext: PluginInitializerContext; + + constructor(initializerContext: PluginInitializerContext) { + this.initializerContext = initializerContext; + } + + public setup(core: CoreSetup, { expressions, visualizations }: ScriptVisPluginDependencies) { + visualizations.createBaseVisualization(scriptVisDefinition); + expressions.registerRenderer(scriptVisRenderer); + expressions.registerFunction(createScriptVisFn); + } + + public start(core: CoreStart) { + // nothing to do here yet + } +} diff --git a/src/plugins/vis_type_script/public/types.ts b/src/plugins/vis_type_script/public/types.ts new file mode 100644 index 00000000000000..5f5e28e1297156 --- /dev/null +++ b/src/plugins/vis_type_script/public/types.ts @@ -0,0 +1,15 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface Arguments { + script: string; +} + +export interface VisParams { + script: Arguments['script']; +} diff --git a/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx new file mode 100644 index 00000000000000..f997c57d5540b7 --- /dev/null +++ b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx @@ -0,0 +1,75 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useCallback } from 'react'; +import { + EuiPanel, + EuiTitle, + EuiLink, + EuiTextArea, + EuiFlexGroup, + EuiFlexItem, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import type { VisEditorOptionsProps } from '../../../../visualizations/public'; +import type { VisParams } from '../../types'; + +function ScriptOptions({ stateParams, setValue }: VisEditorOptionsProps) { + const onScriptUpdate = useCallback( + ({ target: { value } }: React.ChangeEvent) => setValue('script', value), + [setValue] + ); + + return ( + + + + + + +

+ +

+
+
+ + + + + + + + +
+
+ + + + +
+
+ ); +} + +export { ScriptOptions }; diff --git a/src/plugins/vis_type_script/public/vis_definition/editor_options/settings.tsx b/src/plugins/vis_type_script/public/vis_definition/editor_options/settings.tsx new file mode 100644 index 00000000000000..7d35f0aab34e57 --- /dev/null +++ b/src/plugins/vis_type_script/public/vis_definition/editor_options/settings.tsx @@ -0,0 +1,21 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { EuiPanel } from '@elastic/eui'; + +import type { VisEditorOptionsProps } from '../../../../visualizations/public'; +import type { VisParams } from '../../types'; + +function SettingsOptions({ stateParams, setValue }: VisEditorOptionsProps) { + return Foo Options; +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { SettingsOptions as default }; diff --git a/src/plugins/vis_type_script/public/vis_definition/editor_options/settings_lazy.tsx b/src/plugins/vis_type_script/public/vis_definition/editor_options/settings_lazy.tsx new file mode 100644 index 00000000000000..762438bfe148ac --- /dev/null +++ b/src/plugins/vis_type_script/public/vis_definition/editor_options/settings_lazy.tsx @@ -0,0 +1,19 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { lazy, Suspense } from 'react'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +// @ts-ignore +const SettingsOptionsComponent = lazy(() => import('./settings')); + +export const SettingsOptions = (props: any) => ( + }> + + +); diff --git a/src/plugins/vis_type_script/public/vis_definition/index.ts b/src/plugins/vis_type_script/public/vis_definition/index.ts new file mode 100644 index 00000000000000..8765d3a076cd18 --- /dev/null +++ b/src/plugins/vis_type_script/public/vis_definition/index.ts @@ -0,0 +1,59 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +import { DefaultEditorSize } from '../../../vis_default_editor/public'; +import { VisGroups, VisTypeDefinition } from '../../../visualizations/public'; +import { ScriptOptions } from './editor_options/script'; +import { SettingsOptions } from './editor_options/settings_lazy'; +import { toExpressionAst } from '../expression/to_ast'; +import { VisParams } from '../types'; + +export const scriptVisDefinition: VisTypeDefinition = { + name: 'script', + title: 'Script-based', + isAccessible: true, + icon: 'visText', + group: VisGroups.TOOLS, + titleInWizard: i18n.translate('visTypeScript.titleInWizard', { + defaultMessage: 'Script-based', + }), + description: i18n.translate('visTypeScript.description', { + defaultMessage: 'Create custom visualizations with a script (e.g. D3).', + }), + toExpressionAst, + visConfig: { + defaults: {}, + }, + editorConfig: { + optionTabs: [ + { + name: 'advanced', + title: i18n.translate('visTypeMarkdown.tabs.dataText', { + defaultMessage: 'Data', + }), + editor: ScriptOptions, + }, + { + name: 'options', + title: i18n.translate('visTypeMarkdown.tabs.optionsText', { + defaultMessage: 'Options', + }), + editor: SettingsOptions, + }, + ], + enableAutoApply: true, + defaultSize: DefaultEditorSize.LARGE, + }, + options: { + showTimePicker: false, + showFilterBar: false, + }, + inspectorAdapters: {}, +}; diff --git a/src/plugins/vis_type_script/tsconfig.json b/src/plugins/vis_type_script/tsconfig.json new file mode 100644 index 00000000000000..7c32f449351957 --- /dev/null +++ b/src/plugins/vis_type_script/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "public/**/*", + "server/**/*", + "*.ts" + ], + "references": [ + { "path": "../../core/tsconfig.json" }, + { "path": "../expressions/tsconfig.json" }, + { "path": "../visualizations/tsconfig.json" }, + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../vis_default_editor/tsconfig.json" }, + ] +} From f4a068b065ac6f7f68360123264752fdc44b89d6 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Mon, 25 Apr 2022 15:37:58 -0500 Subject: [PATCH 02/34] make script entry fill vertical space --- .../public/vis_definition/editor_options/script.scss | 11 +++++++++++ .../public/vis_definition/editor_options/script.tsx | 4 +++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/plugins/vis_type_script/public/vis_definition/editor_options/script.scss diff --git a/src/plugins/vis_type_script/public/vis_definition/editor_options/script.scss b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.scss new file mode 100644 index 00000000000000..215dddcb88e0e0 --- /dev/null +++ b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.scss @@ -0,0 +1,11 @@ +.visEditor--script { + + .visEditorSidebar__config>*, + .visEditor--script__textarea { + flex-grow: 1; + } + + .scriptEditor { + height: 100%; + } +} \ No newline at end of file diff --git a/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx index f997c57d5540b7..4194baa58db5dd 100644 --- a/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx +++ b/src/plugins/vis_type_script/public/vis_definition/editor_options/script.tsx @@ -21,6 +21,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { VisEditorOptionsProps } from '../../../../visualizations/public'; import type { VisParams } from '../../types'; +import './script.scss'; + function ScriptOptions({ stateParams, setValue }: VisEditorOptionsProps) { const onScriptUpdate = useCallback( ({ target: { value } }: React.ChangeEvent) => setValue('script', value), @@ -29,7 +31,7 @@ function ScriptOptions({ stateParams, setValue }: VisEditorOptionsProps - + From ef7b52b9716a41cf1dbbc687700b76b83badfa32 Mon Sep 17 00:00:00 2001 From: Andrew Tate Date: Mon, 25 Apr 2022 15:38:27 -0500 Subject: [PATCH 03/34] Add sandbox script renderer --- .../public/expression/renderer.tsx | 3 +- .../vis_type_script/public/sandbox/index.tsx | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/plugins/vis_type_script/public/sandbox/index.tsx diff --git a/src/plugins/vis_type_script/public/expression/renderer.tsx b/src/plugins/vis_type_script/public/expression/renderer.tsx index 176acd07b9fd5d..69649fcd1f4c73 100644 --- a/src/plugins/vis_type_script/public/expression/renderer.tsx +++ b/src/plugins/vis_type_script/public/expression/renderer.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import type { ExpressionRenderDefinition } from '../../../expressions'; import { VisualizationContainer } from '../../../visualizations/public'; import { VisParams } from '../types'; +import { VisSandbox } from '../sandbox'; export interface RenderValue { visType: 'script'; @@ -28,7 +29,7 @@ export const scriptVisRenderer: ExpressionRenderDefinition = { render( -
{visParams.script}
+
, domNode ); diff --git a/src/plugins/vis_type_script/public/sandbox/index.tsx b/src/plugins/vis_type_script/public/sandbox/index.tsx new file mode 100644 index 00000000000000..3cb47eaf867a0c --- /dev/null +++ b/src/plugins/vis_type_script/public/sandbox/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; + +const getSandboxDocument = (script: string) => { + // may be possible to remove this iframe-level nonce once we can use the top-level CSP + // see https://github.com/elastic/kibana/issues/101579 for status tracking + const nonce = crypto.randomUUID(); + const d3Url = 'https://unpkg.com/d3@7.4.4/dist/d3.min.js'; + + const onLoadScript = `window.addEventListener('load', () => { + ${script} + })`; + + return ` + + + + + + + + + + + + + `; +}; + +export const VisSandbox: React.FunctionComponent<{ script: string }> = ({ + script: visualizationScript, +}: { + script: string; +}) => { + return ( +