diff --git a/x-pack/index.js b/x-pack/index.js index d5c5a5c51389d..6e7528082d888 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -38,7 +38,7 @@ import { translations } from './plugins/translations'; import { upgradeAssistant } from './plugins/upgrade_assistant'; import { uptime } from './plugins/uptime'; import { ossTelemetry } from './plugins/oss_telemetry'; -import { visualizationEditor } from './plugins/visualization_editor'; +import { visualizationLens } from './plugins/visualization_lens'; module.exports = function (kibana) { return [ @@ -76,6 +76,6 @@ module.exports = function (kibana) { upgradeAssistant(kibana), uptime(kibana), ossTelemetry(kibana), - visualizationEditor(kibana), + visualizationLens(kibana), ]; }; diff --git a/x-pack/plugins/visualization_editor/index.ts b/x-pack/plugins/visualization_editor/index.ts deleted file mode 100644 index 8c77fd2093e25..0000000000000 --- a/x-pack/plugins/visualization_editor/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as Joi from 'joi'; -import { resolve } from 'path'; -import { PLUGIN_ID } from './common'; - -export function visualizationEditor(kibana: any) { - return new kibana.Plugin({ - id: PLUGIN_ID, - configPrefix: `xpack.${PLUGIN_ID}`, - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - uiExports: { - app: { - title: 'Visualization Editor', - description: 'Explore and visualize data.', - main: `plugins/${PLUGIN_ID}/index`, - icon: 'plugins/kibana/assets/visualize.svg', - euiIconType: 'visualizeApp', - }, - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - }, - - config(joi: Joi.Root) { - return joi - .object({ - enabled: joi.boolean().default(true), - }) - .default(); - }, - }); -} \ No newline at end of file diff --git a/x-pack/plugins/visualization_editor/common/constants.ts b/x-pack/plugins/visualization_lens/common/constants.ts similarity index 83% rename from x-pack/plugins/visualization_editor/common/constants.ts rename to x-pack/plugins/visualization_lens/common/constants.ts index b7607abedf84c..147784cce5cc1 100644 --- a/x-pack/plugins/visualization_editor/common/constants.ts +++ b/x-pack/plugins/visualization_lens/common/constants.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN_ID = 'visualization_editor'; +export const PLUGIN_ID = 'visualization_lens'; diff --git a/x-pack/plugins/visualization_editor/common/index.ts b/x-pack/plugins/visualization_lens/common/index.ts similarity index 100% rename from x-pack/plugins/visualization_editor/common/index.ts rename to x-pack/plugins/visualization_lens/common/index.ts diff --git a/x-pack/plugins/visualization_lens/index.ts b/x-pack/plugins/visualization_lens/index.ts new file mode 100644 index 0000000000000..accf5e80b7f26 --- /dev/null +++ b/x-pack/plugins/visualization_lens/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Joi from 'joi'; +import { Server } from 'hapi'; +import { resolve } from 'path'; + +import { PLUGIN_ID } from './common'; + +const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; + +export const visualizationLens = (kibana: any) => { + return new kibana.Plugin({ + id: PLUGIN_ID, + configPrefix: `xpack.${PLUGIN_ID}`, + require: ['kibana', 'elasticsearch', 'xpack_main', 'interpreter'], + publicDir: resolve(__dirname, 'public'), + + uiExports: { + app: { + title: NOT_INTERNATIONALIZED_PRODUCT_NAME, + description: 'Explore and visualize data.', + main: `plugins/${PLUGIN_ID}/app`, + icon: 'plugins/kibana/assets/visualize.svg', + euiIconType: 'visualizeApp', + order: 8950, // Uptime is 8900 + }, + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + }, + + config: () => { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + + init(server: Server) { + server.plugins.xpack_main.registerFeature({ + id: PLUGIN_ID, + name: NOT_INTERNATIONALIZED_PRODUCT_NAME, + icon: 'visualizeApp', + navLinkId: PLUGIN_ID, + app: [PLUGIN_ID, 'kibana'], + catalogue: [PLUGIN_ID], + privileges: { + all: { + api: [PLUGIN_ID], + catalogue: [PLUGIN_ID], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + read: { + api: [PLUGIN_ID], + catalogue: [PLUGIN_ID], + savedObject: { + all: [], + read: [], + }, + ui: ['show'], + }, + }, + }); + }, + }); +}; + +export { editorFrame } from './public'; diff --git a/x-pack/plugins/visualization_editor/package.json b/x-pack/plugins/visualization_lens/package.json similarity index 68% rename from x-pack/plugins/visualization_editor/package.json rename to x-pack/plugins/visualization_lens/package.json index 34cd4146856c0..371152435e413 100644 --- a/x-pack/plugins/visualization_editor/package.json +++ b/x-pack/plugins/visualization_lens/package.json @@ -1,11 +1,11 @@ { "author": "Elastic", - "name": "visualization_editor", + "name": "visualization_lens", "version": "7.0.0", "private": true, "license": "Elastic-License", "devDependencies": {}, "dependencies": { - "@elastic/charts": "^3.11.0" + "@elastic/charts": "^4.0.0" } } \ No newline at end of file diff --git a/x-pack/plugins/visualization_editor/public/index.tsx b/x-pack/plugins/visualization_lens/public/app.tsx similarity index 54% rename from x-pack/plugins/visualization_editor/public/index.tsx rename to x-pack/plugins/visualization_lens/public/app.tsx index e65e404ec4133..e3ffa0447d07c 100644 --- a/x-pack/plugins/visualization_editor/public/index.tsx +++ b/x-pack/plugins/visualization_lens/public/app.tsx @@ -5,23 +5,42 @@ */ import { I18nProvider } from '@kbn/i18n/react'; -import React from 'react'; +import { IScope } from 'angular'; +import React, { useCallback } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import chrome from 'ui/chrome'; + import { PLUGIN_ID } from '../common'; -// TODO: Convert this to the "new platform" way of doing UI -function App($scope: any, $element: Element[]) { - const el = $element[0]; +import { editorFrame } from '.'; - $scope.$on('$destroy', () => unmountComponentAtNode(el)); +// Side effect of loading this is to register +import './indexpattern_datasource'; + +function Lens() { + const renderFrame = useCallback(node => { + if (node !== null) { + editorFrame.render(node); + } + }, []); - return render( + return ( <I18nProvider> - <h1>Visualization Editor</h1> - </I18nProvider>, - el + <div> + <h1>Lens</h1> + + <div ref={renderFrame} /> + </div> + </I18nProvider> ); } +// TODO: Convert this to the "new platform" way of doing UI +function App($scope: IScope, $element: JQLite) { + const el = $element[0]; + $scope.$on('$destroy', () => unmountComponentAtNode(el)); + + return render(<Lens />, el); +} + chrome.setRootController(PLUGIN_ID, App); diff --git a/x-pack/plugins/visualization_lens/public/editor_frame.tsx b/x-pack/plugins/visualization_lens/public/editor_frame.tsx new file mode 100644 index 0000000000000..8534eb3d3e375 --- /dev/null +++ b/x-pack/plugins/visualization_lens/public/editor_frame.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { render } from 'react-dom'; +import { Datasource, Visualization, EditorFrameAPI } from './types'; + +function EditorFrameComponent(props: { + datasources: Array<Datasource<unknown>>; + visualizations: Array<Visualization<unknown>>; +}) { + const renderDatasource = (datasource: Datasource<unknown>) => { + return useCallback( + node => { + datasource.renderDataPanel({ + domElement: node, + }); + }, + [datasource] + ); + }; + + return ( + <div> + <h2>Editor Frame</h2> + + {props.datasources.map((datasource, index) => ( + <div key={index} ref={renderDatasource(datasource)} /> + ))} + </div> + ); +} + +class EditorFrame { + constructor() {} + + private datasources: Array<Datasource<unknown>> = []; + private visualizations: Array<Visualization<unknown>> = []; + + public setup(): EditorFrameAPI { + return { + render: (domElement: Element) => { + render( + <EditorFrameComponent + datasources={this.datasources} + visualizations={this.visualizations} + />, + domElement + ); + }, + registerDatasource: (datasource: Datasource<unknown>) => { + this.datasources.push(datasource); + }, + registerVisualization: (visualization: Visualization<unknown>) => { + this.visualizations.push(visualization); + }, + }; + } + + public stop() { + return {}; + } +} + +export { EditorFrame }; + +export const editorFrame = new EditorFrame().setup(); diff --git a/x-pack/plugins/visualization_editor/public/index.scss b/x-pack/plugins/visualization_lens/public/index.scss similarity index 100% rename from x-pack/plugins/visualization_editor/public/index.scss rename to x-pack/plugins/visualization_lens/public/index.scss diff --git a/x-pack/plugins/visualization_lens/public/index.ts b/x-pack/plugins/visualization_lens/public/index.ts new file mode 100644 index 0000000000000..a48e0c74bb81f --- /dev/null +++ b/x-pack/plugins/visualization_lens/public/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './editor_frame'; +export * from './types'; diff --git a/x-pack/plugins/visualization_lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/visualization_lens/public/indexpattern_datasource/index.ts new file mode 100644 index 0000000000000..d996045cb3156 --- /dev/null +++ b/x-pack/plugins/visualization_lens/public/indexpattern_datasource/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; + * you may not use this file except in compliance with the Elastic License. + */ + +// TODO: Figure out how to separate this out into another plugin +import { editorFrame } from '../../'; + +import { indexPatternDatasource } from './indexpattern'; + +editorFrame.registerDatasource(indexPatternDatasource); + +export * from './indexpattern'; diff --git a/x-pack/plugins/visualization_lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/visualization_lens/public/indexpattern_datasource/indexpattern.tsx new file mode 100644 index 0000000000000..32c80aeb78c5a --- /dev/null +++ b/x-pack/plugins/visualization_lens/public/indexpattern_datasource/indexpattern.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { Datasource, Operation, DataType } from '../'; + +interface IndexPatternPrivateState { + query: object; +} + +class IndexPatternDatasource implements Datasource<IndexPatternPrivateState> { + private state?: IndexPatternPrivateState; + + constructor(state?: IndexPatternPrivateState) { + if (state) { + this.state = state; + } + } + + toExpression() { + return ''; + } + + renderDataPanel({ domElement }: { domElement: Element }) { + render(<div>Index Pattern Data Source</div>, domElement); + } + + getPublicAPI() { + return { + getTableSpec: () => [], + getOperationForColumnId: () => ({ + id: '', + // User-facing label for the operation + label: '', + dataType: 'string' as DataType, + // A bucketed operation has many values the same + isBucketed: false, + }), + + // Called by dimension + getDimensionPanelComponent: (props: any) => ( + domElement: Element, + operations: Operation[] + ) => {}, + + removeColumnInTableSpec: (columnId: string) => [], + moveColumnTo: (columnId: string, targetIndex: number) => {}, + duplicateColumn: (columnId: string) => [], + }; + } + + getDatasourceSuggestionsForField() { + return []; + } + + getDatasourceSuggestionsFromCurrentState() { + return []; + } +} + +export { IndexPatternDatasource }; + +export const indexPatternDatasource = new IndexPatternDatasource(); diff --git a/x-pack/plugins/visualization_lens/public/types.ts b/x-pack/plugins/visualization_lens/public/types.ts new file mode 100644 index 0000000000000..7cb1c1af69a31 --- /dev/null +++ b/x-pack/plugins/visualization_lens/public/types.ts @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface EditorFrameAPI { + render: (domElement: Element) => void; + registerDatasource: (datasource: Datasource<unknown>) => void; + registerVisualization: (visualization: Visualization<unknown>) => void; +} + +export interface EditorFrameState { + visualizations: { [id: string]: object }; + datasources: { [id: string]: object }; + + activeDatasourceId: string; + activeVisualizationId: string; +} + +// Hints the default nesting to the data source. 0 is the highest priority +export type DimensionPriority = 0 | 1 | 2; + +// For switching between visualizations and correctly matching columns +export type DimensionRole = + | 'splitChart' + | 'series' + | 'primary' + | 'secondary' + | 'color' + | 'size' + | string; // Some visualizations will use custom names that have other meaning + +export interface TableMetaInfo { + columnId: string; + operation: Operation; +} + +export interface DatasourceSuggestion<T> { + state: T; + tableMetas: TableMetaInfo[]; +} + +/** + * Interface for the datasource registry + */ +export interface Datasource<T> { + // For initializing from saved object + // initialize: (state?: T) => Promise<T>; + + renderDataPanel: (props: DatasourceDataPanelProps) => void; + + toExpression: (state: T) => string; + + getDatasourceSuggestionsForField: (state: T) => Array<DatasourceSuggestion<T>>; + getDatasourceSuggestionsFromCurrentState: (state: T) => Array<DatasourceSuggestion<T>>; + + getPublicAPI: (state: T, setState: (newState: T) => void) => DatasourcePublicAPI; + // publicAPI: { + // [key in keyof DatasourcePublicAPI]: ( + // state: T, + // setState: (newState: T) => void + // ) => DatasourcePublicAPI[key] + // }; +} + +/** + * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource + */ +export interface DatasourcePublicAPI { + getTableSpec: () => TableSpec; + getOperationForColumnId: () => Operation; + + // Called by dimension + getDimensionPanelComponent: ( + props: DatasourceDimensionPanelProps + ) => (domElement: Element, operations: Operation[]) => void; + + removeColumnInTableSpec: (columnId: string) => TableSpec; + moveColumnTo: (columnId: string, targetIndex: number) => void; + duplicateColumn: (columnId: string) => TableSpec; +} + +export interface DatasourceDataPanelProps { + domElement: Element; +} + +// The only way a visualization has to restrict the query building +export interface DatasourceDimensionPanelProps { + // If no columnId is passed, it will render as empty + columnId?: string; + + // Visualizations can restrict operations based on their own rules + filterOperations: (operation: Operation) => boolean; + + // Visualizations can hint at the role this dimension would play, which + // affects the ordering of the query + suggestedPriority?: DimensionPriority; +} + +export interface DatasourceValidationError { + type: 'error'; +} + +export interface DatasourceValidationValid { + type: 'valid'; +} + +export type DataType = 'string' | 'number' | 'date' | 'boolean'; + +// An operation does not represent a column in the datatable +export interface Operation { + // Operation ID is a reference to the operation + id: string; + // User-facing label for the operation + label: string; + dataType: DataType; + // A bucketed operation has many values the same + isBucketed: boolean; + + // Extra meta-information like cardinality, color +} + +export interface TableSpecColumn { + // Column IDs are the keys for internal state in data sources and visualizations + columnId: string; +} + +// TableSpec is managed by visualizations +export type TableSpec = TableSpecColumn[]; + +export type VisualizationTableRequest = Array<{ + dataType: 'string' | 'number'; + isBucketed: boolean; +}>; + +export interface VisualizationProps<T> { + datasource: DatasourcePublicAPI; + state: T; + setState: (newState: T) => void; +} + +export interface VisualizationSuggestion<T> { + score: number; + title: string; + state: T; + datasourceSuggestionId: string; +} + +export interface Visualization<T> { + // Used to switch to this visualization from another + getInitialStateFromOtherVisualization: ( + options: { + roles: DimensionRole[]; + datasource: DatasourcePublicAPI; + state?: T; + } + ) => T[]; + + renderConfigPanel: (props: VisualizationProps<T>) => void; + + toExpression: (state: T, datasource: DatasourcePublicAPI) => string; + + // For use in transitioning from one viz to another + getMappingOfTableToRoles: (state: T, datasource: DatasourcePublicAPI) => DimensionRole[]; + + // Filter suggestions from datasource to good suggestions, used for suggested visualizations + // Can be used to switch to a better visualization given the data table + getSuggestionsFromTableSpecs: ( + options: { + roles: DimensionRole[]; + tableMetas: { [datasourceSuggestionId: string]: TableMetaInfo }; + state?: T; // State is only passed if the visualization is active + } + ) => Array<VisualizationSuggestion<T>>; +} diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 979d0737d03a9..20f95464c5867 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -26,6 +26,9 @@ "plugins/spaces/*": [ "x-pack/plugins/spaces/public/*" ], + "plugins/visualization_lens": [ + "x-pack/plugins/visualization_lens/public/*" + ], "test_utils/*": [ "x-pack/test_utils/*" ] diff --git a/yarn.lock b/yarn.lock index 5cabe828eb0ca..74142439021e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1317,10 +1317,10 @@ lodash "^4.17.11" to-fast-properties "^2.0.0" -"@elastic/charts@^3.11.0": - version "3.11.3" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-3.11.3.tgz#5a7f00f620020593d7124774471833eafdac200b" - integrity sha512-rssmYs3IwjbBNMFaCfplt9Nub0mqO7Bjq0A7C1iF/E51P1F5ST3HxPI7nLSFWILfpGk5Jx4sqO+61Q6rEehbVg== +"@elastic/charts@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-4.0.0.tgz#321313873985deb69106d479ee6a827eb81c890a" + integrity sha512-j/DfpdsKOx/QEbWJ6CvLlLP6XUYWqAHyuho3+38HngnoZSPzLGLte/ymW1CnaNYthQGTS9/4a47aZCkvQ6EdBw== dependencies: "@types/d3-shape" "^1.3.1" "@types/luxon" "^1.11.1"