From 220eefac80e969472f3f695892eab1254a1c52cc Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 12 Jul 2019 12:52:24 -0400 Subject: [PATCH 01/67] [lens] WIP add support for layers --- x-pack/legacy/plugins/lens/public/index.scss | 3 +- .../dimension_panel/dimension_panel.tsx | 5 +- .../indexpattern_plugin/indexpattern.tsx | 19 +- .../indexpattern_plugin/merge_tables.ts | 57 +++ .../operation_definitions/count.tsx | 13 +- .../operation_definitions/date_histogram.tsx | 6 +- .../operation_definitions/filter_ratio.tsx | 6 +- .../operation_definitions/metrics.tsx | 6 +- .../operation_definitions/terms.tsx | 6 +- .../public/indexpattern_plugin/operations.ts | 27 +- .../public/indexpattern_plugin/plugin.tsx | 2 + .../indexpattern_plugin/to_expression.ts | 148 ++++++-- x-pack/legacy/plugins/lens/public/types.ts | 32 +- .../_xy_config_panel.scss | 4 + .../_xy_expression.scss | 3 + .../public/xy_visualization_plugin/index.scss | 2 + .../multi_column_editor.tsx | 3 + .../public/xy_visualization_plugin/plugin.tsx | 5 +- .../xy_visualization_plugin/to_expression.ts | 55 +-- .../public/xy_visualization_plugin/types.ts | 139 +++++-- .../xy_config_panel.tsx | 338 ++++++++++++------ .../xy_expression.scss | 3 - .../xy_visualization_plugin/xy_expression.tsx | 113 +++--- .../xy_suggestions.test.ts | 8 +- .../xy_visualization_plugin/xy_suggestions.ts | 40 ++- .../xy_visualization.tsx | 21 +- 26 files changed, 728 insertions(+), 336 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss create mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.scss delete mode 100644 x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 6beb75e3b4dbb..6a95e26dfbfd8 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -1,7 +1,8 @@ // Import the EUI global scope so we can use EUI constants @import 'src/legacy/ui/public/styles/_styling_constants'; -@import './xy_visualization_plugin/xy_expression.scss'; +@import './xy_visualization_plugin/index'; +// @import './xy_visualization_plugin/xy_expression.scss'; @import './indexpattern_plugin/indexpattern'; @import './drag_drop/drag_drop.scss'; @import './editor_frame_plugin/editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 55c7cddd7f527..af56eba8fd186 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -10,7 +10,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { Storage } from 'ui/storage'; import { i18n } from '@kbn/i18n'; import { DataSetup } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { DatasourceDimensionPanelProps } from '../../types'; +import { DatasourceDimensionPanelProps, DimensionLayer } from '../../types'; import { IndexPatternColumn, IndexPatternPrivateState, @@ -29,10 +29,11 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { dragDropContext: DragContextState; dataPlugin: DataSetup; storage: Storage; + layer: DimensionLayer; }; export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { - const columns = getPotentialColumns(props.state, props.suggestedPriority); + const columns = getPotentialColumns(props); const filteredColumns = columns.filter(col => { return props.filterOperations(columnToOperation(col)); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 7328684d09777..29aca4abaedf7 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -47,6 +47,7 @@ export interface BaseIndexPatternColumn { // Private operationType: OperationType; suggestedOrder?: DimensionPriority; + layer: 'join' | number; } type Omit = Pick>; @@ -274,6 +275,9 @@ export function getIndexPatternDatasource({ getPublicAPI(state, setState) { return { + supportsLayers: true, + supportsLayerJoin: true, + getTableSpec: () => { return state.columnOrder.map(colId => ({ columnId: colId })); }, @@ -291,6 +295,7 @@ export function getIndexPatternDatasource({ setState={newState => setState(newState)} dataPlugin={data} storage={storage} + layer={props.layer || 0} {...props} /> , @@ -325,9 +330,9 @@ export function getIndexPatternDatasource({ const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); if (hasBucket) { - const column = buildColumnForOperationType(0, hasBucket, undefined, field); + const column = buildColumnForOperationType(0, hasBucket, undefined, 0, field); - const countColumn = buildColumnForOperationType(1, 'count'); + const countColumn = buildColumnForOperationType(1, 'count', undefined, 0); const suggestion: DatasourceSuggestion = { state: { @@ -362,9 +367,15 @@ export function getIndexPatternDatasource({ f => f.name === currentIndexPattern.timeFieldName )!; - const column = buildColumnForOperationType(0, operations[0], undefined, field); + const column = buildColumnForOperationType(0, operations[0], undefined, 0, field); - const dateColumn = buildColumnForOperationType(1, 'date_histogram', undefined, dateField); + const dateColumn = buildColumnForOperationType( + 1, + 'date_histogram', + undefined, + 0, + dateField + ); const suggestion: DatasourceSuggestion = { state: { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts new file mode 100644 index 0000000000000..360f3bc1af646 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts @@ -0,0 +1,57 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; +import { KibanaDatatable } from '../types'; + +interface MergeTables { + joins: string[]; + tables: KibanaDatatable[]; +} + +export const mergeTables: ExpressionFunction< + 'lens_merge_tables', + null, + MergeTables, + KibanaDatatable +> = { + name: 'lens_merge_tables', + type: 'kibana_datatable', + help: i18n.translate('xpack.lens.functions.mergeTables.help', { + defaultMessage: 'A helper to merge any number of kibana tables into a single table', + }), + args: { + joins: { + types: ['string'], + help: i18n.translate('xpack.lens.functions.calculateFilterRatio.id.help', { + defaultMessage: 'The column IDs to join on', + }), + multi: true, + }, + tables: { + types: ['kibana_datatable'], + help: '', + multi: true, + }, + }, + context: { + types: ['null'], + }, + fn(_ctx, { joins, tables }: MergeTables) { + return { + type: 'kibana_datatable', + rows: tables.reduce( + (prev, current) => prev.concat(current.rows), + [] as KibanaDatatable['rows'] + ), + columns: tables.reduce( + (prev, current) => prev.concat(current.columns), + [] as KibanaDatatable['columns'] + ), + }; + }, +}; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index d37504ad32fe5..81fef6caf2c0b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { CountIndexPatternColumn } from '../indexpattern'; -import { DimensionPriority } from '../../types'; +import { DimensionPriority, DimensionLayer } from '../../types'; import { OperationDefinition } from '../operations'; export const countOperation: OperationDefinition = { @@ -15,10 +15,12 @@ export const countOperation: OperationDefinition = { defaultMessage: 'Count', }), isApplicableWithoutField: true, - isApplicableForField: ({ aggregationRestrictions, type }) => { - return false; - }, - buildColumn(operationId: string, suggestedOrder?: DimensionPriority): CountIndexPatternColumn { + isApplicableForField: () => false, + buildColumn( + operationId: string, + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer + ): CountIndexPatternColumn { return { operationId, label: i18n.translate('xpack.lens.indexPattern.countOf', { @@ -28,6 +30,7 @@ export const countOperation: OperationDefinition = { operationType: 'count', suggestedOrder, isBucketed: false, + layer, }; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 753a950f07cdd..e7f056f52811d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; -import { DimensionPriority } from '../../types'; +import { DimensionLayer, DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; @@ -45,7 +45,8 @@ export const dateHistogramOperation: OperationDefinition false, buildColumn( operationId: string, - suggestedOrder?: DimensionPriority + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer ): FilterRatioIndexPatternColumn { return { operationId, @@ -34,6 +35,7 @@ export const filterRatioOperation: OperationDefinition( @@ -32,7 +32,8 @@ function buildMetricOperation( }, buildColumn( operationId: string, - suggestedOrder?: DimensionPriority, + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer, field?: IndexPatternField ): T { if (!field) { @@ -46,6 +47,7 @@ function buildMetricOperation( suggestedOrder, sourceField: field ? field.name : '', isBucketed: false, + layer, } as T; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 98e3443549ced..a822844573e34 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; import { IndexPatternField, TermsIndexPatternColumn } from '../indexpattern'; -import { DimensionPriority } from '../../types'; +import { DimensionLayer, DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; @@ -44,7 +44,8 @@ export const termsOperation: OperationDefinition = { }, buildColumn( operationId: string, - suggestedOrder?: DimensionPriority, + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer, field?: IndexPatternField ): TermsIndexPatternColumn { return { @@ -55,6 +56,7 @@ export const termsOperation: OperationDefinition = { suggestedOrder, sourceField: field ? field.name : '', isBucketed: true, + layer, params: { size: 5, orderBy: { type: 'alphabetical' }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index 8471e2b0ad8f4..5ba0decd29c66 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -6,7 +6,7 @@ import { Storage } from 'ui/storage'; import { DataSetup } from '../../../../../../src/legacy/core_plugins/data/public'; -import { DimensionPriority } from '../types'; +import { DimensionLayer, DimensionPriority } from '../types'; import { IndexPatternColumn, IndexPatternField, @@ -73,7 +73,8 @@ export interface OperationDefinition { isApplicableForField: (field: IndexPatternField) => boolean; buildColumn: ( operationId: string, - suggestedOrder?: DimensionPriority, + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer, field?: IndexPatternField ) => C; paramEditor?: React.ComponentType; @@ -106,16 +107,22 @@ export function getOperationTypesForField(field: IndexPatternField): OperationTy export function buildColumnForOperationType( index: number, op: T, - suggestedOrder?: DimensionPriority, + suggestedOrder: DimensionPriority | undefined, + layer: DimensionLayer, field?: IndexPatternField ): IndexPatternColumn { - return operationDefinitionMap[op].buildColumn(`${index}${op}`, suggestedOrder, field); + return operationDefinitionMap[op].buildColumn(`${index}${op}`, suggestedOrder, layer, field); } -export function getPotentialColumns( - state: IndexPatternPrivateState, - suggestedOrder?: DimensionPriority -): IndexPatternColumn[] { +export function getPotentialColumns({ + state, + suggestedOrder, + layer, +}: { + state: IndexPatternPrivateState; + suggestedOrder?: DimensionPriority; + layer: DimensionLayer; +}): IndexPatternColumn[] { const fields = state.indexPatterns[state.currentIndexPatternId].fields; const columns: IndexPatternColumn[] = fields @@ -123,14 +130,14 @@ export function getPotentialColumns( const validOperations = getOperationTypesForField(field); return validOperations.map(op => - buildColumnForOperationType(index, op, suggestedOrder, field) + buildColumnForOperationType(index, op, suggestedOrder, layer, field) ); }) .reduce((prev, current) => prev.concat(current)); operationDefinitions.forEach(operation => { if (operation.isApplicableWithoutField) { - columns.push(operation.buildColumn(operation.type, suggestedOrder)); + columns.push(operation.buildColumn(operation.type, suggestedOrder, layer)); } }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index ea969219b3e1f..56a7ff37c9ed5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -18,6 +18,7 @@ import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/int import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { calculateFilterRatio } from './filter_ratio'; +import { mergeTables } from './merge_tables'; // TODO these are intermediary types because interpreter is not typed yet // They can get replaced by references to the real interfaces as soon as they @@ -47,6 +48,7 @@ class IndexPatternDatasourcePlugin { ) { interpreter.functionsRegistry.register(() => renameColumns); interpreter.functionsRegistry.register(() => calculateFilterRatio); + interpreter.functionsRegistry.register(() => mergeTables); return getIndexPatternDatasource({ chrome, interpreter, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 67887ef186f09..1fe1751069eb2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -27,16 +27,8 @@ export function toExpression(state: IndexPatternPrivateState) { return operationDefinition.toEsAggsConfig(column, columnId); } - const columnEntries = state.columnOrder.map( - colId => [colId, state.columns[colId]] as [string, IndexPatternColumn] - ); - - if (columnEntries.length) { - const aggs = columnEntries.map(([colId, col]) => { - return getEsAggsConfig(col, colId); - }); - - const idMap = columnEntries.reduce( + function getIdMap(layer: Array<[string, IndexPatternColumn]>) { + return layer.reduce( (currentIdMap, [colId], index) => { return { ...currentIdMap, @@ -45,29 +37,127 @@ export function toExpression(state: IndexPatternPrivateState) { }, {} as Record ); + } - const filterRatios = columnEntries.filter( - ([colId, col]) => col.operationType === 'filter_ratio' - ); + function getExpr( + aggs: unknown[], + idMap: Record, + filterRatios?: Array<[string, IndexPatternColumn]> + ) { + let expr = `esaggs + index="${state.currentIndexPatternId}" + metricsAtAllLevels=false + partialRows=false + aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; - if (filterRatios.length) { - const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2); - aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - return `esaggs - index="${state.currentIndexPatternId}" - metricsAtAllLevels=false - partialRows=false - aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( - idMap - )}' | ${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; + if (filterRatios) { + expr += `${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; } - return `esaggs - index="${state.currentIndexPatternId}" - metricsAtAllLevels=false - partialRows=false - aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; + return expr; + } + + const columnEntries = state.columnOrder.map( + colId => [colId, state.columns[colId]] as [string, IndexPatternColumn] + ); + + if (columnEntries.length) { + const joinLayer = columnEntries.find(([colId, col]) => col.layer === 'join'); + const numericLayers = columnEntries.filter(([colId, col]) => typeof col.layer === 'number'); + + let dataFetchExpression; + + console.log(!!joinLayer, numericLayers.length); + if (joinLayer && numericLayers.length > 1) { + const groupedLayers = _.groupBy(numericLayers, ([colId, col]) => col.layer); + + const tableFetchExpressions = Object.values(groupedLayers) + .map(layer => { + const [buckets, metrics] = _.partition(layer, ([colId, col]) => col.isBucketed); + + return buckets.concat([joinLayer], metrics); + }) + .map(layer => { + const aggs = layer.map(([colId, col]) => { + return getEsAggsConfig(col, colId); + }); + + const idMap = getIdMap(layer); + + // TODO: Duplicate logic here, need to refactor + const filterRatios = layer.filter(([colId, col]) => col.operationType === 'filter_ratio'); + + if (filterRatios.length) { + const countColumn = buildColumnForOperationType( + columnEntries.length, + 'count', + layer[0][1].suggestedOrder, + layer[0][1].layer + ); + aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); + + // return `esaggs + // index="${state.currentIndexPatternId}" + // metricsAtAllLevels=false + // partialRows=false + // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( + // idMap + // )}' | ${filterRatios + // .map(([id]) => `lens_calculate_filter_ratio id=${id}`) + // .join(' | ')}`; + return getExpr(aggs, idMap, filterRatios); + } + + // const idMap = getIdMap(layer); + + // return `esaggs + // index="${state.currentIndexPatternId}" + // metricsAtAllLevels=false + // partialRows=false + // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( + // idMap + // )}'`; + return getExpr(aggs, idMap); + }) + .map(layer => `tables={${layer}}`); + + dataFetchExpression = `lens_merge_tables joins="${joinLayer[0]}" ${tableFetchExpressions.join( + '\n' + )} | clog `; + } else { + const aggs = columnEntries.map(([colId, col]) => { + return getEsAggsConfig(col, colId); + }); + + const filterRatios = columnEntries.filter( + ([colId, col]) => col.operationType === 'filter_ratio' + ); + + const idMap = getIdMap(columnEntries); + if (filterRatios.length) { + const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2, 0); + aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); + + // const idMap = getIdMap(columnEntries); + + // return `esaggs + // index="${state.currentIndexPatternId}" + // metricsAtAllLevels=false + // partialRows=false + // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( + // idMap + // )}' | ${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; + dataFetchExpression = getExpr(aggs, idMap, filterRatios); + } else { + dataFetchExpression = getExpr(aggs, idMap); + } + } + + // const idMap = getIdMap(columnEntries); + + // return dataFetchExpression + `| lens_rename_columns idMap='${JSON.stringify(idMap)}'`; + console.log(dataFetchExpression); + return dataFetchExpression; } return null; diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 9812db7440891..cacb9a231e3b9 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -27,15 +27,19 @@ export interface EditorFrameSetup { // Hints the default nesting to the data source. 0 is the highest priority export type DimensionPriority = 0 | 1 | 2; -export interface TableColumn { +// 0 is the default layer, layers are joined sequentially but there can only be one 'join' layer +export type DimensionLayer = 'join' | number; + +export interface TableSuggestionColumn { columnId: string; operation: Operation; + layer?: DimensionLayer; } export interface TableSuggestion { datasourceSuggestionId: number; isMultiRow: boolean; - columns: TableColumn[]; + columns: TableSuggestionColumn[]; } export interface DatasourceSuggestion { @@ -69,6 +73,11 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { + // Static properties provided to visualizations to indicate whether this datasource requires "layered" + // queries which indicate the level of nesting + supportsLayers: boolean; + supportsLayerJoin: boolean; + getTableSpec: () => TableSpec; getOperationForColumnId: (columnId: string) => Operation | null; @@ -80,6 +89,15 @@ export interface DatasourcePublicAPI { duplicateColumn: (columnId: string) => TableSpec; } +export interface TableSpecColumn { + // Column IDs are the keys for internal state in data sources and visualizations + columnId: string; + layer?: DimensionLayer; +} + +// TableSpec is managed by visualizations +export type TableSpec = TableSpecColumn[]; + export interface DatasourceDataPanelProps { state: T; dragDropContext: DragContextState; @@ -99,6 +117,8 @@ export interface DatasourceDimensionPanelProps { // Visualizations can hint at the role this dimension would play, which // affects the default ordering of the query suggestedPriority?: DimensionPriority; + + layer?: DimensionLayer; } export type DataType = 'string' | 'number' | 'date' | 'boolean'; @@ -121,14 +141,6 @@ export interface Operation { // 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[]; - // This is a temporary type definition, to be replaced with // the official Kibana Datatable type definition. export interface KibanaDatatable { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss new file mode 100644 index 0000000000000..ccdd3774b5d7b --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss @@ -0,0 +1,4 @@ +.lnsXyConfigPanel-layer { + border: 1px solid $euiColorLightestShade; + background-color: $euiColorEmpty; +} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss new file mode 100644 index 0000000000000..ec94ebdf235b0 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_expression.scss @@ -0,0 +1,3 @@ +.lnsChart { + height: 100%; +} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.scss new file mode 100644 index 0000000000000..9508c8cb9a05c --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/index.scss @@ -0,0 +1,2 @@ +@import "./_xy_config_panel"; +@import "./_xy_expression"; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx index f0f804eacf864..82cce8f52cfcc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx @@ -21,6 +21,7 @@ interface Props { filterOperations: (op: Operation) => boolean; suggestedPriority?: 0 | 1 | 2 | undefined; testSubj: string; + layer: number; } export function MultiColumnEditor({ @@ -32,6 +33,7 @@ export function MultiColumnEditor({ filterOperations, suggestedPriority, testSubj, + layer, }: Props) { return ( <> @@ -45,6 +47,7 @@ export function MultiColumnEditor({ dragDropContext, filterOperations, suggestedPriority, + layer, }} /> {i === accessors.length - 1 ? null : ( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx index 9c9482a05e8f1..390b469e61510 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx @@ -13,7 +13,7 @@ import { } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { InterpreterSetup, RenderFunction } from '../interpreter_types'; import { xyChart, xyChartRenderer } from './xy_expression'; -import { legendConfig, xConfig, yConfig } from './types'; +import { legendConfig, xConfig, layerConfig } from './types'; export interface XyVisualizationPluginSetupPlugins { interpreter: InterpreterSetup; @@ -25,7 +25,8 @@ class XyVisualizationPlugin { setup(_core: CoreSetup | null, { interpreter }: XyVisualizationPluginSetupPlugins) { interpreter.functionsRegistry.register(() => legendConfig); interpreter.functionsRegistry.register(() => xConfig); - interpreter.functionsRegistry.register(() => yConfig); + // interpreter.functionsRegistry.register(() => yConfig); + interpreter.functionsRegistry.register(() => layerConfig); interpreter.functionsRegistry.register(() => xyChart); interpreter.renderersRegistry.register(() => xyChartRenderer as RenderFunction); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 80ee1ca4e67da..7a0e73cb1c4b4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -10,11 +10,13 @@ import { DatasourcePublicAPI } from '../types'; export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => { const labels: Partial> = {}; - state.y.accessors.forEach(columnId => { - const operation = datasource.getOperationForColumnId(columnId); - if (operation && operation.label) { - labels[columnId] = operation.label; - } + state.layers.forEach(layer => { + layer.accessors.forEach(columnId => { + const operation = datasource.getOperationForColumnId(columnId); + if (operation && operation.label) { + labels[columnId] = operation.label; + } + }); }); return buildExpression(state, labels); @@ -30,7 +32,6 @@ export const buildExpression = ( type: 'function', function: 'lens_xy_chart', arguments: { - seriesType: [state.seriesType], legend: [ { type: 'expression', @@ -64,28 +65,28 @@ export const buildExpression = ( ], }, ], - y: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_yConfig', - arguments: { - title: [state.y.title], - showGridlines: [state.y.showGridlines], - position: [state.y.position], - accessors: state.y.accessors, - hide: [Boolean(state.y.hide)], - labels: state.y.accessors.map(accessor => { - return columnLabels[accessor] || accessor; - }), - }, + layers: state.layers.map(layer => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + title: [layer.title], + showGridlines: [layer.showGridlines], + position: [layer.position], + hide: [Boolean(layer.hide)], + + splitSeriesAccessors: layer.splitSeriesAccessors, + seriesType: [layer.seriesType], + labels: layer.accessors.map(accessor => { + return columnLabels[accessor] || accessor; + }), + accessors: layer.accessors, }, - ], - }, - ], - splitSeriesAccessors: state.splitSeriesAccessors, + }, + ], + })), }, }, ], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index d2b3b962f10ee..5572da27b28af 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -81,66 +81,120 @@ export interface YState extends AxisConfig { accessors: string[]; } -export type YConfig = AxisConfig & - YState & { - labels: string[]; - }; +// export type YConfig = AxisConfig & +// YState & { +// labels: string[]; +// }; + +// type YConfigResult = YConfig & { type: 'lens_xy_yConfig' }; + +// export const yConfig: ExpressionFunction<'lens_xy_yConfig', null, YConfig, YConfigResult> = { +// name: 'lens_xy_yConfig', +// aliases: [], +// type: 'lens_xy_yConfig', +// help: `Configure the xy chart's y axis`, +// context: { +// types: ['null'], +// }, +// args: { +// ...axisConfig, +// accessors: { +// types: ['string'], +// help: 'The columns to display on the y axis.', +// multi: true, +// }, +// labels: { +// types: ['string'], +// help: '', +// multi: true, +// }, +// }, +// fn: function fn(_context: unknown, args: YConfig) { +// return { +// type: 'lens_xy_yConfig', +// ...args, +// }; +// }, +// }; -type YConfigResult = YConfig & { type: 'lens_xy_yConfig' }; +export interface XConfig extends AxisConfig { + accessor: string; +} + +type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; -export const yConfig: ExpressionFunction<'lens_xy_yConfig', null, YConfig, YConfigResult> = { - name: 'lens_xy_yConfig', +export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConfigResult> = { + name: 'lens_xy_xConfig', aliases: [], - type: 'lens_xy_yConfig', - help: `Configure the xy chart's y axis`, + type: 'lens_xy_xConfig', + help: `Configure the xy chart's x axis`, context: { types: ['null'], }, args: { ...axisConfig, - accessors: { - types: ['string'], - help: 'The columns to display on the y axis.', - multi: true, - }, - labels: { + accessor: { types: ['string'], - help: '', - multi: true, + help: 'The column to display on the x axis.', }, }, - fn: function fn(_context: unknown, args: YConfig) { + fn: function fn(_context: unknown, args: XConfig) { return { - type: 'lens_xy_yConfig', + type: 'lens_xy_xConfig', ...args, }; }, }; -export interface XConfig extends AxisConfig { - accessor: string; -} - -type XConfigResult = XConfig & { type: 'lens_xy_xConfig' }; +type LayerConfigResult = LayerConfig & { type: 'lens_xy_layer' }; -export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConfigResult> = { - name: 'lens_xy_xConfig', +export const layerConfig: ExpressionFunction< + 'lens_xy_layer', + null, + LayerConfig, + LayerConfigResult +> = { + name: 'lens_xy_layer', aliases: [], - type: 'lens_xy_xConfig', - help: `Configure the xy chart's x axis`, + type: 'lens_xy_layer', + help: `Configure a layer in the xy chart`, context: { types: ['null'], }, args: { ...axisConfig, - accessor: { + seriesType: { types: ['string'], - help: 'The column to display on the x axis.', + options: [ + 'bar', + 'line', + 'area', + 'horizontal_bar', + 'bar_stacked', + 'area_stacked', + 'horizontal_bar_stacked', + ], + help: 'The type of chart to display.', + }, + splitSeriesAccessors: { + types: ['string'], + help: 'The columns to split by', + multi: true, + }, + accessors: { + types: ['string'], + help: 'The columns to display on the y axis.', + multi: true, + }, + labels: { + types: ['string'], + help: '', + multi: true, }, }, - fn: function fn(_context: unknown, args: XConfig) { + fn: function fn(_context: unknown, args: LayerConfig) { return { - type: 'lens_xy_xConfig', + type: 'lens_xy_layer', ...args, }; }, @@ -155,20 +209,29 @@ export type SeriesType = | 'horizontal_bar_stacked' | 'area_stacked'; -export interface XYArgs { +type LayerConfig = AxisConfig & { + accessors: string[]; + labels: string[]; seriesType: SeriesType; + splitSeriesAccessors: string[]; +}; + +export interface XYArgs { + // seriesType: SeriesType; legend: LegendConfig; - y: YConfig; + // y: YConfig; x: XConfig; - splitSeriesAccessors: string[]; + // splitSeriesAccessors: string[]; + layers: LayerConfig[]; } export interface XYState { - seriesType: SeriesType; + // seriesType: SeriesType; legend: LegendConfig; - y: YState; + // y: YState; x: XConfig; - splitSeriesAccessors: string[]; + layers: LayerConfig[]; + // splitSeriesAccessors: string[]; } export type State = XYState; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index fdaa448584536..62e77ecebcb35 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -6,19 +6,23 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Position } from '@elastic/charts'; import { EuiFieldText, + EuiButton, EuiButtonGroup, EuiForm, EuiFormRow, EuiSwitch, + EuiPanel, IconType, } from '@elastic/eui'; import { State, SeriesType } from './types'; import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; import { MultiColumnEditor } from './multi_column_editor'; +import { generateId } from '../id_generator'; const chartTypeIcons: Array<{ id: SeriesType; label: string; iconType: IconType }> = [ { @@ -95,11 +99,24 @@ const positionIcons = [ }, ]; +type UnwrapArray = T extends Array ? P : T; + +function updateLayer(state: State, layer: UnwrapArray, index: number): State { + const newLayers = [...state.layers]; + newLayers[index] = layer; + + return { + ...state, + layers: newLayers, + }; +} + export function XYConfigPanel(props: VisualizationProps) { const { state, datasource, setState } = props; return ( + {/* ) { name="chartType" className="eui-displayInlineBlock" data-test-subj="lnsXY_seriesType" - options={chartTypeIcons.map(type => - type.id.includes('stacked') && state.splitSeriesAccessors.length === 0 - ? { ...type, isDisabled: true } - : type - )} + // options={chartTypeIcons.map(type => + // type.id.includes('stacked') && state.splitSeriesAccessors.length === 0 + // ? { ...type, isDisabled: true } + // : type + // )} + options={chartTypeIcons} idSelected={state.seriesType} onChange={seriesType => { const isHorizontal = seriesType === 'horizontal_bar'; setState({ ...state, seriesType: seriesType as SeriesType, - x: { - ...state.x, - position: isHorizontal ? Position.Left : Position.Bottom, - }, - y: { - ...state.y, - position: isHorizontal ? Position.Bottom : Position.Left, - }, + // x: { + // ...state.x, + // position: isHorizontal ? Position.Left : Position.Bottom, + // }, + // y: { + // ...state.y, + // position: isHorizontal ? Position.Bottom : Position.Left, + // }, }); }} isIconOnly /> + */} + {/* ) { /> )} - - - - setState({ ...state, splitSeriesAccessors: [...state.splitSeriesAccessors, accessor] }) - } - onRemove={accessor => - setState({ - ...state, - splitSeriesAccessors: state.splitSeriesAccessors.filter(col => col !== accessor), - }) - } - filterOperations={op => op.isBucketed && op.dataType !== 'date'} - suggestedPriority={0} - testSubj="splitSeriesDimensionPanel" - /> - + */} - <> + + {/* ) { })} /> + */} ) { nativeProps={{ columnId: state.x.accessor, dragDropContext: props.dragDropContext, - // TODO: Filter out invalid x-dimension operations - filterOperations: () => true, + filterOperations: operation => operation.isBucketed, + layer: datasource.supportsLayers && datasource.supportsLayerJoin ? 'join' : 0, }} /> + {/* ) { } /> - + */} + - - <> - - ( + + + setState({ ...state, y: { ...state.y, title: e.target.value } })} - aria-label={i18n.translate('xpack.lens.xyChart.yTitleAriaLabel', { - defaultMessage: 'Title', + > + + // type.id.includes('stacked') && state.splitSeriesAccessors.length === 0 + // ? { ...type, isDisabled: true } + // : type + // )} + options={chartTypeIcons} + idSelected={layer.seriesType} + onChange={seriesType => { + setState( + updateLayer(state, { ...layer, seriesType: seriesType as SeriesType }, index) + ); + }} + isIconOnly + /> + + + - + > + + setState( + updateLayer( + state, + { ...layer, splitSeriesAccessors: [...layer.splitSeriesAccessors, accessor] }, + index + ) + ) + } + onRemove={accessor => + setState( + updateLayer( + state, + { + ...layer, + splitSeriesAccessors: layer.splitSeriesAccessors.filter( + col => col !== accessor + ), + }, + index + ) + ) + } + filterOperations={op => op.isBucketed && op.dataType !== 'date'} + suggestedPriority={0} + layer={index} + testSubj="splitSeriesDimensionPanel" + /> + - - - setState({ - ...state, - y: { - ...state.y, - accessors: [...state.y.accessors, accessor], - }, - }) - } - onRemove={accessor => - setState({ - ...state, - y: { - ...state.y, - accessors: state.y.accessors.filter(col => col !== accessor), + + <> + {/* + + setState({ ...state, y: { ...state.y, title: e.target.value } })} + aria-label={i18n.translate('xpack.lens.xyChart.yTitleAriaLabel', { + defaultMessage: 'Title', + })} + /> + + */} + + + + setState( + updateLayer( + state, + { + ...layer, + accessors: [...layer.accessors, accessor], + }, + index + ) + ) + } + onRemove={accessor => + setState( + updateLayer( + state, + { + ...layer, + accessors: layer.accessors.filter(col => col !== accessor), + }, + index + ) + ) + } + filterOperations={op => !op.isBucketed && op.dataType === 'number'} + testSubj="yDimensionPanel" + layer={index} + /> + + + {/* + + + setState({ ...state, y: { ...state.y, showGridlines: !state.y.showGridlines } }) + } + /> + + */} + + + + + ))} + + {datasource.supportsLayers && ( + + { + setState({ + ...state, + layers: [ + ...state.layers, + { + seriesType: 'bar_stacked', + accessors: [generateId()], + title: '', + showGridlines: false, + position: Position.Left, + labels: [''], + splitSeriesAccessors: [], }, - }) - } - filterOperations={op => !op.isBucketed && op.dataType === 'number'} - testSubj="yDimensionPanel" - /> - + ], + }); + }} + iconType="plusInCircle" + > + + + + )} - - - setState({ ...state, y: { ...state.y, showGridlines: !state.y.showGridlines } }) - } - /> - - + + + + ); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss deleted file mode 100644 index 9ba7326af6a56..0000000000000 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.scss +++ /dev/null @@ -1,3 +0,0 @@ - .lnsChart { - height: 100%; - } \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 2392114e26999..c4375af4b5d2e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -37,36 +37,27 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArg type: 'render', help: 'An X/Y chart', args: { - seriesType: { - types: ['string'], - options: [ - 'bar', - 'line', - 'area', - 'horizontal_bar', - 'bar_stacked', - 'line_stacked', - 'area_stacked', - 'horizontal_bar_stacked', - ], - help: 'The type of chart to display.', - }, legend: { types: ['lens_xy_legendConfig'], help: 'Configure the chart legend.', }, - y: { - types: ['lens_xy_yConfig'], - help: 'The y axis configuration', - }, + // y: { + // types: ['lens_xy_yConfig'], + // help: 'The y axis configuration', + // }, x: { types: ['lens_xy_xConfig'], help: 'The x axis configuration', }, - splitSeriesAccessors: { - types: ['string'], + // splitSeriesAccessors: { + // types: ['string'], + // multi: true, + // help: 'The columns used to split the series.', + // }, + layers: { + types: ['lens_xy_layer'], + help: 'Layers of visual series', multi: true, - help: 'The columns used to split the series.', }, }, context: { @@ -102,30 +93,7 @@ export const xyChartRenderer: RenderFunction = { }; export function XYChart({ data, args }: XYChartProps) { - const { legend, x, y, splitSeriesAccessors, seriesType } = args; - // TODO: Stop mapping data once elastic-charts allows axis naming - // https://github.com/elastic/elastic-charts/issues/245 - const seriesProps = { - splitSeriesAccessors, - stackAccessors: seriesType.includes('stacked') ? [x.accessor] : [], - id: getSpecId(y.labels.join(',')), - xAccessor: x.accessor, - yAccessors: y.labels, - data: data.rows.map(row => { - const newRow: typeof row = {}; - - // Remap data to { 'Count of documents': 5 } - Object.keys(row).forEach(key => { - const labelIndex = y.accessors.indexOf(key); - if (labelIndex > -1) { - newRow[y.labels[labelIndex]] = row[key]; - } else { - newRow[key] = row[key]; - } - }); - return newRow; - }), - }; + const { legend, x, layers } = args; return ( @@ -133,7 +101,7 @@ export function XYChart({ data, args }: XYChartProps) { showLegend={legend.isVisible} legendPosition={legend.position} showLegendDisplayValue={false} - rotation={seriesType.includes('horizontal') ? 90 : 0} + rotation={layers.some(({ seriesType }) => seriesType.includes('horizontal')) ? 90 : 0} /> - {seriesType === 'line' ? ( - - ) : seriesType === 'bar' || - seriesType === 'bar_stacked' || - seriesType === 'horizontal_bar' || - seriesType === 'horizontal_bar_stacked' ? ( - - ) : ( - - )} + {layers.map(({ splitSeriesAccessors, seriesType, labels, accessors }, index) => { + const seriesProps = { + key: index, + splitSeriesAccessors, + stackAccessors: seriesType.includes('stacked') ? [x.accessor] : [], + id: getSpecId(labels.join(',')), + xAccessor: x.accessor, + yAccessors: labels, + data: data.rows.map(row => { + const newRow: typeof row = {}; + + // Remap data to { 'Count of documents': 5 } + Object.keys(row).forEach(key => { + const labelIndex = accessors.indexOf(key); + if (labelIndex > -1) { + newRow[labels[labelIndex]] = row[key]; + } else { + newRow[key] = row[key]; + } + }); + return newRow; + }), + }; + + return seriesType === 'line' ? ( + + ) : seriesType === 'bar' || + seriesType === 'bar_stacked' || + seriesType === 'horizontal_bar' || + seriesType === 'horizontal_bar_stacked' ? ( + + ) : ( + + ); + })} ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index 9ea0345f2367a..8e1e2d274c940 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -5,7 +5,7 @@ */ import { getSuggestions } from './xy_suggestions'; -import { TableColumn, VisualizationSuggestion } from '../types'; +import { TableSuggestionColumn, VisualizationSuggestion } from '../types'; import { State } from './types'; import { Ast } from '@kbn/interpreter/target/common'; import { generateId } from '../id_generator'; @@ -13,7 +13,7 @@ import { generateId } from '../id_generator'; jest.mock('../id_generator'); describe('xy_suggestions', () => { - function numCol(columnId: string): TableColumn { + function numCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { @@ -25,7 +25,7 @@ describe('xy_suggestions', () => { }; } - function strCol(columnId: string): TableColumn { + function strCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { @@ -37,7 +37,7 @@ describe('xy_suggestions', () => { }; } - function dateCol(columnId: string): TableColumn { + function dateCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index 213901aa3cd18..e56269af6c116 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -6,7 +6,12 @@ import { partition } from 'lodash'; import { Position } from '@elastic/charts'; -import { SuggestionRequest, VisualizationSuggestion, TableColumn, TableSuggestion } from '../types'; +import { + SuggestionRequest, + VisualizationSuggestion, + TableSuggestionColumn, + TableSuggestion, +} from '../types'; import { State } from './types'; import { generateId } from '../id_generator'; import { buildExpression } from './to_expression'; @@ -58,7 +63,7 @@ function getSuggestionForColumns(table: TableSuggestion): VisualizationSuggestio // This shuffles columns around so that the left-most column defualts to: // date, string, boolean, then number, in that priority. We then use this // order to pluck out the x column, and the split / stack column. -function prioritizeColumns(columns: TableColumn[]) { +function prioritizeColumns(columns: TableSuggestionColumn[]) { return [...columns].sort( (a, b) => columnSortOrder[a.operation.dataType] - columnSortOrder[b.operation.dataType] ); @@ -66,9 +71,9 @@ function prioritizeColumns(columns: TableColumn[]) { function getSuggestion( datasourceSuggestionId: number, - xValue: TableColumn, - yValues: TableColumn[], - splitBy?: TableColumn + xValue: TableSuggestionColumn, + yValues: TableSuggestionColumn[], + splitBy?: TableSuggestionColumn ): VisualizationSuggestion { const yTitle = yValues.map(col => col.operation.label).join(' & '); const xTitle = xValue.operation.label; @@ -79,20 +84,24 @@ function getSuggestion( const title = `${yTitle} ${preposition} ${xTitle}`; const state: State = { legend: { isVisible: true, position: Position.Right }, - seriesType: splitBy && isDate ? 'line' : 'bar', - splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [generateId()], + // seriesType: splitBy && isDate ? 'line' : 'bar', x: { accessor: xValue.columnId, position: Position.Bottom, showGridlines: false, title: xTitle, }, - y: { - accessors: yValues.map(col => col.columnId), - position: Position.Left, - showGridlines: false, - title: yTitle, - }, + layers: [ + { + seriesType: splitBy && isDate ? 'line' : 'bar', + splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [generateId()], + accessors: yValues.map(col => col.columnId), + labels: [''], + position: Position.Left, + showGridlines: false, + title: yTitle, + }, + ], }; const labels: Partial> = {}; @@ -115,10 +124,7 @@ function getSuggestion( ...state.x, hide: true, }, - y: { - ...state.y, - hide: true, - }, + layers: state.layers.map(layer => ({ ...layer, hide: true })), legend: { ...state.legend, isVisible: false, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 9408fc7d7f428..436265d212cd2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -21,7 +21,7 @@ export const xyVisualization: Visualization = { initialize(datasource, state) { return ( state || { - seriesType: 'bar', + // seriesType: 'bar', title: 'Empty XY Chart', legend: { isVisible: true, position: Position.Right }, x: { @@ -30,13 +30,18 @@ export const xyVisualization: Visualization = { showGridlines: false, title: 'X', }, - y: { - accessors: [generateId()], - position: Position.Left, - showGridlines: false, - title: 'Y', - }, - splitSeriesAccessors: [generateId()], + layers: [ + { + seriesType: 'bar_stacked', + accessors: [generateId()], + position: Position.Left, + showGridlines: false, + title: 'Y', + labels: [''], + // splitSeriesAccessors: [generateId()], + splitSeriesAccessors: [], + }, + ], } ); }, From 26601aa717a327b8b6a6d417e5499495068c5984 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 15 Jul 2019 11:16:07 -0400 Subject: [PATCH 02/67] [lens] WIP switch to nested tables --- .../editor_frame/config_panel_wrapper.tsx | 18 +- .../editor_frame/editor_frame.tsx | 82 +++-- .../editor_frame_plugin/editor_frame/save.ts | 4 +- .../editor_frame/state_management.ts | 109 +++++-- .../public/editor_frame_plugin/plugin.tsx | 6 + .../dimension_panel/dimension_panel.tsx | 11 +- .../indexpattern_plugin/indexpattern.tsx | 273 +++++++++-------- .../operation_definitions/count.tsx | 5 +- .../operation_definitions/date_histogram.tsx | 10 +- .../operation_definitions/filter_ratio.tsx | 19 +- .../operation_definitions/metrics.tsx | 5 +- .../operation_definitions/terms.tsx | 23 +- .../public/indexpattern_plugin/operations.ts | 18 +- .../indexpattern_plugin/state_helpers.ts | 74 +++-- .../indexpattern_plugin/to_expression.ts | 279 ++++++++++-------- x-pack/legacy/plugins/lens/public/types.ts | 27 +- .../multi_column_editor.tsx | 7 +- .../xy_visualization_plugin/to_expression.ts | 44 +-- .../public/xy_visualization_plugin/types.ts | 51 +--- .../xy_config_panel.tsx | 144 ++++++--- .../xy_visualization_plugin/xy_expression.tsx | 18 +- .../xy_visualization_plugin/xy_suggestions.ts | 22 +- .../xy_visualization.tsx | 16 +- 23 files changed, 775 insertions(+), 490 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index 677b37beab190..19cdd1508fc5e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useContext } from 'react'; import { EuiSelect } from '@elastic/eui'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; -import { Visualization, DatasourcePublicAPI } from '../../types'; +import { Visualization, DatasourcePublicAPI, FramePublicAPI } from '../../types'; import { DragContext } from '../../drag_drop'; interface ConfigPanelWrapperProps { @@ -17,11 +17,13 @@ interface ConfigPanelWrapperProps { activeVisualizationId: string | null; dispatch: (action: Action) => void; datasourcePublicAPI: DatasourcePublicAPI; + framePublicAPI: FramePublicAPI; } function getSuggestedVisualizationState( - visualization: Visualization, - datasource: DatasourcePublicAPI + frame: FramePublicAPI, + visualization: Visualization + // datasource: DatasourcePublicAPI ) { const suggestions = visualization.getSuggestions({ tables: [ @@ -37,10 +39,10 @@ function getSuggestedVisualizationState( }); if (!suggestions.length) { - return visualization.initialize(datasource); + return visualization.initialize(frame); } - return visualization.initialize(datasource, suggestions[0].state); + return visualization.initialize(frame, suggestions[0].state); } export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -66,8 +68,9 @@ export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { value={props.activeVisualizationId || undefined} onChange={e => { const newState = getSuggestedVisualizationState( - props.visualizationMap[e.target.value], - props.datasourcePublicAPI + props.framePublicAPI, + props.visualizationMap[e.target.value] + // props.datasourcePublicAPI ); props.dispatch({ type: 'SWITCH_VISUALIZATION', @@ -84,6 +87,7 @@ export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { state: props.visualizationState, setState: setVisualizationState, datasource: props.datasourcePublicAPI, + frame: props.framePublicAPI, }} /> )} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 9c1e06f54178b..31e3f44e4bda8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useReducer, useMemo } from 'react'; import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { Datasource, Visualization } from '../../types'; +import { Datasource, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel_wrapper'; @@ -24,6 +24,7 @@ export interface EditorFrameProps { store: SavedObjectStore; datasourceMap: Record; visualizationMap: Record; + layerToDatasourceId: Record; redirectTo: (path: string) => void; initialDatasourceId: string | null; initialVisualizationId: string | null; @@ -39,9 +40,9 @@ export function EditorFrame(props: EditorFrameProps) { // as soon as datasource is available and memoize it const datasourcePublicAPI = useMemo( () => - state.datasource.activeId && !state.datasource.isLoading - ? props.datasourceMap[state.datasource.activeId].getPublicAPI( - state.datasource.state, + state.activeDatasourceId && !state.datasources[state.activeDatasourceId].isLoading + ? props.datasourceMap[state.activeDatasourceId].getPublicAPI( + state.datasources[state.activeDatasourceId].state, (newState: unknown) => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', @@ -52,12 +53,33 @@ export function EditorFrame(props: EditorFrameProps) { : undefined, [ props.datasourceMap, - state.datasource.isLoading, - state.datasource.activeId, - state.datasource.state, + state.activeDatasourceId, + state.datasources[state.activeDatasourceId!], + // state.datasource.isLoading, + // state.activeDatasourceId, + // state.datasource.state, ] ); + const framePublicAPI: FramePublicAPI = { + layerIdToDatasource: state.layerToDatasourceId, + datasourceLayers: {}, + addNewLayer: () => { + const newLayerId = 'second'; + + dispatch({ + type: 'CREATE_LAYER', + newLayerId, + }); + + return newLayerId; + }, + }; + + // const layerToDatasource = { + // 0: datasourcePublicAPI, + // }; + useEffect(() => { if (props.doc) { dispatch({ @@ -75,8 +97,9 @@ export function EditorFrame(props: EditorFrameProps) { // Initialize current datasource useEffect(() => { let datasourceGotSwitched = false; - if (state.datasource.isLoading && state.datasource.activeId) { - props.datasourceMap[state.datasource.activeId] + // if (state.datasource.isLoading && state.activeDatasourceId) { + if (state.activeDatasourceId && state.datasources[state.activeDatasourceId].isLoading) { + props.datasourceMap[state.activeDatasourceId] .initialize(props.doc && props.doc.state.datasource) .then(datasourceState => { if (!datasourceGotSwitched) { @@ -92,7 +115,7 @@ export function EditorFrame(props: EditorFrameProps) { datasourceGotSwitched = true; }; } - }, [props.doc, state.datasource.activeId, state.datasource.isLoading]); + }, [props.doc, state.activeDatasourceId, state.datasources[state.activeDatasourceId!].isLoading]); // Initialize visualization as soon as datasource is ready useEffect(() => { @@ -103,7 +126,7 @@ export function EditorFrame(props: EditorFrameProps) { ) { const initialVisualizationState = props.visualizationMap[ state.visualization.activeId - ].initialize(datasourcePublicAPI); + ].initialize(framePublicAPI, datasourcePublicAPI); dispatch({ type: 'UPDATE_VISUALIZATION_STATE', newState: initialVisualizationState, @@ -112,8 +135,8 @@ export function EditorFrame(props: EditorFrameProps) { }, [datasourcePublicAPI, state.visualization.activeId, state.visualization.state]); const datasource = - state.datasource.activeId && !state.datasource.isLoading - ? props.datasourceMap[state.datasource.activeId] + state.activeDatasourceId && !state.datasources[state.activeDatasourceId].isLoading + ? props.datasourceMap[state.activeDatasourceId] : undefined; const visualization = state.visualization.activeId @@ -138,7 +161,7 @@ export function EditorFrame(props: EditorFrameProps) { }).catch(onError); } }} - disabled={state.saving || !state.datasource.activeId || !state.visualization.activeId} + disabled={state.saving || !state.activeDatasourceId || !state.visualization.activeId} > {i18n.translate('xpack.lens.editorFrame.Save', { defaultMessage: 'Save', @@ -149,9 +172,15 @@ export function EditorFrame(props: EditorFrameProps) { dataPanel={ } @@ -162,6 +191,7 @@ export function EditorFrame(props: EditorFrameProps) { datasourcePublicAPI={datasourcePublicAPI!} dispatch={dispatch} visualizationState={state.visualization.state} + framePublicAPI={framePublicAPI} /> } workspacePanel={ @@ -170,7 +200,9 @@ export function EditorFrame(props: EditorFrameProps) { activeDatasource={datasource} activeVisualizationId={state.visualization.activeId} datasourcePublicAPI={datasourcePublicAPI!} - datasourceState={state.datasource.state} + datasourceState={ + state.activeDatasourceId ? state.datasources[state.activeDatasourceId].state : null + } visualizationState={state.visualization.state} visualizationMap={props.visualizationMap} dispatch={dispatch} @@ -182,7 +214,9 @@ export function EditorFrame(props: EditorFrameProps) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 472220e83a44e..deb074789cc57 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -32,9 +32,11 @@ export async function save({ title: state.title, type: 'lens', visualizationType: state.visualization.activeId, - datasourceType: state.datasource.activeId, + // datasourceType: state.datasource.activeId, + datasourceType: state.activeDatasourceId, state: { datasource: datasource.getPersistableState(state.datasource.state), + // datasources: datasource.getPersistableState(state.datasource.state), visualization: visualization.getPersistableState(state.visualization.state), }, }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index df56544e5c134..a476b2822b2b2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -16,11 +16,18 @@ export interface EditorFrameState { activeId: string | null; state: unknown; }; - datasource: { - activeId: string | null; - state: unknown; - isLoading: boolean; - }; + datasources: Record< + string, + { + state: unknown; + isLoading: boolean; + } + >; + activeDatasourceId: string | null; + // datasource: { + // activeId: string | null; + // }; + layerToDatasourceId: Record; } export type Action = @@ -61,17 +68,27 @@ export type Action = | { type: 'SWITCH_DATASOURCE'; newDatasourceId: string; + } + | { + type: 'CREATE_LAYER'; + newLayerId: string; }; export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { saving: false, title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), - datasource: { - state: null, - isLoading: Boolean(props.initialDatasourceId), - activeId: props.initialDatasourceId, - }, + datasources: props.initialDatasourceId + ? { + [props.initialDatasourceId]: { + state: null, + isLoading: Boolean(props.initialDatasourceId), + // activeId: props.initialDatasourceId, + }, + } + : {}, + activeDatasourceId: props.initialDatasourceId, + layerToDatasourceId: {}, visualization: { state: null, activeId: props.initialVisualizationId, @@ -94,12 +111,23 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, persistedId: action.doc.id, title: action.doc.title, - datasource: { - ...state.datasource, - activeId: action.doc.datasourceType || null, - isLoading: true, - state: action.doc.state.datasource, - }, + // datasource: { + // ...state.datasource, + // activeId: action.doc.datasourceType || null, + // isLoading: true, + // state: action.doc.state.datasource, + // }, + datasources: action.doc.datasourceType + ? { + ...state.datasources, + [action.doc.datasourceType]: { + isLoading: true, + state: action.doc.state.datasource, + }, + } + : state.datasources, + activeDatasourceId: action.doc.datasourceType || null, + visualization: { ...state.visualization, activeId: action.doc.visualizationType, @@ -109,12 +137,20 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'SWITCH_DATASOURCE': return { ...state, - datasource: { - ...state.datasource, - isLoading: true, - state: null, - activeId: action.newDatasourceId, + // datasource: { + // ...state.datasource, + // isLoading: true, + // state: null, + // activeId: action.newDatasourceId, + // }, + datasources: { + ...state.datasources, + [action.newDatasourceId]: { + state: null, + isLoading: true, + }, }, + activeDatasourceId: action.newDatasourceId, visualization: { ...state.visualization, // purge visualization on datasource switch @@ -130,19 +166,26 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta activeId: action.newVisualizationId, state: action.initialState, }, - datasource: { - ...state.datasource, - state: action.datasourceState ? action.datasourceState : state.datasource.state, - }, + // datasource: { + // ...state.datasource, + // state: action.datasourceState ? action.datasourceState : state.datasource.state, + // }, + // datasources: { + // ...state.datasources, + // [state.activeDatasourceId]: + // state: action.datasourceState ? action.datasourceState : state.datasource.state, + // }, }; case 'UPDATE_DATASOURCE_STATE': return { ...state, - datasource: { - ...state.datasource, + datasources: { + ...state.datasources, // when the datasource state is updated, the initialization is complete - isLoading: false, - state: action.newState, + [state.activeDatasourceId!]: { + isLoading: false, + state: action.newState, + }, }, }; case 'UPDATE_VISUALIZATION_STATE': @@ -156,6 +199,14 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta state: action.newState, }, }; + case 'CREATE_LAYER': + return { + ...state, + layerToDatasourceId: { + ...state.layerToDatasourceId, + [action.newLayerId]: state.activeDatasourceId!, + }, + }; default: return state; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index b55a84d32c17a..9349d6c0907a2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -21,6 +21,7 @@ import { EditorFrameSetup, EditorFrameInstance, ErrorCallback, + // DatasourcePublicAPI, } from '../types'; import { EditorFrame } from './editor_frame'; import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; @@ -47,6 +48,7 @@ interface RenderProps extends InitializationResult { onError: ErrorCallback; datasources: Record; visualizations: Record; + layerToDatasourceId: Record; // Maps layer ID to datasource ID expressionRenderer: ExpressionRenderer; } @@ -56,6 +58,7 @@ export class EditorFramePlugin { private ExpressionRenderer: ExpressionRenderer | null = null; private readonly datasources: Record = {}; private readonly visualizations: Record = {}; + private readonly layerToDatasourceId: Record = {}; private createInstance(): EditorFrameInstance { let domElement: Element; @@ -88,6 +91,7 @@ export class EditorFramePlugin { store={store} datasources={this.datasources} visualizations={this.visualizations} + layerToDatasourceId={this.layerToDatasourceId} expressionRenderer={this.ExpressionRenderer!} /> )} @@ -175,6 +179,7 @@ export function InitializedEditor({ store, datasources, visualizations, + layerToDatasourceId, expressionRenderer, }: RenderProps) { const firstDatasourceId = Object.keys(datasources)[0]; @@ -191,6 +196,7 @@ export function InitializedEditor({ store={store} datasourceMap={datasources} visualizationMap={visualizations} + layerToDatasourceId={layerToDatasourceId} initialDatasourceId={(doc && doc.datasourceType) || firstDatasourceId || null} initialVisualizationId={(doc && doc.visualizationType) || firstVisualizationId || null} ExpressionRenderer={expressionRenderer} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index af56eba8fd186..62966890a3dff 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -29,17 +29,20 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { dragDropContext: DragContextState; dataPlugin: DataSetup; storage: Storage; - layer: DimensionLayer; + // layer: DimensionLayer; + layerId: string; }; export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { + const layerId = props.layerId; const columns = getPotentialColumns(props); const filteredColumns = columns.filter(col => { return props.filterOperations(columnToOperation(col)); }); - const selectedColumn: IndexPatternColumn | null = props.state.columns[props.columnId] || null; + const selectedColumn: IndexPatternColumn | null = + props.state.layers[layerId].columns[props.columnId] || null; function findColumnByField(field: IndexPatternField) { return filteredColumns.find(col => hasField(col) && col.sourceField === field.name); @@ -66,7 +69,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp return; } - props.setState(changeColumn(props.state, props.columnId, column)); + props.setState(changeColumn(props.state, layerId, props.columnId, column)); }} > @@ -89,7 +92,7 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp defaultMessage: 'Remove', })} onClick={() => { - props.setState(deleteColumn(props.state, props.columnId)); + props.setState(deleteColumn(props.state, layerId, props.columnId)); }} /> diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 29aca4abaedf7..62ed3f78d32f1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -47,7 +47,8 @@ export interface BaseIndexPatternColumn { // Private operationType: OperationType; suggestedOrder?: DimensionPriority; - layer: 'join' | number; + // layer: 'join' | number; + isJoin?: boolean; } type Omit = Pick>; @@ -125,8 +126,13 @@ export interface IndexPatternField { export interface IndexPatternPersistedState { currentIndexPatternId: string; - columnOrder: string[]; - columns: Record; + layers: Record< + string, + { + columnOrder: string[]; + columns: Record; + } + >; } export type IndexPatternPrivateState = IndexPatternPersistedState & { @@ -255,13 +261,24 @@ export function getIndexPatternDatasource({ return { currentIndexPatternId: indexPatternObjects ? indexPatternObjects[0].id : '', indexPatterns, - columns: {}, - columnOrder: [], + layers: { + first: { + columns: {}, + columnOrder: [], + }, + }, }; }, - getPersistableState({ currentIndexPatternId, columns, columnOrder }: IndexPatternPrivateState) { - return { currentIndexPatternId, columns, columnOrder }; + // getPersistableState({ currentIndexPatternId, columns, columnOrder }: IndexPatternPrivateState) { + getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { + return { currentIndexPatternId, layers }; + }, + + insertLayer(state: IndexPatternPrivateState, newLayerId: string) { + return { + ...state, + }; }, toExpression, @@ -275,17 +292,27 @@ export function getIndexPatternDatasource({ getPublicAPI(state, setState) { return { - supportsLayers: true, - supportsLayerJoin: true, + // supportsLayers: true, + // supportsLayerJoin: true, getTableSpec: () => { - return state.columnOrder.map(colId => ({ columnId: colId })); + // return state.columnOrder.map(colId => ({ columnId: colId })); + return []; }, getOperationForColumnId: (columnId: string) => { - if (!state.columns[columnId]) { - return null; + const layer = Object.values(state.layers).find(l => + l.columnOrder.find(id => id === columnId) + ); + + if (layer) { + return columnToOperation(layer.columns[columnId]); } - return columnToOperation(state.columns[columnId]); + return null; + + // if (!state.columns[columnId]) { + // return null; + // } + // return columnToOperation(state.columns[columnId]); }, renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { render( @@ -295,7 +322,8 @@ export function getIndexPatternDatasource({ setState={newState => setState(newState)} dataPlugin={data} storage={storage} - layer={props.layer || 0} + // layer={props.layer || 0} + layerId={props.layerId || 'first'} {...props} /> , @@ -304,11 +332,11 @@ export function getIndexPatternDatasource({ }, removeColumnInTableSpec: (columnId: string) => { - setState({ - ...state, - columnOrder: state.columnOrder.filter(id => id !== columnId), - columns: removeProperty(columnId, state.columns), - }); + // setState({ + // ...state, + // columnOrder: state.columnOrder.filter(id => id !== columnId), + // columns: removeProperty(columnId, state.columns), + // }); }, moveColumnTo: (columnId: string, targetIndex: number) => {}, duplicateColumn: (columnId: string) => [], @@ -319,92 +347,92 @@ export function getIndexPatternDatasource({ state, item ): Array> { - const field: IndexPatternField = item as IndexPatternField; - - if (Object.keys(state.columns).length) { - // Not sure how to suggest multiple fields yet - return []; - } - - const operations = getOperationTypesForField(field); - const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); - - if (hasBucket) { - const column = buildColumnForOperationType(0, hasBucket, undefined, 0, field); - - const countColumn = buildColumnForOperationType(1, 'count', undefined, 0); - - const suggestion: DatasourceSuggestion = { - state: { - ...state, - columns: { - col1: column, - col2: countColumn, - }, - columnOrder: ['col1', 'col2'], - }, - - table: { - columns: [ - { - columnId: 'col1', - operation: columnToOperation(column), - }, - { - columnId: 'col2', - operation: columnToOperation(countColumn), - }, - ], - isMultiRow: true, - datasourceSuggestionId: 0, - }, - }; - - return [suggestion]; - } else if (state.indexPatterns[state.currentIndexPatternId].timeFieldName) { - const currentIndexPattern = state.indexPatterns[state.currentIndexPatternId]; - const dateField = currentIndexPattern.fields.find( - f => f.name === currentIndexPattern.timeFieldName - )!; - - const column = buildColumnForOperationType(0, operations[0], undefined, 0, field); - - const dateColumn = buildColumnForOperationType( - 1, - 'date_histogram', - undefined, - 0, - dateField - ); - - const suggestion: DatasourceSuggestion = { - state: { - ...state, - columns: { - col1: dateColumn, - col2: column, - }, - columnOrder: ['col1', 'col2'], - }, - - table: { - columns: [ - { - columnId: 'col1', - operation: columnToOperation(column), - }, - { - columnId: 'col2', - operation: columnToOperation(dateColumn), - }, - ], - isMultiRow: true, - datasourceSuggestionId: 0, - }, - }; - - return [suggestion]; - } + // const field: IndexPatternField = item as IndexPatternField; + + // if (Object.keys(state.columns).length) { + // // Not sure how to suggest multiple fields yet + // return []; + // } + + // const operations = getOperationTypesForField(field); + // const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); + + // if (hasBucket) { + // const column = buildColumnForOperationType(0, hasBucket, undefined, 0, field); + + // const countColumn = buildColumnForOperationType(1, 'count', undefined, 0); + + // const suggestion: DatasourceSuggestion = { + // state: { + // ...state, + // columns: { + // col1: column, + // col2: countColumn, + // }, + // columnOrder: ['col1', 'col2'], + // }, + + // table: { + // columns: [ + // { + // columnId: 'col1', + // operation: columnToOperation(column), + // }, + // { + // columnId: 'col2', + // operation: columnToOperation(countColumn), + // }, + // ], + // isMultiRow: true, + // datasourceSuggestionId: 0, + // }, + // }; + + // return [suggestion]; + // } else if (state.indexPatterns[state.currentIndexPatternId].timeFieldName) { + // const currentIndexPattern = state.indexPatterns[state.currentIndexPatternId]; + // const dateField = currentIndexPattern.fields.find( + // f => f.name === currentIndexPattern.timeFieldName + // )!; + + // const column = buildColumnForOperationType(0, operations[0], undefined, 0, field); + + // const dateColumn = buildColumnForOperationType( + // 1, + // 'date_histogram', + // undefined, + // 0, + // dateField + // ); + + // const suggestion: DatasourceSuggestion = { + // state: { + // ...state, + // columns: { + // col1: dateColumn, + // col2: column, + // }, + // columnOrder: ['col1', 'col2'], + // }, + + // table: { + // columns: [ + // { + // columnId: 'col1', + // operation: columnToOperation(column), + // }, + // { + // columnId: 'col2', + // operation: columnToOperation(dateColumn), + // }, + // ], + // isMultiRow: true, + // datasourceSuggestionId: 0, + // }, + // }; + + // return [suggestion]; + // } return []; }, @@ -412,23 +440,24 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsFromCurrentState( state ): Array> { - if (!state.columnOrder.length) { - return []; - } - return [ - { - state, - - table: { - columns: state.columnOrder.map(id => ({ - columnId: id, - operation: columnToOperation(state.columns[id]), - })), - isMultiRow: true, - datasourceSuggestionId: 0, - }, - }, - ]; + return []; + // if (!state.columnOrder.length) { + // return []; + // } + // return [ + // { + // state, + + // table: { + // columns: state.columnOrder.map(id => ({ + // columnId: id, + // operation: columnToOperation(state.columns[id]), + // })), + // isMultiRow: true, + // datasourceSuggestionId: 0, + // }, + // }, + // ]; }, }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index 81fef6caf2c0b..f0e46c9ffa3b3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -19,7 +19,8 @@ export const countOperation: OperationDefinition = { buildColumn( operationId: string, suggestedOrder: DimensionPriority | undefined, - layer: DimensionLayer + // layer: DimensionLayer + layerId: string ): CountIndexPatternColumn { return { operationId, @@ -30,7 +31,7 @@ export const countOperation: OperationDefinition = { operationType: 'count', suggestedOrder, isBucketed: false, - layer, + // layer, }; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index e7f056f52811d..14236a6b956bb 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -46,7 +46,8 @@ export const dateHistogramOperation: OperationDefinition { - const column = state.columns[columnId] as DateHistogramIndexPatternColumn; + paramEditor: ({ state, setState, columnId, layerId }) => { + const column = state.layers[layerId].columns[columnId] as DateHistogramIndexPatternColumn; const field = column && @@ -141,6 +142,7 @@ export const dateHistogramOperation: OperationDefinition { + paramEditor: ({ state, setState, columnId: currentColumnId, dataPlugin, storage, layerId }) => { const [hasDenominator, setDenominator] = useState(false); const { QueryBarInput } = dataPlugin!.query.ui; @@ -76,7 +77,8 @@ export const filterRatioOperation: OperationDefinition( buildColumn( operationId: string, suggestedOrder: DimensionPriority | undefined, - layer: DimensionLayer, + layerId: string, + // layer: DimensionLayer, field?: IndexPatternField ): T { if (!field) { @@ -47,7 +48,7 @@ function buildMetricOperation( suggestedOrder, sourceField: field ? field.name : '', isBucketed: false, - layer, + // layer, } as T; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index a822844573e34..f4d9230366317 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -45,7 +45,8 @@ export const termsOperation: OperationDefinition = { buildColumn( operationId: string, suggestedOrder: DimensionPriority | undefined, - layer: DimensionLayer, + // layer: DimensionLayer, + layerId: string, field?: IndexPatternField ): TermsIndexPatternColumn { return { @@ -56,7 +57,7 @@ export const termsOperation: OperationDefinition = { suggestedOrder, sourceField: field ? field.name : '', isBucketed: true, - layer, + // layerId, params: { size: 5, orderBy: { type: 'alphabetical' }, @@ -80,8 +81,8 @@ export const termsOperation: OperationDefinition = { missingBucketLabel: 'Missing', }, }), - paramEditor: ({ state, setState, columnId: currentColumnId }) => { - const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; + paramEditor: ({ state, setState, columnId: currentColumnId, layerId }) => { + const currentColumn = state.layers[layerId].columns[currentColumnId] as TermsIndexPatternColumn; const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { if (orderBy.type === 'alphabetical') { @@ -101,7 +102,7 @@ export const termsOperation: OperationDefinition = { }; } - const orderOptions = Object.entries(state.columns) + const orderOptions = Object.entries(state.layers[layerId].columns) .filter(([_columnId, column]) => !column.isBucketed) .map(([columnId, column]) => { return { @@ -129,7 +130,9 @@ export const termsOperation: OperationDefinition = { value={currentColumn.params.size} showInput onChange={(e: React.ChangeEvent) => - setState(updateColumnParam(state, currentColumn, 'size', Number(e.target.value))) + setState( + updateColumnParam(state, layerId, currentColumn, 'size', Number(e.target.value)) + ) } aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { defaultMessage: 'Number of values', @@ -146,7 +149,13 @@ export const termsOperation: OperationDefinition = { value={toValue(currentColumn.params.orderBy)} onChange={(e: React.ChangeEvent) => setState( - updateColumnParam(state, currentColumn, 'orderBy', fromValue(e.target.value)) + updateColumnParam( + state, + layerId, + currentColumn, + 'orderBy', + fromValue(e.target.value) + ) ) } aria-label={i18n.translate('xpack.lens.indexPattern.terms.orderBy', { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index 5ba0decd29c66..a4a5ec7153f85 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -61,6 +61,7 @@ export interface ParamEditorProps { state: IndexPatternPrivateState; setState: (newState: IndexPatternPrivateState) => void; columnId: string; + layerId: string; dataPlugin?: DataSetup; storage?: Storage; } @@ -74,7 +75,8 @@ export interface OperationDefinition { buildColumn: ( operationId: string, suggestedOrder: DimensionPriority | undefined, - layer: DimensionLayer, + // layer: DimensionLayer, + layerId: string, field?: IndexPatternField ) => C; paramEditor?: React.ComponentType; @@ -108,20 +110,22 @@ export function buildColumnForOperationType( index: number, op: T, suggestedOrder: DimensionPriority | undefined, - layer: DimensionLayer, + // layer: DimensionLayer, + layerId: string, field?: IndexPatternField ): IndexPatternColumn { - return operationDefinitionMap[op].buildColumn(`${index}${op}`, suggestedOrder, layer, field); + return operationDefinitionMap[op].buildColumn(`${index}${op}`, suggestedOrder, layerId, field); } export function getPotentialColumns({ state, suggestedOrder, - layer, + layerId, }: { state: IndexPatternPrivateState; suggestedOrder?: DimensionPriority; - layer: DimensionLayer; + // layer: DimensionLayer; + layerId: string; }): IndexPatternColumn[] { const fields = state.indexPatterns[state.currentIndexPatternId].fields; @@ -130,14 +134,14 @@ export function getPotentialColumns({ const validOperations = getOperationTypesForField(field); return validOperations.map(op => - buildColumnForOperationType(index, op, suggestedOrder, layer, field) + buildColumnForOperationType(index, op, suggestedOrder, layerId, field) ); }) .reduce((prev, current) => prev.concat(current)); operationDefinitions.forEach(operation => { if (operation.isApplicableWithoutField) { - columns.push(operation.buildColumn(operation.type, suggestedOrder, layer)); + columns.push(operation.buildColumn(operation.type, suggestedOrder, layerId)); } }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index 0ee3222c1e5d7..e9407253e784f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -34,40 +34,48 @@ export function updateColumnParam< K extends keyof C['params'] >( state: IndexPatternPrivateState, + layerId: string, currentColumn: C, paramName: K, value: C['params'][K] ): IndexPatternPrivateState { - const columnId = Object.entries(state.columns).find( + const columnId = Object.entries(state.layers[layerId].columns).find( ([_columnId, column]) => column === currentColumn )![0]; - if (!('params' in state.columns[columnId])) { + if (!('params' in state.layers[layerId].columns[columnId])) { throw new Error('Invariant: no params in this column'); } return { ...state, - columns: { - ...state.columns, - [columnId]: ({ - ...currentColumn, - params: { - ...currentColumn.params, - [paramName]: value, + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + columns: { + ...state.layers[layerId].columns, + [columnId]: ({ + ...currentColumn, + params: { + ...currentColumn.params, + [paramName]: value, + }, + } as unknown) as IndexPatternColumn, }, - } as unknown) as IndexPatternColumn, + }, }, }; } export function changeColumn( state: IndexPatternPrivateState, + layerId: string, columnId: string, newColumn: IndexPatternColumn, { keepParams }: { keepParams: boolean } = { keepParams: true } ) { - const oldColumn = state.columns[columnId]; + const oldColumn = state.layers[layerId].columns[columnId]; const updatedColumn = keepParams && @@ -77,28 +85,54 @@ export function changeColumn( ? ({ ...newColumn, params: oldColumn.params } as IndexPatternColumn) : newColumn; - const newColumns: IndexPatternPrivateState['columns'] = { - ...state.columns, + const newColumns: Record = { + ...state.layers[layerId].columns, [columnId]: updatedColumn, }; return { ...state, - columnOrder: getColumnOrder(newColumns), - columns: newColumns, + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + columnOrder: getColumnOrder(newColumns), + columns: newColumns, + }, + }, }; } -export function deleteColumn(state: IndexPatternPrivateState, columnId: string) { - const newColumns: IndexPatternPrivateState['columns'] = { - ...state.columns, +export function deleteColumn(state: IndexPatternPrivateState, layerId: string, columnId: string) { + // const newColumns: IndexPatternPrivateState['columns'] = { + // ...state.columns, + // }; + const newColumns: Record = { + ...state.layers[layerId].columns, }; delete newColumns[columnId]; + // return { + // ...state, + // columns: newColumns, + // columnOrder: getColumnOrder(newColumns), + // }; + + // const newColumns: Record = { + // ...state.layers[layerId].columns, + // [columnId]: updatedColumn, + // }; + return { ...state, - columns: newColumns, - columnOrder: getColumnOrder(newColumns), + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + columnOrder: getColumnOrder(newColumns), + columns: newColumns, + }, + }, }; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 1fe1751069eb2..39118246b27c4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -13,8 +13,12 @@ import { OperationDefinition, } from './operations'; -export function toExpression(state: IndexPatternPrivateState) { - if (state.columnOrder.length === 0) { +function getExpressionForLayer( + currentIndexPatternId: string, + columns: Record, + columnOrder: string[] +) { + if (columnOrder.length === 0) { return null; } @@ -27,8 +31,16 @@ export function toExpression(state: IndexPatternPrivateState) { return operationDefinition.toEsAggsConfig(column, columnId); } - function getIdMap(layer: Array<[string, IndexPatternColumn]>) { - return layer.reduce( + const columnEntries = columnOrder.map( + colId => [colId, columns[colId]] as [string, IndexPatternColumn] + ); + + if (columnEntries.length) { + const aggs = columnEntries.map(([colId, col]) => { + return getEsAggsConfig(col, colId); + }); + + const idMap = columnEntries.reduce( (currentIdMap, [colId], index) => { return { ...currentIdMap, @@ -37,128 +49,155 @@ export function toExpression(state: IndexPatternPrivateState) { }, {} as Record ); - } - function getExpr( - aggs: unknown[], - idMap: Record, - filterRatios?: Array<[string, IndexPatternColumn]> - ) { - let expr = `esaggs - index="${state.currentIndexPatternId}" - metricsAtAllLevels=false - partialRows=false - aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; - - if (filterRatios) { - expr += `${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; + const filterRatios = columnEntries.filter( + ([colId, col]) => col.operationType === 'filter_ratio' + ); + + if (filterRatios.length) { + const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2, 'layerId'); + aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); + + return `esaggs + index="${currentIndexPatternId}" + metricsAtAllLevels=false + partialRows=false + aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( + idMap + )}' | ${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; } - return expr; + return `esaggs + index="${currentIndexPatternId}" + metricsAtAllLevels=false + partialRows=false + aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; } - const columnEntries = state.columnOrder.map( - colId => [colId, state.columns[colId]] as [string, IndexPatternColumn] - ); - - if (columnEntries.length) { - const joinLayer = columnEntries.find(([colId, col]) => col.layer === 'join'); - const numericLayers = columnEntries.filter(([colId, col]) => typeof col.layer === 'number'); - - let dataFetchExpression; - - console.log(!!joinLayer, numericLayers.length); - if (joinLayer && numericLayers.length > 1) { - const groupedLayers = _.groupBy(numericLayers, ([colId, col]) => col.layer); - - const tableFetchExpressions = Object.values(groupedLayers) - .map(layer => { - const [buckets, metrics] = _.partition(layer, ([colId, col]) => col.isBucketed); - - return buckets.concat([joinLayer], metrics); - }) - .map(layer => { - const aggs = layer.map(([colId, col]) => { - return getEsAggsConfig(col, colId); - }); - - const idMap = getIdMap(layer); - - // TODO: Duplicate logic here, need to refactor - const filterRatios = layer.filter(([colId, col]) => col.operationType === 'filter_ratio'); - - if (filterRatios.length) { - const countColumn = buildColumnForOperationType( - columnEntries.length, - 'count', - layer[0][1].suggestedOrder, - layer[0][1].layer - ); - aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - // return `esaggs - // index="${state.currentIndexPatternId}" - // metricsAtAllLevels=false - // partialRows=false - // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( - // idMap - // )}' | ${filterRatios - // .map(([id]) => `lens_calculate_filter_ratio id=${id}`) - // .join(' | ')}`; - return getExpr(aggs, idMap, filterRatios); - } - - // const idMap = getIdMap(layer); - - // return `esaggs - // index="${state.currentIndexPatternId}" - // metricsAtAllLevels=false - // partialRows=false - // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( - // idMap - // )}'`; - return getExpr(aggs, idMap); - }) - .map(layer => `tables={${layer}}`); - - dataFetchExpression = `lens_merge_tables joins="${joinLayer[0]}" ${tableFetchExpressions.join( - '\n' - )} | clog `; - } else { - const aggs = columnEntries.map(([colId, col]) => { - return getEsAggsConfig(col, colId); - }); - - const filterRatios = columnEntries.filter( - ([colId, col]) => col.operationType === 'filter_ratio' - ); - - const idMap = getIdMap(columnEntries); - if (filterRatios.length) { - const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2, 0); - aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - // const idMap = getIdMap(columnEntries); - - // return `esaggs - // index="${state.currentIndexPatternId}" - // metricsAtAllLevels=false - // partialRows=false - // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( - // idMap - // )}' | ${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; - dataFetchExpression = getExpr(aggs, idMap, filterRatios); - } else { - dataFetchExpression = getExpr(aggs, idMap); - } - } - - // const idMap = getIdMap(columnEntries); + return null; +} - // return dataFetchExpression + `| lens_rename_columns idMap='${JSON.stringify(idMap)}'`; - console.log(dataFetchExpression); - return dataFetchExpression; - } +export function toExpression(state: IndexPatternPrivateState) { + const expressions = Object.entries(state.layers).map(([id, layer]) => [ + id, + getExpressionForLayer(state.currentIndexPatternId, layer.columns, layer.columnOrder), + ]); + + return `lens_merge_tables joins="" ${expressions.map(expr => `table={${expr}}`).join(' ')}`; + + // if (state.columnOrder.length === 0) { + // return null; + // } + + // function getEsAggsConfig(column: C, columnId: string) { + // // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, + // // but this is made sure by the typing of the operation map + // const operationDefinition = (operationDefinitionMap[ + // column.operationType + // ] as unknown) as OperationDefinition; + // return operationDefinition.toEsAggsConfig(column, columnId); + // } + + // function getIdMap(layer: Array<[string, IndexPatternColumn]>) { + // return layer.reduce( + // (currentIdMap, [colId], index) => { + // return { + // ...currentIdMap, + // [`col-${index}-${colId}`]: colId, + // }; + // }, + // {} as Record + // ); + // } + + // function getExpr( + // aggs: unknown[], + // idMap: Record, + // filterRatios?: Array<[string, IndexPatternColumn]> + // ) { + // let expr = `esaggs + // index="${state.currentIndexPatternId}" + // metricsAtAllLevels=false + // partialRows=false + // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; + + // if (filterRatios) { + // expr += `${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; + // } + + // return expr; + // } + + // const columnEntries = state.columnOrder.map( + // colId => [colId, state.columns[colId]] as [string, IndexPatternColumn] + // ); + + // if (columnEntries.length) { + // const joinLayer = columnEntries.find(([colId, col]) => col.layer === 'join'); + // const numericLayers = columnEntries.filter(([colId, col]) => typeof col.layer === 'number'); + + // let dataFetchExpression; + + // if (joinLayer && numericLayers.length > 1) { + // const groupedLayers = _.groupBy(numericLayers, ([colId, col]) => col.layer); + + // const tableFetchExpressions = Object.values(groupedLayers) + // .map(layer => { + // const [buckets, metrics] = _.partition(layer, ([colId, col]) => col.isBucketed); + + // return buckets.concat([joinLayer], metrics); + // }) + // .map(layer => { + // const aggs = layer.map(([colId, col]) => { + // return getEsAggsConfig(col, colId); + // }); + + // const idMap = getIdMap(layer); + + // // TODO: Duplicate logic here, need to refactor + // const filterRatios = layer.filter(([colId, col]) => col.operationType === 'filter_ratio'); + + // if (filterRatios.length) { + // const countColumn = buildColumnForOperationType( + // columnEntries.length, + // 'count', + // layer[0][1].suggestedOrder, + // layer[0][1].layer + // ); + // aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); + + // return getExpr(aggs, idMap, filterRatios); + // } + + // return getExpr(aggs, idMap); + // }) + // .map(layer => `tables={${layer}}`); + + // dataFetchExpression = `lens_merge_tables joins="${joinLayer[0]}" ${tableFetchExpressions.join( + // '\n' + // )} | clog `; + // } else { + // const aggs = columnEntries.map(([colId, col]) => { + // return getEsAggsConfig(col, colId); + // }); + + // const filterRatios = columnEntries.filter( + // ([colId, col]) => col.operationType === 'filter_ratio' + // ); + + // const idMap = getIdMap(columnEntries); + // if (filterRatios.length) { + // const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2, 0); + // aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); + + // dataFetchExpression = getExpr(aggs, idMap, filterRatios); + // } else { + // dataFetchExpression = getExpr(aggs, idMap); + // } + // } + + // return dataFetchExpression; + // } return null; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index cacb9a231e3b9..d20763a0ae4ca 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -59,6 +59,8 @@ export interface Datasource { // Given the current state, which parts should be saved? getPersistableState: (state: T) => P; + insertLayer: (state: T, newLayerId: string) => T; + renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; toExpression: (state: T) => Ast | string | null; @@ -73,11 +75,6 @@ export interface Datasource { * This is an API provided to visualizations by the frame, which calls the publicAPI on the datasource */ export interface DatasourcePublicAPI { - // Static properties provided to visualizations to indicate whether this datasource requires "layered" - // queries which indicate the level of nesting - supportsLayers: boolean; - supportsLayerJoin: boolean; - getTableSpec: () => TableSpec; getOperationForColumnId: (columnId: string) => Operation | null; @@ -106,7 +103,9 @@ export interface DatasourceDataPanelProps { // The only way a visualization has to restrict the query building export interface DatasourceDimensionPanelProps { - // If no columnId is passed, it will render as empty + // layerId: DimensionLayer; + layerId: string; + columnId: string; dragDropContext: DragContextState; @@ -117,8 +116,6 @@ export interface DatasourceDimensionPanelProps { // Visualizations can hint at the role this dimension would play, which // affects the default ordering of the query suggestedPriority?: DimensionPriority; - - layer?: DimensionLayer; } export type DataType = 'string' | 'number' | 'date' | 'boolean'; @@ -152,6 +149,7 @@ export interface KibanaDatatable { export interface VisualizationProps { dragDropContext: DragContextState; datasource: DatasourcePublicAPI; + frame: FramePublicAPI; state: T; setState: (newState: T) => void; } @@ -171,15 +169,24 @@ export interface VisualizationSuggestion { previewIcon: string; } +export interface FramePublicAPI { + datasourceLayers: Record; + layerIdToDatasource: Record; + // Adds a new layer. This triggers a re-render + addNewLayer: () => void; +} + export interface Visualization { // For initializing from saved object - initialize: (datasource: DatasourcePublicAPI, state?: P) => T; + // initialize: (frame: FramePublicAPI, datasource: DatasourcePublicAPI, state?: P) => T; + initialize: (frame: FramePublicAPI, state?: P) => T; getPersistableState: (state: T) => P; renderConfigPanel: (domElement: Element, props: VisualizationProps) => void; - toExpression: (state: T, datasource: DatasourcePublicAPI) => Ast | string | null; + // toExpression: (state: T, datasource: DatasourcePublicAPI) => Ast | string | null; + toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; // The frame will call this function on all visualizations when the table changes, or when // rendering additional ways of using the data diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx index 82cce8f52cfcc..9a276c9e69307 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx @@ -21,7 +21,8 @@ interface Props { filterOperations: (op: Operation) => boolean; suggestedPriority?: 0 | 1 | 2 | undefined; testSubj: string; - layer: number; + // layer: number; + layerId: string; } export function MultiColumnEditor({ @@ -33,7 +34,7 @@ export function MultiColumnEditor({ filterOperations, suggestedPriority, testSubj, - layer, + layerId, }: Props) { return ( <> @@ -47,7 +48,7 @@ export function MultiColumnEditor({ dragDropContext, filterOperations, suggestedPriority, - layer, + layerId, }} /> {i === accessors.length - 1 ? null : ( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 7a0e73cb1c4b4..b8ac934378d57 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -6,11 +6,14 @@ import { Ast } from '@kbn/interpreter/common'; import { State } from './types'; -import { DatasourcePublicAPI } from '../types'; +import { FramePublicAPI } from '../types'; -export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => { +// export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => { +export const toExpression = (state: State, frame: FramePublicAPI): Ast => { const labels: Partial> = {}; + // const datasource = frame.datasourceLayers.first; state.layers.forEach(layer => { + const datasource = frame.datasourceLayers[layer.layerId]; layer.accessors.forEach(columnId => { const operation = datasource.getOperationForColumnId(columnId); if (operation && operation.label) { @@ -47,24 +50,24 @@ export const buildExpression = ( ], }, ], - x: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_xConfig', - arguments: { - title: [state.x.title], - showGridlines: [state.x.showGridlines], - position: [state.x.position], - accessor: [state.x.accessor], - hide: [Boolean(state.x.hide)], - }, - }, - ], - }, - ], + // x: [ + // { + // type: 'expression', + // chain: [ + // { + // type: 'function', + // function: 'lens_xy_xConfig', + // arguments: { + // title: [state.x.title], + // showGridlines: [state.x.showGridlines], + // position: [state.x.position], + // accessor: [state.x.accessor], + // hide: [Boolean(state.x.hide)], + // }, + // }, + // ], + // }, + // ], layers: state.layers.map(layer => ({ type: 'expression', chain: [ @@ -77,6 +80,7 @@ export const buildExpression = ( position: [layer.position], hide: [Boolean(layer.hide)], + xAccessor: [layer.xAccessor], splitSeriesAccessors: layer.splitSeriesAccessors, seriesType: [layer.seriesType], labels: layer.accessors.map(accessor => { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 5572da27b28af..bf57c2ebe15d2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -81,42 +81,6 @@ export interface YState extends AxisConfig { accessors: string[]; } -// export type YConfig = AxisConfig & -// YState & { -// labels: string[]; -// }; - -// type YConfigResult = YConfig & { type: 'lens_xy_yConfig' }; - -// export const yConfig: ExpressionFunction<'lens_xy_yConfig', null, YConfig, YConfigResult> = { -// name: 'lens_xy_yConfig', -// aliases: [], -// type: 'lens_xy_yConfig', -// help: `Configure the xy chart's y axis`, -// context: { -// types: ['null'], -// }, -// args: { -// ...axisConfig, -// accessors: { -// types: ['string'], -// help: 'The columns to display on the y axis.', -// multi: true, -// }, -// labels: { -// types: ['string'], -// help: '', -// multi: true, -// }, -// }, -// fn: function fn(_context: unknown, args: YConfig) { -// return { -// type: 'lens_xy_yConfig', -// ...args, -// }; -// }, -// }; - export interface XConfig extends AxisConfig { accessor: string; } @@ -163,6 +127,14 @@ export const layerConfig: ExpressionFunction< }, args: { ...axisConfig, + layerId: { + types: ['string'], + help: '', + }, + xAccessor: { + types: ['string'], + help: '', + }, seriesType: { types: ['string'], options: [ @@ -210,6 +182,9 @@ export type SeriesType = | 'area_stacked'; type LayerConfig = AxisConfig & { + layerId: string; + xAccessor: string; + accessors: string[]; labels: string[]; seriesType: SeriesType; @@ -220,7 +195,7 @@ export interface XYArgs { // seriesType: SeriesType; legend: LegendConfig; // y: YConfig; - x: XConfig; + // x: XConfig; // splitSeriesAccessors: string[]; layers: LayerConfig[]; } @@ -229,7 +204,7 @@ export interface XYState { // seriesType: SeriesType; legend: LegendConfig; // y: YState; - x: XConfig; + // x: XConfig; layers: LayerConfig[]; // splitSeriesAccessors: string[]; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 62e77ecebcb35..fbad4f42eb7b3 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -112,7 +112,8 @@ function updateLayer(state: State, layer: UnwrapArray, index: n } export function XYConfigPanel(props: VisualizationProps) { - const { state, datasource, setState } = props; + const { state, datasource, setState, frame } = props; + // console.log(state); return ( @@ -196,13 +197,13 @@ export function XYConfigPanel(props: VisualizationProps) { )} */} - - - {/* + */} + {/* ) { */} - ) { /> - {/* ) { } /> + // + // */} - - {state.layers.map((layer, index) => ( @@ -287,6 +287,68 @@ export function XYConfigPanel(props: VisualizationProps) { /> + + + {/* + + setState({ ...state, x: { ...state.x, title: e.target.value } })} + aria-label={i18n.translate('xpack.lens.xyChart.xTitleAriaLabel', { + defaultMessage: 'Title', + })} + /> + + */} + + + operation.isBucketed, + // layer: + // datasource.supportsLayers && datasource.supportsLayerJoin ? 'join' : index, + layerId: 'first', + }} + /> + + + {/* + + + setState({ ...state, x: { ...state.x, showGridlines: !state.x.showGridlines } }) + } + /> + + */} + + + ) { } filterOperations={op => op.isBucketed && op.dataType !== 'date'} suggestedPriority={0} - layer={index} + // layer={index} + layerId={'first'} testSubj="splitSeriesDimensionPanel" /> @@ -387,7 +450,8 @@ export function XYConfigPanel(props: VisualizationProps) { } filterOperations={op => !op.isBucketed && op.dataType === 'number'} testSubj="yDimensionPanel" - layer={index} + // layer={index} + layerId={'first'} /> @@ -411,33 +475,33 @@ export function XYConfigPanel(props: VisualizationProps) { ))} - {datasource.supportsLayers && ( - - { - setState({ - ...state, - layers: [ - ...state.layers, - { - seriesType: 'bar_stacked', - accessors: [generateId()], - title: '', - showGridlines: false, - position: Position.Left, - labels: [''], - splitSeriesAccessors: [], - }, - ], - }); - }} - iconType="plusInCircle" - > - - - - )} + + { + frame.addNewLayer(); + + // setState({ + // ...state, + // layers: [ + // ...state.layers, + // { + // seriesType: 'bar_stacked', + // accessors: [generateId()], + // title: '', + // showGridlines: false, + // position: Position.Left, + // labels: [''], + // splitSeriesAccessors: [], + // }, + // ], + // }); + }} + iconType="plusInCircle" + > + + + diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index c4375af4b5d2e..1f779f9572ca3 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -93,7 +93,7 @@ export const xyChartRenderer: RenderFunction = { }; export function XYChart({ data, args }: XYChartProps) { - const { legend, x, layers } = args; + const { legend, layers } = args; return ( @@ -106,10 +106,12 @@ export function XYChart({ data, args }: XYChartProps) { - {layers.map(({ splitSeriesAccessors, seriesType, labels, accessors }, index) => { + {layers.map(({ splitSeriesAccessors, seriesType, labels, accessors, xAccessor }, index) => { const seriesProps = { key: index, splitSeriesAccessors, - stackAccessors: seriesType.includes('stacked') ? [x.accessor] : [], + stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], id: getSpecId(labels.join(',')), - xAccessor: x.accessor, + xAccessor, yAccessors: labels, data: data.rows.map(row => { const newRow: typeof row = {}; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index e56269af6c116..914986a7bd92c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -85,14 +85,16 @@ function getSuggestion( const state: State = { legend: { isVisible: true, position: Position.Right }, // seriesType: splitBy && isDate ? 'line' : 'bar', - x: { - accessor: xValue.columnId, - position: Position.Bottom, - showGridlines: false, - title: xTitle, - }, + // x: { + // accessor: xValue.columnId, + // position: Position.Bottom, + // showGridlines: false, + // title: xTitle, + // }, layers: [ { + layerId: 'first', + xAccessor: xValue.columnId, seriesType: splitBy && isDate ? 'line' : 'bar', splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [generateId()], accessors: yValues.map(col => col.columnId), @@ -120,10 +122,10 @@ function getSuggestion( previewExpression: buildExpression( { ...state, - x: { - ...state.x, - hide: true, - }, + // x: { + // ...state.x, + // hide: true, + // }, layers: state.layers.map(layer => ({ ...layer, hide: true })), legend: { ...state.legend, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 436265d212cd2..90121cc3c3672 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -18,20 +18,22 @@ import { generateId } from '../id_generator'; export const xyVisualization: Visualization = { getSuggestions, - initialize(datasource, state) { + initialize(frame, state) { return ( state || { // seriesType: 'bar', title: 'Empty XY Chart', legend: { isVisible: true, position: Position.Right }, - x: { - accessor: generateId(), - position: Position.Bottom, - showGridlines: false, - title: 'X', - }, + // x: { + // accessor: generateId(), + // position: Position.Bottom, + // showGridlines: false, + // title: 'X', + // }, layers: [ { + layerId: 'first', + xAccessor: generateId(), seriesType: 'bar_stacked', accessors: [generateId()], position: Position.Left, From 99187a7f7e5b4aa229dd2c1dc7e06796c821da0b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jul 2019 16:58:49 +0200 Subject: [PATCH 03/67] add embeddable for lens WIP --- x-pack/legacy/plugins/lens/index.ts | 3 + .../app_plugin/embeddable/lens_embeddable.ts | 207 ++++++++++++++++++ .../embeddable/lens_embeddable_factory.ts | 109 +++++++++ 3 files changed, 319 insertions(+) create mode 100644 x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts create mode 100644 x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 88a1b25fc4e39..21ae342052fb1 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -26,6 +26,9 @@ export const lens: LegacyPluginInitializer = kibana => { description: 'Explore and visualize data.', main: `plugins/${PLUGIN_ID}/index`, }, + embeddableFactories: [ + 'plugins/lens/app_plugin/embeddable/embeddable_factory' + ], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, visTypes: ['plugins/lens/register_vis_type_alias'], diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts new file mode 100644 index 0000000000000..a9e574fdbf505 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts @@ -0,0 +1,207 @@ +/* + * 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 _ from 'lodash'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { render, unmountComponentAtNode } from 'react-dom'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +import { Embeddable } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { I18nContext } from 'ui/i18n'; + +import { GisMap } from '../connected_components/gis_map'; +import { createMapStore } from '../reducers/store'; +import { getInitialLayers } from '../angular/get_initial_layers'; +import { + setGotoWithCenter, + replaceLayerList, + setQuery, + setRefreshConfig, + disableScrollZoom, +} from '../actions/map_actions'; +import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; +import { + setReadOnly, + setFilterable, + setIsLayerTOCOpen, + setOpenTOCDetails, +} from '../actions/ui_actions'; +import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; +import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { getMapCenter, getMapZoom } from '../selectors/map_selectors'; +import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; + +export class MapEmbeddable extends Embeddable { + type = MAP_SAVED_OBJECT_TYPE; + + constructor(config, initialInput, parent) { + super( + initialInput, + { + editUrl: config.editUrl, + indexPatterns: config.indexPatterns, + editable: config.editable, + defaultTitle: config.savedMap.title + }, + parent); + + this._savedMap = config.savedMap; + this._store = createMapStore(); + + this._subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); + } + + getInspectorAdapters() { + return getInspectorAdapters(this._store.getState()); + } + + onContainerStateChanged(containerState) { + if (!_.isEqual(containerState.timeRange, this._prevTimeRange) || + !_.isEqual(containerState.query, this._prevQuery) || + !_.isEqual(containerState.filters, this._prevFilters)) { + this._dispatchSetQuery(containerState); + } + + if (!_.isEqual(containerState.refreshConfig, this._prevRefreshConfig)) { + this._dispatchSetRefreshConfig(containerState); + } + } + + _dispatchSetQuery({ query, timeRange, filters }) { + this._prevTimeRange = timeRange; + this._prevQuery = query; + this._prevFilters = filters; + this._store.dispatch(setQuery({ + filters: filters.filter(filter => !filter.meta.disabled), + query, + timeFilters: timeRange, + })); + } + + _dispatchSetRefreshConfig({ refreshConfig }) { + this._prevRefreshConfig = refreshConfig; + this._store.dispatch(setRefreshConfig({ + isPaused: refreshConfig.pause, + interval: refreshConfig.value, + })); + } + + /** + * + * @param {HTMLElement} domNode + * @param {ContainerState} containerState + */ + render(domNode) { + this._store.dispatch(setReadOnly(true)); + this._store.dispatch(setFilterable(true)); + this._store.dispatch(disableScrollZoom()); + + if (_.has(this.input, 'isLayerTOCOpen')) { + this._store.dispatch(setIsLayerTOCOpen(this.input.isLayerTOCOpen)); + } else if (this._savedMap.uiStateJSON) { + const uiState = JSON.parse(this._savedMap.uiStateJSON); + this._store.dispatch(setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN))); + } + + if (_.has(this.input, 'openTOCDetails')) { + this._store.dispatch(setOpenTOCDetails(this.input.openTOCDetails)); + } else if (this._savedMap.uiStateJSON) { + const uiState = JSON.parse(this._savedMap.uiStateJSON); + this._store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); + } + + if (this.input.mapCenter) { + this._store.dispatch(setGotoWithCenter({ + lat: this.input.mapCenter.lat, + lon: this.input.mapCenter.lon, + zoom: this.input.mapCenter.zoom, + })); + } else if (this._savedMap.mapStateJSON) { + const mapState = JSON.parse(this._savedMap.mapStateJSON); + this._store.dispatch(setGotoWithCenter({ + lat: mapState.center.lat, + lon: mapState.center.lon, + zoom: mapState.zoom, + })); + } + const layerList = getInitialLayers(this._savedMap.layerListJSON); + this._store.dispatch(replaceLayerList(layerList)); + this._dispatchSetQuery(this.input); + this._dispatchSetRefreshConfig(this.input); + + render( + + + + + , + domNode + ); + + this._unsubscribeFromStore = this._store.subscribe(() => { + this._handleStoreChanges(); + }); + } + + destroy() { + super.destroy(); + if (this._unsubscribeFromStore) { + this._unsubscribeFromStore(); + } + this._savedMap.destroy(); + if (this._domNode) { + unmountComponentAtNode(this._domNode); + } + + if (this._subscription) { + this._subscription.unsubscribe(); + } + } + + reload() { + this._dispatchSetQuery({ + query: this._prevQuery, + timeRange: this._prevTimeRange, + filters: this._prevFilters + }); + } + + _handleStoreChanges() { + + const center = getMapCenter(this._store.getState()); + const zoom = getMapZoom(this._store.getState()); + + + const mapCenter = this.input.mapCenter || {}; + if (!mapCenter + || mapCenter.lat !== center.lat + || mapCenter.lon !== center.lon + || mapCenter.zoom !== zoom) { + this.updateInput({ + mapCenter: { + lat: center.lat, + lon: center.lon, + zoom: zoom, + } + }); + } + + const isLayerTOCOpen = getIsLayerTOCOpen(this._store.getState()); + if (this.input.isLayerTOCOpen !== isLayerTOCOpen) { + this.updateInput({ + isLayerTOCOpen + }); + } + + const openTOCDetails = getOpenTOCDetails(this._store.getState()); + if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) { + this.updateInput({ + openTOCDetails + }); + } + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts new file mode 100644 index 0000000000000..f67f945f114d0 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts @@ -0,0 +1,109 @@ +/* + * 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 _ from 'lodash'; +import chrome from 'ui/chrome'; +import { capabilities } from 'ui/capabilities'; +import { i18n } from '@kbn/i18n'; +import { + EmbeddableFactory, + embeddableFactories, + ErrorEmbeddable +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { MapEmbeddable } from './map_embeddable'; +import { indexPatternService } from '../kibana_services'; + +import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; +import { createMapStore } from '../reducers/store'; +import { addLayerWithoutDataSync } from '../actions/map_actions'; +import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; +import '../angular/services/gis_map_saved_object_loader'; +import 'ui/vis/map/service_settings'; + +export class MapEmbeddableFactory extends EmbeddableFactory { + type = MAP_SAVED_OBJECT_TYPE; + + constructor() { + super({ + savedObjectMetaData: { + name: i18n.translate('xpack.maps.mapSavedObjectLabel', { + defaultMessage: 'Map', + }), + type: MAP_SAVED_OBJECT_TYPE, + getIconForSavedObject: () => APP_ICON, + }, + }); + } + isEditable() { + return capabilities.get().maps.save; + } + + // Not supported yet for maps types. + canCreateNew() { return false; } + + getDisplayName() { + return i18n.translate('xpack.maps.embeddableDisplayName', { + defaultMessage: 'map', + }); + } + + async _getIndexPatterns(layerListJSON) { + // Need to extract layerList from store to get queryable index pattern ids + const store = createMapStore(); + try { + JSON.parse(layerListJSON).forEach(layerDescriptor => { + store.dispatch(addLayerWithoutDataSync(layerDescriptor)); + }); + } catch (error) { + throw new Error(i18n.translate('xpack.maps.mapEmbeddableFactory', { + defaultMessage: 'Unable to load map, malformed saved object', + })); + } + const queryableIndexPatternIds = getQueryableUniqueIndexPatternIds(store.getState()); + + const promises = queryableIndexPatternIds.map(async (indexPatternId) => { + try { + return await indexPatternService.get(indexPatternId); + } catch (error) { + // Unable to load index pattern, better to not throw error so map embeddable can render + // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern + return null; + } + }); + const indexPatterns = await Promise.all(promises); + return _.compact(indexPatterns); + } + + async createFromSavedObject( + savedObjectId, + input, + parent + ) { + const $injector = await chrome.dangerouslyGetActiveInjector(); + const savedObjectLoader = $injector.get('gisMapSavedObjectLoader'); + + const savedMap = await savedObjectLoader.get(savedObjectId); + const indexPatterns = await this._getIndexPatterns(savedMap.layerListJSON); + + return new MapEmbeddable( + { + savedMap, + editUrl: chrome.addBasePath(createMapPath(savedObjectId)), + indexPatterns, + editable: this.isEditable(), + }, + input, + parent + ); + } + + async create(input) { + window.location.href = chrome.addBasePath(createMapPath('')); + return new ErrorEmbeddable('Maps can only be created from a saved object', input); + } +} + +embeddableFactories.set(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); From 9fc03e35d17890eb05142f58953b3d8493b0efbc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 16 Jul 2019 19:57:26 +0200 Subject: [PATCH 04/67] work on lens embeddable --- .../app_plugin/embeddable/lens_embeddable.ts | 207 ------------------ .../app_plugin/embeddable/lens_embeddable.tsx | 67 ++++++ .../embeddable/lens_embeddable_factory.ts | 93 +++----- .../editor_frame_plugin/editor_frame/save.ts | 8 +- 4 files changed, 103 insertions(+), 272 deletions(-) delete mode 100644 x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts create mode 100644 x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts deleted file mode 100644 index a9e574fdbf505..0000000000000 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.ts +++ /dev/null @@ -1,207 +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 _ from 'lodash'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { render, unmountComponentAtNode } from 'react-dom'; -import 'mapbox-gl/dist/mapbox-gl.css'; - -import { Embeddable } from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; -import { I18nContext } from 'ui/i18n'; - -import { GisMap } from '../connected_components/gis_map'; -import { createMapStore } from '../reducers/store'; -import { getInitialLayers } from '../angular/get_initial_layers'; -import { - setGotoWithCenter, - replaceLayerList, - setQuery, - setRefreshConfig, - disableScrollZoom, -} from '../actions/map_actions'; -import { DEFAULT_IS_LAYER_TOC_OPEN } from '../reducers/ui'; -import { - setReadOnly, - setFilterable, - setIsLayerTOCOpen, - setOpenTOCDetails, -} from '../actions/ui_actions'; -import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; -import { getInspectorAdapters } from '../reducers/non_serializable_instances'; -import { getMapCenter, getMapZoom } from '../selectors/map_selectors'; -import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; - -export class MapEmbeddable extends Embeddable { - type = MAP_SAVED_OBJECT_TYPE; - - constructor(config, initialInput, parent) { - super( - initialInput, - { - editUrl: config.editUrl, - indexPatterns: config.indexPatterns, - editable: config.editable, - defaultTitle: config.savedMap.title - }, - parent); - - this._savedMap = config.savedMap; - this._store = createMapStore(); - - this._subscription = this.getInput$().subscribe((input) => this.onContainerStateChanged(input)); - } - - getInspectorAdapters() { - return getInspectorAdapters(this._store.getState()); - } - - onContainerStateChanged(containerState) { - if (!_.isEqual(containerState.timeRange, this._prevTimeRange) || - !_.isEqual(containerState.query, this._prevQuery) || - !_.isEqual(containerState.filters, this._prevFilters)) { - this._dispatchSetQuery(containerState); - } - - if (!_.isEqual(containerState.refreshConfig, this._prevRefreshConfig)) { - this._dispatchSetRefreshConfig(containerState); - } - } - - _dispatchSetQuery({ query, timeRange, filters }) { - this._prevTimeRange = timeRange; - this._prevQuery = query; - this._prevFilters = filters; - this._store.dispatch(setQuery({ - filters: filters.filter(filter => !filter.meta.disabled), - query, - timeFilters: timeRange, - })); - } - - _dispatchSetRefreshConfig({ refreshConfig }) { - this._prevRefreshConfig = refreshConfig; - this._store.dispatch(setRefreshConfig({ - isPaused: refreshConfig.pause, - interval: refreshConfig.value, - })); - } - - /** - * - * @param {HTMLElement} domNode - * @param {ContainerState} containerState - */ - render(domNode) { - this._store.dispatch(setReadOnly(true)); - this._store.dispatch(setFilterable(true)); - this._store.dispatch(disableScrollZoom()); - - if (_.has(this.input, 'isLayerTOCOpen')) { - this._store.dispatch(setIsLayerTOCOpen(this.input.isLayerTOCOpen)); - } else if (this._savedMap.uiStateJSON) { - const uiState = JSON.parse(this._savedMap.uiStateJSON); - this._store.dispatch(setIsLayerTOCOpen(_.get(uiState, 'isLayerTOCOpen', DEFAULT_IS_LAYER_TOC_OPEN))); - } - - if (_.has(this.input, 'openTOCDetails')) { - this._store.dispatch(setOpenTOCDetails(this.input.openTOCDetails)); - } else if (this._savedMap.uiStateJSON) { - const uiState = JSON.parse(this._savedMap.uiStateJSON); - this._store.dispatch(setOpenTOCDetails(_.get(uiState, 'openTOCDetails', []))); - } - - if (this.input.mapCenter) { - this._store.dispatch(setGotoWithCenter({ - lat: this.input.mapCenter.lat, - lon: this.input.mapCenter.lon, - zoom: this.input.mapCenter.zoom, - })); - } else if (this._savedMap.mapStateJSON) { - const mapState = JSON.parse(this._savedMap.mapStateJSON); - this._store.dispatch(setGotoWithCenter({ - lat: mapState.center.lat, - lon: mapState.center.lon, - zoom: mapState.zoom, - })); - } - const layerList = getInitialLayers(this._savedMap.layerListJSON); - this._store.dispatch(replaceLayerList(layerList)); - this._dispatchSetQuery(this.input); - this._dispatchSetRefreshConfig(this.input); - - render( - - - - - , - domNode - ); - - this._unsubscribeFromStore = this._store.subscribe(() => { - this._handleStoreChanges(); - }); - } - - destroy() { - super.destroy(); - if (this._unsubscribeFromStore) { - this._unsubscribeFromStore(); - } - this._savedMap.destroy(); - if (this._domNode) { - unmountComponentAtNode(this._domNode); - } - - if (this._subscription) { - this._subscription.unsubscribe(); - } - } - - reload() { - this._dispatchSetQuery({ - query: this._prevQuery, - timeRange: this._prevTimeRange, - filters: this._prevFilters - }); - } - - _handleStoreChanges() { - - const center = getMapCenter(this._store.getState()); - const zoom = getMapZoom(this._store.getState()); - - - const mapCenter = this.input.mapCenter || {}; - if (!mapCenter - || mapCenter.lat !== center.lat - || mapCenter.lon !== center.lon - || mapCenter.zoom !== zoom) { - this.updateInput({ - mapCenter: { - lat: center.lat, - lon: center.lon, - zoom: zoom, - } - }); - } - - const isLayerTOCOpen = getIsLayerTOCOpen(this._store.getState()); - if (this.input.isLayerTOCOpen !== isLayerTOCOpen) { - this.updateInput({ - isLayerTOCOpen - }); - } - - const openTOCDetails = getOpenTOCDetails(this._store.getState()); - if (!_.isEqual(this.input.openTOCDetails, openTOCDetails)) { - this.updateInput({ - openTOCDetails - }); - } - } -} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx new file mode 100644 index 0000000000000..095dab1c29354 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.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 _ from 'lodash'; +import React from 'react'; +import { Provider } from 'react-redux'; +import { render, unmountComponentAtNode } from 'react-dom'; + +import { Embeddable, EmbeddableOutput, IContainer, EmbeddableInput } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { I18nContext } from 'ui/i18n'; +import { Document } from '../../persistence'; +import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; + +const ExpressionRendererComponent = data.expressions.ExpressionRenderer; + +export interface LensEmbeddableInput extends EmbeddableInput { + savedVis: Document; +} + + +export class LensEmbeddable extends Embeddable { + type = 'lens'; + + private savedVis: Document; + private domNode: HTMLElement | Element | undefined; + + constructor(input: LensEmbeddableInput, output: EmbeddableOutput, parent?: IContainer) { + super( + input, + output, + parent); + + this.savedVis = input.savedVis; + } + + /** + * + * @param {HTMLElement} domNode + * @param {ContainerState} containerState + */ + render(domNode: HTMLElement | Element) { + this.domNode = domNode; + render( + { + // TODO error handling + }} + />, domNode); + } + + destroy() { + super.destroy(); + if (this.domNode) { + unmountComponentAtNode(this.domNode); + } + } + + reload() { + // TODO re-render here once we actually pass context down to the visualization + } +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts index f67f945f114d0..fe31859d95476 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts @@ -11,99 +11,66 @@ import { i18n } from '@kbn/i18n'; import { EmbeddableFactory, embeddableFactories, - ErrorEmbeddable -} from '../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; -import { MapEmbeddable } from './map_embeddable'; -import { indexPatternService } from '../kibana_services'; + ErrorEmbeddable, + EmbeddableInput, + IContainer +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { LensEmbeddable } from './lens_embeddable'; +import { SavedObjectIndexStore } from '../../persistence'; -import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/constants'; -import { createMapStore } from '../reducers/store'; -import { addLayerWithoutDataSync } from '../actions/map_actions'; -import { getQueryableUniqueIndexPatternIds } from '../selectors/map_selectors'; -import '../angular/services/gis_map_saved_object_loader'; -import 'ui/vis/map/service_settings'; -export class MapEmbeddableFactory extends EmbeddableFactory { - type = MAP_SAVED_OBJECT_TYPE; +export class LensEmbeddableFactory extends EmbeddableFactory { + type = 'lens'; constructor() { super({ savedObjectMetaData: { - name: i18n.translate('xpack.maps.mapSavedObjectLabel', { - defaultMessage: 'Map', + name: i18n.translate('xpack.lens.lensSavedObjectLabel', { + defaultMessage: 'Lens Visualization', }), - type: MAP_SAVED_OBJECT_TYPE, - getIconForSavedObject: () => APP_ICON, + type: 'lens', + getIconForSavedObject: () => 'happyFace', }, }); } isEditable() { - return capabilities.get().maps.save; + // TODO make it possible + return false; } // Not supported yet for maps types. canCreateNew() { return false; } getDisplayName() { - return i18n.translate('xpack.maps.embeddableDisplayName', { - defaultMessage: 'map', + return i18n.translate('xpack.lens.embeddableDisplayName', { + defaultMessage: 'lens', }); } - async _getIndexPatterns(layerListJSON) { - // Need to extract layerList from store to get queryable index pattern ids - const store = createMapStore(); - try { - JSON.parse(layerListJSON).forEach(layerDescriptor => { - store.dispatch(addLayerWithoutDataSync(layerDescriptor)); - }); - } catch (error) { - throw new Error(i18n.translate('xpack.maps.mapEmbeddableFactory', { - defaultMessage: 'Unable to load map, malformed saved object', - })); - } - const queryableIndexPatternIds = getQueryableUniqueIndexPatternIds(store.getState()); - - const promises = queryableIndexPatternIds.map(async (indexPatternId) => { - try { - return await indexPatternService.get(indexPatternId); - } catch (error) { - // Unable to load index pattern, better to not throw error so map embeddable can render - // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern - return null; - } - }); - const indexPatterns = await Promise.all(promises); - return _.compact(indexPatterns); - } - async createFromSavedObject( - savedObjectId, - input, - parent + savedObjectId: string, + input: Partial, + parent?: IContainer ) { - const $injector = await chrome.dangerouslyGetActiveInjector(); - const savedObjectLoader = $injector.get('gisMapSavedObjectLoader'); - - const savedMap = await savedObjectLoader.get(savedObjectId); - const indexPatterns = await this._getIndexPatterns(savedMap.layerListJSON); + const store = new SavedObjectIndexStore(chrome.getSavedObjectsClient()); + const savedVis = await store.load(savedObjectId); - return new MapEmbeddable( + // TODO do I need to pass in edit url stuff? + return new LensEmbeddable( { - savedMap, - editUrl: chrome.addBasePath(createMapPath(savedObjectId)), - indexPatterns, - editable: this.isEditable(), + savedVis, + id: savedObjectId }, input, parent ); } - async create(input) { - window.location.href = chrome.addBasePath(createMapPath('')); - return new ErrorEmbeddable('Maps can only be created from a saved object', input); + async create(input: EmbeddableInput) { + // TODO fix this + window.location.href = chrome.addBasePath('lens/abc'); + return new ErrorEmbeddable('Lens can only be created from a saved object', input); } } -embeddableFactories.set(MAP_SAVED_OBJECT_TYPE, new MapEmbeddableFactory()); +embeddableFactories.set('lens', new LensEmbeddableFactory()); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 472220e83a44e..63f66cc3d1b5a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -6,14 +6,16 @@ import { Action, EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; +import { buildExpression } from './expression_helpers'; +import { Datasource, Visualization } from '../../types'; export interface Props { - datasource: { getPersistableState: (state: unknown) => unknown }; + datasource: Datasource; dispatch: (value: Action) => void; redirectTo: (path: string) => void; state: EditorFrameState; store: { save: (doc: Document) => Promise<{ id: string }> }; - visualization: { getPersistableState: (state: unknown) => unknown }; + visualization: Visualization; } export async function save({ @@ -27,6 +29,8 @@ export async function save({ try { dispatch({ type: 'SAVING', isSaving: true }); + const expression = buildExpression(visualization, state.visualization.state, datasource, state.datasource.state, datasource.getPublicAPI(state.datasource.state, () => {})); + const doc = await store.save({ id: state.persistedId, title: state.title, From 84bf126f610d32ae29e4aeff48fde833edd71729 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 16 Jul 2019 15:28:26 -0400 Subject: [PATCH 05/67] Get basic layering to work --- .../plugins/lens/public/app_plugin/plugin.tsx | 2 +- .../editor_frame/config_panel_wrapper.tsx | 31 ++-- .../editor_frame/editor_frame.tsx | 153 +++++++++--------- .../editor_frame/expression_helpers.ts | 75 ++++++--- .../editor_frame/state_management.ts | 80 ++++----- .../editor_frame/workspace_panel.tsx | 57 ++++--- .../merge_tables.ts | 0 .../public/editor_frame_plugin/plugin.tsx | 32 +++- .../dimension_panel/dimension_panel.tsx | 2 + .../dimension_panel/popover_editor.tsx | 11 +- .../indexpattern_plugin/indexpattern.tsx | 21 ++- .../public/indexpattern_plugin/plugin.tsx | 4 +- .../indexpattern_plugin/to_expression.ts | 25 ++- x-pack/legacy/plugins/lens/public/types.ts | 10 +- .../xy_visualization_plugin/to_expression.ts | 11 +- .../public/xy_visualization_plugin/types.ts | 6 + .../xy_config_panel.tsx | 56 +++---- .../xy_visualization.tsx | 31 ++-- 18 files changed, 360 insertions(+), 247 deletions(-) rename x-pack/legacy/plugins/lens/public/{indexpattern_plugin => editor_frame_plugin}/merge_tables.ts (100%) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index a559c0a94465b..51f2193ef4691 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -30,7 +30,7 @@ export class AppPlugin { editorFrame.registerDatasource('indexpattern', indexPattern); editorFrame.registerVisualization('xy', xyVisualization); - editorFrame.registerVisualization('datatable', datatableVisualization); + // editorFrame.registerVisualization('datatable', datatableVisualization); this.instance = editorFrame.createInstance({}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index 19cdd1508fc5e..c36acb798b8ae 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -16,7 +16,7 @@ interface ConfigPanelWrapperProps { visualizationMap: Record; activeVisualizationId: string | null; dispatch: (action: Action) => void; - datasourcePublicAPI: DatasourcePublicAPI; + // datasourcePublicAPI: DatasourcePublicAPI; framePublicAPI: FramePublicAPI; } @@ -25,24 +25,25 @@ function getSuggestedVisualizationState( visualization: Visualization // datasource: DatasourcePublicAPI ) { - const suggestions = visualization.getSuggestions({ - tables: [ - { - datasourceSuggestionId: 0, - isMultiRow: true, - columns: datasource.getTableSpec().map(col => ({ - ...col, - operation: datasource.getOperationForColumnId(col.columnId)!, - })), - }, - ], - }); + const suggestions = []; + // const suggestions = visualization.getSuggestions({ + // tables: [ + // { + // datasourceSuggestionId: 0, + // isMultiRow: true, + // columns: datasource.getTableSpec().map(col => ({ + // ...col, + // operation: datasource.getOperationForColumnId(col.columnId)!, + // })), + // }, + // ], + // }); if (!suggestions.length) { return visualization.initialize(frame); } - return visualization.initialize(frame, suggestions[0].state); + // return visualization.initialize(frame, suggestions[0].state); } export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { @@ -86,7 +87,7 @@ export function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { dragDropContext: context, state: props.visualizationState, setState: setVisualizationState, - datasource: props.datasourcePublicAPI, + // datasource: props.datasourcePublicAPI, frame: props.framePublicAPI, }} /> diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 31e3f44e4bda8..957ec65fab0a2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useEffect, useReducer, useMemo } from 'react'; +import React, { useEffect, useReducer } from 'react'; import { EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { Datasource, FramePublicAPI, Visualization } from '../../types'; +import { Datasource, DatasourcePublicAPI, FramePublicAPI, Visualization } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; import { ConfigPanelWrapper } from './config_panel_wrapper'; @@ -24,7 +24,7 @@ export interface EditorFrameProps { store: SavedObjectStore; datasourceMap: Record; visualizationMap: Record; - layerToDatasourceId: Record; + // layerToDatasourceId: Record; redirectTo: (path: string) => void; initialDatasourceId: string | null; initialVisualizationId: string | null; @@ -36,50 +36,79 @@ export function EditorFrame(props: EditorFrameProps) { const [state, dispatch] = useReducer(reducer, props, getInitialState); const { onError } = props; - // create public datasource api for current state - // as soon as datasource is available and memoize it - const datasourcePublicAPI = useMemo( - () => - state.activeDatasourceId && !state.datasources[state.activeDatasourceId].isLoading - ? props.datasourceMap[state.activeDatasourceId].getPublicAPI( - state.datasources[state.activeDatasourceId].state, - (newState: unknown) => { + const allLoaded = Object.values(state.datasourceStates).every(({ isLoading }) => !isLoading); + + // Initialize current datasource and all active datasources + useEffect(() => { + if (!allLoaded) { + Object.entries(state.datasourceMap).forEach(([datasourceId, datasource]) => { + if (state.datasourceStates[datasourceId].isLoading) { + datasource + .initialize(props.doc && props.doc.state.datasource) + .then(datasourceState => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', - newState, + newState: datasourceState, }); - } - ) - : undefined, - [ - props.datasourceMap, - state.activeDatasourceId, - state.datasources[state.activeDatasourceId!], - // state.datasource.isLoading, - // state.activeDatasourceId, - // state.datasource.state, - ] - ); + }) + .catch(onError); + } + }); + } + }, [props.doc, Object.keys(state.datasourceStates), state.activeDatasourceId]); + + const datasourceEntries: Array<[string, DatasourcePublicAPI]> = Object.keys( + state.datasourceMap + ).map(key => { + const ds = state.datasourceMap[key]; + return [ + key, + ds.getPublicAPI(state.datasourceStates[key].state, (newState: unknown) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + newState, + }); + }), + ]; + }); + + const layerToDatasourceId: Record = {}; + const datasourceLayers: Record = {}; + datasourceEntries.forEach(([id, publicAPI]) => { + const stateWrapper = state.datasourceStates[id]; + if (stateWrapper.isLoading) { + return; + } + const dsState = stateWrapper.state; + const layers = state.datasourceMap[id].getLayers(dsState); + + layers.forEach(layer => { + layerToDatasourceId[layer] = id; + datasourceLayers[layer] = publicAPI; + }); + }); const framePublicAPI: FramePublicAPI = { - layerIdToDatasource: state.layerToDatasourceId, - datasourceLayers: {}, + layerIdToDatasource: layerToDatasourceId, + datasourceLayers, addNewLayer: () => { const newLayerId = 'second'; + const newState = state.datasourceMap[state.activeDatasourceId!].insertLayer( + state.datasourceStates[state.activeDatasourceId!].state, + newLayerId + ); + dispatch({ type: 'CREATE_LAYER', newLayerId, + newDatasourceState: newState, }); return newLayerId; }, }; - // const layerToDatasource = { - // 0: datasourcePublicAPI, - // }; - useEffect(() => { if (props.doc) { dispatch({ @@ -94,48 +123,21 @@ export function EditorFrame(props: EditorFrameProps) { } }, [props.doc]); - // Initialize current datasource + // Initialize visualization as soon as all datasource is ready useEffect(() => { - let datasourceGotSwitched = false; - // if (state.datasource.isLoading && state.activeDatasourceId) { - if (state.activeDatasourceId && state.datasources[state.activeDatasourceId].isLoading) { - props.datasourceMap[state.activeDatasourceId] - .initialize(props.doc && props.doc.state.datasource) - .then(datasourceState => { - if (!datasourceGotSwitched) { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - newState: datasourceState, - }); - } - }) - .catch(onError); - - return () => { - datasourceGotSwitched = true; - }; - } - }, [props.doc, state.activeDatasourceId, state.datasources[state.activeDatasourceId!].isLoading]); - - // Initialize visualization as soon as datasource is ready - useEffect(() => { - if ( - datasourcePublicAPI && - state.visualization.state === null && - state.visualization.activeId !== null - ) { + if (allLoaded && state.visualization.state === null && state.visualization.activeId !== null) { const initialVisualizationState = props.visualizationMap[ state.visualization.activeId - ].initialize(framePublicAPI, datasourcePublicAPI); + ].initialize(framePublicAPI); dispatch({ type: 'UPDATE_VISUALIZATION_STATE', newState: initialVisualizationState, }); } - }, [datasourcePublicAPI, state.visualization.activeId, state.visualization.state]); + }, [allLoaded, state.visualization.activeId, state.visualization.state]); const datasource = - state.activeDatasourceId && !state.datasources[state.activeDatasourceId].isLoading + state.activeDatasourceId && !state.datasourceStates[state.activeDatasourceId].isLoading ? props.datasourceMap[state.activeDatasourceId] : undefined; @@ -174,11 +176,13 @@ export function EditorFrame(props: EditorFrameProps) { datasourceMap={props.datasourceMap} activeDatasource={state.activeDatasourceId} datasourceState={ - state.activeDatasourceId ? state.datasources[state.activeDatasourceId].state : null + state.activeDatasourceId + ? state.datasourceStates[state.activeDatasourceId].state + : null } datasourceIsLoading={ state.activeDatasourceId - ? state.datasources[state.activeDatasourceId].isLoading + ? state.datasourceStates[state.activeDatasourceId].isLoading : true } dispatch={dispatch} @@ -188,7 +192,7 @@ export function EditorFrame(props: EditorFrameProps) { , + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + > ): Ast | null { - const datasourceExpression = datasource.toExpression(datasourceState); + const datasourceExpressions: Array = []; - if (datasourceExpression === null || visualizationExpression === null) { + Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => { + const state = datasourceStates[datasourceId].state; + const layers = datasource.getLayers(datasourceStates[datasourceId].state); + + layers.forEach(layerId => { + const result = datasource.toExpression(state, layerId); + if (result) { + datasourceExpressions.push(result); + } + }); + }); + + if (datasourceExpressions.length === 0 || visualizationExpression === null) { return null; } - const parsedDatasourceExpression = - typeof datasourceExpression === 'string' - ? fromExpression(datasourceExpression) - : datasourceExpression; + const parsedDatasourceExpressions = datasourceExpressions.map(expr => + typeof expr === 'string' ? fromExpression(expr) : expr + ); const parsedVisualizationExpression = typeof visualizationExpression === 'string' ? fromExpression(visualizationExpression) : visualizationExpression; + + const chainedExpr = parsedDatasourceExpressions + .map(expr => expr.chain) + .reduce((prev, current) => prev.concat(current), []); + return { type: 'expression', - chain: [...parsedDatasourceExpression.chain, ...parsedVisualizationExpression.chain], + chain: chainedExpr.concat([...parsedVisualizationExpression.chain]), }; } -export function buildExpression( - visualization: Visualization | null, - visualizationState: unknown, - datasource: Datasource, - datasourceState: unknown, - datasourcePublicAPI: DatasourcePublicAPI -): Ast | null { +export function buildExpression({ + visualization, + visualizationState, + datasourceMap, + datasourceStates, + framePublicAPI, +}: { + visualization: Visualization | null; + visualizationState: unknown; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + framePublicAPI: FramePublicAPI; +}): Ast | null { if (visualization === null) { return null; } - const visualizationExpression = visualization.toExpression( - visualizationState, - datasourcePublicAPI - ); + const visualizationExpression = visualization.toExpression(visualizationState, framePublicAPI); - return prependDatasourceExpression(visualizationExpression, datasource, datasourceState); + return prependDatasourceExpression(visualizationExpression, datasourceMap, datasourceStates); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index a476b2822b2b2..2e302cd7102a9 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { Datasource, FramePublicAPI } from '../../types'; import { EditorFrameProps } from '../editor_frame'; import { Document } from '../../persistence/saved_object_store'; @@ -16,18 +17,10 @@ export interface EditorFrameState { activeId: string | null; state: unknown; }; - datasources: Record< - string, - { - state: unknown; - isLoading: boolean; - } - >; + datasourceMap: Record>; + datasourceStates: Record; activeDatasourceId: string | null; - // datasource: { - // activeId: string | null; - // }; - layerToDatasourceId: Record; + layerIdToDatasource: FramePublicAPI['layerIdToDatasource']; } export type Action = @@ -72,27 +65,32 @@ export type Action = | { type: 'CREATE_LAYER'; newLayerId: string; + newDatasourceState: unknown; + } + | { + type: 'UPDATE_LAYERS'; + layerToDatasourceId: Record; }; export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { saving: false, title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), - datasources: props.initialDatasourceId + datasourceMap: props.datasourceMap, + datasourceStates: props.initialDatasourceId ? { [props.initialDatasourceId]: { state: null, isLoading: Boolean(props.initialDatasourceId), - // activeId: props.initialDatasourceId, }, } : {}, activeDatasourceId: props.initialDatasourceId, - layerToDatasourceId: {}, visualization: { state: null, activeId: props.initialVisualizationId, }, + layerIdToDatasource: {}, }; }; @@ -111,21 +109,15 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, persistedId: action.doc.id, title: action.doc.title, - // datasource: { - // ...state.datasource, - // activeId: action.doc.datasourceType || null, - // isLoading: true, - // state: action.doc.state.datasource, - // }, - datasources: action.doc.datasourceType + datasourceStates: action.doc.datasourceType ? { - ...state.datasources, + ...state.datasourceStates, [action.doc.datasourceType]: { isLoading: true, state: action.doc.state.datasource, }, } - : state.datasources, + : state.datasourceStates, activeDatasourceId: action.doc.datasourceType || null, visualization: { @@ -137,14 +129,8 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'SWITCH_DATASOURCE': return { ...state, - // datasource: { - // ...state.datasource, - // isLoading: true, - // state: null, - // activeId: action.newDatasourceId, - // }, - datasources: { - ...state.datasources, + datasourceStates: { + ...state.datasourceStates, [action.newDatasourceId]: { state: null, isLoading: true, @@ -166,25 +152,20 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta activeId: action.newVisualizationId, state: action.initialState, }, - // datasource: { - // ...state.datasource, - // state: action.datasourceState ? action.datasourceState : state.datasource.state, - // }, - // datasources: { - // ...state.datasources, - // [state.activeDatasourceId]: - // state: action.datasourceState ? action.datasourceState : state.datasource.state, - // }, + }; + case 'UPDATE_LAYERS': + return { + ...state, + layerIdToDatasource: action.layerToDatasourceId, }; case 'UPDATE_DATASOURCE_STATE': return { ...state, - datasources: { - ...state.datasources, - // when the datasource state is updated, the initialization is complete + datasourceStates: { + ...state.datasourceStates, [state.activeDatasourceId!]: { - isLoading: false, state: action.newState, + isLoading: false, }, }, }; @@ -202,10 +183,17 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'CREATE_LAYER': return { ...state, - layerToDatasourceId: { - ...state.layerToDatasourceId, + layerIdToDatasource: { + ...state.layerIdToDatasource, [action.newLayerId]: state.activeDatasourceId!, }, + datasourceStates: { + ...state.datasourceStates, + [state.activeDatasourceId!]: { + state: action.newDatasourceState, + isLoading: false, + }, + }, }; default: return state; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 58817e7c19ad1..c187b436c8eb1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -10,38 +10,51 @@ import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { toExpression } from '@kbn/interpreter/common'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; import { Action } from './state_management'; -import { Datasource, Visualization, DatasourcePublicAPI } from '../../types'; +import { Datasource, Visualization, FramePublicAPI } from '../../types'; import { DragDrop, DragContext } from '../../drag_drop'; import { getSuggestions, toSwitchAction } from './suggestion_helpers'; import { buildExpression } from './expression_helpers'; export interface WorkspacePanelProps { - activeDatasource: Datasource; - datasourceState: unknown; + // activeDatasource: Datasource; + // datasourceState: unknown; activeVisualizationId: string | null; visualizationMap: Record; visualizationState: unknown; - datasourcePublicAPI: DatasourcePublicAPI; + // datasourcePublicAPI: DatasourcePublicAPI; + activeDatasourceId: string | null; + datasourceMap: Record; + datasourceStates: Record< + string, + { + state: unknown; + isLoading: boolean; + } + >; + framePublicAPI: FramePublicAPI; dispatch: (action: Action) => void; ExpressionRenderer: ExpressionRenderer; } export function WorkspacePanel({ - activeDatasource, + // activeDatasource, + activeDatasourceId, activeVisualizationId, - datasourceState, + // datasourceState, visualizationMap, visualizationState, - datasourcePublicAPI, + // datasourcePublicAPI, + datasourceMap, + datasourceStates, + framePublicAPI, dispatch, ExpressionRenderer: ExpressionRendererComponent, }: WorkspacePanelProps) { const dragDropContext = useContext(DragContext); function onDrop(item: unknown) { - const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsForField( - datasourceState, - item - ); + const datasourceSuggestions = datasourceMap[ + activeDatasourceId! + ].getDatasourceSuggestionsForField(datasourceStates[activeDatasourceId!].state, item); const suggestions = getSuggestions( datasourceSuggestions, @@ -80,22 +93,26 @@ export function WorkspacePanel({ : null; const expression = useMemo(() => { try { - return buildExpression( - activeVisualization, + return buildExpression({ + visualization: activeVisualization, visualizationState, - activeDatasource, - datasourceState, - datasourcePublicAPI - ); + // activeDatasource, + datasourceMap, + datasourceStates, + // datasourceState, + framePublicAPI, + // datasourcePublicAPI + }); } catch (e) { setExpressionError(e.toString()); } }, [ activeVisualization, visualizationState, - activeDatasource, - datasourceState, - datasourcePublicAPI, + activeDatasourceId, + // activeDatasource, + datasourceStates, + // datasourcePublicAPI, ]); useEffect(() => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts similarity index 100% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/merge_tables.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 9349d6c0907a2..8763da31d6954 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -6,6 +6,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { Registry } from '@kbn/interpreter/target/common'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup } from 'src/core/public'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; @@ -15,6 +16,8 @@ import { ExpressionRenderer, } from '../../../../../../src/legacy/core_plugins/data/public'; import { data } from '../../../../../../src/legacy/core_plugins/data/public/setup'; +import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public'; +import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { Datasource, Visualization, @@ -26,9 +29,18 @@ import { import { EditorFrame } from './editor_frame'; import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; import { InitializableComponent } from './initializable_component'; +import { mergeTables } from './merge_tables'; + +export interface InterpreterSetup { + functionsRegistry: Registry< + ExpressionFunction, + ExpressionFunction + >; +} export interface EditorFrameSetupPlugins { data: DataSetup; + interpreter: InterpreterSetup; } interface InitializationResult { @@ -48,7 +60,7 @@ interface RenderProps extends InitializationResult { onError: ErrorCallback; datasources: Record; visualizations: Record; - layerToDatasourceId: Record; // Maps layer ID to datasource ID + // layerToDatasourceId: Record; // Maps layer ID to datasource ID expressionRenderer: ExpressionRenderer; } @@ -58,7 +70,7 @@ export class EditorFramePlugin { private ExpressionRenderer: ExpressionRenderer | null = null; private readonly datasources: Record = {}; private readonly visualizations: Record = {}; - private readonly layerToDatasourceId: Record = {}; + // private readonly layerToDatasourceId: Record = {}; private createInstance(): EditorFrameInstance { let domElement: Element; @@ -91,7 +103,7 @@ export class EditorFramePlugin { store={store} datasources={this.datasources} visualizations={this.visualizations} - layerToDatasourceId={this.layerToDatasourceId} + // layerToDatasourceId={this.layerToDatasourceId} expressionRenderer={this.ExpressionRenderer!} /> )} @@ -116,7 +128,12 @@ export class EditorFramePlugin { }; } - public setup(_core: CoreSetup | null, plugins: EditorFrameSetupPlugins): EditorFrameSetup { + public setup( + _core: CoreSetup | null, + plugins: EditorFrameSetupPlugins // { interpreter, data, storage, toastNotifications: toast }: IndexPatternDatasourcePluginPlugins + ): EditorFrameSetup { + plugins.interpreter.functionsRegistry.register(() => mergeTables); + this.ExpressionRenderer = plugins.data.expressions.ExpressionRenderer; return { createInstance: this.createInstance.bind(this), @@ -139,6 +156,9 @@ const editorFrame = new EditorFramePlugin(); export const editorFrameSetup = () => editorFrame.setup(null, { data, + interpreter: { + functionsRegistry, + }, }); export const editorFrameStop = () => editorFrame.stop(); @@ -179,7 +199,7 @@ export function InitializedEditor({ store, datasources, visualizations, - layerToDatasourceId, + // layerToDatasourceId, expressionRenderer, }: RenderProps) { const firstDatasourceId = Object.keys(datasources)[0]; @@ -196,7 +216,7 @@ export function InitializedEditor({ store={store} datasourceMap={datasources} visualizationMap={visualizations} - layerToDatasourceId={layerToDatasourceId} + // layerToDatasourceId={layerToDatasourceId} initialDatasourceId={(doc && doc.datasourceType) || firstDatasourceId || null} initialVisualizationId={(doc && doc.visualizationType) || firstVisualizationId || null} ExpressionRenderer={expressionRenderer} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 5f72915307549..a9205b3a55f77 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -42,6 +42,8 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp return props.filterOperations(columnToOperation(col)); }); + const layer = props.state.layers[layerId]; + const selectedColumn: IndexPatternColumn | null = props.state.layers[layerId].columns[props.columnId] || null; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 560007d44862e..23d0ffaa36002 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -59,7 +59,7 @@ export interface PopoverEditorProps extends IndexPatternDimensionPanelProps { } export function PopoverEditor(props: PopoverEditorProps) { - const { selectedColumn, filteredColumns, state, columnId, setState } = props; + const { selectedColumn, filteredColumns, state, columnId, setState, layerId } = props; const [isPopoverOpen, setPopoverOpen] = useState(false); const [ incompatibleSelectedOperationType, @@ -105,7 +105,7 @@ export function PopoverEditor(props: PopoverEditorProps) { !hasField(selectedColumn) || col.sourceField === selectedColumn.sourceField) )!; - setState(changeColumn(state, columnId, newColumn)); + setState(changeColumn(state, layerId, columnId, newColumn)); }, }) ), @@ -148,10 +148,10 @@ export function PopoverEditor(props: PopoverEditorProps) { selectedColumn={selectedColumn} incompatibleSelectedOperationType={incompatibleSelectedOperationType} onDeleteColumn={() => { - setState(deleteColumn(state, columnId)); + setState(deleteColumn(state, layerId, columnId)); }} onChangeColumn={column => { - setState(changeColumn(state, columnId, column)); + setState(changeColumn(state, layerId, columnId, column)); setInvalidOperationType(null); }} /> @@ -186,6 +186,7 @@ export function PopoverEditor(props: PopoverEditorProps) { columnId={columnId} storage={props.storage} dataPlugin={props.dataPlugin} + layerId={layerId} /> )} {!incompatibleSelectedOperationType && selectedColumn && ( @@ -195,7 +196,7 @@ export function PopoverEditor(props: PopoverEditorProps) { value={selectedColumn.label} onChange={e => { setState( - changeColumn(state, columnId, { + changeColumn(state, layerId, columnId, { ...selectedColumn, label: e.target.value, }) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 57d44b38d10fc..b386162ac4022 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -263,10 +263,10 @@ export function getIndexPatternDatasource({ currentIndexPatternId: indexPatternObjects ? indexPatternObjects[0].id : '', indexPatterns, layers: { - first: { - columns: {}, - columnOrder: [], - }, + // first: { + // columns: {}, + // columnOrder: [], + // }, }, }; }, @@ -279,9 +279,20 @@ export function getIndexPatternDatasource({ insertLayer(state: IndexPatternPrivateState, newLayerId: string) { return { ...state, + layers: { + ...state.layers, + [newLayerId]: { + columns: {}, + columnOrder: [], + }, + }, }; }, + getLayers(state: IndexPatternPrivateState) { + return Object.keys(state.layers); + }, + toExpression, renderDataPanel( @@ -324,7 +335,7 @@ export function getIndexPatternDatasource({ dataPlugin={data} storage={storage} // layer={props.layer || 0} - layerId={props.layerId || 'first'} + layerId={props.layerId} {...props} /> , diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index 56a7ff37c9ed5..f516ff5c3cd06 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -18,7 +18,7 @@ import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/int import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { calculateFilterRatio } from './filter_ratio'; -import { mergeTables } from './merge_tables'; +// import { mergeTables } from './merge_tables'; // TODO these are intermediary types because interpreter is not typed yet // They can get replaced by references to the real interfaces as soon as they @@ -48,7 +48,7 @@ class IndexPatternDatasourcePlugin { ) { interpreter.functionsRegistry.register(() => renameColumns); interpreter.functionsRegistry.register(() => calculateFilterRatio); - interpreter.functionsRegistry.register(() => mergeTables); + // interpreter.functionsRegistry.register(() => mergeTables); return getIndexPatternDatasource({ chrome, interpreter, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index a4c006125a7ce..19cd02a0d6346 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -15,6 +15,7 @@ import { function getExpressionForLayer( currentIndexPatternId: string, + layerId: string, columns: Record, columnOrder: string[] ) { @@ -60,7 +61,7 @@ function getExpressionForLayer( op: 'count', columns, suggestedOrder: 2, - layerId: 'first', + layerId, }); aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); @@ -83,13 +84,23 @@ function getExpressionForLayer( return null; } -export function toExpression(state: IndexPatternPrivateState) { - const expressions = Object.entries(state.layers).map(([id, layer]) => [ - id, - getExpressionForLayer(state.currentIndexPatternId, layer.columns, layer.columnOrder), - ]); +export function toExpression(state: IndexPatternPrivateState, layerId: string) { + // const expressions = Object.entries(state.layers).map(([id, layer]) => [ + // id, + // getExpressionForLayer(state.currentIndexPatternId, layer.columns, layer.columnOrder), + // ]); + + if (state.layers[layerId]) { + // return `lens_merge_tables joins="" ${expressions.map(expr => `table={${expr}}`).join(' ')}`; + return getExpressionForLayer( + state.currentIndexPatternId, + layerId, + state.layers[layerId].columns, + state.layers[layerId].columnOrder + ); + } - return `lens_merge_tables joins="" ${expressions.map(expr => `table={${expr}}`).join(' ')}`; + return null; // if (state.columnOrder.length === 0) { // return null; diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index d20763a0ae4ca..391cc70f7f5fb 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -60,10 +60,12 @@ export interface Datasource { getPersistableState: (state: T) => P; insertLayer: (state: T, newLayerId: string) => T; + getLayers: (state: T) => string[]; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; - toExpression: (state: T) => Ast | string | null; + // toExpression: (state: T) => Ast | string | null; + toExpression: (state: T, layerId: string) => Ast | string | null; getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; @@ -148,7 +150,7 @@ export interface KibanaDatatable { export interface VisualizationProps { dragDropContext: DragContextState; - datasource: DatasourcePublicAPI; + // datasource: DatasourcePublicAPI; frame: FramePublicAPI; state: T; setState: (newState: T) => void; @@ -173,7 +175,7 @@ export interface FramePublicAPI { datasourceLayers: Record; layerIdToDatasource: Record; // Adds a new layer. This triggers a re-render - addNewLayer: () => void; + addNewLayer: () => string; } export interface Visualization { @@ -191,4 +193,6 @@ export interface Visualization { // The frame will call this function on all visualizations when the table changes, or when // rendering additional ways of using the data getSuggestions: (options: SuggestionRequest) => Array>; + + getLayerIds: (state: T) => string[]; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index b8ac934378d57..63c6d3749ab7d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -9,11 +9,17 @@ import { State } from './types'; import { FramePublicAPI } from '../types'; // export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => { -export const toExpression = (state: State, frame: FramePublicAPI): Ast => { +export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => { const labels: Partial> = {}; + if (!state || !state.layers.length) { + return null; + } // const datasource = frame.datasourceLayers.first; state.layers.forEach(layer => { const datasource = frame.datasourceLayers[layer.layerId]; + if (!datasource) { + return; + } layer.accessors.forEach(columnId => { const operation = datasource.getOperationForColumnId(columnId); if (operation && operation.label) { @@ -75,6 +81,9 @@ export const buildExpression = ( type: 'function', function: 'lens_xy_layer', arguments: { + layerId: [layer.layerId], + datasourceId: [layer.datasourceId], + title: [layer.title], showGridlines: [layer.showGridlines], position: [layer.position], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index bf57c2ebe15d2..31e5cfdd17818 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -131,6 +131,10 @@ export const layerConfig: ExpressionFunction< types: ['string'], help: '', }, + datasourceId: { + types: ['string'], + help: '', + }, xAccessor: { types: ['string'], help: '', @@ -183,6 +187,8 @@ export type SeriesType = type LayerConfig = AxisConfig & { layerId: string; + datasourceId: string; + xAccessor: string; accessors: string[]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index fbad4f42eb7b3..2e16ee4ffaacc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -112,8 +112,7 @@ function updateLayer(state: State, layer: UnwrapArray, index: n } export function XYConfigPanel(props: VisualizationProps) { - const { state, datasource, setState, frame } = props; - // console.log(state); + const { state, setState, frame } = props; return ( @@ -257,7 +256,7 @@ export function XYConfigPanel(props: VisualizationProps) { */} {state.layers.map((layer, index) => ( - + ) { defaultMessage: 'X Axis', })} > - + <> {/* ) { > operation.isBucketed, // layer: // datasource.supportsLayers && datasource.supportsLayerJoin ? 'join' : index, - layerId: 'first', + layerId: layer.layerId, }} /> @@ -346,7 +345,7 @@ export function XYConfigPanel(props: VisualizationProps) { /> */} - + ) { > setState( @@ -384,7 +383,7 @@ export function XYConfigPanel(props: VisualizationProps) { filterOperations={op => op.isBucketed && op.dataType !== 'date'} suggestedPriority={0} // layer={index} - layerId={'first'} + layerId={layer.layerId} testSubj="splitSeriesDimensionPanel" /> @@ -422,7 +421,7 @@ export function XYConfigPanel(props: VisualizationProps) { > setState( @@ -451,7 +450,7 @@ export function XYConfigPanel(props: VisualizationProps) { filterOperations={op => !op.isBucketed && op.dataType === 'number'} testSubj="yDimensionPanel" // layer={index} - layerId={'first'} + layerId={layer.layerId} /> @@ -479,23 +478,26 @@ export function XYConfigPanel(props: VisualizationProps) { { - frame.addNewLayer(); + const newId = frame.addNewLayer(); - // setState({ - // ...state, - // layers: [ - // ...state.layers, - // { - // seriesType: 'bar_stacked', - // accessors: [generateId()], - // title: '', - // showGridlines: false, - // position: Position.Left, - // labels: [''], - // splitSeriesAccessors: [], - // }, - // ], - // }); + setState({ + ...state, + layers: [ + ...state.layers, + { + layerId: newId, + datasourceId: 'indexpattern', // TODO: Don't hard code + xAccessor: generateId(), + seriesType: 'bar_stacked', + accessors: [generateId()], + title: '', + showGridlines: false, + position: Position.Left, + labels: [''], + splitSeriesAccessors: [], + }, + ], + }); }} iconType="plusInCircle" > diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 90121cc3c3672..1acbe4d34fb6d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -30,26 +30,29 @@ export const xyVisualization: Visualization = { // showGridlines: false, // title: 'X', // }, - layers: [ - { - layerId: 'first', - xAccessor: generateId(), - seriesType: 'bar_stacked', - accessors: [generateId()], - position: Position.Left, - showGridlines: false, - title: 'Y', - labels: [''], - // splitSeriesAccessors: [generateId()], - splitSeriesAccessors: [], - }, - ], + // layers: [ + // { + // layerId: 'first', + // xAccessor: generateId(), + // seriesType: 'bar_stacked', + // accessors: [generateId()], + // position: Position.Left, + // showGridlines: false, + // title: 'Y', + // labels: [''], + // // splitSeriesAccessors: [generateId()], + // splitSeriesAccessors: [], + // }, + // ], + layers: [], } ); }, getPersistableState: state => state, + getLayerIds: state => state.layers.map(({ layerId }) => layerId), + renderConfigPanel: (domElement, props) => render( From 63fb14bb06aaadbf2700c54d6b3f4726b3f5be05 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 16 Jul 2019 17:02:46 -0400 Subject: [PATCH 06/67] Load multiple tables and render in one chart --- .../editor_frame/editor_frame.tsx | 3 +- .../editor_frame/expression_helpers.ts | 27 +++--- .../editor_frame_plugin/merge_tables.ts | 28 +++--- .../dimension_panel/dimension_panel.tsx | 2 - .../indexpattern_plugin/indexpattern.tsx | 24 +++-- .../operation_definitions/count.tsx | 10 +-- .../operation_definitions/date_histogram.tsx | 5 +- .../operation_definitions/filter_ratio.tsx | 3 +- .../operation_definitions/metrics.tsx | 3 +- .../operation_definitions/terms.tsx | 3 +- .../public/indexpattern_plugin/operations.ts | 10 ++- .../public/indexpattern_plugin/plugin.tsx | 2 - .../indexpattern_plugin/to_expression.ts | 11 ++- x-pack/legacy/plugins/lens/public/types.ts | 7 +- .../xy_config_panel.tsx | 12 +++ .../xy_visualization_plugin/xy_expression.tsx | 87 +++++++++++-------- 16 files changed, 141 insertions(+), 96 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 957ec65fab0a2..b2021c9d4c73d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -18,6 +18,7 @@ import { WorkspacePanel } from './workspace_panel'; import { SavedObjectStore, Document } from '../../persistence/saved_object_store'; import { save } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; +import { generateId } from '../../id_generator'; export interface EditorFrameProps { doc?: Document; @@ -92,7 +93,7 @@ export function EditorFrame(props: EditorFrameProps) { layerIdToDatasource: layerToDatasourceId, datasourceLayers, addNewLayer: () => { - const newLayerId = 'second'; + const newLayerId = generateId(); const newState = state.datasourceMap[state.activeDatasourceId!].insertLayer( state.datasourceStates[state.activeDatasourceId!].state, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index 86348de8b319c..3cc5beed0669b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ast, fromExpression } from '@kbn/interpreter/common'; +import { Ast, fromExpression, ExpressionFunctionAST } from '@kbn/interpreter/common'; import { Visualization, Datasource, FramePublicAPI } from '../../types'; export function prependDatasourceExpression( @@ -18,7 +18,7 @@ export function prependDatasourceExpression( } > ): Ast | null { - const datasourceExpressions: Array = []; + const datasourceExpressions: Array<[string, Ast | string]> = []; Object.entries(datasourceMap).forEach(([datasourceId, datasource]) => { const state = datasourceStates[datasourceId].state; @@ -27,7 +27,7 @@ export function prependDatasourceExpression( layers.forEach(layerId => { const result = datasource.toExpression(state, layerId); if (result) { - datasourceExpressions.push(result); + datasourceExpressions.push([layerId, result]); } }); }); @@ -35,22 +35,27 @@ export function prependDatasourceExpression( if (datasourceExpressions.length === 0 || visualizationExpression === null) { return null; } - - const parsedDatasourceExpressions = datasourceExpressions.map(expr => - typeof expr === 'string' ? fromExpression(expr) : expr + const parsedDatasourceExpressions: Array<[string, Ast]> = datasourceExpressions.map( + ([layerId, expr]) => [layerId, typeof expr === 'string' ? fromExpression(expr) : expr] ); + + const datafetchExpression: ExpressionFunctionAST = { + type: 'function', + function: 'lens_merge_tables', + arguments: { + layerIds: parsedDatasourceExpressions.map(([id]) => id), + tables: parsedDatasourceExpressions.map(([id, expr]) => expr), + }, + }; + const parsedVisualizationExpression = typeof visualizationExpression === 'string' ? fromExpression(visualizationExpression) : visualizationExpression; - const chainedExpr = parsedDatasourceExpressions - .map(expr => expr.chain) - .reduce((prev, current) => prev.concat(current), []); - return { type: 'expression', - chain: chainedExpr.concat([...parsedVisualizationExpression.chain]), + chain: [datafetchExpression, ...parsedVisualizationExpression.chain], }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index 360f3bc1af646..96264ff979b1e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -9,7 +9,7 @@ import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { KibanaDatatable } from '../types'; interface MergeTables { - joins: string[]; + layerIds: string[]; tables: KibanaDatatable[]; } @@ -25,11 +25,9 @@ export const mergeTables: ExpressionFunction< defaultMessage: 'A helper to merge any number of kibana tables into a single table', }), args: { - joins: { + layerIds: { types: ['string'], - help: i18n.translate('xpack.lens.functions.calculateFilterRatio.id.help', { - defaultMessage: 'The column IDs to join on', - }), + help: '', multi: true, }, tables: { @@ -41,17 +39,19 @@ export const mergeTables: ExpressionFunction< context: { types: ['null'], }, - fn(_ctx, { joins, tables }: MergeTables) { + fn(_ctx, { layerIds, tables }: MergeTables) { + const row: Record = {}; + + tables.forEach((table, index) => { + row[layerIds[index]] = table; + }); return { type: 'kibana_datatable', - rows: tables.reduce( - (prev, current) => prev.concat(current.rows), - [] as KibanaDatatable['rows'] - ), - columns: tables.reduce( - (prev, current) => prev.concat(current.columns), - [] as KibanaDatatable['columns'] - ), + columns: layerIds.map(layerId => ({ + id: layerId, + name: '', + })), + rows: [row], }; }, }; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index a9205b3a55f77..5f72915307549 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -42,8 +42,6 @@ export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProp return props.filterOperations(columnToOperation(col)); }); - const layer = props.state.layers[layerId]; - const selectedColumn: IndexPatternColumn | null = props.state.layers[layerId].columns[props.columnId] || null; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index b386162ac4022..542999286c24c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -15,6 +15,7 @@ import { DimensionPriority, DatasourceSuggestion, Operation, + DatasourceLayerPanelProps, } from '../types'; import { Query } from '../../../../../../src/legacy/core_plugins/data/public/query'; import { getIndexPatterns } from './loader'; @@ -47,8 +48,7 @@ export interface BaseIndexPatternColumn { // Private operationType: OperationType; suggestedOrder?: DimensionPriority; - // layer: 'join' | number; - isJoin?: boolean; + indexPatternId: string; } type Omit = Pick>; @@ -132,6 +132,8 @@ export interface IndexPatternPersistedState { { columnOrder: string[]; columns: Record; + // Each layer is tied to the index pattern that created it + indexPatternId: string; } >; } @@ -262,16 +264,10 @@ export function getIndexPatternDatasource({ return { currentIndexPatternId: indexPatternObjects ? indexPatternObjects[0].id : '', indexPatterns, - layers: { - // first: { - // columns: {}, - // columnOrder: [], - // }, - }, + layers: {}, }; }, - // getPersistableState({ currentIndexPatternId, columns, columnOrder }: IndexPatternPrivateState) { getPersistableState({ currentIndexPatternId, layers }: IndexPatternPrivateState) { return { currentIndexPatternId, layers }; }, @@ -282,6 +278,7 @@ export function getIndexPatternDatasource({ layers: { ...state.layers, [newLayerId]: { + indexPatternId: state.currentIndexPatternId, columns: {}, columnOrder: [], }, @@ -343,6 +340,15 @@ export function getIndexPatternDatasource({ ); }, + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => { + render( + + {state.indexPatterns[state.layers[props.layerId].indexPatternId].title} + , + domElement + ); + }, + removeColumnInTableSpec: (columnId: string) => { // setState({ // ...state, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index 9baf16875cef0..37cec316ccc35 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -16,14 +16,7 @@ export const countOperation: OperationDefinition = { }), isApplicableWithoutField: true, isApplicableForField: () => false, - buildColumn({ operationId, suggestedOrder }) { - // }: { - // operationId: string; - // suggestedOrder: DimensionPriority | undefined; - // // columns: {}; - // // layer: DimensionLayer - // // layerId: string; - // }): CountIndexPatternColumn { + buildColumn({ operationId, suggestedOrder, indexPatternId }) { return { operationId, label: i18n.translate('xpack.lens.indexPattern.countOf', { @@ -33,6 +26,7 @@ export const countOperation: OperationDefinition = { operationType: 'count', suggestedOrder, isBucketed: false, + indexPatternId, // layer, }; }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index 0a257579a34e4..c42bd73ead185 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -48,19 +48,21 @@ export const dateHistogramOperation: OperationDefinition false, - buildColumn({ operationId, suggestedOrder, columns, layerId }) { + buildColumn({ operationId, suggestedOrder, indexPatternId, columns, layerId }) { // operationId: string, // suggestedOrder: DimensionPriority | undefined, // columns: Partial>, @@ -39,6 +39,7 @@ export const filterRatioOperation: OperationDefinition( fieldType === 'number' && (!aggregationRestrictions || aggregationRestrictions[type]) ); }, - buildColumn({ operationId, suggestedOrder, field }): T { + buildColumn({ operationId, suggestedOrder, field, indexPatternId }): T { // }: { // operationId: string; // suggestedOrder: DimensionPriority | undefined; @@ -49,6 +49,7 @@ function buildMetricOperation( suggestedOrder, sourceField: field ? field.name : '', isBucketed: false, + indexPatternId, // layer, } as T; }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 8060e99c2925a..6cf0f749a359e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -47,7 +47,7 @@ export const termsOperation: OperationDefinition = { type === 'string' && (!aggregationRestrictions || aggregationRestrictions.terms) ); }, - buildColumn({ operationId, suggestedOrder, columns, field }) { + buildColumn({ operationId, suggestedOrder, columns, field, indexPatternId }) { // }: { // operationId: string; // suggestedOrder: DimensionPriority | undefined; @@ -67,6 +67,7 @@ export const termsOperation: OperationDefinition = { suggestedOrder, sourceField: field ? field.name : '', isBucketed: true, + indexPatternId, // layerId, params: { size: 5, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index e091a90b6d352..bfe6ae79b7830 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -76,6 +76,7 @@ export interface OperationDefinition { operationId: string; suggestedOrder: DimensionPriority | undefined; layerId: string; + indexPatternId: string; columns: Partial>; field?: IndexPatternField; }) => C; @@ -116,6 +117,7 @@ export function buildColumnForOperationType({ columns, field, layerId, + indexPatternId, suggestedOrder, }: { index: number; @@ -123,6 +125,7 @@ export function buildColumnForOperationType({ columns: Partial>; suggestedOrder: DimensionPriority | undefined; layerId: string; + indexPatternId: string; field?: IndexPatternField; }): IndexPatternColumn { return operationDefinitionMap[op].buildColumn({ @@ -131,6 +134,7 @@ export function buildColumnForOperationType({ suggestedOrder, field, layerId, + indexPatternId, }); } @@ -143,7 +147,9 @@ export function getPotentialColumns({ suggestedOrder?: DimensionPriority; layerId: string; }): IndexPatternColumn[] { - const fields = state.indexPatterns[state.currentIndexPatternId].fields; + const indexPattern = state.layers[layerId].indexPatternId; + + const fields = state.indexPatterns[indexPattern].fields; const columns: IndexPatternColumn[] = fields .map((field, index) => { @@ -156,6 +162,7 @@ export function getPotentialColumns({ columns: state.layers[layerId].columns, suggestedOrder, field, + indexPatternId: state.layers[layerId].indexPatternId, layerId, }) ); @@ -170,6 +177,7 @@ export function getPotentialColumns({ suggestedOrder, layerId, columns: state.layers[layerId].columns, + indexPatternId: state.layers[layerId].indexPatternId, }) ); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx index f516ff5c3cd06..ea969219b3e1f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/plugin.tsx @@ -18,7 +18,6 @@ import { functionsRegistry } from '../../../../../../src/legacy/core_plugins/int import { getIndexPatternDatasource } from './indexpattern'; import { renameColumns } from './rename_columns'; import { calculateFilterRatio } from './filter_ratio'; -// import { mergeTables } from './merge_tables'; // TODO these are intermediary types because interpreter is not typed yet // They can get replaced by references to the real interfaces as soon as they @@ -48,7 +47,6 @@ class IndexPatternDatasourcePlugin { ) { interpreter.functionsRegistry.register(() => renameColumns); interpreter.functionsRegistry.register(() => calculateFilterRatio); - // interpreter.functionsRegistry.register(() => mergeTables); return getIndexPatternDatasource({ chrome, interpreter, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 19cd02a0d6346..142ce017cc11f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -14,7 +14,8 @@ import { } from './operations'; function getExpressionForLayer( - currentIndexPatternId: string, + // currentIndexPatternId: string, + indexPatternId: string, layerId: string, columns: Record, columnOrder: string[] @@ -62,11 +63,12 @@ function getExpressionForLayer( columns, suggestedOrder: 2, layerId, + indexPatternId, }); aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); return `esaggs - index="${currentIndexPatternId}" + index="${indexPatternId}" metricsAtAllLevels=false partialRows=false aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify( @@ -75,7 +77,7 @@ function getExpressionForLayer( } return `esaggs - index="${currentIndexPatternId}" + index="${indexPatternId}" metricsAtAllLevels=false partialRows=false aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; @@ -93,7 +95,8 @@ export function toExpression(state: IndexPatternPrivateState, layerId: string) { if (state.layers[layerId]) { // return `lens_merge_tables joins="" ${expressions.map(expr => `table={${expr}}`).join(' ')}`; return getExpressionForLayer( - state.currentIndexPatternId, + // state.currentIndexPatternId, + state.layers[layerId].indexPatternId, layerId, state.layers[layerId].columns, state.layers[layerId].columnOrder diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 391cc70f7f5fb..07547b4f64c60 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -82,6 +82,7 @@ export interface DatasourcePublicAPI { // Render can be called many times renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => void; + renderLayerPanel: (domElement: Element, props: DatasourceLayerPanelProps) => void; removeColumnInTableSpec: (columnId: string) => void; moveColumnTo: (columnId: string, targetIndex: number) => void; @@ -105,9 +106,7 @@ export interface DatasourceDataPanelProps { // The only way a visualization has to restrict the query building export interface DatasourceDimensionPanelProps { - // layerId: DimensionLayer; layerId: string; - columnId: string; dragDropContext: DragContextState; @@ -120,6 +119,10 @@ export interface DatasourceDimensionPanelProps { suggestedPriority?: DimensionPriority; } +export interface DatasourceLayerPanelProps { + layerId: string; +} + export type DataType = 'string' | 'number' | 'date' | 'boolean'; // An operation represents a column in a table, not any information diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 2e16ee4ffaacc..8d97411db6488 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -258,6 +258,18 @@ export function XYConfigPanel(props: VisualizationProps) { {state.layers.map((layer, index) => ( + + + + - {layers.map(({ splitSeriesAccessors, seriesType, labels, accessors, xAccessor }, index) => { - const seriesProps = { - key: index, - splitSeriesAccessors, - stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], - id: getSpecId(labels.join(',')), - xAccessor, - yAccessors: labels, - data: data.rows.map(row => { - const newRow: typeof row = {}; - - // Remap data to { 'Count of documents': 5 } - Object.keys(row).forEach(key => { - const labelIndex = accessors.indexOf(key); - if (labelIndex > -1) { - newRow[labels[labelIndex]] = row[key]; - } else { - newRow[key] = row[key]; - } - }); - return newRow; - }), - }; - - return seriesType === 'line' ? ( - - ) : seriesType === 'bar' || - seriesType === 'bar_stacked' || - seriesType === 'horizontal_bar' || - seriesType === 'horizontal_bar_stacked' ? ( - - ) : ( - - ); - })} + {layers.map( + ({ splitSeriesAccessors, seriesType, labels, accessors, xAccessor, layerId }, index) => { + const seriesDataRow = data.rows.find(row => row[layerId]); + const seriesData = seriesDataRow ? seriesDataRow[layerId] : null; + + if (!seriesData) { + return; + } + + const idForCaching = accessors.concat([xAccessor], splitSeriesAccessors).join(','); + + const seriesProps = { + key: index, + splitSeriesAccessors, + stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], + id: getSpecId(idForCaching), + xAccessor, + yAccessors: labels, + data: (seriesData as KibanaDatatable).rows.map(row => { + const newRow: typeof row = {}; + + // Remap data to { 'Count of documents': 5 } + Object.keys(row).forEach(key => { + const labelIndex = accessors.indexOf(key); + if (labelIndex > -1) { + newRow[labels[labelIndex]] = row[key]; + } else { + newRow[key] = row[key]; + } + }); + return newRow; + }), + }; + + return seriesType === 'line' ? ( + + ) : seriesType === 'bar' || + seriesType === 'bar_stacked' || + seriesType === 'horizontal_bar' || + seriesType === 'horizontal_bar_stacked' ? ( + + ) : ( + + ); + } + )} ); } From 4aa80f23d6691f135f07be5c417b2b0d848a0566 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 16 Jul 2019 17:19:39 -0400 Subject: [PATCH 07/67] Fix priority ordering --- .../dimension_panel/dimension_panel.tsx | 2 +- .../indexpattern_plugin/indexpattern.test.tsx | 8 ++--- .../indexpattern_plugin/indexpattern.tsx | 4 +-- .../operation_definitions/count.tsx | 4 +-- .../operation_definitions/date_histogram.tsx | 6 ++-- .../operation_definitions/filter_ratio.tsx | 6 ++-- .../operation_definitions/metrics.tsx | 6 ++-- .../operation_definitions/terms.tsx | 6 ++-- .../indexpattern_plugin/operations.test.ts | 36 ++++++++++--------- .../public/indexpattern_plugin/operations.ts | 17 ++++----- .../indexpattern_plugin/state_helpers.test.ts | 6 ++-- .../indexpattern_plugin/state_helpers.ts | 4 +-- .../indexpattern_plugin/to_expression.ts | 2 +- .../xy_config_panel.tsx | 1 + 14 files changed, 57 insertions(+), 51 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 5f72915307549..63f9b1ce6dec2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -36,7 +36,7 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { export function IndexPatternDimensionPanel(props: IndexPatternDimensionPanelProps) { const layerId = props.layerId; - const columns = getPotentialColumns(props); + const columns = getPotentialColumns(props, props.suggestedPriority); const filteredColumns = columns.filter(col => { return props.filterOperations(columnToOperation(col)); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index b8d19fc1987bb..d49b0fe5897c8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -538,20 +538,20 @@ describe('IndexPattern Data Source', () => { operationId: 'bar', operationType: 'max', sourceField: 'baz', - suggestedOrder: 0, + suggestedPriority: 0, }; const columns: Record = { a: { ...sampleColumn, - suggestedOrder: 0, + suggestedPriority: 0, }, b: { ...sampleColumn, - suggestedOrder: 1, + suggestedPriority: 1, }, c: { ...sampleColumn, - suggestedOrder: 2, + suggestedPriority: 2, }, }; const api = indexPatternDatasource.getPublicAPI( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 542999286c24c..cbea53e152016 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -47,7 +47,7 @@ export interface BaseIndexPatternColumn { // Private operationType: OperationType; - suggestedOrder?: DimensionPriority; + suggestedPriority?: DimensionPriority; indexPatternId: string; } @@ -59,7 +59,7 @@ type ParameterlessIndexPatternColumn< export interface FieldBasedIndexPatternColumn extends BaseIndexPatternColumn { sourceField: string; - suggestedOrder?: DimensionPriority; + suggestedPriority?: DimensionPriority; } export interface DateHistogramIndexPatternColumn extends FieldBasedIndexPatternColumn { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index 37cec316ccc35..e754c43fba86f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -16,7 +16,7 @@ export const countOperation: OperationDefinition = { }), isApplicableWithoutField: true, isApplicableForField: () => false, - buildColumn({ operationId, suggestedOrder, indexPatternId }) { + buildColumn({ operationId, suggestedPriority, indexPatternId }) { return { operationId, label: i18n.translate('xpack.lens.indexPattern.countOf', { @@ -24,7 +24,7 @@ export const countOperation: OperationDefinition = { }), dataType: 'number', operationType: 'count', - suggestedOrder, + suggestedPriority, isBucketed: false, indexPatternId, // layer, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index c42bd73ead185..fba90f415f49c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -45,14 +45,14 @@ export const dateHistogramOperation: OperationDefinition false, - buildColumn({ operationId, suggestedOrder, indexPatternId, columns, layerId }) { + buildColumn({ operationId, suggestedPriority, indexPatternId, columns, layerId }) { // operationId: string, - // suggestedOrder: DimensionPriority | undefined, + // suggestedPriority: DimensionPriority | undefined, // columns: Partial>, // // layer: DimensionLayer // layerId: string @@ -37,7 +37,7 @@ export const filterRatioOperation: OperationDefinition( fieldType === 'number' && (!aggregationRestrictions || aggregationRestrictions[type]) ); }, - buildColumn({ operationId, suggestedOrder, field, indexPatternId }): T { + buildColumn({ operationId, suggestedPriority, field, indexPatternId }): T { // }: { // operationId: string; - // suggestedOrder: DimensionPriority | undefined; + // suggestedPriority: DimensionPriority | undefined; // layerId: string; // columns: {}; // field?: IndexPatternField; @@ -46,7 +46,7 @@ function buildMetricOperation( label: ofName(field ? field.name : ''), dataType: 'number', operationType: type, - suggestedOrder, + suggestedPriority, sourceField: field ? field.name : '', isBucketed: false, indexPatternId, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 6cf0f749a359e..046a2332329b6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -47,10 +47,10 @@ export const termsOperation: OperationDefinition = { type === 'string' && (!aggregationRestrictions || aggregationRestrictions.terms) ); }, - buildColumn({ operationId, suggestedOrder, columns, field, indexPatternId }) { + buildColumn({ operationId, suggestedPriority, columns, field, indexPatternId }) { // }: { // operationId: string; - // suggestedOrder: DimensionPriority | undefined; + // suggestedPriority: DimensionPriority | undefined; // layerId: string; // columns: Partial>; // field?: IndexPatternField; @@ -64,7 +64,7 @@ export const termsOperation: OperationDefinition = { label: ofName(field ? field.name : ''), dataType: 'string', operationType: 'terms', - suggestedOrder, + suggestedPriority, sourceField: field ? field.name : '', isBucketed: true, indexPatternId, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts index a835fa76ade8b..e03cc27e2d057 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.test.ts @@ -148,19 +148,23 @@ describe('getOperationTypesForField', () => { state = { indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: 'h', + layers: { + first: { + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + }, }, }, }, @@ -168,13 +172,13 @@ describe('getOperationTypesForField', () => { }); it('should include priority', () => { - const columns = getPotentialColumns(state, 1); + const columns = getPotentialColumns({ state, suggestedPriority: 1, layerId: 'first' }); - expect(columns.every(col => col.suggestedOrder === 1)).toEqual(true); + expect(columns.every(col => col.suggestedPriority === 1)).toEqual(true); }); it('should list operations by field for a regular index pattern', () => { - const columns = getPotentialColumns(state); + const columns = getPotentialColumns({ state, layerId: 'first' }); expect( columns.map(col => [hasField(col) ? col.sourceField : '_documents_', col.operationType]) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index bfe6ae79b7830..b4f4b5964bb58 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -74,7 +74,7 @@ export interface OperationDefinition { isApplicableForField: (field: IndexPatternField) => boolean; buildColumn: (arg: { operationId: string; - suggestedOrder: DimensionPriority | undefined; + suggestedPriority: DimensionPriority | undefined; layerId: string; indexPatternId: string; columns: Partial>; @@ -118,12 +118,12 @@ export function buildColumnForOperationType({ field, layerId, indexPatternId, - suggestedOrder, + suggestedPriority, }: { index: number; op: T; columns: Partial>; - suggestedOrder: DimensionPriority | undefined; + suggestedPriority: DimensionPriority | undefined; layerId: string; indexPatternId: string; field?: IndexPatternField; @@ -131,7 +131,7 @@ export function buildColumnForOperationType({ return operationDefinitionMap[op].buildColumn({ operationId: `${index}${op}`, columns, - suggestedOrder, + suggestedPriority, field, layerId, indexPatternId, @@ -140,11 +140,12 @@ export function buildColumnForOperationType({ export function getPotentialColumns({ state, - suggestedOrder, + // suggestedPriority, + suggestedPriority, layerId, }: { state: IndexPatternPrivateState; - suggestedOrder?: DimensionPriority; + suggestedPriority?: DimensionPriority; layerId: string; }): IndexPatternColumn[] { const indexPattern = state.layers[layerId].indexPatternId; @@ -160,7 +161,7 @@ export function getPotentialColumns({ index, op, columns: state.layers[layerId].columns, - suggestedOrder, + suggestedPriority, field, indexPatternId: state.layers[layerId].indexPatternId, layerId, @@ -174,7 +175,7 @@ export function getPotentialColumns({ columns.push( operation.buildColumn({ operationId: operation.type, - suggestedOrder, + suggestedPriority, layerId, columns: state.layers[layerId].columns, indexPatternId: state.layers[layerId].indexPatternId, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index 2f4684d5c617d..6693dc257ffd2 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -370,7 +370,7 @@ describe('state_helpers', () => { }, orderDirection: 'asc', }, - suggestedOrder: 2, + suggestedPriority: 2, }, col2: { operationId: 'op2', @@ -381,7 +381,7 @@ describe('state_helpers', () => { // Private operationType: 'avg', sourceField: 'bytes', - suggestedOrder: 0, + suggestedPriority: 0, }, col3: { operationId: 'op3', @@ -392,7 +392,7 @@ describe('state_helpers', () => { // Private operationType: 'date_histogram', sourceField: 'timestamp', - suggestedOrder: 1, + suggestedPriority: 1, params: { interval: '1d', }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index ba95e9c4935f1..69b4e92ad7566 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -141,8 +141,8 @@ export function getColumnOrder(columns: Record): str .sort(([id, col], [id2, col2]) => { return ( // Sort undefined orders last - (col.suggestedOrder !== undefined ? col.suggestedOrder : Number.MAX_SAFE_INTEGER) - - (col2.suggestedOrder !== undefined ? col2.suggestedOrder : Number.MAX_SAFE_INTEGER) + (col.suggestedPriority !== undefined ? col.suggestedPriority : Number.MAX_SAFE_INTEGER) - + (col2.suggestedPriority !== undefined ? col2.suggestedPriority : Number.MAX_SAFE_INTEGER) ); }) .map(([id]) => id) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 142ce017cc11f..78133e4186131 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -61,7 +61,7 @@ function getExpressionForLayer( index: columnEntries.length, op: 'count', columns, - suggestedOrder: 2, + suggestedPriority: 2, layerId, indexPatternId, }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 8d97411db6488..193e8a84c3b81 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -461,6 +461,7 @@ export function XYConfigPanel(props: VisualizationProps) { } filterOperations={op => !op.isBucketed && op.dataType === 'number'} testSubj="yDimensionPanel" + // suggestedPriority={2} // layer={index} layerId={layer.layerId} /> From 2e6aa45dc2d2c29a84cc24fb1ee16eb7446c0461 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 17 Jul 2019 11:58:20 +0200 Subject: [PATCH 08/67] working on embedding lens vis in dashboard --- .../legacy/plugins/lens/common/constants.ts | 6 ++ x-pack/legacy/plugins/lens/index.ts | 8 +-- x-pack/legacy/plugins/lens/mappings.json | 3 + .../embeddable/expression_wrapper.tsx | 60 ++++++++++++++++++ .../app_plugin/embeddable/lens_embeddable.tsx | 61 +++++++++++++------ .../embeddable/lens_embeddable_factory.ts | 38 ++++++++---- .../editor_frame_plugin/editor_frame/save.ts | 2 + .../public/persistence/saved_object_store.ts | 3 +- 8 files changed, 144 insertions(+), 37 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx diff --git a/x-pack/legacy/plugins/lens/common/constants.ts b/x-pack/legacy/plugins/lens/common/constants.ts index eeb295fd776e2..ef9c760bf4aa4 100644 --- a/x-pack/legacy/plugins/lens/common/constants.ts +++ b/x-pack/legacy/plugins/lens/common/constants.ts @@ -5,3 +5,9 @@ */ export const PLUGIN_ID = 'lens'; + +export const BASE_URL = 'app/lens'; + +export function getEditPath(id: string) { + return `/${BASE_URL}#/edit/${encodeURIComponent(id)}` +} \ No newline at end of file diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index 21ae342052fb1..c9b60823905b3 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -9,7 +9,7 @@ import { Server } from 'hapi'; import { resolve } from 'path'; import { LegacyPluginInitializer } from 'src/legacy/types'; import mappings from './mappings.json'; -import { PLUGIN_ID } from './common'; +import { PLUGIN_ID, getEditPath } from './common'; const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; @@ -27,7 +27,7 @@ export const lens: LegacyPluginInitializer = kibana => { main: `plugins/${PLUGIN_ID}/index`, }, embeddableFactories: [ - 'plugins/lens/app_plugin/embeddable/embeddable_factory' + 'plugins/lens/app_plugin/embeddable/lens_embeddable_factory' ], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, @@ -38,7 +38,7 @@ export const lens: LegacyPluginInitializer = kibana => { isImportableAndExportable: true, getTitle: (obj: { attributes: { title: string } }) => obj.attributes.title, getInAppUrl: (obj: { id: string }) => ({ - path: `/app/lens#/edit/${encodeURIComponent(obj.id)}`, + path: getEditPath(obj.id), uiCapabilitiesPath: 'lens.show', }), }, @@ -69,7 +69,7 @@ export const lens: LegacyPluginInitializer = kibana => { all: [], read: [], }, - ui: ['show'], + ui: ['save', 'show'], }, read: { api: [PLUGIN_ID], diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json index 4c860a7171829..3d8cdadb4f52f 100644 --- a/x-pack/legacy/plugins/lens/mappings.json +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -12,6 +12,9 @@ }, "state": { "type": "text" + }, + "expression": { + "type": "text" } } } diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx new file mode 100644 index 0000000000000..b8ae8c1348ac8 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx @@ -0,0 +1,60 @@ +/* + * 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 _ from 'lodash'; +import React, { useState, useEffect } from 'react'; + +import { I18nContext } from 'ui/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; +import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; + +export interface ExpressionWrapperProps { + ExpressionRenderer: ExpressionRenderer; + expression: string; +} + +export function ExpressionWrapper({ + ExpressionRenderer: ExpressionRendererComponent, + expression, +}: ExpressionWrapperProps) { + const [expressionError, setExpressionError] = useState(undefined); + useEffect(() => { + // reset expression error if component attempts to run it again + if (expressionError) { + setExpressionError(undefined); + } + }, [expression]); + return ( + + {expressionError ? ( + + + + + + + + + + + ) : ( + { + setExpressionError(e); + // TODO error handling + }} + /> + )} + + ); +} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx index 095dab1c29354..3b37acfd782d7 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx @@ -6,34 +6,57 @@ import _ from 'lodash'; import React from 'react'; -import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Embeddable, EmbeddableOutput, IContainer, EmbeddableInput } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; -import { I18nContext } from 'ui/i18n'; -import { Document } from '../../persistence'; +import { + Embeddable, + EmbeddableOutput, + IContainer, + EmbeddableInput, +} from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; +import { Document, DOC_TYPE } from '../../persistence'; import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; +import { ExpressionWrapper } from './expression_wrapper'; const ExpressionRendererComponent = data.expressions.ExpressionRenderer; -export interface LensEmbeddableInput extends EmbeddableInput { +export interface LensEmbeddableConfiguration { savedVis: Document; + editUrl: string; + editable: boolean; +} + +export interface LensEmbeddableInput extends EmbeddableInput { + // timeRange?: TimeRange; + // query?: Query; + // filters?: Filter[]; } +export interface LensEmbeddableOutput extends EmbeddableOutput {} -export class LensEmbeddable extends Embeddable { - type = 'lens'; +export class LensEmbeddable extends Embeddable { + type = DOC_TYPE; private savedVis: Document; private domNode: HTMLElement | Element | undefined; - constructor(input: LensEmbeddableInput, output: EmbeddableOutput, parent?: IContainer) { + constructor( + { savedVis, editUrl, editable }: LensEmbeddableConfiguration, + initialInput: LensEmbeddableInput, + parent?: IContainer + ) { super( - input, - output, - parent); + initialInput, + { + defaultTitle: savedVis.title, + savedObjectId: savedVis.id!, + editable, + editUrl + }, + parent + ); - this.savedVis = input.savedVis; + this.savedVis = savedVis; } /** @@ -44,14 +67,12 @@ export class LensEmbeddable extends Embeddable { render(domNode: HTMLElement | Element) { this.domNode = domNode; render( - { - // TODO error handling - }} - />, domNode); + , + domNode + ); } destroy() { diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts index fe31859d95476..20848f7748008 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable_factory.ts @@ -13,14 +13,25 @@ import { embeddableFactories, ErrorEmbeddable, EmbeddableInput, - IContainer + IContainer, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; import { LensEmbeddable } from './lens_embeddable'; -import { SavedObjectIndexStore } from '../../persistence'; +import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; +import { indexPatternDatasourceSetup } from '../../indexpattern_plugin'; +import { xyVisualizationSetup } from '../../xy_visualization_plugin'; +import { editorFrameSetup } from '../../editor_frame_plugin'; +import { datatableVisualizationSetup } from '../../datatable_visualization_plugin'; +import { getEditPath } from '../../../common'; +// bootstrap shimmed plugins to register everything necessary to the expression. +// the new platform will take care of this once in place +indexPatternDatasourceSetup(); +datatableVisualizationSetup(); +xyVisualizationSetup(); +editorFrameSetup(); export class LensEmbeddableFactory extends EmbeddableFactory { - type = 'lens'; + type = DOC_TYPE; constructor() { super({ @@ -28,18 +39,20 @@ export class LensEmbeddableFactory extends EmbeddableFactory { name: i18n.translate('xpack.lens.lensSavedObjectLabel', { defaultMessage: 'Lens Visualization', }), - type: 'lens', - getIconForSavedObject: () => 'happyFace', + type: DOC_TYPE, + getIconForSavedObject: () => 'faceHappy', }, }); } - isEditable() { - // TODO make it possible - return false; + + public isEditable() { + return capabilities.get().lens.save as boolean; } // Not supported yet for maps types. - canCreateNew() { return false; } + canCreateNew() { + return false; + } getDisplayName() { return i18n.translate('xpack.lens.embeddableDisplayName', { @@ -49,7 +62,7 @@ export class LensEmbeddableFactory extends EmbeddableFactory { async createFromSavedObject( savedObjectId: string, - input: Partial, + input: Partial & { id: string }, parent?: IContainer ) { const store = new SavedObjectIndexStore(chrome.getSavedObjectsClient()); @@ -59,7 +72,8 @@ export class LensEmbeddableFactory extends EmbeddableFactory { return new LensEmbeddable( { savedVis, - id: savedObjectId + editUrl: chrome.addBasePath(getEditPath(savedObjectId)), + editable: this.isEditable(), }, input, parent @@ -67,7 +81,7 @@ export class LensEmbeddableFactory extends EmbeddableFactory { } async create(input: EmbeddableInput) { - // TODO fix this + // TODO fix this window.location.href = chrome.addBasePath('lens/abc'); return new ErrorEmbeddable('Lens can only be created from a saved object', input); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 63f66cc3d1b5a..f0c5c91a14e9b 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -8,6 +8,7 @@ import { Action, EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { buildExpression } from './expression_helpers'; import { Datasource, Visualization } from '../../types'; +import { toExpression } from '@kbn/interpreter/target/common'; export interface Props { datasource: Datasource; @@ -37,6 +38,7 @@ export async function save({ type: 'lens', visualizationType: state.visualization.activeId, datasourceType: state.datasource.activeId, + expression: expression ? toExpression(expression) : '', state: { datasource: datasource.getPersistableState(state.datasource.state), visualization: visualization.getPersistableState(state.visualization.state), diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index 930ee36ea3729..7947d33d3f8f3 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -12,13 +12,14 @@ export interface Document { visualizationType: string | null; datasourceType: string | null; title: string; + expression: string; state: { datasource: unknown; visualization: unknown; }; } -const DOC_TYPE = 'lens'; +export const DOC_TYPE = 'lens'; interface SavedObjectClient { create: (type: string, object: SavedObjectAttributes) => Promise<{ id: string }>; From 201a130743536b0f7e269e5fa3dea08930b6f302 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 17 Jul 2019 15:05:54 +0200 Subject: [PATCH 09/67] continue working on embeddables --- .../loader/pipeline_helpers/run_pipeline.ts | 1 + .../editor_frame/expression_helpers.ts | 41 +++++++++++++- .../editor_frame_plugin/editor_frame/save.ts | 1 + .../editor_frame/workspace_panel.tsx | 17 +++--- .../embeddable/expression_wrapper.tsx | 19 +++++-- .../embeddable/lens_embeddable.tsx | 53 ++++++++++++++++--- .../embeddable/lens_embeddable_factory.ts | 19 +++++-- x-pack/legacy/plugins/lens/public/index.ts | 2 + .../indexpattern_plugin/indexpattern.tsx | 6 +++ .../public/persistence/saved_object_store.ts | 2 + x-pack/legacy/plugins/lens/public/types.ts | 6 +++ .../xy_visualization_plugin/xy_expression.tsx | 3 ++ 12 files changed, 145 insertions(+), 25 deletions(-) rename x-pack/legacy/plugins/lens/public/{app_plugin => editor_frame_plugin}/embeddable/expression_wrapper.tsx (75%) rename x-pack/legacy/plugins/lens/public/{app_plugin => editor_frame_plugin}/embeddable/lens_embeddable.tsx (54%) rename x-pack/legacy/plugins/lens/public/{app_plugin => editor_frame_plugin}/embeddable/lens_embeddable_factory.ts (78%) diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts index 1551ab755ea5b..c897bd4ba7c6c 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts @@ -37,6 +37,7 @@ export const runPipeline = async ( context: object, handlers: RunPipelineHandlers ) => { + console.log(expression); const ast = fromExpression(expression); const { interpreter } = await getInterpreter(); const pipelineResponse = await interpreter.interpretAst(ast, context, handlers); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index 8566035c29bf2..84a7ffa95f6c8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -5,6 +5,9 @@ */ import { Ast, fromExpression } from '@kbn/interpreter/common'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; import { Visualization, Datasource, DatasourcePublicAPI } from '../../types'; export function prependDatasourceExpression( @@ -32,6 +35,38 @@ export function prependDatasourceExpression( }; } +export function prependKibanaContext( + expression: Ast | string | null, + { + timeRange, + query, + filters, + }: { + timeRange?: TimeRange; + query?: Query; + filters?: Filter[]; + } +): Ast | null { + if (!expression) return null; + const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression; + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'kibana_context', + arguments: { + timeRange: timeRange ? [JSON.stringify(timeRange)] : [], + query: query ? [JSON.stringify(query)] : [], + filters: filters ? [JSON.stringify(filters)] : [], + }, + }, + ...parsedExpression.chain, + ], + }; +} + export function buildExpression( visualization: Visualization | null, visualizationState: unknown, @@ -47,5 +82,9 @@ export function buildExpression( datasourcePublicAPI ); - return prependDatasourceExpression(visualizationExpression, datasource, datasourceState); + return prependKibanaContext( + prependDatasourceExpression(visualizationExpression, datasource, datasourceState), + // TODO pass in context from query/filter bar here + {} + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index f0c5c91a14e9b..1dc02789d0116 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -40,6 +40,7 @@ export async function save({ datasourceType: state.datasource.activeId, expression: expression ? toExpression(expression) : '', state: { + datasourceMetaData: datasource.getMetaData(state.datasource.state), datasource: datasource.getPersistableState(state.datasource.state), visualization: visualization.getPersistableState(state.visualization.state), }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index 58817e7c19ad1..c76a27563b713 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -6,7 +6,7 @@ import React, { useState, useEffect, useMemo, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { EuiCodeBlock, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { toExpression } from '@kbn/interpreter/common'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; import { Action } from './state_management'; @@ -111,22 +111,23 @@ export function WorkspacePanel({ if (expressionError) { return ( - <> -

+ + {/* TODO word this differently because expressions should not be exposed at this level */} -

+ {expression && ( - <> + {toExpression(expression)} - - + )} + {JSON.stringify(expressionError, null, 2)} - + +
); } else { return ( diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx similarity index 75% rename from x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx index b8ae8c1348ac8..e6c5e84b69083 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/expression_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx @@ -10,27 +10,38 @@ import React, { useState, useEffect } from 'react'; import { I18nContext } from 'ui/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; +import { prependKibanaContext } from '../../editor_frame_plugin/editor_frame/expression_helpers'; export interface ExpressionWrapperProps { ExpressionRenderer: ExpressionRenderer; expression: string; + context: { + timeRange?: TimeRange; + query?: Query; + filters?: Filter[]; + }; } export function ExpressionWrapper({ ExpressionRenderer: ExpressionRendererComponent, expression, + context, }: ExpressionWrapperProps) { const [expressionError, setExpressionError] = useState(undefined); + const contextualizedExpression = prependKibanaContext(expression, context); useEffect(() => { // reset expression error if component attempts to run it again if (expressionError) { setExpressionError(undefined); } - }, [expression]); + }, [contextualizedExpression]); return ( - {expressionError ? ( + {contextualizedExpression === null || expressionError ? ( @@ -47,11 +58,9 @@ export function ExpressionWrapper({ ) : ( { setExpressionError(e); - // TODO error handling }} /> )} diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx similarity index 54% rename from x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx index 3b37acfd782d7..764073af8fa02 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/embeddable/lens_embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx @@ -8,6 +8,9 @@ import _ from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query, StaticIndexPattern } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; import { Embeddable, EmbeddableOutput, @@ -17,6 +20,7 @@ import { import { Document, DOC_TYPE } from '../../persistence'; import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; import { ExpressionWrapper } from './expression_wrapper'; +import { Subscription } from 'rxjs'; const ExpressionRendererComponent = data.expressions.ExpressionRenderer; @@ -24,24 +28,32 @@ export interface LensEmbeddableConfiguration { savedVis: Document; editUrl: string; editable: boolean; + indexPatterns?: StaticIndexPattern[]; } export interface LensEmbeddableInput extends EmbeddableInput { - // timeRange?: TimeRange; - // query?: Query; - // filters?: Filter[]; + timeRange?: TimeRange; + query?: Query; + filters?: Filter[]; } -export interface LensEmbeddableOutput extends EmbeddableOutput {} +export interface LensEmbeddableOutput extends EmbeddableOutput { + indexPatterns?: StaticIndexPattern[]; +} export class LensEmbeddable extends Embeddable { type = DOC_TYPE; private savedVis: Document; private domNode: HTMLElement | Element | undefined; + private subscription: Subscription; + + private prevTimeRange?: TimeRange; + private prevQuery?: Query; + private prevFilters?: Filter[]; constructor( - { savedVis, editUrl, editable }: LensEmbeddableConfiguration, + { savedVis, editUrl, editable, indexPatterns }: LensEmbeddableConfiguration, initialInput: LensEmbeddableInput, parent?: IContainer ) { @@ -51,12 +63,29 @@ export class LensEmbeddable extends Embeddable this.onContainerStateChanged(input)); + this.onContainerStateChanged(initialInput); + } + + onContainerStateChanged(containerState: LensEmbeddableInput) { + const cleanedFilters = containerState.filters ? containerState.filters.filter(filter => !filter.meta.disabled) : undefined; + if (!_.isEqual(containerState.timeRange, this.prevTimeRange) || + !_.isEqual(containerState.query, this.prevQuery) || + !_.isEqual(cleanedFilters, this.prevFilters)) { + this.prevTimeRange = containerState.timeRange; + this.prevQuery = containerState.query; + this.prevFilters = cleanedFilters; + if (this.domNode) { + this.render(this.domNode); + } + } } /** @@ -70,6 +99,11 @@ export class LensEmbeddable extends Embeddable, domNode ); @@ -80,9 +114,14 @@ export class LensEmbeddable extends Embeddable { + try { + return await indexPatternService.get(indexPatternId); + } catch (error) { + // Unable to load index pattern, better to not throw error so map embeddable can render + // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern + return null; + } + }); + const indexPatterns = await Promise.all(promises); + return new LensEmbeddable( { savedVis, editUrl: chrome.addBasePath(getEditPath(savedObjectId)), editable: this.isEditable(), + indexPatterns }, input, parent @@ -81,8 +94,6 @@ export class LensEmbeddableFactory extends EmbeddableFactory { } async create(input: EmbeddableInput) { - // TODO fix this - window.location.href = chrome.addBasePath('lens/abc'); return new ErrorEmbeddable('Lens can only be created from a saved object', input); } } diff --git a/x-pack/legacy/plugins/lens/public/index.ts b/x-pack/legacy/plugins/lens/public/index.ts index aec29edaa728a..8e232e878e91a 100644 --- a/x-pack/legacy/plugins/lens/public/index.ts +++ b/x-pack/legacy/plugins/lens/public/index.ts @@ -14,6 +14,8 @@ import 'uiExports/fieldFormats'; import 'uiExports/search'; import 'uiExports/visRequestHandlers'; import 'uiExports/visResponseHandlers'; +// Used for kibana_context function +import 'uiExports/savedObjectTypes'; import { render, unmountComponentAtNode } from 'react-dom'; import { IScope } from 'angular'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 01d6ffd6018de..c4e8aa828dc3c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -266,6 +266,12 @@ export function getIndexPatternDatasource({ toExpression, + getMetaData(state: IndexPatternPrivateState) { + return { + filterableIndexPatterns: state.currentIndexPatternId ? [state.currentIndexPatternId] : [] + }; + }, + renderDataPanel( domElement: Element, props: DatasourceDataPanelProps diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index 7947d33d3f8f3..b81e4f4406529 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -5,6 +5,7 @@ */ import { SavedObjectAttributes } from 'target/types/server'; +import { DatasourceMetaData } from '../types'; export interface Document { id?: string; @@ -14,6 +15,7 @@ export interface Document { title: string; expression: string; state: { + datasourceMetaData: DatasourceMetaData; datasource: unknown; visualization: unknown; }; diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 9812db7440891..9aa6e67ed2546 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -43,6 +43,10 @@ export interface DatasourceSuggestion { table: TableSuggestion; } +export interface DatasourceMetaData { + filterableIndexPatterns: string[]; +} + /** * Interface for the datasource registry */ @@ -59,6 +63,8 @@ export interface Datasource { toExpression: (state: T) => Ast | string | null; + getMetaData: (state: T) => DatasourceMetaData; + getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 2392114e26999..2da65da2c5e10 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -102,6 +102,9 @@ export const xyChartRenderer: RenderFunction = { }; export function XYChart({ data, args }: XYChartProps) { + if (data.rows.length === 0) { + return

No data found

; + } const { legend, x, y, splitSeriesAccessors, seriesType } = args; // TODO: Stop mapping data once elastic-charts allows axis naming // https://github.com/elastic/elastic-charts/issues/245 From d213ef4fcd16c701dc884731285ecbcc6ff680cc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 17 Jul 2019 15:38:20 +0200 Subject: [PATCH 10/67] clean up embeddable stuff --- .../legacy/plugins/lens/common/constants.ts | 4 +- x-pack/legacy/plugins/lens/index.ts | 4 +- .../editor_frame_plugin/editor_frame/save.ts | 10 +++- .../editor_frame/workspace_panel.tsx | 4 +- .../{lens_embeddable.tsx => embeddable.tsx} | 30 ++++++----- ...dable_factory.ts => embeddable_factory.ts} | 52 ++++++++----------- .../public/editor_frame_plugin/plugin.tsx | 19 ++++++- .../indexpattern_plugin/indexpattern.tsx | 2 +- .../lens/public/register_embeddable.ts | 17 ++++++ 9 files changed, 87 insertions(+), 55 deletions(-) rename x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/{lens_embeddable.tsx => embeddable.tsx} (81%) rename x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/{lens_embeddable_factory.ts => embeddable_factory.ts} (56%) create mode 100644 x-pack/legacy/plugins/lens/public/register_embeddable.ts diff --git a/x-pack/legacy/plugins/lens/common/constants.ts b/x-pack/legacy/plugins/lens/common/constants.ts index ef9c760bf4aa4..edcc34c58881a 100644 --- a/x-pack/legacy/plugins/lens/common/constants.ts +++ b/x-pack/legacy/plugins/lens/common/constants.ts @@ -9,5 +9,5 @@ export const PLUGIN_ID = 'lens'; export const BASE_URL = 'app/lens'; export function getEditPath(id: string) { - return `/${BASE_URL}#/edit/${encodeURIComponent(id)}` -} \ No newline at end of file + return `/${BASE_URL}#/edit/${encodeURIComponent(id)}`; +} diff --git a/x-pack/legacy/plugins/lens/index.ts b/x-pack/legacy/plugins/lens/index.ts index c9b60823905b3..d89194ef09d69 100644 --- a/x-pack/legacy/plugins/lens/index.ts +++ b/x-pack/legacy/plugins/lens/index.ts @@ -26,9 +26,7 @@ export const lens: LegacyPluginInitializer = kibana => { description: 'Explore and visualize data.', main: `plugins/${PLUGIN_ID}/index`, }, - embeddableFactories: [ - 'plugins/lens/app_plugin/embeddable/lens_embeddable_factory' - ], + embeddableFactories: ['plugins/lens/register_embeddable'], styleSheetPaths: resolve(__dirname, 'public/index.scss'), mappings, visTypes: ['plugins/lens/register_vis_type_alias'], diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 1dc02789d0116..58a4fe704a4cf 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { toExpression } from '@kbn/interpreter/target/common'; import { Action, EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; import { buildExpression } from './expression_helpers'; import { Datasource, Visualization } from '../../types'; -import { toExpression } from '@kbn/interpreter/target/common'; export interface Props { datasource: Datasource; @@ -30,7 +30,13 @@ export async function save({ try { dispatch({ type: 'SAVING', isSaving: true }); - const expression = buildExpression(visualization, state.visualization.state, datasource, state.datasource.state, datasource.getPublicAPI(state.datasource.state, () => {})); + const expression = buildExpression( + visualization, + state.visualization.state, + datasource, + state.datasource.state, + datasource.getPublicAPI(state.datasource.state, () => {}) + ); const doc = await store.save({ id: state.persistedId, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index c76a27563b713..8177f9221988e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -125,8 +125,8 @@ export function WorkspacePanel({
)} - {JSON.stringify(expressionError, null, 2)} - + {JSON.stringify(expressionError, null, 2)} +
); } else { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx similarity index 81% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx rename to x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx index 764073af8fa02..d7cac994fd26a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx @@ -11,6 +11,7 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { TimeRange } from 'ui/timefilter/time_history'; import { Query, StaticIndexPattern } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; +import { Subscription } from 'rxjs'; import { Embeddable, EmbeddableOutput, @@ -20,7 +21,6 @@ import { import { Document, DOC_TYPE } from '../../persistence'; import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; import { ExpressionWrapper } from './expression_wrapper'; -import { Subscription } from 'rxjs'; const ExpressionRendererComponent = data.expressions.ExpressionRenderer; @@ -64,27 +64,31 @@ export class LensEmbeddable extends Embeddable this.onContainerStateChanged(input)); + this.subscription = this.getInput$().subscribe(input => this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); } onContainerStateChanged(containerState: LensEmbeddableInput) { - const cleanedFilters = containerState.filters ? containerState.filters.filter(filter => !filter.meta.disabled) : undefined; - if (!_.isEqual(containerState.timeRange, this.prevTimeRange) || - !_.isEqual(containerState.query, this.prevQuery) || - !_.isEqual(cleanedFilters, this.prevFilters)) { - this.prevTimeRange = containerState.timeRange; - this.prevQuery = containerState.query; - this.prevFilters = cleanedFilters; - if (this.domNode) { - this.render(this.domNode); - } + const cleanedFilters = containerState.filters + ? containerState.filters.filter(filter => !filter.meta.disabled) + : undefined; + if ( + !_.isEqual(containerState.timeRange, this.prevTimeRange) || + !_.isEqual(containerState.query, this.prevQuery) || + !_.isEqual(cleanedFilters, this.prevFilters) + ) { + this.prevTimeRange = containerState.timeRange; + this.prevQuery = containerState.query; + this.prevFilters = cleanedFilters; + if (this.domNode) { + this.render(this.domNode); + } } } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable_factory.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts similarity index 56% rename from x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable_factory.ts rename to x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts index fb2716184f9f9..59b694b83f329 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/lens_embeddable_factory.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable_factory.ts @@ -5,38 +5,28 @@ */ import _ from 'lodash'; -import chrome from 'ui/chrome'; -import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; +import { Chrome } from 'ui/chrome'; -export const indexPatternService = data.indexPatterns.indexPatterns; import { capabilities } from 'ui/capabilities'; import { i18n } from '@kbn/i18n'; +import { DataSetup } from 'src/legacy/core_plugins/data/public'; import { EmbeddableFactory, - embeddableFactories, ErrorEmbeddable, EmbeddableInput, IContainer, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; -import { LensEmbeddable } from './lens_embeddable'; +import { LensEmbeddable } from './embeddable'; import { SavedObjectIndexStore, DOC_TYPE } from '../../persistence'; -import { indexPatternDatasourceSetup } from '../../indexpattern_plugin'; -import { xyVisualizationSetup } from '../../xy_visualization_plugin'; -import { editorFrameSetup } from '../../editor_frame_plugin'; -import { datatableVisualizationSetup } from '../../datatable_visualization_plugin'; import { getEditPath } from '../../../common'; -// bootstrap shimmed plugins to register everything necessary to the expression. -// the new platform will take care of this once in place -indexPatternDatasourceSetup(); -datatableVisualizationSetup(); -xyVisualizationSetup(); -editorFrameSetup(); - export class LensEmbeddableFactory extends EmbeddableFactory { type = DOC_TYPE; - constructor() { + private chrome: Chrome; + private indexPatternService: DataSetup['indexPatterns']['indexPatterns']; + + constructor(chrome: Chrome, indexPatternService: DataSetup['indexPatterns']['indexPatterns']) { super({ savedObjectMetaData: { name: i18n.translate('xpack.lens.lensSavedObjectLabel', { @@ -46,6 +36,8 @@ export class LensEmbeddableFactory extends EmbeddableFactory { getIconForSavedObject: () => 'faceHappy', }, }); + this.chrome = chrome; + this.indexPatternService = indexPatternService; } public isEditable() { @@ -67,26 +59,28 @@ export class LensEmbeddableFactory extends EmbeddableFactory { input: Partial & { id: string }, parent?: IContainer ) { - const store = new SavedObjectIndexStore(chrome.getSavedObjectsClient()); + const store = new SavedObjectIndexStore(this.chrome.getSavedObjectsClient()); const savedVis = await store.load(savedObjectId); - const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map(async (indexPatternId) => { - try { - return await indexPatternService.get(indexPatternId); - } catch (error) { - // Unable to load index pattern, better to not throw error so map embeddable can render - // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern - return null; + const promises = savedVis.state.datasourceMetaData.filterableIndexPatterns.map( + async indexPatternId => { + try { + return await this.indexPatternService.get(indexPatternId); + } catch (error) { + // Unable to load index pattern, better to not throw error so map embeddable can render + // Error will be surfaced by map embeddable since it too will be unable to locate the index pattern + return null; + } } - }); + ); const indexPatterns = await Promise.all(promises); return new LensEmbeddable( { savedVis, - editUrl: chrome.addBasePath(getEditPath(savedObjectId)), + editUrl: this.chrome.addBasePath(getEditPath(savedObjectId)), editable: this.isEditable(), - indexPatterns + indexPatterns, }, input, parent @@ -97,5 +91,3 @@ export class LensEmbeddableFactory extends EmbeddableFactory { return new ErrorEmbeddable('Lens can only be created from a saved object', input); } } - -embeddableFactories.set('lens', new LensEmbeddableFactory()); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index b55a84d32c17a..533cb0289d951 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -9,7 +9,11 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; import { CoreSetup } from 'src/core/public'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; -import chrome from 'ui/chrome'; +import chrome, { Chrome } from 'ui/chrome'; +import { + EmbeddablePlugin, + embeddablePlugin, +} from '../../../../../../src/legacy/core_plugins/embeddable_api/public'; import { DataSetup, ExpressionRenderer, @@ -25,9 +29,12 @@ import { import { EditorFrame } from './editor_frame'; import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; import { InitializableComponent } from './initializable_component'; +import { LensEmbeddableFactory } from './embeddable/embeddable_factory'; export interface EditorFrameSetupPlugins { data: DataSetup; + chrome: Chrome; + embeddables: EmbeddablePlugin; } interface InitializationResult { @@ -54,13 +61,14 @@ export class EditorFramePlugin { constructor() {} private ExpressionRenderer: ExpressionRenderer | null = null; + private chrome: Chrome | null = null; private readonly datasources: Record = {}; private readonly visualizations: Record = {}; private createInstance(): EditorFrameInstance { let domElement: Element; - const store = new SavedObjectIndexStore(chrome.getSavedObjectsClient()); + const store = new SavedObjectIndexStore(this.chrome!.getSavedObjectsClient()); function unmount() { if (domElement) { @@ -114,6 +122,11 @@ export class EditorFramePlugin { public setup(_core: CoreSetup | null, plugins: EditorFrameSetupPlugins): EditorFrameSetup { this.ExpressionRenderer = plugins.data.expressions.ExpressionRenderer; + this.chrome = plugins.chrome; + plugins.embeddables.addEmbeddableFactory( + new LensEmbeddableFactory(plugins.chrome, plugins.data.indexPatterns) + ); + return { createInstance: this.createInstance.bind(this), registerDatasource: (name, datasource) => { @@ -135,6 +148,8 @@ const editorFrame = new EditorFramePlugin(); export const editorFrameSetup = () => editorFrame.setup(null, { data, + chrome, + embeddables: embeddablePlugin, }); export const editorFrameStop = () => editorFrame.stop(); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index c4e8aa828dc3c..8cf151f01d2a3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -268,7 +268,7 @@ export function getIndexPatternDatasource({ getMetaData(state: IndexPatternPrivateState) { return { - filterableIndexPatterns: state.currentIndexPatternId ? [state.currentIndexPatternId] : [] + filterableIndexPatterns: state.currentIndexPatternId ? [state.currentIndexPatternId] : [], }; }, diff --git a/x-pack/legacy/plugins/lens/public/register_embeddable.ts b/x-pack/legacy/plugins/lens/public/register_embeddable.ts new file mode 100644 index 0000000000000..0364b1a4eb65b --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/register_embeddable.ts @@ -0,0 +1,17 @@ +/* + * 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 { indexPatternDatasourceSetup } from './indexpattern_plugin'; +import { xyVisualizationSetup } from './xy_visualization_plugin'; +import { editorFrameSetup } from './editor_frame_plugin'; +import { datatableVisualizationSetup } from './datatable_visualization_plugin'; + +// bootstrap shimmed plugins to register everything necessary (expression functions and embeddables). +// the new platform will take care of this once in place. +indexPatternDatasourceSetup(); +datatableVisualizationSetup(); +xyVisualizationSetup(); +editorFrameSetup(); From 6b06f766b258f6f371082304bf531617f21b16f1 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 17 Jul 2019 15:57:22 +0200 Subject: [PATCH 11/67] clean up type errors --- .../editor_frame/save.test.ts | 30 +++++++++---------- .../editor_frame/state_management.test.ts | 2 ++ .../lens/public/editor_frame_plugin/mocks.tsx | 5 ++-- .../editor_frame_plugin/plugin.test.tsx | 18 +++++++++-- .../persistence/saved_object_store.test.ts | 10 +++++++ 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index ef4eed2b63a5d..8965ab456746d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -6,13 +6,14 @@ import { save, Props } from './save'; import { Action } from './state_management'; +import { createMockDatasource, createMockVisualization } from '../mocks'; describe('save editor frame state', () => { const saveArgs: Props = { dispatch: jest.fn(), redirectTo: jest.fn(), - datasource: { getPersistableState: x => x }, - visualization: { getPersistableState: x => x }, + datasource: createMockDatasource(), + visualization: createMockVisualization(), state: { title: 'aaa', datasource: { activeId: '1', isLoading: false, state: {} }, @@ -88,29 +89,26 @@ describe('save editor frame state', () => { const store = { save: jest.fn(async () => ({ id: 'bar' })), }; + const datasource = createMockDatasource(); + datasource.getPersistableState.mockImplementation(state => ({ + stuff: `${state}_datsource_persisted`, + })); + + const visualization = createMockVisualization(); + visualization.getPersistableState.mockImplementation(state => ({ + stuff: `${state}_vis_persisted`, + })); await save({ ...saveArgs, store, - datasource: { - getPersistableState(state) { - return { - stuff: `${state}_datsource_persisted`, - }; - }, - }, + datasource, state: { title: 'bbb', datasource: { activeId: '1', isLoading: false, state: '2' }, saving: false, visualization: { activeId: '3', state: '4' }, }, - visualization: { - getPersistableState(state) { - return { - things: `${state}_vis_persisted`, - }; - }, - }, + visualization, }); expect(store.save).toHaveBeenCalledWith({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index 5f1861269dc0e..4b412bfc18d9e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -328,7 +328,9 @@ describe('editor_frame state management', () => { doc: { datasourceType: 'a', id: 'b', + expression: '', state: { + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { foo: 'c' }, visualization: { bar: 'd' }, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 6cf0223191f63..9cfc2b95bf6b2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -11,7 +11,7 @@ import { EditorFrameSetupPlugins } from './plugin'; export function createMockVisualization(): jest.Mocked { return { - getPersistableState: jest.fn(_state => ({})), + getPersistableState: jest.fn(_state => _state), getSuggestions: jest.fn(_options => []), initialize: jest.fn((_datasource, _state?) => ({})), renderConfigPanel: jest.fn(), @@ -36,11 +36,12 @@ export function createMockDatasource(): DatasourceMock { return { getDatasourceSuggestionsForField: jest.fn((_state, item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []), - getPersistableState: jest.fn(), + getPersistableState: jest.fn(x => x), getPublicAPI: jest.fn((_state, _setState) => publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), toExpression: jest.fn(_state => null), + getMetaData: jest.fn(_state => ({ filterableIndexPatterns: [] })), // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index ae169aa67148e..bff0a1ddc918c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -78,7 +78,14 @@ describe('editor_frame plugin', () => { const doc: Document = { datasourceType: 'indexpattern', id: 'hoi', - state: { datasource: 'foo', visualization: 'bar' }, + expression: '', + state: { + datasource: 'foo', + visualization: 'bar', + datasourceMetaData: { + filterableIndexPatterns: [], + }, + }, title: 'shazm', visualizationType: 'fanci', type: 'lens', @@ -186,7 +193,14 @@ describe('editor_frame plugin', () => { doc={{ datasourceType: 'b', visualizationType: 'd', - state: { visualization: 'viz', datasource: 'data' }, + expression: '', + state: { + visualization: 'viz', + datasource: 'data', + datasourceMetaData: { + filterableIndexPatterns: [], + }, + }, title: 'ttt', }} datasources={{ a: createMockDatasource(), b: createMockDatasource() }} diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts index 53d2a0cc08ad1..ce9a3dd9d13aa 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts @@ -27,7 +27,9 @@ describe('LensStore', () => { title: 'Hello', visualizationType: 'bar', datasourceType: 'indexpattern', + expression: '', state: { + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, @@ -49,7 +51,9 @@ describe('LensStore', () => { datasourceType: 'indexpattern', title: 'Hello', visualizationType: 'bar', + expression: '', state: JSON.stringify({ + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }), @@ -63,7 +67,9 @@ describe('LensStore', () => { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', datasourceType: 'indexpattern', + expression: '', state: { + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: 'lotr' }, visualization: { gear: ['staff', 'pointy hat'] }, }, @@ -74,7 +80,9 @@ describe('LensStore', () => { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', datasourceType: 'indexpattern', + expression: '', state: { + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: 'lotr' }, visualization: { gear: ['staff', 'pointy hat'] }, }, @@ -85,7 +93,9 @@ describe('LensStore', () => { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', datasourceType: 'indexpattern', + expression: '', state: JSON.stringify({ + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: 'lotr' }, visualization: { gear: ['staff', 'pointy hat'] }, }), From f78ce1a1efcf4b2e09fedc57edcefd1f5574a3a0 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Wed, 17 Jul 2019 18:42:58 -0400 Subject: [PATCH 12/67] Reduce quantity of linting errors --- .../editor_frame/save.test.ts | 178 +++++++----- .../editor_frame_plugin/editor_frame/save.ts | 17 +- .../editor_frame/workspace_panel.test.tsx | 124 ++++++--- .../editor_frame_plugin/merge_tables.test.ts | 36 +++ .../lens/public/editor_frame_plugin/mocks.tsx | 22 +- .../editor_frame_plugin/plugin.test.tsx | 20 +- .../indexpattern_plugin/indexpattern.test.tsx | 122 ++++---- .../indexpattern_plugin/indexpattern.tsx | 1 - .../date_histogram.test.tsx | 105 ++++--- .../filter_ratio.test.tsx | 82 ++++-- .../operation_definitions/terms.test.tsx | 189 ++++++++----- .../public/indexpattern_plugin/operations.ts | 2 +- .../indexpattern_plugin/state_helpers.test.ts | 263 +++++++++++------- .../persistence/saved_object_store.test.ts | 41 ++- .../public/persistence/saved_object_store.ts | 7 +- x-pack/legacy/plugins/lens/public/types.ts | 1 - .../__snapshots__/xy_expression.test.tsx.snap | 37 ++- .../xy_visualization.test.ts.snap | 71 ++--- .../public/xy_visualization_plugin/types.ts | 2 +- .../xy_config_panel.test.tsx | 170 ++++++----- .../xy_expression.test.tsx | 160 +++++++---- .../xy_suggestions.test.ts | 152 +++++----- .../xy_visualization_plugin/xy_suggestions.ts | 1 + .../xy_visualization.test.ts | 51 ++-- 24 files changed, 1153 insertions(+), 701 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index ef4eed2b63a5d..c8f79a2682dcc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -6,16 +6,22 @@ import { save, Props } from './save'; import { Action } from './state_management'; +import { createMockDatasource } from '../mocks'; describe('save editor frame state', () => { const saveArgs: Props = { dispatch: jest.fn(), redirectTo: jest.fn(), - datasource: { getPersistableState: x => x }, + activeDatasources: {}, + // datasource: { getPersistableState: x => x }, visualization: { getPersistableState: x => x }, state: { title: 'aaa', - datasource: { activeId: '1', isLoading: false, state: {} }, + // datasource: { activeId: '1', isLoading: false, state: {} }, + datasourceMap: {}, + datasourceStates: {}, + activeDatasourceId: 'indexpattern', + layerIdToDatasource: {}, saving: false, visualization: { activeId: '2', state: {} }, }, @@ -41,12 +47,13 @@ describe('save editor frame state', () => { await save({ ...saveArgs, dispatch, - state: { - title: 'aaa', - datasource: { activeId: '1', isLoading: false, state: {} }, - saving: false, - visualization: { activeId: '2', state: {} }, - }, + // state: { + // ...saveArgs.state, + // title: 'aaa', + // datasource: { activeId: '1', isLoading: false, state: {} }, + // saving: false, + // visualization: { activeId: '2', state: {} }, + // }, store: { async save() { saved = true; @@ -66,12 +73,12 @@ describe('save editor frame state', () => { save({ ...saveArgs, dispatch, - state: { - title: 'aaa', - datasource: { activeId: '1', isLoading: false, state: {} }, - saving: false, - visualization: { activeId: '2', state: {} }, - }, + // state: { + // title: 'aaa', + // datasource: { activeId: '1', isLoading: false, state: {} }, + // saving: false, + // visualization: { activeId: '2', state: {} }, + // }, store: { async save() { throw new Error('aw shnap!'); @@ -88,22 +95,47 @@ describe('save editor frame state', () => { const store = { save: jest.fn(async () => ({ id: 'bar' })), }; + const datasource = createMockDatasource(); + datasource.getPersistableState.mockImplementation(state => ({ + stuff: `${state}_datasource_persisted`, + })); await save({ ...saveArgs, store, - datasource: { - getPersistableState(state) { - return { - stuff: `${state}_datsource_persisted`, - }; - }, - }, + // state: { + // title: 'bbb', + // // datasource: { activeId: '1', isLoading: false, state: '2' }, + // datasourceStates + // saving: false, + // visualization: { activeId: '3', state: '4' }, + // }, + state: { title: 'bbb', - datasource: { activeId: '1', isLoading: false, state: '2' }, + // datasource: { activeId: '1', isLoading: false, state: {} }, + datasourceStates: { + indexpattern: { + state: 'hello', + isLoading: false, + }, + }, + datasourceMap: { + // indexpattern: createMockDatasource() + // getPersistableState(state) { + // return { + // stuff: `${state}_datsource_persisted`, + // }; + // }, + // }, + indexpattern: datasource, + }, + // datasourceStates: {}, + activeDatasourceId: 'indexpattern', + layerIdToDatasource: {}, saving: false, - visualization: { activeId: '3', state: '4' }, + visualization: { activeId: '2', state: {} }, }, + visualization: { getPersistableState(state) { return { @@ -114,10 +146,16 @@ describe('save editor frame state', () => { }); expect(store.save).toHaveBeenCalledWith({ - datasourceType: '1', + // datasourceType: '1', + activeDatasourceId: 'indexpattern', id: undefined, state: { - datasource: { stuff: '2_datsource_persisted' }, + // datasource: { stuff: '2_datsource_persisted' }, + datasourceStates: { + indexpattern: { + stuff: '2_datasource_persisted', + }, + }, visualization: { things: '4_vis_persisted' }, }, title: 'bbb', @@ -126,52 +164,52 @@ describe('save editor frame state', () => { }); }); - it('redirects to the edit screen if the id changes', async () => { - const redirectTo = jest.fn(); - const dispatch = jest.fn(); - await save({ - ...saveArgs, - dispatch, - redirectTo, - state: { - title: 'ccc', - datasource: { activeId: '1', isLoading: false, state: {} }, - saving: false, - visualization: { activeId: '2', state: {} }, - }, - store: { - async save() { - return { id: 'bazinga' }; - }, - }, - }); + // it('redirects to the edit screen if the id changes', async () => { + // const redirectTo = jest.fn(); + // const dispatch = jest.fn(); + // await save({ + // ...saveArgs, + // dispatch, + // redirectTo, + // state: { + // title: 'ccc', + // datasource: { activeId: '1', isLoading: false, state: {} }, + // saving: false, + // visualization: { activeId: '2', state: {} }, + // }, + // store: { + // async save() { + // return { id: 'bazinga' }; + // }, + // }, + // }); - expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PERSISTED_ID', id: 'bazinga' }); - expect(redirectTo).toHaveBeenCalledWith('/edit/bazinga'); - }); + // expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PERSISTED_ID', id: 'bazinga' }); + // expect(redirectTo).toHaveBeenCalledWith('/edit/bazinga'); + // }); - it('does not redirect to the edit screen if the id does not change', async () => { - const redirectTo = jest.fn(); - const dispatch = jest.fn(); - await save({ - ...saveArgs, - dispatch, - redirectTo, - state: { - title: 'ddd', - datasource: { activeId: '1', isLoading: false, state: {} }, - persistedId: 'foo', - saving: false, - visualization: { activeId: '2', state: {} }, - }, - store: { - async save() { - return { id: 'foo' }; - }, - }, - }); + // it('does not redirect to the edit screen if the id does not change', async () => { + // const redirectTo = jest.fn(); + // const dispatch = jest.fn(); + // await save({ + // ...saveArgs, + // dispatch, + // redirectTo, + // state: { + // title: 'ddd', + // datasource: { activeId: '1', isLoading: false, state: {} }, + // persistedId: 'foo', + // saving: false, + // visualization: { activeId: '2', state: {} }, + // }, + // store: { + // async save() { + // return { id: 'foo' }; + // }, + // }, + // }); - expect(dispatch.mock.calls.some(({ type }) => type === 'UPDATE_PERSISTED_ID')).toBeFalsy(); - expect(redirectTo).not.toHaveBeenCalled(); - }); + // expect(dispatch.mock.calls.some(({ type }) => type === 'UPDATE_PERSISTED_ID')).toBeFalsy(); + // expect(redirectTo).not.toHaveBeenCalled(); + // }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index deb074789cc57..87a3de6c3e8e5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -8,7 +8,8 @@ import { Action, EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; export interface Props { - datasource: { getPersistableState: (state: unknown) => unknown }; + // datasource: { getPersistableState: (state: unknown) => unknown }; + activeDatasources: Record unknown }>; dispatch: (value: Action) => void; redirectTo: (path: string) => void; state: EditorFrameState; @@ -17,7 +18,8 @@ export interface Props { } export async function save({ - datasource, + // datasource, + activeDatasources, dispatch, redirectTo, state, @@ -27,15 +29,22 @@ export async function save({ try { dispatch({ type: 'SAVING', isSaving: true }); + const datasourceStates: Record = {}; + Object.entries(activeDatasources).forEach(([id, datasource]) => { + datasourceStates[id] = datasource.getPersistableState(state.datasourceStates[id].state); + }); + const doc = await store.save({ id: state.persistedId, title: state.title, type: 'lens', visualizationType: state.visualization.activeId, // datasourceType: state.datasource.activeId, - datasourceType: state.activeDatasourceId, + // datasourceType: state.activeDatasourceId, + activeDatasourceId: state.activeDatasourceId, state: { - datasource: datasource.getPersistableState(state.datasource.state), + // datasource: datasource.getPersistableState(state.datasource.state), + datasourceStates, // datasources: datasource.getPersistableState(state.datasource.state), visualization: visualization.getPersistableState(state.visualization.state), }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 5dc613baf0176..8c0efae5c0f57 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -13,6 +13,7 @@ import { createMockDatasource, createExpressionRendererMock, DatasourceMock, + createMockFramePublicAPI, } from '../mocks'; import { InnerWorkspacePanel, WorkspacePanelProps } from './workspace_panel'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; @@ -44,14 +45,15 @@ describe('workspace_panel', () => { it('should render an explanatory text if no visualization is active', () => { instance = mount( {}} ExpressionRenderer={expressionRendererMock} /> @@ -64,14 +66,15 @@ describe('workspace_panel', () => { it('should render an explanatory text if the visualization does not produce an expression', () => { instance = mount( 'datasource' }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => null }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -84,14 +87,17 @@ describe('workspace_panel', () => { it('should render an explanatory text if the datasource does not produce an expression', () => { instance = mount( null }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} + // activeDatasource={{ ...mockDatasource, toExpression: () => null }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -102,19 +108,40 @@ describe('workspace_panel', () => { }); it('should render the resulting expression using the expression renderer', () => { + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.layerIdToDatasource = { + first: 'mock', + }; + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + // const mockDatasource = createMockDatasource(); + mockDatasource.toExpression.mockReturnValue('datasource'); + + // framePublicAPI.datasourceLayers instance = mount( 'datasource', + activeDatasourceId={'mock'} + datasourceStates={{ + mock: { + state: {}, + isLoading: false, + }, + }} + datasourceMap={{ + mock: mockDatasource, }} - datasourceState={{}} + framePublicAPI={framePublicAPI} + // activeDatasource={{ + // ...mockDatasource, + // toExpression: () => 'datasource', + // }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -143,17 +170,20 @@ Object { it('should show an error message if the expression fails to parse', () => { instance = mount( 'datasource ||', - }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} + // activeDatasource={{ + // ...mockDatasource, + // toExpression: () => 'datasource ||', + // }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -171,17 +201,20 @@ Object { instance = mount( 'datasource', - }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} + // activeDatasource={{ + // ...mockDatasource, + // toExpression: () => 'datasource', + // }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -204,17 +237,20 @@ Object { instance = mount( 'datasource', - }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} + // activeDatasource={{ + // ...mockDatasource, + // toExpression: () => 'datasource', + // }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -240,17 +276,20 @@ Object { instance = mount( 'datasource', - }} - datasourceState={{}} + activeDatasourceId={'mock'} + datasourceStates={{}} + datasourceMap={{}} + framePublicAPI={createMockFramePublicAPI()} + // activeDatasource={{ + // ...mockDatasource, + // toExpression: () => 'datasource', + // }} + // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, }} visualizationState={{}} - datasourcePublicAPI={mockDatasource.publicAPIMock} dispatch={() => {}} ExpressionRenderer={expressionRendererMock} /> @@ -283,14 +322,17 @@ Object { mockDispatch = jest.fn(); instance = mount( diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts new file mode 100644 index 0000000000000..8724a1d4fb560 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mergeTables } from './merge_tables'; +import { KibanaDatatable } from 'src/legacy/core_plugins/interpreter/types'; + +describe('lens_merge_tables', () => { + it('should produce a row with the nested table as defined', () => { + const sampleTable1: KibanaDatatable = { + type: 'kibana_datatable', + columns: [{ id: 'bucket', name: 'A' }, { id: 'count', name: 'Count' }], + rows: [{ bucket: 'a', count: 5 }, { bucket: 'b', count: 10 }], + }; + + const sampleTable2: KibanaDatatable = { + type: 'kibana_datatable', + columns: [{ id: 'bucket', name: 'C' }, { id: 'avg', name: 'Average' }], + rows: [{ bucket: 'a', avg: 2.5 }, { bucket: 'b', avg: 9 }], + }; + + expect( + mergeTables.fn( + null, + { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, + {} + ) + ).toEqual({ + columns: [{ id: 'first', name: '' }, { id: 'second', name: '' }], + rows: [{ first: sampleTable1, second: sampleTable2 }], + type: 'kibana_datatable', + }); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 6cf0223191f63..6c3896ea477e7 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -6,16 +6,17 @@ import React from 'react'; import { DataSetup, ExpressionRendererProps } from 'src/legacy/core_plugins/data/public'; -import { DatasourcePublicAPI, Visualization, Datasource } from '../types'; +import { DatasourcePublicAPI, FramePublicAPI, Visualization, Datasource } from '../types'; import { EditorFrameSetupPlugins } from './plugin'; export function createMockVisualization(): jest.Mocked { return { getPersistableState: jest.fn(_state => ({})), getSuggestions: jest.fn(_options => []), - initialize: jest.fn((_datasource, _state?) => ({})), + initialize: jest.fn((_frame, _state?) => ({})), renderConfigPanel: jest.fn(), - toExpression: jest.fn((_state, _datasource) => null), + toExpression: jest.fn((_state, _frame) => null), + getLayerIds: jest.fn(_state => []), }; } @@ -28,6 +29,7 @@ export function createMockDatasource(): DatasourceMock { getTableSpec: jest.fn(() => []), getOperationForColumnId: jest.fn(), renderDimensionPanel: jest.fn(), + renderLayerPanel: jest.fn(), removeColumnInTableSpec: jest.fn(), moveColumnTo: jest.fn(), duplicateColumn: jest.fn(), @@ -40,7 +42,9 @@ export function createMockDatasource(): DatasourceMock { getPublicAPI: jest.fn((_state, _setState) => publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), - toExpression: jest.fn(_state => null), + toExpression: jest.fn((_frame, _state) => null), + insertLayer: jest.fn((_state, _newLayerId) => {}), + getLayers: jest.fn(_state => []), // this is an additional property which doesn't exist on real datasources // but can be used to validate whether specific API mock functions are called @@ -48,6 +52,16 @@ export function createMockDatasource(): DatasourceMock { }; } +export type FrameMock = jest.Mocked; + +export function createMockFramePublicAPI(): FrameMock { + return { + datasourceLayers: {}, + layerIdToDatasource: {}, + addNewLayer: jest.fn(() => ''), + }; +} + type Omit = Pick>; export type MockedDependencies = Omit & { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index ae169aa67148e..84582c215deee 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -76,9 +76,15 @@ describe('editor_frame plugin', () => { it('should load the document, if persistedId is defined', async () => { const doc: Document = { - datasourceType: 'indexpattern', + // datasourceType: 'indexpattern', id: 'hoi', - state: { datasource: 'foo', visualization: 'bar' }, + activeDatasourceId: 'indexpattern', + state: { + datasourceStates: { + indexpattern: 'foo', + }, + visualization: 'bar', + }, title: 'shazm', visualizationType: 'fanci', type: 'lens', @@ -184,9 +190,15 @@ describe('editor_frame plugin', () => { const component = mount( { persistedState = { currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'My Op', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - sourceField: 'op', - params: { - size: 5, - orderBy: { type: 'alphabetical' }, - orderDirection: 'asc', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'My Op', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + sourceField: 'op', + params: { + size: 5, + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + }, + indexPatternId: '1', + }, }, }, }, @@ -178,40 +184,47 @@ describe('IndexPattern Data Source', () => { describe('#toExpression', () => { it('should generate an empty expression when no columns are selected', async () => { const state = await indexPatternDatasource.initialize(); - expect(indexPatternDatasource.toExpression(state)).toEqual(null); + expect(indexPatternDatasource.toExpression(state, 'first')).toEqual(null); }); it('should generate an expression for an aggregated query', async () => { const queryPersistedState: IndexPatternPersistedState = { currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - operationId: 'op1', - label: 'Count of Documents', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', - }, - col2: { - operationId: 'op2', - label: 'Date', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'timestamp', - params: { - interval: '1d', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Count of Documents', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + indexPatternId: '1', + }, + col2: { + operationId: 'op2', + label: 'Date', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: '1d', + }, + indexPatternId: '1', + }, }, }, }, }; const state = await indexPatternDatasource.initialize(queryPersistedState); - expect(indexPatternDatasource.toExpression(state)).toMatchInlineSnapshot(` + expect(indexPatternDatasource.toExpression(state, 'first')).toMatchInlineSnapshot(` "esaggs index=\\"1\\" metricsAtAllLevels=false @@ -228,8 +241,7 @@ describe('IndexPattern Data Source', () => { beforeEach(async () => { initialState = await indexPatternDatasource.initialize({ currentIndexPatternId: '1', - columnOrder: [], - columns: {}, + layers: {}, }); }); @@ -348,8 +360,6 @@ describe('IndexPattern Data Source', () => { it('should not make any suggestions for a number without a time field', async () => { const state: IndexPatternPrivateState = { currentIndexPatternId: '1', - columnOrder: [], - columns: {}, indexPatterns: { 1: { id: '1', @@ -364,6 +374,13 @@ describe('IndexPattern Data Source', () => { ], }, }, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, }; const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(state, { @@ -424,8 +441,13 @@ describe('IndexPattern Data Source', () => { expect( indexPatternDatasource.getDatasourceSuggestionsFromCurrentState({ indexPatterns: expectedIndexPatterns, - columnOrder: [], - columns: {}, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, currentIndexPatternId: '1', }) ).toEqual([]); @@ -489,6 +511,7 @@ describe('IndexPattern Data Source', () => { operationType: 'max', sourceField: 'baz', suggestedPriority: 0, + indexPatternId: '1', }; const columns: Record = { a: { @@ -507,8 +530,13 @@ describe('IndexPattern Data Source', () => { const api = indexPatternDatasource.getPublicAPI( { ...initialState, - columnOrder: ['a', 'b', 'c'], - columns, + layers: { + ...initialState.layers, + first: { + ...initialState.layers.first, + columnOrder: ['a', 'b', 'c'], + }, + }, }, setState ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 4b538294804e1..1a7afb76697aa 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -20,7 +20,6 @@ import { Query } from '../../../../../../src/legacy/core_plugins/data/public/que import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; -import { buildColumnForOperationType, getOperationTypesForField } from './operations'; import { IndexPatternDatasourcePluginPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { Datasource, DataType } from '..'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx index e823040fee752..2704a594960e3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx @@ -16,6 +16,7 @@ describe('date_histogram', () => { beforeEach(() => { state = { + currentIndexPatternId: '1', indexPatterns: { 1: { id: '1', @@ -31,21 +32,26 @@ describe('date_histogram', () => { ], }, }, - currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'Value of timestamp', - dataType: 'date', - isBucketed: true, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Value of timestamp', + dataType: 'date', + isBucketed: true, - // Private - operationType: 'date_histogram', - params: { - interval: 'w', + // Private + operationType: 'date_histogram', + params: { + interval: 'w', + }, + sourceField: 'timestamp', + indexPatternId: '1', + }, }, - sourceField: 'timestamp', }, }, }; @@ -53,28 +59,42 @@ describe('date_histogram', () => { describe('buildColumn', () => { it('should create column object with default params', () => { - const column = dateHistogramOperation.buildColumn('op', {}, 0, { - name: 'timestamp', - type: 'date', - esTypes: ['date'], - aggregatable: true, - searchable: true, + const column = dateHistogramOperation.buildColumn({ + operationId: 'op', + columns: {}, + suggestedPriority: 0, + layerId: 'first', + indexPatternId: '1', + field: { + name: 'timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + }, }); expect(column.params.interval).toEqual('h'); }); it('should create column object with restrictions', () => { - const column = dateHistogramOperation.buildColumn('op', {}, 0, { - name: 'timestamp', - type: 'date', - esTypes: ['date'], - aggregatable: true, - searchable: true, - aggregationRestrictions: { - date_histogram: { - agg: 'date_histogram', - time_zone: 'UTC', - calendar_interval: '1y', + const column = dateHistogramOperation.buildColumn({ + operationId: 'op', + columns: {}, + suggestedPriority: 0, + layerId: 'first', + indexPatternId: '1', + field: { + name: 'timestamp', + type: 'date', + esTypes: ['date'], + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + time_zone: 'UTC', + calendar_interval: '1y', + }, }, }, }); @@ -86,7 +106,7 @@ describe('date_histogram', () => { describe('toEsAggsConfig', () => { it('should reflect params correctly', () => { const esAggsConfig = dateHistogramOperation.toEsAggsConfig( - state.columns.col1 as DateHistogramIndexPatternColumn, + state.layers.first.columns.col1 as DateHistogramIndexPatternColumn, 'col1' ); expect(esAggsConfig).toEqual( @@ -104,7 +124,7 @@ describe('date_histogram', () => { it('should render current value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); expect(instance.find(EuiRange).prop('value')).toEqual(1); @@ -113,7 +133,7 @@ describe('date_histogram', () => { it('should update state with the interval value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); instance.find(EuiRange).prop('onChange')!({ @@ -123,12 +143,18 @@ describe('date_histogram', () => { } as React.ChangeEvent); expect(setStateSpy).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: { - ...state.columns.col1, - params: { - interval: 'd', + layers: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + interval: 'd', + }, + }, }, }, }, @@ -161,6 +187,7 @@ describe('date_histogram', () => { }} setState={setStateSpy} columnId="col1" + layerId="first" /> ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx index 9dd6b06eb5c59..732fe48420b96 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx @@ -26,19 +26,25 @@ describe('filter_ratio', () => { }, }, currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'Filter Ratio', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'filter_ratio', - params: { - numerator: { query: '', language: 'kuery' }, - denominator: { query: '', language: 'kuery' }, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Filter Ratio', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'filter_ratio', + params: { + numerator: { query: '', language: 'kuery' }, + denominator: { query: '', language: 'kuery' }, + }, + indexPatternId: '1', + }, }, }, }, @@ -64,7 +70,13 @@ describe('filter_ratio', () => { describe('buildColumn', () => { it('should create column object with default params', () => { - const column = filterRatioOperation.buildColumn('op', {}, 0); + const column = filterRatioOperation.buildColumn({ + operationId: 'op', + indexPatternId: '1', + layerId: 'first', + columns: {}, + suggestedPriority: undefined, + }); expect(column.params.numerator).toEqual({ query: '', language: 'kuery' }); expect(column.params.denominator).toEqual({ query: '', language: 'kuery' }); }); @@ -73,7 +85,7 @@ describe('filter_ratio', () => { describe('toEsAggsConfig', () => { it('should reflect params correctly', () => { const esAggsConfig = filterRatioOperation.toEsAggsConfig( - state.columns.col1 as FilterRatioIndexPatternColumn, + state.layers.first.columns.col1 as FilterRatioIndexPatternColumn, 'col1' ); expect(esAggsConfig).toEqual( @@ -100,6 +112,7 @@ describe('filter_ratio', () => { expect(() => { shallowWithIntl( { it('should show only the numerator by default', () => { const wrapper = shallowWithIntl( { const setState = jest.fn(); const wrapper = shallowWithIntl( { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - col1: { - ...state.columns.col1, - params: { - numerator: { query: 'geo.src : "US"', language: 'kuery' }, - denominator: { query: '', language: 'kuery' }, + layers: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + col1: { + ...state.layers.first.columns.col1, + params: { + numerator: { query: 'geo.src : "US"', language: 'kuery' }, + denominator: { query: '', language: 'kuery' }, + }, + }, }, }, }, @@ -160,6 +181,7 @@ describe('filter_ratio', () => { const setState = jest.fn(); const wrapper = shallowWithIntl( { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - col1: { - ...state.columns.col1, - params: { - numerator: { query: '', language: 'kuery' }, - denominator: { query: 'geo.src : "US"', language: 'kuery' }, + laeyrs: { + ...state.layers, + first: { + ...state.layers.first, + columns: { + col1: { + ...state.layers.first.columns.col1, + params: { + numerator: { query: '', language: 'kuery' }, + denominator: { query: 'geo.src : "US"', language: 'kuery' }, + }, + }, }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx index 9c69b4fd4e20e..e5f5a5eb12bfd 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx @@ -18,31 +18,38 @@ describe('terms', () => { state = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - operationId: 'op1', - label: 'Top value of category', - dataType: 'string', - isBucketed: true, - - // Private - operationType: 'terms', - params: { - orderBy: { type: 'alphabetical' }, - size: 5, - orderDirection: 'asc', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Top value of category', + dataType: 'string', + isBucketed: true, + + // Private + operationType: 'terms', + params: { + orderBy: { type: 'alphabetical' }, + size: 5, + orderDirection: 'asc', + }, + sourceField: 'category', + indexPatternId: '1', + }, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + indexPatternId: '1', + }, }, - sourceField: 'category', - }, - col2: { - operationId: 'op1', - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', }, }, }; @@ -51,7 +58,7 @@ describe('terms', () => { describe('toEsAggsConfig', () => { it('should reflect params correctly', () => { const esAggsConfig = termsOperation.toEsAggsConfig( - state.columns.col1 as TermsIndexPatternColumn, + state.layers.first.columns.col1 as TermsIndexPatternColumn, 'col1' ); expect(esAggsConfig).toEqual( @@ -68,15 +75,22 @@ describe('terms', () => { describe('buildColumn', () => { it('should use existing metric column as order column', () => { - const termsColumn = termsOperation.buildColumn('abc', { - col1: { - operationId: 'op1', - label: 'Count', - dataType: 'number', - isBucketed: false, + const termsColumn = termsOperation.buildColumn({ + operationId: 'abc', + layerId: 'first', + indexPatternId: '1', + suggestedPriority: undefined, + columns: { + col1: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, - // Private - operationType: 'count', + // Private + operationType: 'count', + indexPatternId: '1', + }, }, }); expect(termsColumn.params).toEqual( @@ -103,6 +117,7 @@ describe('terms', () => { orderDirection: 'asc', }, sourceField: 'category', + indexPatternId: '1', }; const updatedColumn = termsOperation.onOtherColumnChanged!(initialColumn, { col1: { @@ -113,6 +128,7 @@ describe('terms', () => { // Private operationType: 'count', + indexPatternId: '1', }, }); expect(updatedColumn).toBe(initialColumn); @@ -134,6 +150,7 @@ describe('terms', () => { orderDirection: 'asc', }, sourceField: 'category', + indexPatternId: '1', }, {} ); @@ -160,6 +177,7 @@ describe('terms', () => { orderDirection: 'asc', }, sourceField: 'category', + indexPatternId: '1', }, { col1: { @@ -174,6 +192,7 @@ describe('terms', () => { interval: 'w', }, sourceField: 'timestamp', + indexPatternId: '1', }, } ); @@ -189,7 +208,7 @@ describe('terms', () => { it('should render current order by value and options', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); const select = instance.find('[data-test-subj="indexPattern-terms-orderBy"]').find(EuiSelect); @@ -208,25 +227,32 @@ describe('terms', () => { ); @@ -238,7 +264,7 @@ describe('terms', () => { it('should update state with the order by value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); instance @@ -252,15 +278,20 @@ describe('terms', () => { expect(setStateSpy).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: { - ...state.columns.col1, - params: { - ...(state.columns.col1 as TermsIndexPatternColumn).params, - orderBy: { - type: 'column', - columnId: 'col2', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + orderBy: { + type: 'column', + columnId: 'col2', + }, + }, }, }, }, @@ -271,7 +302,7 @@ describe('terms', () => { it('should render current order direction value and options', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); const select = instance @@ -285,7 +316,7 @@ describe('terms', () => { it('should update state with the order direction value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); instance @@ -299,13 +330,17 @@ describe('terms', () => { expect(setStateSpy).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: { - ...state.columns.col1, - params: { - ...(state.columns.col1 as TermsIndexPatternColumn).params, - orderDirection: 'desc', + layers: { + first: { + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + orderDirection: 'desc', + }, + }, }, }, }, @@ -315,7 +350,7 @@ describe('terms', () => { it('should render current size value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); expect(instance.find(EuiRange).prop('value')).toEqual(5); @@ -324,7 +359,7 @@ describe('terms', () => { it('should update state with the size value', () => { const setStateSpy = jest.fn(); const instance = shallow( - + ); instance.find(EuiRange).prop('onChange')!({ @@ -334,13 +369,17 @@ describe('terms', () => { } as React.ChangeEvent); expect(setStateSpy).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: { - ...state.columns.col1, - params: { - ...(state.columns.col1 as TermsIndexPatternColumn).params, - size: 7, + layers: { + first: { + columns: { + ...state.layers.first.columns, + col1: { + ...state.layers.first.columns.col1, + params: { + ...(state.layers.first.columns.col1 as TermsIndexPatternColumn).params, + size: 7, + }, + }, }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index 79fd49567b146..f73f4b47a516d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -6,7 +6,7 @@ import { Storage } from 'ui/storage'; import { DataSetup } from '../../../../../../src/legacy/core_plugins/data/public'; -import { DimensionLayer, DimensionPriority } from '../types'; +import { DimensionPriority } from '../types'; import { IndexPatternColumn, IndexPatternField, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index 6693dc257ffd2..3c4bbae93c8b3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -32,27 +32,36 @@ describe('state_helpers', () => { orderDirection: 'asc', size: 5, }, + indexPatternId: '', }; const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: termsColumn, - col2: { - operationId: 'op1', - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + indexPatternId: '1', + }, + }, }, }, }; - expect(deleteColumn(state, 'col2').columns).toEqual({ + expect( + deleteColumn({ state, columnId: 'col2', layerId: 'first' }).layers.first.columns + ).toEqual({ col1: termsColumn, }); }); @@ -72,27 +81,38 @@ describe('state_helpers', () => { orderDirection: 'asc', size: 5, }, + indexPatternId: '', }; const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: termsColumn, - col2: { - operationId: 'op1', - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + indexPatternId: '1', + }, + }, }, }, }; - deleteColumn(state, 'col2'); + deleteColumn({ + state, + columnId: 'col2', + layerId: 'first', + }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { col1: termsColumn, @@ -114,18 +134,26 @@ describe('state_helpers', () => { interval: '1d', }, sourceField: 'timestamp', + indexPatternId: '1', }; const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: currentColumn, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: currentColumn, + }, + }, }, }; - expect(updateColumnParam(state, currentColumn, 'interval', 'M').columns.col1).toEqual({ + expect( + updateColumnParam(state, 'first', currentColumn, 'interval', 'M').layers.first.columns.col1 + ).toEqual({ ...currentColumn, params: { interval: 'M' }, }); @@ -137,43 +165,56 @@ describe('state_helpers', () => { const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: { - operationId: 'op1', - label: 'Average of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'avg', - sourceField: 'bytes', - }, - col2: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + operationId: 'op1', + label: 'Average of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'avg', + sourceField: 'bytes', + indexPatternId: '1', + }, + col2: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }; expect( - changeColumn(state, 'col2', { - operationId: 'op1', - label: 'Date histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', + changeColumn({ + state, + columnId: 'col2', + layerId: 'first', + newColumn: { + operationId: 'op1', + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + indexPatternId: '1', }, - sourceField: 'timestamp', }) ).toEqual( expect.objectContaining({ @@ -186,37 +227,49 @@ describe('state_helpers', () => { const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'Date histogram of timestamp', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Date histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + sourceField: 'timestamp', + params: { + interval: 'h', + }, + indexPatternId: '1', + }, + }, + }, + }, + }; + expect( + changeColumn({ + state, + layerId: 'first', + columnId: 'col2', + newColumn: { + operationId: 'op2', + label: 'Date histogram of order_date', dataType: 'date', isBucketed: true, // Private operationType: 'date_histogram', - sourceField: 'timestamp', + sourceField: 'order_date', params: { - interval: 'h', + interval: 'w', }, + indexPatternId: '1', }, - }, - }; - expect( - changeColumn(state, 'col2', { - operationId: 'op2', - label: 'Date histogram of order_date', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - sourceField: 'order_date', - params: { - interval: 'w', - }, - }).columns.col1 + }).layers.first.columns.col1 ).toEqual( expect.objectContaining({ params: { interval: 'h' }, @@ -239,6 +292,7 @@ describe('state_helpers', () => { orderDirection: 'asc', size: 5, }, + indexPatternId: '1', }; const newColumn: AvgIndexPatternColumn = { @@ -250,27 +304,39 @@ describe('state_helpers', () => { // Private operationType: 'avg', sourceField: 'bytes', + indexPatternId: '1', }; const state: IndexPatternPrivateState = { indexPatterns: {}, currentIndexPatternId: '1', - columnOrder: ['col1', 'col2'], - columns: { - col1: termsColumn, - col2: { - operationId: 'op1', - label: 'Count', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'count', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: termsColumn, + col2: { + operationId: 'op1', + label: 'Count', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'count', + indexPatternId: '1', + }, + }, }, }, }; - changeColumn(state, 'col2', newColumn); + changeColumn({ + state, + layerId: 'first', + columnId: 'col2', + newColumn, + }); expect(operationDefinitionMap.terms.onOtherColumnChanged).toHaveBeenCalledWith(termsColumn, { col1: termsColumn, @@ -299,6 +365,7 @@ describe('state_helpers', () => { params: { interval: 'h', }, + indexPatternId: '1', }, }) ).toEqual(['col1']); @@ -323,6 +390,7 @@ describe('state_helpers', () => { }, orderDirection: 'asc', }, + indexPatternId: '1', }, col2: { operationId: 'op2', @@ -333,6 +401,7 @@ describe('state_helpers', () => { // Private operationType: 'avg', sourceField: 'bytes', + indexPatternId: '1', }, col3: { operationId: 'op3', @@ -346,6 +415,7 @@ describe('state_helpers', () => { params: { interval: '1d', }, + indexPatternId: '1', }, }) ).toEqual(['col1', 'col3', 'col2']); @@ -371,6 +441,7 @@ describe('state_helpers', () => { orderDirection: 'asc', }, suggestedPriority: 2, + indexPatternId: '1', }, col2: { operationId: 'op2', @@ -382,6 +453,7 @@ describe('state_helpers', () => { operationType: 'avg', sourceField: 'bytes', suggestedPriority: 0, + indexPatternId: '1', }, col3: { operationId: 'op3', @@ -396,6 +468,7 @@ describe('state_helpers', () => { params: { interval: '1d', }, + indexPatternId: '1', }, }) ).toEqual(['col3', 'col1', 'col2']); diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts index 53d2a0cc08ad1..60cd67535a3c2 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts @@ -26,9 +26,13 @@ describe('LensStore', () => { const doc = await store.save({ title: 'Hello', visualizationType: 'bar', - datasourceType: 'indexpattern', + // datasourceType: 'indexpattern', + activeDatasourceId: 'indexpattern', state: { - datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, + // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, }); @@ -37,20 +41,27 @@ describe('LensStore', () => { id: 'FOO', title: 'Hello', visualizationType: 'bar', - datasourceType: 'indexpattern', + // datasourceType: 'indexpattern', + activeDatasourceId: 'indexPattern', state: { - datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, + // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, }); expect(client.create).toHaveBeenCalledTimes(1); expect(client.create).toHaveBeenCalledWith('lens', { - datasourceType: 'indexpattern', + // datasourceType: 'indexpattern', title: 'Hello', visualizationType: 'bar', state: JSON.stringify({ - datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, + // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }), }); @@ -62,9 +73,13 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - datasourceType: 'indexpattern', + // datasourceType: 'indexpattern', + activeDatasourceId: 'indexpattern', state: { - datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + }, visualization: { gear: ['staff', 'pointy hat'] }, }, }); @@ -75,7 +90,10 @@ describe('LensStore', () => { visualizationType: 'line', datasourceType: 'indexpattern', state: { - datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: 'lotr' }, + }, visualization: { gear: ['staff', 'pointy hat'] }, }, }); @@ -86,7 +104,10 @@ describe('LensStore', () => { visualizationType: 'line', datasourceType: 'indexpattern', state: JSON.stringify({ - datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, + datasourceStates: { + indexpattern: { type: 'index_pattern', indexPattern: 'lotr' }, + }, visualization: { gear: ['staff', 'pointy hat'] }, }), }); diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index 930ee36ea3729..b87d3e0f03168 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -10,10 +10,13 @@ export interface Document { id?: string; type?: string; visualizationType: string | null; - datasourceType: string | null; + activeDatasourceId: string | null; + // datasourceType: string | null; title: string; + // The state is saved as a JSON string for now state: { - datasource: unknown; + datasourceStates: Record; + // datasource: unknown; visualization: unknown; }; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 07547b4f64c60..34223175d1aaf 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -190,7 +190,6 @@ export interface Visualization { renderConfigPanel: (domElement: Element, props: VisualizationProps) => void; - // toExpression: (state: T, datasource: DatasourcePublicAPI) => Ast | string | null; toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; // The frame will call this function on all visualizations when the table changes, or when diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index 0c072d1fe5bf2..fb5fdfc183dea 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -15,7 +15,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` id="x" position="bottom" showGridLines={false} - title="C" + title="X" /> - { moveColumnTo: () => {}, removeColumnInTableSpec: () => [], renderDimensionPanel: () => {}, + renderLayerPanel: () => {}, }; } function testState(): State { return { legend: { isVisible: true, position: Position.Right }, - seriesType: 'bar', - splitSeriesAccessors: [], - x: { - accessor: 'foo', - position: Position.Bottom, - showGridlines: true, - title: 'X', - }, - y: { - accessors: ['bar'], - position: Position.Left, - showGridlines: true, - title: 'Y', - }, + layers: [ + { + seriesType: 'bar', + layerId: 'first', + datasourceId: '', + splitSeriesAccessors: [], + xAccessor: 'foo', + position: Position.Bottom, + showGridlines: true, + title: 'X', + accessors: ['bar'], + labels: [''], + }, + ], }; } @@ -62,7 +64,8 @@ describe('XYConfigPanel', () => { const component = mount( {}} state={testState()} /> @@ -91,12 +94,14 @@ describe('XYConfigPanel', () => { }); test('enables all stacked chart types when there is a split series', () => { + const state = testState(); const component = mount( {}} - state={{ ...testState(), splitSeriesAccessors: ['c'] }} + state={{ ...state, layers: [{ ...state.layers[0], splitSeriesAccessors: ['c'] }] }} /> ); @@ -115,9 +120,11 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -156,7 +163,8 @@ describe('XYConfigPanel', () => { const component = mount( @@ -182,7 +190,8 @@ describe('XYConfigPanel', () => { const component = mount( @@ -214,7 +223,8 @@ describe('XYConfigPanel', () => { const component = mount( @@ -235,17 +245,18 @@ describe('XYConfigPanel', () => { }); test('the x dimension panel accepts any operations', () => { - const datasource = { - ...mockDatasource(), - renderDimensionPanel: jest.fn(), - }; + // const datasource = { + // ...mockDatasource(), + // renderDimensionPanel: jest.fn(), + // }; const state = testState(); const component = mount( ); @@ -275,9 +286,11 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -301,7 +314,8 @@ describe('XYConfigPanel', () => { const component = mount( @@ -322,17 +336,19 @@ describe('XYConfigPanel', () => { }); test('the y dimension panel accepts numeric operations', () => { - const datasource = { - ...mockDatasource(), - renderDimensionPanel: jest.fn(), - }; + // const datasource = { + // ...mockDatasource(), + // renderDimensionPanel: jest.fn(), + // }; const state = testState(); const component = mount( ); @@ -355,19 +371,19 @@ describe('XYConfigPanel', () => { }); test('allows removal of y dimensions', () => { - const removeColumnInTableSpec = jest.fn(); - const datasource = { - ...mockDatasource(), - removeColumnInTableSpec, + const frame = createMockFramePublicAPI(); + const datasourceMock = createMockDatasource().publicAPIMock; + frame.datasourceLayers = { + first: datasourceMock, }; const setState = jest.fn(); const state = testState(); const component = mount( ); @@ -377,8 +393,8 @@ describe('XYConfigPanel', () => { expect(setState.mock.calls[0][0]).toMatchObject({ y: { accessors: ['a', 'c'] }, }); - expect(removeColumnInTableSpec).toHaveBeenCalledTimes(1); - expect(removeColumnInTableSpec).toHaveBeenCalledWith('b'); + expect(datasourceMock.removeColumnInTableSpec).toHaveBeenCalledTimes(1); + expect(datasourceMock.removeColumnInTableSpec).toHaveBeenCalledWith('b'); }); test('allows adding y dimensions', () => { @@ -388,9 +404,11 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -409,9 +427,11 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -423,30 +443,30 @@ describe('XYConfigPanel', () => { }); }); - test('allows toggling the y axis gridlines', () => { - const toggleYGridlines = (showGridlines: boolean) => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_yShowGridlines').onChange as Function)(); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(toggleYGridlines(true)).toMatchObject({ - y: { showGridlines: false }, - }); - expect(toggleYGridlines(false)).toMatchObject({ - y: { showGridlines: true }, - }); - }); + // test('allows toggling the y axis gridlines', () => { + // const toggleYGridlines = (showGridlines: boolean) => { + // const setState = jest.fn(); + // const state = testState(); + // const component = mount( + // + // ); + + // (testSubj(component, 'lnsXY_yShowGridlines').onChange as Function)(); + + // expect(setState).toHaveBeenCalledTimes(1); + // return setState.mock.calls[0][0]; + // }; + + // expect(toggleYGridlines(true)).toMatchObject({ + // y: { showGridlines: false }, + // }); + // expect(toggleYGridlines(false)).toMatchObject({ + // y: { showGridlines: true }, + // }); + // }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index 7e1124a0213d4..fc8014b118d06 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -9,35 +9,59 @@ import { xyChart, XYChart } from './xy_expression'; import { KibanaDatatable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; -import { XYArgs, LegendConfig, legendConfig, XConfig, xConfig, YConfig, yConfig } from './types'; +import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerConfig } from './types'; function sampleArgs() { const data: KibanaDatatable = { type: 'kibana_datatable', - columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], - rows: [{ a: 1, b: 2, c: 3 }, { a: 1, b: 5, c: 4 }], + columns: [{ id: 'first', name: '' }], + rows: [ + { + first: { + type: 'kibana_datatable', + columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], + rows: [{ a: 1, b: 2, c: 3 }, { a: 1, b: 5, c: 4 }], + }, + }, + ], }; const args: XYArgs = { - seriesType: 'line', + // seriesType: 'line', legend: { isVisible: false, position: Position.Top, }, - y: { - accessors: ['a', 'b'], - labels: ['Label A', 'Label B'], - position: Position.Left, - showGridlines: false, - title: 'A and B', - }, - x: { - accessor: 'c', - position: Position.Bottom, - showGridlines: false, - title: 'C', - }, - splitSeriesAccessors: [], + layers: [ + { + layerId: 'first', + datasourceId: 'indexpattern', + seriesType: 'line', + + xAccessor: 'c', + accessors: ['a', 'b'], + labels: ['Label A', 'Label B'], + position: Position.Left, + showGridlines: false, + title: 'A and B', + + splitSeriesAccessors: [], + }, + ], + // y: { + // accessors: ['a', 'b'], + // labels: ['Label A', 'Label B'], + // position: Position.Left, + // showGridlines: false, + // title: 'A and B', + // }, + // x: { + // accessor: 'c', + // position: Position.Bottom, + // showGridlines: false, + // title: 'C', + // }, + // splitSeriesAccessors: [], }; return { data, args }; @@ -57,31 +81,22 @@ describe('xy_expression', () => { }); }); - test('xConfig produces the correct arguments', () => { - const args: XConfig = { - accessor: 'foo', - position: Position.Right, - showGridlines: true, - title: 'Foooo!', - }; - - expect(xConfig.fn(null, args, {})).toEqual({ - type: 'lens_xy_xConfig', - ...args, - }); - }); - - test('yConfig produces the correct arguments', () => { - const args: YConfig = { - accessors: ['bar'], - labels: [''], - position: Position.Bottom, - showGridlines: true, - title: 'Barrrrrr!', + test('layerConfig produces the correct arguments', () => { + const args: LayerConfig = { + layerId: 'first', + datasourceId: 'indexpattern', + seriesType: 'line', + xAccessor: 'c', + accessors: ['a', 'b'], + labels: ['Label A', 'Label B'], + position: Position.Left, + showGridlines: false, + title: 'A and B', + splitSeriesAccessors: [], }; - expect(yConfig.fn(null, args, {})).toEqual({ - type: 'lens_xy_yConfig', + expect(layerConfig.fn(null, args, {})).toEqual({ + type: 'lens_xy_layerConfig', ...args, }); }); @@ -103,7 +118,12 @@ describe('xy_expression', () => { test('it renders line', () => { const { data, args } = sampleArgs(); - const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); expect(component.find(LineSeries)).toHaveLength(1); }); @@ -111,7 +131,13 @@ describe('xy_expression', () => { test('it renders bar', () => { const { data, args } = sampleArgs(); - const component = shallow(); + // const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); expect(component.find(BarSeries)).toHaveLength(1); }); @@ -119,7 +145,13 @@ describe('xy_expression', () => { test('it renders area', () => { const { data, args } = sampleArgs(); - const component = shallow(); + // const component = shallow(); + const component = shallow( + + ); expect(component).toMatchSnapshot(); expect(component.find(AreaSeries)).toHaveLength(1); }); @@ -127,8 +159,14 @@ describe('xy_expression', () => { test('it renders horizontal bar', () => { const { data, args } = sampleArgs(); + // const component = shallow( + // + // ); const component = shallow( - + ); expect(component).toMatchSnapshot(); expect(component.find(BarSeries)).toHaveLength(1); @@ -137,8 +175,14 @@ describe('xy_expression', () => { test('it renders stacked bar', () => { const { data, args } = sampleArgs(); + // const component = shallow( + // + // ); const component = shallow( - + ); expect(component).toMatchSnapshot(); expect(component.find(BarSeries)).toHaveLength(1); @@ -148,8 +192,14 @@ describe('xy_expression', () => { test('it renders stacked area', () => { const { data, args } = sampleArgs(); + // const component = shallow( + // + // ); const component = shallow( - + ); expect(component).toMatchSnapshot(); expect(component.find(AreaSeries)).toHaveLength(1); @@ -159,8 +209,14 @@ describe('xy_expression', () => { test('it renders stacked horizontal bar', () => { const { data, args } = sampleArgs(); + // const component = shallow( + // + // ); const component = shallow( - + ); expect(component).toMatchSnapshot(); expect(component.find(BarSeries)).toHaveLength(1); @@ -171,7 +227,13 @@ describe('xy_expression', () => { test('it remaps rows based on the labels', () => { const { data, args } = sampleArgs(); - const chart = shallow(); + // const chart = shallow(); + const chart = shallow( + + ); const barSeries = chart.find(BarSeries); expect(barSeries.prop('yAccessors')).toEqual(['Label A', 'Label B']); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index 8e1e2d274c940..f46a1733431d0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -52,14 +52,14 @@ describe('xy_suggestions', () => { // Helper that plucks out the important part of a suggestion for // most test assertions function suggestionSubset(suggestion: VisualizationSuggestion) { - const { seriesType, splitSeriesAccessors, x, y } = suggestion.state; - - return { - seriesType, - splitSeriesAccessors, - x: x.accessor, - y: y.accessors, - }; + return suggestion.state.layers.map( + ({ seriesType, splitSeriesAccessors, xAccessor, accessors }) => ({ + seriesType, + splitSeriesAccessors, + x: xAccessor, + y: accessors, + }) + ); } test('ignores invalid combinations', () => { @@ -103,16 +103,18 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Object { - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "aaa", - ], - "x": "date", - "y": Array [ - "bytes", - ], - } + Array [ + Object { + "seriesType": "bar", + "splitSeriesAccessors": Array [ + "aaa", + ], + "x": "date", + "y": Array [ + "bytes", + ], + }, + ] `); }); @@ -129,18 +131,20 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Object { - "seriesType": "line", - "splitSeriesAccessors": Array [ - "product", - ], - "x": "date", - "y": Array [ - "price", - "quantity", - ], - } - `); + Array [ + Object { + "seriesType": "line", + "splitSeriesAccessors": Array [ + "product", + ], + "x": "date", + "y": Array [ + "price", + "quantity", + ], + }, + ] + `); }); test('supports multiple suggestions', () => { @@ -163,26 +167,30 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect([suggestionSubset(s1), suggestionSubset(s2)]).toMatchInlineSnapshot(` Array [ - Object { - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "bbb", - ], - "x": "date", - "y": Array [ - "price", - ], - }, - Object { - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "ccc", - ], - "x": "country", - "y": Array [ - "count", - ], - }, + Array [ + Object { + "seriesType": "bar", + "splitSeriesAccessors": Array [ + "bbb", + ], + "x": "date", + "y": Array [ + "price", + ], + }, + ], + Array [ + Object { + "seriesType": "bar", + "splitSeriesAccessors": Array [ + "ccc", + ], + "x": "country", + "y": Array [ + "count", + ], + }, + ], ] `); }); @@ -200,16 +208,18 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Object { - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "ddd", - ], - "x": "quantity", - "y": Array [ - "price", - ], - } + Array [ + Object { + "seriesType": "bar", + "splitSeriesAccessors": Array [ + "ddd", + ], + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] `); }); @@ -237,16 +247,18 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Object { - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "eee", - ], - "x": "mybool", - "y": Array [ - "num votes", - ], - } + Array [ + Object { + "seriesType": "bar", + "splitSeriesAccessors": Array [ + "eee", + ], + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] `); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index 914986a7bd92c..59589907b8278 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -94,6 +94,7 @@ function getSuggestion( layers: [ { layerId: 'first', + datasourceId: '', xAccessor: xValue.columnId, seriesType: splitBy && isDate ? 'line' : 'bar', splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [generateId()], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index a2eeffaab4a7b..ad06a447eba69 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -9,7 +9,7 @@ import { Position } from '@elastic/charts'; import { Ast } from '@kbn/interpreter/target/common'; import { Operation } from '../types'; import { State } from './types'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; jest.mock('../id_generator'); @@ -17,20 +17,20 @@ jest.mock('../id_generator'); function exampleState(): State { return { legend: { position: Position.Bottom, isVisible: true }, - seriesType: 'area', - splitSeriesAccessors: [], - x: { - accessor: 'a', - position: Position.Bottom, - showGridlines: true, - title: 'Baz', - }, - y: { - accessors: ['b', 'c'], - position: Position.Left, - showGridlines: true, - title: 'Bar', - }, + layers: [ + { + layerId: 'first', + datasourceId: '', + labels: [''], + seriesType: 'area', + splitSeriesAccessors: [], + position: Position.Bottom, + showGridlines: true, + title: 'Baz', + xAccessor: 'a', + accessors: ['b', 'c'], + }, + ], }; } @@ -41,12 +41,13 @@ describe('xy_visualization', () => { .mockReturnValueOnce('test-id1') .mockReturnValueOnce('test-id2') .mockReturnValue('test-id3'); - const mockDatasource = createMockDatasource(); - const initialState = xyVisualization.initialize(mockDatasource.publicAPIMock); + const mockFrame = createMockFramePublicAPI(); + const initialState = xyVisualization.initialize(mockFrame); - expect(initialState.x.accessor).toBeDefined(); - expect(initialState.y.accessors[0]).toBeDefined(); - expect(initialState.x.accessor).not.toEqual(initialState.y.accessors[0]); + expect(initialState.layers).toHaveLength(1); + expect(initialState.layers[0].xAccessor).toBeDefined(); + expect(initialState.layers[0].accessors[0]).toBeDefined(); + expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); expect(initialState).toMatchInlineSnapshot(` Object { @@ -78,9 +79,9 @@ describe('xy_visualization', () => { }); it('loads from persisted state', () => { - expect( - xyVisualization.initialize(createMockDatasource().publicAPIMock, exampleState()) - ).toEqual(exampleState()); + expect(xyVisualization.initialize(createMockFramePublicAPI(), exampleState())).toEqual( + exampleState() + ); }); }); @@ -93,7 +94,7 @@ describe('xy_visualization', () => { describe('#toExpression', () => { it('should map to a valid AST', () => { expect( - xyVisualization.toExpression(exampleState(), createMockDatasource().publicAPIMock) + xyVisualization.toExpression(exampleState(), createMockFramePublicAPI()) ).toMatchSnapshot(); }); @@ -110,7 +111,7 @@ describe('xy_visualization', () => { const expression = xyVisualization.toExpression( exampleState(), - mockDatasource.publicAPIMock + createMockFramePublicAPI() )! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledTimes(2); From 998cf81cbd81bfed17855a8a6c5f9f70c079ec7c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 18 Jul 2019 10:17:16 +0200 Subject: [PATCH 13/67] fix test failures --- .../editor_frame/editor_frame.test.tsx | 41 ++++++++++------- .../editor_frame/save.test.ts | 6 ++- .../editor_frame/workspace_panel.test.tsx | 45 +++++++++++-------- .../lens/public/editor_frame_plugin/mocks.tsx | 6 +++ .../editor_frame_plugin/plugin.test.tsx | 4 +- .../persistence/saved_object_store.test.ts | 2 + 6 files changed, 68 insertions(+), 36 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index d3f7666d0ac5d..2ec4533fb27e5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -266,22 +266,31 @@ describe('editor_frame', () => { instance.update(); expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "filters": Array [], + "query": Array [], + "timeRange": Array [], + }, + "function": "kibana_context", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index 8965ab456746d..e2ae5081b5ca6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -96,7 +96,7 @@ describe('save editor frame state', () => { const visualization = createMockVisualization(); visualization.getPersistableState.mockImplementation(state => ({ - stuff: `${state}_vis_persisted`, + things: `${state}_vis_persisted`, })); await save({ ...saveArgs, @@ -114,8 +114,12 @@ describe('save editor frame state', () => { expect(store.save).toHaveBeenCalledWith({ datasourceType: '1', id: undefined, + expression: '', state: { datasource: { stuff: '2_datsource_persisted' }, + datasourceMetaData: { + filterableIndexPatterns: [], + }, visualization: { things: '4_vis_persisted' }, }, title: 'bbb', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index dadce6d856bd3..0fcb94cfb6614 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -121,22 +121,31 @@ describe('workspace_panel', () => { ); expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "filters": Array [], + "query": Array [], + "timeRange": Array [], + }, + "function": "kibana_context", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); }); describe('expression failures', () => { @@ -159,7 +168,7 @@ Object { /> ); - expect(instance.find('[data-test-subj="expression-failure"]')).toHaveLength(1); + expect(instance.find('EuiFlexItem[data-test-subj="expression-failure"]')).toHaveLength(1); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); @@ -192,7 +201,7 @@ Object { instance.update(); - expect(instance.find('[data-test-subj="expression-failure"]')).toHaveLength(1); + expect(instance.find('EuiFlexItem[data-test-subj="expression-failure"]')).toHaveLength(1); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 9cfc2b95bf6b2..dda536d9e50e8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -70,5 +70,11 @@ export function createMockDependencies() { run: jest.fn(_ => Promise.resolve({ type: 'render', as: 'test', value: undefined })), }, }, + embeddables: { + addEmbeddableFactory: jest.fn(), + }, + chrome: { + getSavedObjectsClient: () => {}, + }, } as unknown) as MockedDependencies; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index bff0a1ddc918c..b4c3e734547fe 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -13,7 +13,7 @@ import { createMockVisualization, } from './mocks'; import { SavedObjectStore, Document } from '../persistence'; -import { shallow, mount } from 'enzyme'; +import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; jest.mock('ui/chrome', () => ({ getSavedObjectsClient: jest.fn(), @@ -21,6 +21,8 @@ jest.mock('ui/chrome', () => ({ // mock away actual data plugin to prevent all of it being loaded jest.mock('../../../../../../src/legacy/core_plugins/data/public/setup', () => {}); +jest.mock('../../../../../../src/legacy/core_plugins/embeddable_api/public', () => {}); +jest.mock('./embeddable/embeddable_factory', () => ({ LensEmbeddableFactory: class Mock {} })); function mockStore(): SavedObjectStore { return { diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts index ce9a3dd9d13aa..afbede21e1bc1 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts @@ -40,7 +40,9 @@ describe('LensStore', () => { title: 'Hello', visualizationType: 'bar', datasourceType: 'indexpattern', + expression: '', state: { + datasourceMetaData: { filterableIndexPatterns: [] }, datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, From 88466b24bc04dd29bf6cf3e052d8db706606d448 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 18 Jul 2019 11:55:39 +0200 Subject: [PATCH 14/67] rename stuff and clean up --- .../embeddable/embeddable.test.tsx | 184 ++++++++++++++++++ .../embeddable/embeddable.tsx | 51 +++-- .../embeddable/embeddable_factory.ts | 25 ++- .../embeddable/expression_wrapper.tsx | 14 +- .../editor_frame_plugin/plugin.test.tsx | 2 +- .../public/editor_frame_plugin/plugin.tsx | 4 +- 6 files changed, 243 insertions(+), 37 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx new file mode 100644 index 0000000000000..4564b7c1f8fb5 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx @@ -0,0 +1,184 @@ +/* + * 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 { Embeddable } from './embeddable'; +import { TimeRange } from 'ui/timefilter/time_history'; +import { Query } from 'src/legacy/core_plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { Document } from '../../persistence'; +import { act } from 'react-dom/test-utils'; + +const savedVis: Document = { + datasourceType: '', + expression: 'my | expression', + state: { + visualization: {}, + datasource: {}, + datasourceMetaData: { + filterableIndexPatterns: [], + }, + }, + title: 'My title', + visualizationType: '', +}; + +describe('embeddable', () => { + it('should render expression with expression renderer', () => { + const mountpoint = document.createElement('div'); + const expressionRenderer = jest.fn(_props => null); + const embeddable = new Embeddable( + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123' } + ); + embeddable.render(mountpoint); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + expect(expressionRenderer.mock.calls[0][0]!.expression).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "filters": Array [], + "query": Array [], + "timeRange": Array [], + }, + "function": "kibana_context", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "my", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "expression", + "type": "function", + }, + ], + "type": "expression", + } + `); + mountpoint.remove(); + }); + + it('should display error if expression renderering fails', () => { + const mountpoint = document.createElement('div'); + const expressionRenderer = jest.fn(_props => null); + + const embeddable = new Embeddable( + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123' } + ); + embeddable.render(mountpoint); + + act(() => { + expressionRenderer.mock.calls[0][0]!.onRenderFailure({}); + }); + + expect(mountpoint.innerHTML).toContain("Visualization couldn't be displayed"); + + mountpoint.remove(); + }); + + it('should prepend context to the expression chain', () => { + const mountpoint = document.createElement('div'); + const expressionRenderer = jest.fn(_props => null); + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + + const embeddable = new Embeddable( + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123', timeRange, query, filters } + ); + embeddable.render(mountpoint); + + expect(expressionRenderer.mock.calls[0][0]!.expression.chain[0].arguments).toEqual({ + timeRange: [JSON.stringify(timeRange)], + query: [JSON.stringify(query)], + filters: [JSON.stringify(filters)], + }); + mountpoint.remove(); + }); + + it('should re-render if new input is pushed', () => { + const mountpoint = document.createElement('div'); + const expressionRenderer = jest.fn(_props => null); + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; + + const embeddable = new Embeddable( + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123' } + ); + embeddable.render(mountpoint); + + embeddable.updateInput({ + timeRange, + query, + filters, + }); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); + + expect(expressionRenderer.mock.calls[1][0]!.expression.chain[0].arguments).toEqual({ + timeRange: [JSON.stringify(timeRange)], + query: [JSON.stringify(query)], + filters: [JSON.stringify(filters)], + }); + mountpoint.remove(); + }); + + it('should not re-render if only change is in disabled filter', () => { + const mountpoint = document.createElement('div'); + const expressionRenderer = jest.fn(_props => null); + const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; + const query: Query = { language: 'kquery', query: '' }; + const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; + + const embeddable = new Embeddable( + expressionRenderer, + { + editUrl: '', + editable: true, + savedVis, + }, + { id: '123', timeRange, query, filters } + ); + embeddable.render(mountpoint); + + embeddable.updateInput({ + timeRange, + query, + filters: [{ meta: { alias: 'test', negate: true, disabled: true } }], + }); + + expect(expressionRenderer).toHaveBeenCalledTimes(1); + mountpoint.remove(); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx index d7cac994fd26a..3d623891ce32c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx @@ -9,21 +9,18 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { TimeRange } from 'ui/timefilter/time_history'; -import { Query, StaticIndexPattern } from 'src/legacy/core_plugins/data/public'; +import { Query, StaticIndexPattern, ExpressionRenderer } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { Subscription } from 'rxjs'; import { - Embeddable, + Embeddable as AbstractEmbeddable, EmbeddableOutput, IContainer, EmbeddableInput, } from '../../../../../../../src/legacy/core_plugins/embeddable_api/public/index'; import { Document, DOC_TYPE } from '../../persistence'; -import { data } from '../../../../../../../src/legacy/core_plugins/data/public/setup'; import { ExpressionWrapper } from './expression_wrapper'; -const ExpressionRendererComponent = data.expressions.ExpressionRenderer; - export interface LensEmbeddableConfiguration { savedVis: Document; editUrl: string; @@ -41,18 +38,23 @@ export interface LensEmbeddableOutput extends EmbeddableOutput { indexPatterns?: StaticIndexPattern[]; } -export class LensEmbeddable extends Embeddable { +export class Embeddable extends AbstractEmbeddable { type = DOC_TYPE; + private expressionRenderer: ExpressionRenderer; private savedVis: Document; private domNode: HTMLElement | Element | undefined; private subscription: Subscription; - private prevTimeRange?: TimeRange; - private prevQuery?: Query; - private prevFilters?: Filter[]; + private currentContext: { + timeRange?: TimeRange; + query?: Query; + filters?: Filter[]; + lastReloadRequestTime?: number; + } = {}; constructor( + expressionRenderer: ExpressionRenderer, { savedVis, editUrl, editable, indexPatterns }: LensEmbeddableConfiguration, initialInput: LensEmbeddableInput, parent?: IContainer @@ -69,6 +71,8 @@ export class LensEmbeddable extends Embeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); @@ -79,13 +83,17 @@ export class LensEmbeddable extends Embeddable !filter.meta.disabled) : undefined; if ( - !_.isEqual(containerState.timeRange, this.prevTimeRange) || - !_.isEqual(containerState.query, this.prevQuery) || - !_.isEqual(cleanedFilters, this.prevFilters) + !_.isEqual(containerState.timeRange, this.currentContext.timeRange) || + !_.isEqual(containerState.query, this.currentContext.query) || + !_.isEqual(cleanedFilters, this.currentContext.filters) ) { - this.prevTimeRange = containerState.timeRange; - this.prevQuery = containerState.query; - this.prevFilters = cleanedFilters; + this.currentContext = { + timeRange: containerState.timeRange, + query: containerState.query, + lastReloadRequestTime: this.currentContext.lastReloadRequestTime, + filters: cleanedFilters, + }; + if (this.domNode) { this.render(this.domNode); } @@ -101,13 +109,9 @@ export class LensEmbeddable extends Embeddable, domNode ); @@ -124,6 +128,11 @@ export class LensEmbeddable extends Embeddable + Boolean(indexPattern) + ); - return new LensEmbeddable( + return new Embeddable( + this.expressionRenderer, { savedVis, editUrl: this.chrome.addBasePath(getEditPath(savedObjectId)), diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx index e6c5e84b69083..7c883f3d7a0e5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx @@ -5,9 +5,9 @@ */ import _ from 'lodash'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; -import { I18nContext } from 'ui/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; import { TimeRange } from 'ui/timefilter/time_history'; @@ -23,6 +23,7 @@ export interface ExpressionWrapperProps { timeRange?: TimeRange; query?: Query; filters?: Filter[]; + lastReloadRequestTime?: number; }; } @@ -32,7 +33,10 @@ export function ExpressionWrapper({ context, }: ExpressionWrapperProps) { const [expressionError, setExpressionError] = useState(undefined); - const contextualizedExpression = prependKibanaContext(expression, context); + const contextualizedExpression = useMemo(() => prependKibanaContext(expression, context), [ + expression, + context, + ]); useEffect(() => { // reset expression error if component attempts to run it again if (expressionError) { @@ -40,7 +44,7 @@ export function ExpressionWrapper({ } }, [contextualizedExpression]); return ( - + {contextualizedExpression === null || expressionError ? ( @@ -64,6 +68,6 @@ export function ExpressionWrapper({ }} /> )} - + ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index b4c3e734547fe..72445c8b17361 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -22,7 +22,7 @@ jest.mock('ui/chrome', () => ({ // mock away actual data plugin to prevent all of it being loaded jest.mock('../../../../../../src/legacy/core_plugins/data/public/setup', () => {}); jest.mock('../../../../../../src/legacy/core_plugins/embeddable_api/public', () => {}); -jest.mock('./embeddable/embeddable_factory', () => ({ LensEmbeddableFactory: class Mock {} })); +jest.mock('./embeddable/embeddable_factory', () => ({ EmbeddableFactory: class Mock {} })); function mockStore(): SavedObjectStore { return { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 533cb0289d951..3dd5e7f132740 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -29,7 +29,7 @@ import { import { EditorFrame } from './editor_frame'; import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; import { InitializableComponent } from './initializable_component'; -import { LensEmbeddableFactory } from './embeddable/embeddable_factory'; +import { EmbeddableFactory } from './embeddable/embeddable_factory'; export interface EditorFrameSetupPlugins { data: DataSetup; @@ -124,7 +124,7 @@ export class EditorFramePlugin { this.ExpressionRenderer = plugins.data.expressions.ExpressionRenderer; this.chrome = plugins.chrome; plugins.embeddables.addEmbeddableFactory( - new LensEmbeddableFactory(plugins.chrome, plugins.data.indexPatterns) + new EmbeddableFactory(plugins.chrome, this.ExpressionRenderer, plugins.data.indexPatterns) ); return { From b4e096346568ab51c9975e109b5af37103328a8c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 18 Jul 2019 12:09:39 +0200 Subject: [PATCH 15/67] remove unrelated changes --- .../public/visualize/loader/pipeline_helpers/run_pipeline.ts | 1 - .../lens/public/xy_visualization_plugin/xy_expression.tsx | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts index c897bd4ba7c6c..1551ab755ea5b 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts @@ -37,7 +37,6 @@ export const runPipeline = async ( context: object, handlers: RunPipelineHandlers ) => { - console.log(expression); const ast = fromExpression(expression); const { interpreter } = await getInterpreter(); const pipelineResponse = await interpreter.interpretAst(ast, context, handlers); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 2da65da2c5e10..2392114e26999 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -102,9 +102,6 @@ export const xyChartRenderer: RenderFunction = { }; export function XYChart({ data, args }: XYChartProps) { - if (data.rows.length === 0) { - return

No data found

; - } const { legend, x, y, splitSeriesAccessors, seriesType } = args; // TODO: Stop mapping data once elastic-charts allows axis naming // https://github.com/elastic/elastic-charts/issues/245 From 6b2454bf6dfc180007d7486481f0f79ea5c83fc8 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 18 Jul 2019 15:08:37 -0400 Subject: [PATCH 16/67] Ensure that new xy layer state has a split column --- .../xy_config_panel.tsx | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 193e8a84c3b81..bc572df37198d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -18,7 +18,7 @@ import { EuiPanel, IconType, } from '@elastic/eui'; -import { State, SeriesType } from './types'; +import { State, SeriesType, LayerConfig } from './types'; import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; import { MultiColumnEditor } from './multi_column_editor'; @@ -111,6 +111,21 @@ function updateLayer(state: State, layer: UnwrapArray, index: n }; } +function newLayerState(layerId: string): LayerConfig { + return { + layerId, + datasourceId: 'indexpattern', // TODO: Don't hard code + xAccessor: generateId(), + seriesType: 'bar_stacked', + accessors: [generateId()], + title: '', + showGridlines: false, + position: Position.Left, + labels: [''], + splitSeriesAccessors: [generateId()], + }; +} + export function XYConfigPanel(props: VisualizationProps) { const { state, setState, frame } = props; @@ -491,25 +506,9 @@ export function XYConfigPanel(props: VisualizationProps) { { - const newId = frame.addNewLayer(); - setState({ ...state, - layers: [ - ...state.layers, - { - layerId: newId, - datasourceId: 'indexpattern', // TODO: Don't hard code - xAccessor: generateId(), - seriesType: 'bar_stacked', - accessors: [generateId()], - title: '', - showGridlines: false, - position: Position.Left, - labels: [''], - splitSeriesAccessors: [], - }, - ], + layers: [...state.layers, newLayerState(frame.addNewLayer())], }); }} iconType="plusInCircle" From 94c97d75fb361925bb93692535fe98764f61bb61 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 18 Jul 2019 15:22:45 -0400 Subject: [PATCH 17/67] Make the "add" y / split the trailing accessor --- .../dimension_panel/_summary.scss | 3 +- .../dimension_panel/popover_editor.tsx | 32 +++++++++++-------- .../multi_column_editor.tsx | 20 ++++++------ .../xy_config_panel.tsx | 11 ++++--- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss index f16b2acc8ba03..b7f4e22ee251d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/_summary.scss @@ -16,6 +16,7 @@ .lnsConfigPanel__summaryPopoverAnchor { max-width: 100%; + display: block; } .lnsConfigPanel__summaryIcon { @@ -23,7 +24,7 @@ } .lnsConfigPanel__summaryLink { - max-width: 100%; + width: 100%; display: flex; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index 0ab25e956deca..f1d9ebf155a31 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -17,6 +17,7 @@ import { EuiFormRow, EuiFieldText, EuiLink, + EuiButton, } from '@elastic/eui'; import classNames from 'classnames'; import { IndexPatternColumn, OperationType } from '../indexpattern'; @@ -137,19 +138,24 @@ export function PopoverEditor(props: PopoverEditorProps) { className="lnsConfigPanel__summaryPopover" anchorClassName="lnsConfigPanel__summaryPopoverAnchor" button={ - { - setPopoverOpen(true); - }} - data-test-subj="indexPattern-configure-dimension" - > - {selectedColumn - ? selectedColumn.label - : i18n.translate('xpack.lens.indexPattern.configureDimensionLabel', { - defaultMessage: 'Configure dimension', - })} - + selectedColumn ? ( + { + setPopoverOpen(true); + }} + data-test-subj="indexPattern-configure-dimension" + > + {selectedColumn.label} + + ) : ( + setPopoverOpen(true)} + iconType="plusInCircle" + /> + ) } isOpen={isPopoverOpen} closePopover={() => { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx index 9a276c9e69307..0a756f05cf427 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { EuiButtonIcon, EuiButton } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../native_renderer'; -import { generateId } from '../id_generator'; import { DatasourcePublicAPI, Operation } from '../types'; import { DragContextState } from '../drag_drop'; @@ -17,7 +16,7 @@ interface Props { datasource: DatasourcePublicAPI; dragDropContext: DragContextState; onRemove: (accessor: string) => void; - onAdd: (accessor: string) => void; + onAdd: () => void; filterOperations: (op: Operation) => boolean; suggestedPriority?: 0 | 1 | 2 | undefined; testSubj: string; @@ -36,6 +35,14 @@ export function MultiColumnEditor({ testSubj, layerId, }: Props) { + const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); + + useEffect(() => { + if (lastOperation !== null) { + setTimeout(onAdd); + } + }, [lastOperation]); + return ( <> {accessors.map((accessor, i) => ( @@ -68,11 +75,6 @@ export function MultiColumnEditor({ )} ))} - onAdd(generateId())} - iconType="plusInCircle" - /> ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index bc572df37198d..d9d038a359ea5 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -384,11 +384,14 @@ export function XYConfigPanel(props: VisualizationProps) { accessors={layer.splitSeriesAccessors} datasource={frame.datasourceLayers[layer.layerId]} dragDropContext={props.dragDropContext} - onAdd={accessor => + onAdd={() => setState( updateLayer( state, - { ...layer, splitSeriesAccessors: [...layer.splitSeriesAccessors, accessor] }, + { + ...layer, + splitSeriesAccessors: [...layer.splitSeriesAccessors, generateId()], + }, index ) ) @@ -450,13 +453,13 @@ export function XYConfigPanel(props: VisualizationProps) { accessors={layer.accessors} datasource={frame.datasourceLayers[layer.layerId]} dragDropContext={props.dragDropContext} - onAdd={accessor => + onAdd={() => setState( updateLayer( state, { ...layer, - accessors: [...layer.accessors, accessor], + accessors: [...layer.accessors, generateId()], }, index ) From cbea4e8f1d55a2755a7894758421179b6d30e922 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 18 Jul 2019 15:25:36 -0400 Subject: [PATCH 18/67] Various fixes for datasource public API and implementation --- .../editor_frame/editor_frame.tsx | 53 +- .../editor_frame/save.test.ts | 4 +- .../editor_frame/state_management.ts | 29 +- .../editor_frame/workspace_panel.test.tsx | 9 +- .../lens/public/editor_frame_plugin/mocks.tsx | 4 +- .../public/editor_frame_plugin/plugin.tsx | 4 +- .../indexpattern_plugin/datapanel.test.tsx | 34 +- .../dimension_panel/dimension_panel.test.tsx | 462 ++++++++++++------ .../indexpattern_plugin/indexpattern.test.tsx | 14 +- .../indexpattern_plugin/indexpattern.tsx | 22 +- .../date_histogram.test.tsx | 2 +- .../filter_ratio.test.tsx | 2 +- .../operation_definitions/terms.test.tsx | 2 + .../public/indexpattern_plugin/operations.ts | 40 +- .../indexpattern_plugin/state_helpers.test.ts | 13 +- .../indexpattern_plugin/to_expression.ts | 124 ----- x-pack/legacy/plugins/lens/public/types.ts | 5 +- 17 files changed, 431 insertions(+), 392 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index b2021c9d4c73d..321f366ade53a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -45,7 +45,7 @@ export function EditorFrame(props: EditorFrameProps) { Object.entries(state.datasourceMap).forEach(([datasourceId, datasource]) => { if (state.datasourceStates[datasourceId].isLoading) { datasource - .initialize(props.doc && props.doc.state.datasource) + .initialize(props.doc && props.doc.state.datasourceStates[datasourceId]) .then(datasourceState => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', @@ -58,24 +58,25 @@ export function EditorFrame(props: EditorFrameProps) { } }, [props.doc, Object.keys(state.datasourceStates), state.activeDatasourceId]); - const datasourceEntries: Array<[string, DatasourcePublicAPI]> = Object.keys( - state.datasourceMap - ).map(key => { - const ds = state.datasourceMap[key]; - return [ - key, - ds.getPublicAPI(state.datasourceStates[key].state, (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - newState, - }); - }), - ]; - }); + // const datasourceEntries: Array<[string, DatasourcePublicAPI]> = Object.keys( + // state.datasourceMap + // ).map(key => { + // const ds = state.datasourceMap[key]; + // return [ + // key, + // ds.getPublicAPI(state.datasourceStates[key].state, (newState: unknown) => { + // dispatch({ + // type: 'UPDATE_DATASOURCE_STATE', + // newState, + // }); + // }), + // ]; + // }); - const layerToDatasourceId: Record = {}; + // const layerToDatasourceId: Record = {}; const datasourceLayers: Record = {}; - datasourceEntries.forEach(([id, publicAPI]) => { + // datasourceEntries.forEach(([id, publicAPI]) => { + Object.keys(state.datasourceMap).forEach(id => { const stateWrapper = state.datasourceStates[id]; if (stateWrapper.isLoading) { return; @@ -84,13 +85,24 @@ export function EditorFrame(props: EditorFrameProps) { const layers = state.datasourceMap[id].getLayers(dsState); layers.forEach(layer => { - layerToDatasourceId[layer] = id; + const publicAPI = state.datasourceMap[id].getPublicAPI( + dsState, + (newState: unknown) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + newState, + }); + }, + layer + ); + + // layerToDatasourceId[layer] = id; datasourceLayers[layer] = publicAPI; }); }); const framePublicAPI: FramePublicAPI = { - layerIdToDatasource: layerToDatasourceId, + // layerIdToDatasource: layerToDatasourceId, datasourceLayers, addNewLayer: () => { const newLayerId = generateId(); @@ -155,7 +167,8 @@ export function EditorFrame(props: EditorFrameProps) { onClick={() => { if (datasource && visualization) { save({ - datasource, + // datasource, + activeDatasources: props.datasourceMap, dispatch, visualization, state, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index c8f79a2682dcc..55faded176e51 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -21,7 +21,7 @@ describe('save editor frame state', () => { datasourceMap: {}, datasourceStates: {}, activeDatasourceId: 'indexpattern', - layerIdToDatasource: {}, + // layerIdToDatasource: {}, saving: false, visualization: { activeId: '2', state: {} }, }, @@ -131,7 +131,7 @@ describe('save editor frame state', () => { }, // datasourceStates: {}, activeDatasourceId: 'indexpattern', - layerIdToDatasource: {}, + // layerIdToDatasource: {}, saving: false, visualization: { activeId: '2', state: {} }, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index 2e302cd7102a9..9d245cdbc88d5 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -20,7 +20,7 @@ export interface EditorFrameState { datasourceMap: Record>; datasourceStates: Record; activeDatasourceId: string | null; - layerIdToDatasource: FramePublicAPI['layerIdToDatasource']; + // layerIdToDatasource: FramePublicAPI['layerIdToDatasource']; } export type Action = @@ -66,10 +66,6 @@ export type Action = type: 'CREATE_LAYER'; newLayerId: string; newDatasourceState: unknown; - } - | { - type: 'UPDATE_LAYERS'; - layerToDatasourceId: Record; }; export const getInitialState = (props: EditorFrameProps): EditorFrameState => { @@ -90,7 +86,7 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { state: null, activeId: props.initialVisualizationId, }, - layerIdToDatasource: {}, + // layerIdToDatasource: {}, }; }; @@ -109,16 +105,16 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, persistedId: action.doc.id, title: action.doc.title, - datasourceStates: action.doc.datasourceType + datasourceStates: action.doc.activeDatasourceId ? { ...state.datasourceStates, - [action.doc.datasourceType]: { + [action.doc.activeDatasourceId]: { isLoading: true, - state: action.doc.state.datasource, + state: action.doc.state.datasourceStates[action.doc.activeDatasourceId], }, } : state.datasourceStates, - activeDatasourceId: action.doc.datasourceType || null, + activeDatasourceId: action.doc.activeDatasourceId || null, visualization: { ...state.visualization, @@ -153,11 +149,6 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta state: action.initialState, }, }; - case 'UPDATE_LAYERS': - return { - ...state, - layerIdToDatasource: action.layerToDatasourceId, - }; case 'UPDATE_DATASOURCE_STATE': return { ...state, @@ -183,10 +174,10 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'CREATE_LAYER': return { ...state, - layerIdToDatasource: { - ...state.layerIdToDatasource, - [action.newLayerId]: state.activeDatasourceId!, - }, + // layerIdToDatasource: { + // ...state.layerIdToDatasource, + // [action.newLayerId]: state.activeDatasourceId!, + // }, datasourceStates: { ...state.datasourceStates, [state.activeDatasourceId!]: { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 8c0efae5c0f57..2f751d6d82622 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -109,9 +109,9 @@ describe('workspace_panel', () => { it('should render the resulting expression using the expression renderer', () => { const framePublicAPI = createMockFramePublicAPI(); - framePublicAPI.layerIdToDatasource = { - first: 'mock', - }; + // framePublicAPI.layerIdToDatasource = { + // first: 'mock', + // }; framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; @@ -349,6 +349,7 @@ Object { { state: {}, table: expectedTable, + layerId: '1', }, ]); mockVisualization.getSuggestions.mockReturnValueOnce([ @@ -391,6 +392,7 @@ Object { isMultiRow: true, columns: [], }, + layerId: '1', }, { state: {}, @@ -399,6 +401,7 @@ Object { isMultiRow: true, columns: [], }, + layerId: '1', }, ]); mockVisualization.getSuggestions.mockReturnValueOnce([ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 6c3896ea477e7..d83144511c5ca 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -39,7 +39,7 @@ export function createMockDatasource(): DatasourceMock { getDatasourceSuggestionsForField: jest.fn((_state, item) => []), getDatasourceSuggestionsFromCurrentState: jest.fn(_state => []), getPersistableState: jest.fn(), - getPublicAPI: jest.fn((_state, _setState) => publicAPIMock), + getPublicAPI: jest.fn((_state, _setState, _layerId) => publicAPIMock), initialize: jest.fn((_state?) => Promise.resolve()), renderDataPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), @@ -57,7 +57,7 @@ export type FrameMock = jest.Mocked; export function createMockFramePublicAPI(): FrameMock { return { datasourceLayers: {}, - layerIdToDatasource: {}, + // layerIdToDatasource: {}, addNewLayer: jest.fn(() => ''), }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 8763da31d6954..8495777587e93 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -103,7 +103,6 @@ export class EditorFramePlugin { store={store} datasources={this.datasources} visualizations={this.visualizations} - // layerToDatasourceId={this.layerToDatasourceId} expressionRenderer={this.ExpressionRenderer!} /> )} @@ -216,8 +215,7 @@ export function InitializedEditor({ store={store} datasourceMap={datasources} visualizationMap={visualizations} - // layerToDatasourceId={layerToDatasourceId} - initialDatasourceId={(doc && doc.datasourceType) || firstDatasourceId || null} + initialDatasourceId={(doc && doc.activeDatasourceId) || firstDatasourceId || null} initialVisualizationId={(doc && doc.visualizationType) || firstVisualizationId || null} ExpressionRenderer={expressionRenderer} redirectTo={path => routeProps.history.push(path)} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx index a77305a5c3178..40b35019e3bff 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/datapanel.test.tsx @@ -18,20 +18,26 @@ jest.mock('./loader'); const initialState: IndexPatternPrivateState = { currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'My Op', - dataType: 'string', - isBucketed: true, - operationType: 'terms', - sourceField: 'op', - params: { - size: 5, - orderDirection: 'asc', - orderBy: { - type: 'alphabetical', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'My Op', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'op', + params: { + size: 5, + orderDirection: 'asc', + orderBy: { + type: 'alphabetical', + }, + }, + indexPatternId: '1', }, }, }, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 2af755568c2a7..bccf85642ceb8 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -81,20 +81,26 @@ describe('IndexPatternDimensionPanel', () => { state = { indexPatterns: expectedIndexPatterns, currentIndexPatternId: '1', - columnOrder: ['col1'], - columns: { - col1: { - operationId: 'op1', - label: 'Date Histogram of timestamp', - dataType: 'date', - isBucketed: true, - - // Private - operationType: 'date_histogram', - params: { - interval: '1d', + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + indexPatternId: '1', + }, }, - sourceField: 'timestamp', }, }, }; @@ -108,6 +114,7 @@ describe('IndexPatternDimensionPanel', () => { state, setState, columnId: 'col1', + layerId: 'first', filterOperations: () => true, dataPlugin: data, storage: localStorage, @@ -135,10 +142,13 @@ describe('IndexPatternDimensionPanel', () => { it('should pass the right arguments to getPotentialColumns', async () => { wrapper = shallow(); - expect(getPotentialColumns as jest.Mock).toHaveBeenCalledWith( - state.indexPatterns[state.currentIndexPatternId].fields, - 1 - ); + // expect(getPotentialColumns as jest.Mock).toHaveBeenCalledWith({ + expect(getPotentialColumns).toHaveBeenCalledWith({ + fields: state.indexPatterns[state.currentIndexPatternId].fields, + suggestedPriority: 1, + layerId: 'first', + layer: state.layers.first, + }); }); it('should call the filterOperations function', () => { @@ -198,17 +208,23 @@ describe('IndexPatternDimensionPanel', () => { {...defaultProps} state={{ ...state, - columns: { - ...state.columns, - col1: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }} @@ -235,17 +251,23 @@ describe('IndexPatternDimensionPanel', () => { {...defaultProps} state={{ ...state, - columns: { - ...state.columns, - col1: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }} @@ -269,17 +291,23 @@ describe('IndexPatternDimensionPanel', () => { it('should keep the operation when switching to another field compatible with this operation', () => { const initialState: IndexPatternPrivateState = { ...state, - columns: { - ...state.columns, - col1: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }; @@ -297,13 +325,18 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...initialState, - columns: { - ...state.columns, - col1: expect.objectContaining({ - operationType: 'max', - sourceField: 'memory', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'max', + sourceField: 'memory', + // Other parts of this don't matter for this test + }), + }, + }, }, }); }); @@ -322,13 +355,18 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + // Other parts of this don't matter for this test + }), + }, + }, }, }); }); @@ -339,17 +377,23 @@ describe('IndexPatternDimensionPanel', () => { {...defaultProps} state={{ ...state, - columns: { - ...state.columns, - col1: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }} @@ -364,13 +408,18 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: expect.objectContaining({ - operationType: 'min', - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + // Other parts of this don't matter for this test + }), + }, + }, }, }); }); @@ -402,12 +451,17 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col1: expect.objectContaining({ - label: 'New Label', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + label: 'New Label', + // Other parts of this don't matter for this test + }), + }, + }, }, }); }); @@ -504,11 +558,17 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - col1: expect.objectContaining({ - sourceField: 'source', - operationType: 'terms', - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: expect.objectContaining({ + sourceField: 'source', + operationType: 'terms', + }), + }, + }, }, }); }); @@ -530,15 +590,20 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, }, - columnOrder: ['col1', 'col2'], }); }); @@ -596,31 +661,42 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: { - ...state.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, }, - columnOrder: ['col1', 'col2'], }); }); it('should use helper function when changing the function', () => { const initialState: IndexPatternPrivateState = { ...state, - columns: { - ...state.columns, - col1: { - operationId: 'op1', - label: 'Max of bytes', - dataType: 'number', - isBucketed: false, - - // Private - operationType: 'max', - sourceField: 'bytes', + layers: { + first: { + ...state.layers.first, + columns: { + ...state.layers.first.columns, + col1: { + operationId: 'op1', + label: 'Max of bytes', + dataType: 'number', + isBucketed: false, + + // Private + operationType: 'max', + sourceField: 'bytes', + indexPatternId: '1', + }, + }, }, }, }; @@ -635,14 +711,15 @@ describe('IndexPatternDimensionPanel', () => { .prop('onClick')!({} as React.MouseEvent<{}, MouseEvent>); }); - expect(changeColumn).toHaveBeenCalledWith( - initialState, - 'col1', - expect.objectContaining({ + expect(changeColumn).toHaveBeenCalledWith({ + state: initialState, + columnId: 'col1', + layerId: 'first', + newColumn: expect.objectContaining({ sourceField: 'bytes', operationType: 'min', - }) - ); + }), + }); }); it('should clear the dimension with the clear button', () => { @@ -658,8 +735,13 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: {}, - columnOrder: [], + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + }, }); }); @@ -674,16 +756,38 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...state, - columns: {}, - columnOrder: [], + layers: { + first: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + }, }); }); describe('drag and drop', () => { - function dragDropState() { + function dragDropState(): IndexPatternPrivateState { + // return {0 + // ...state, + // // currentIndexPatternId: 'foo', + // // indexPatterns: { + // // foo: { + // // id: 'foo', + // // title: 'Foo pattern', + // // fields: [ + // // { + // // aggregatable: true, + // // name: 'bar', + // // searchable: true, + // // type: 'number', + // // }, + // // ], + // // }, + // // }, + // }; return { - ...state, - currentIndexPatternId: 'foo', + // indexPatterns: expectedIndexPatterns, indexPatterns: { foo: { id: 'foo', @@ -698,11 +802,51 @@ describe('IndexPatternDimensionPanel', () => { ], }, }, + currentIndexPatternId: '1', + layers: { + myLayer: { + indexPatternId: 'foo', + columnOrder: ['col1'], + columns: { + col1: { + operationId: 'op1', + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + + // Private + operationType: 'date_histogram', + params: { + interval: '1d', + }, + sourceField: 'timestamp', + indexPatternId: 'foo', + }, + }, + }, + }, }; + + // setState = jest.fn(); + + // dragDropContext = createMockedDragDropContext(); + + // defaultProps = { + // dragDropContext, + // state, + // setState, + // columnId: 'col1', + // layerId: 'first', + // filterOperations: () => true, + // dataPlugin: data, + // storage: localStorage, + // }; } it('is not droppable if no drag is happening', () => { - wrapper = mount(); + wrapper = mount( + + ); expect( wrapper @@ -721,6 +865,7 @@ describe('IndexPatternDimensionPanel', () => { dragging: { name: 'bar' }, }} state={dragDropState()} + layerId="myLayer" /> ); @@ -742,6 +887,7 @@ describe('IndexPatternDimensionPanel', () => { }} state={dragDropState()} filterOperations={() => false} + layerId="myLayer" /> ); @@ -763,6 +909,7 @@ describe('IndexPatternDimensionPanel', () => { }} state={dragDropState()} filterOperations={op => op.dataType === 'number'} + layerId="myLayer" /> ); @@ -787,6 +934,7 @@ describe('IndexPatternDimensionPanel', () => { state={testState} columnId={'col2'} filterOperations={op => op.dataType === 'number'} + layerId="myLayer" /> ); @@ -800,17 +948,22 @@ describe('IndexPatternDimensionPanel', () => { }); expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - columns: expect.objectContaining({ - ...testState.columns, - col2: expect.objectContaining({ - dataType: 'number', - sourceField: 'bar', - }), - }), - }) - ); + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: { + ...testState.layers.myLayer, + columnOrder: ['col1', 'col2'], + columns: { + ...testState.layers.myLayer.columns, + col2: expect.objectContaining({ + dataType: 'number', + sourceField: 'bar', + }), + }, + }, + }, + }); }); it('updates a column when a field is dropped', () => { @@ -825,6 +978,7 @@ describe('IndexPatternDimensionPanel', () => { }} state={testState} filterOperations={op => op.dataType === 'number'} + layerId="myLayer" /> ); @@ -838,16 +992,19 @@ describe('IndexPatternDimensionPanel', () => { }); expect(setState).toBeCalledTimes(1); - expect(setState).toHaveBeenCalledWith( - expect.objectContaining({ - columns: expect.objectContaining({ - col1: expect.objectContaining({ - dataType: 'number', - sourceField: 'bar', + expect(setState).toHaveBeenCalledWith({ + ...testState, + layers: { + myLayer: expect.objectContaining({ + columns: expect.objectContaining({ + col1: expect.objectContaining({ + dataType: 'number', + sourceField: 'bar', + }), }), }), - }) - ); + }, + }); }); it('ignores drops of incompatible fields', () => { @@ -862,6 +1019,7 @@ describe('IndexPatternDimensionPanel', () => { }} state={testState} filterOperations={op => op.dataType === 'number'} + layerId="myLayer" /> ); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 0d39efb6cd8b8..ddd9f9fdee435 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -159,8 +159,7 @@ describe('IndexPattern Data Source', () => { expect(state).toEqual({ currentIndexPatternId: '1', indexPatterns: expectedIndexPatterns, - columns: {}, - columnOrder: [], + layers: {}, }); }); @@ -486,7 +485,7 @@ describe('IndexPattern Data Source', () => { beforeEach(async () => { const initialState = await indexPatternDatasource.initialize(persistedState); - publicAPI = indexPatternDatasource.getPublicAPI(initialState, () => {}); + publicAPI = indexPatternDatasource.getPublicAPI(initialState, () => {}, 'first'); }); describe('getTableSpec', () => { @@ -531,20 +530,21 @@ describe('IndexPattern Data Source', () => { { ...initialState, layers: { - ...initialState.layers, first: { ...initialState.layers.first, + columns, columnOrder: ['a', 'b', 'c'], }, }, }, - setState + setState, + 'first' ); api.removeColumnInTableSpec('b'); - expect(setState.mock.calls[0][0].columnOrder).toEqual(['a', 'c']); - expect(setState.mock.calls[0][0].columns).toEqual({ + expect(setState.mock.calls[0][0].layers.first.columnOrder).toEqual(['a', 'c']); + expect(setState.mock.calls[0][0].layers.first.columns).toEqual({ a: columns.a, c: columns.c, }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 1a7afb76697aa..bfbad76b45236 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -20,6 +20,7 @@ import { Query } from '../../../../../../src/legacy/core_plugins/data/public/que import { getIndexPatterns } from './loader'; import { toExpression } from './to_expression'; import { IndexPatternDimensionPanel } from './dimension_panel'; +import { buildColumnForOperationType, getOperationTypesForField } from './operations'; import { IndexPatternDatasourcePluginPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; import { Datasource, DataType } from '..'; @@ -256,14 +257,15 @@ export function getIndexPatternDatasource({ ); }, - getPublicAPI(state, setState) { + getPublicAPI(state, setState, layerId) { return { // supportsLayers: true, // supportsLayerJoin: true, getTableSpec: () => { + return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); // return state.columnOrder.map(colId => ({ columnId: colId })); - return []; + // return []; }, getOperationForColumnId: (columnId: string) => { const layer = Object.values(state.layers).find(l => @@ -307,11 +309,17 @@ export function getIndexPatternDatasource({ }, removeColumnInTableSpec: (columnId: string) => { - // setState({ - // ...state, - // columnOrder: state.columnOrder.filter(id => id !== columnId), - // columns: removeProperty(columnId, state.columns), - // }); + setState({ + ...state, + layers: { + ...state.layers, + [layerId]: { + ...state.layers[layerId], + columnOrder: state.layers[layerId].columnOrder.filter(id => id !== columnId), + columns: removeProperty(columnId, state.layers[layerId].columns), + }, + }, + }); }, moveColumnTo: () => {}, duplicateColumn: () => [], diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx index 2704a594960e3..6c2280ae38b7e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.test.tsx @@ -73,7 +73,7 @@ describe('date_histogram', () => { searchable: true, }, }); - expect(column.params.interval).toEqual('h'); + expect(column.params.interval).toEqual('d'); }); it('should create column object with restrictions', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx index 732fe48420b96..08e162e01b80a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.test.tsx @@ -209,7 +209,7 @@ describe('filter_ratio', () => { expect(setState).toHaveBeenCalledWith({ ...state, - laeyrs: { + layers: { ...state.layers, first: { ...state.layers.first, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx index e5f5a5eb12bfd..139808bc196af 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.test.tsx @@ -332,6 +332,7 @@ describe('terms', () => { ...state, layers: { first: { + ...state.layers.first, columns: { ...state.layers.first.columns, col1: { @@ -371,6 +372,7 @@ describe('terms', () => { ...state, layers: { first: { + ...state.layers.first, columns: { ...state.layers.first.columns, col1: { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts index f73f4b47a516d..b8ec30c43e88b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -140,55 +140,36 @@ export function buildColumnForOperationType({ } export function getPotentialColumns({ - // state, - // suggestedPriority, fields, suggestedPriority, layerId, layer, }: { fields: IndexPatternField[]; - // state: IndexPatternPrivateState; suggestedPriority?: DimensionPriority; layerId: string; layer: IndexPatternLayer; }): IndexPatternColumn[] { - // const result: IndexPatternColumn[] = fields - // const indexPattern = state.layers[layerId].indexPatternId; - - // const fields = state.indexPatterns[indexPattern].fields; - - // const columns: IndexPatternColumn[] = fields - // ======= - // export function getPotentialColumns( - // fields: IndexPatternField[], - // suggestedOrder?: DimensionPriority - // ): IndexPatternColumn[] { - // const result: IndexPatternColumn[] = fields - // >>>>>>> origin/feature/lens const result: IndexPatternColumn[] = fields .map((field, index) => { const validOperations = getOperationTypesForField(field); - return validOperations.map( - op => - buildColumnForOperationType({ - index, - op, - columns: layer.columns, - suggestedPriority, - field, - indexPatternId: layer.indexPatternId, - layerId, - }) - // buildColumnForOperationType(index, op, {}, suggestedOrder, field) + return validOperations.map(op => + buildColumnForOperationType({ + index, + op, + columns: layer.columns, + suggestedPriority, + field, + indexPatternId: layer.indexPatternId, + layerId, + }) ); }) .reduce((prev, current) => prev.concat(current)); operationDefinitions.forEach(operation => { if (operation.isApplicableWithoutField) { - // columns.push( result.push( operation.buildColumn({ operationId: operation.type, @@ -198,7 +179,6 @@ export function getPotentialColumns({ indexPatternId: layer.indexPatternId, }) ); - // result.push(operation.buildColumn(operation.type, {}, suggestedOrder)); } }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts index 3c4bbae93c8b3..2c9eef5fbfd4c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.test.ts @@ -216,11 +216,14 @@ describe('state_helpers', () => { indexPatternId: '1', }, }) - ).toEqual( - expect.objectContaining({ - columnOrder: ['col2', 'col1'], - }) - ); + ).toEqual({ + ...state, + layers: { + first: expect.objectContaining({ + columnOrder: ['col2', 'col1'], + }), + }, + }); }); it('should carry over params from old column if the operation type stays the same', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts index 78133e4186131..8b9c4d3d9df27 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/to_expression.ts @@ -14,7 +14,6 @@ import { } from './operations'; function getExpressionForLayer( - // currentIndexPatternId: string, indexPatternId: string, layerId: string, columns: Record, @@ -87,15 +86,8 @@ function getExpressionForLayer( } export function toExpression(state: IndexPatternPrivateState, layerId: string) { - // const expressions = Object.entries(state.layers).map(([id, layer]) => [ - // id, - // getExpressionForLayer(state.currentIndexPatternId, layer.columns, layer.columnOrder), - // ]); - if (state.layers[layerId]) { - // return `lens_merge_tables joins="" ${expressions.map(expr => `table={${expr}}`).join(' ')}`; return getExpressionForLayer( - // state.currentIndexPatternId, state.layers[layerId].indexPatternId, layerId, state.layers[layerId].columns, @@ -104,120 +96,4 @@ export function toExpression(state: IndexPatternPrivateState, layerId: string) { } return null; - - // if (state.columnOrder.length === 0) { - // return null; - // } - - // function getEsAggsConfig(column: C, columnId: string) { - // // Typescript is not smart enough to infer that definitionMap[C['operationType']] is always OperationDefinition, - // // but this is made sure by the typing of the operation map - // const operationDefinition = (operationDefinitionMap[ - // column.operationType - // ] as unknown) as OperationDefinition; - // return operationDefinition.toEsAggsConfig(column, columnId); - // } - - // function getIdMap(layer: Array<[string, IndexPatternColumn]>) { - // return layer.reduce( - // (currentIdMap, [colId], index) => { - // return { - // ...currentIdMap, - // [`col-${index}-${colId}`]: colId, - // }; - // }, - // {} as Record - // ); - // } - - // function getExpr( - // aggs: unknown[], - // idMap: Record, - // filterRatios?: Array<[string, IndexPatternColumn]> - // ) { - // let expr = `esaggs - // index="${state.currentIndexPatternId}" - // metricsAtAllLevels=false - // partialRows=false - // aggConfigs='${JSON.stringify(aggs)}' | lens_rename_columns idMap='${JSON.stringify(idMap)}'`; - - // if (filterRatios) { - // expr += `${filterRatios.map(([id]) => `lens_calculate_filter_ratio id=${id}`).join(' | ')}`; - // } - - // return expr; - // } - - // const columnEntries = state.columnOrder.map( - // colId => [colId, state.columns[colId]] as [string, IndexPatternColumn] - // ); - - // if (columnEntries.length) { - // const joinLayer = columnEntries.find(([colId, col]) => col.layer === 'join'); - // const numericLayers = columnEntries.filter(([colId, col]) => typeof col.layer === 'number'); - - // let dataFetchExpression; - - // if (joinLayer && numericLayers.length > 1) { - // const groupedLayers = _.groupBy(numericLayers, ([colId, col]) => col.layer); - - // const tableFetchExpressions = Object.values(groupedLayers) - // .map(layer => { - // const [buckets, metrics] = _.partition(layer, ([colId, col]) => col.isBucketed); - - // return buckets.concat([joinLayer], metrics); - // }) - // .map(layer => { - // const aggs = layer.map(([colId, col]) => { - // return getEsAggsConfig(col, colId); - // }); - - // const idMap = getIdMap(layer); - - // // TODO: Duplicate logic here, need to refactor - // const filterRatios = layer.filter(([colId, col]) => col.operationType === 'filter_ratio'); - - // if (filterRatios.length) { - // const countColumn = buildColumnForOperationType( - // columnEntries.length, - // 'count', - // layer[0][1].suggestedOrder, - // layer[0][1].layer - // ); - // aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - // return getExpr(aggs, idMap, filterRatios); - // } - - // return getExpr(aggs, idMap); - // }) - // .map(layer => `tables={${layer}}`); - - // dataFetchExpression = `lens_merge_tables joins="${joinLayer[0]}" ${tableFetchExpressions.join( - // '\n' - // )} | clog `; - // } else { - // const aggs = columnEntries.map(([colId, col]) => { - // return getEsAggsConfig(col, colId); - // }); - - // const filterRatios = columnEntries.filter( - // ([colId, col]) => col.operationType === 'filter_ratio' - // ); - - // const idMap = getIdMap(columnEntries); - // if (filterRatios.length) { - // const countColumn = buildColumnForOperationType(columnEntries.length, 'count', 2, 0); - // aggs.push(getEsAggsConfig(countColumn, 'filter-ratio')); - - // dataFetchExpression = getExpr(aggs, idMap, filterRatios); - // } else { - // dataFetchExpression = getExpr(aggs, idMap); - // } - // } - - // return dataFetchExpression; - // } - - // return null; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 34223175d1aaf..cb9f3c090d81b 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -45,6 +45,7 @@ export interface TableSuggestion { export interface DatasourceSuggestion { state: T; table: TableSuggestion; + layerId: string; } /** @@ -70,7 +71,7 @@ export interface Datasource { getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; getDatasourceSuggestionsFromCurrentState: (state: T) => Array>; - getPublicAPI: (state: T, setState: (newState: T) => void) => DatasourcePublicAPI; + getPublicAPI: (state: T, setState: (newState: T) => void, layerId: string) => DatasourcePublicAPI; } /** @@ -176,7 +177,7 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; - layerIdToDatasource: Record; + // layerIdToDatasource: Record; // Adds a new layer. This triggers a re-render addNewLayer: () => string; } From 762bd4474cc1bf6e2525334b87afe4fc23f5b54f Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 18 Jul 2019 15:28:10 -0400 Subject: [PATCH 19/67] Unify datasource deletion and accessor removal --- .../dimension_panel/dimension_panel.tsx | 5 ++++- x-pack/legacy/plugins/lens/public/types.ts | 1 + .../multi_column_editor.tsx | 20 ++----------------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 6fbe968255189..933051afff3e0 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -10,7 +10,7 @@ import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { Storage } from 'ui/storage'; import { i18n } from '@kbn/i18n'; import { DataSetup } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { DatasourceDimensionPanelProps, DimensionLayer } from '../../types'; +import { DatasourceDimensionPanelProps } from '../../types'; import { IndexPatternColumn, IndexPatternPrivateState, @@ -120,6 +120,9 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan columnId: props.columnId, }) ); + if (props.onRemove) { + props.onRemove(props.columnId); + } }} /> diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 34223175d1aaf..b30112f9ddb29 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -117,6 +117,7 @@ export interface DatasourceDimensionPanelProps { // Visualizations can hint at the role this dimension would play, which // affects the default ordering of the query suggestedPriority?: DimensionPriority; + onRemove?: (accessor: string) => void; } export interface DatasourceLayerPanelProps { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx index 0a756f05cf427..30f376d43a05e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx @@ -5,8 +5,6 @@ */ import React, { useEffect } from 'react'; -import { EuiButtonIcon } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { NativeRenderer } from '../native_renderer'; import { DatasourcePublicAPI, Operation } from '../types'; import { DragContextState } from '../drag_drop'; @@ -45,7 +43,7 @@ export function MultiColumnEditor({ return ( <> - {accessors.map((accessor, i) => ( + {accessors.map(accessor => (
- {i === accessors.length - 1 ? null : ( - { - datasource.removeColumnInTableSpec(accessor); - onRemove(accessor); - }} - aria-label={i18n.translate('xpack.lens.xyChart.removeAriaLabel', { - defaultMessage: 'Remove', - })} - /> - )}
))} From 5001bf2ad8552331621f49359b711efc3e347265 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 18 Jul 2019 15:44:26 -0400 Subject: [PATCH 20/67] Fix broken scss --- .../lens/public/xy_visualization_plugin/_xy_config_panel.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss index ccdd3774b5d7b..9d5538130260d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/_xy_config_panel.scss @@ -1,4 +1,3 @@ .lnsXyConfigPanel-layer { border: 1px solid $euiColorLightestShade; - background-color: $euiColorEmpty; } From e111fdc3ebbe62ae438ace0c67c620acc21fb831 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 18 Jul 2019 15:58:05 -0400 Subject: [PATCH 21/67] Fix xy visualization TypeScript errors? --- .../operation_definitions/terms.tsx | 4 +-- .../xy_config_panel.test.tsx | 14 +------- .../xy_config_panel.tsx | 34 +------------------ .../xy_visualization.tsx | 3 +- 4 files changed, 4 insertions(+), 51 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 046a2332329b6..72c74f1d809ee 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -7,9 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow, EuiRange, EuiSelect } from '@elastic/eui'; -import { DimensionLayer } from '../../types'; -import { IndexPatternField, TermsIndexPatternColumn, IndexPatternColumn } from '../indexpattern'; -import { DimensionPriority } from '../../types'; +import { TermsIndexPatternColumn, IndexPatternColumn } from '../indexpattern'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index d5acde5e58686..20c5fb58f239c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -9,7 +9,7 @@ import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; import { XYConfigPanel } from './xy_config_panel'; -import { DatasourcePublicAPI, DatasourceDimensionPanelProps, Operation } from '../types'; +import { DatasourceDimensionPanelProps, Operation } from '../types'; import { State, SeriesType } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; @@ -21,18 +21,6 @@ jest.mock('../id_generator'); describe('XYConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; - function mockDatasource(): DatasourcePublicAPI { - return { - duplicateColumn: () => [], - getOperationForColumnId: () => null, - getTableSpec: () => [], - moveColumnTo: () => {}, - removeColumnInTableSpec: () => [], - renderDimensionPanel: () => {}, - renderLayerPanel: () => {}, - }; - } - function testState(): State { return { legend: { isVisible: true, position: Position.Right }, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index d9d038a359ea5..7d92cecbcec7a 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -8,16 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Position } from '@elastic/charts'; -import { - EuiFieldText, - EuiButton, - EuiButtonGroup, - EuiForm, - EuiFormRow, - EuiSwitch, - EuiPanel, - IconType, -} from '@elastic/eui'; +import { EuiButton, EuiButtonGroup, EuiForm, EuiFormRow, EuiPanel, IconType } from '@elastic/eui'; import { State, SeriesType, LayerConfig } from './types'; import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; @@ -76,29 +67,6 @@ const chartTypeIcons: Array<{ id: SeriesType; label: string; iconType: IconType }, ]; -const positionIcons = [ - { - id: Position.Left, - label: 'Left', - iconType: 'arrowLeft', - }, - { - id: Position.Top, - label: 'Top', - iconType: 'arrowUp', - }, - { - id: Position.Bottom, - label: 'Bottom', - iconType: 'arrowDown', - }, - { - id: Position.Right, - label: 'Right', - iconType: 'arrowRight', - }, -]; - type UnwrapArray = T extends Array ? P : T; function updateLayer(state: State, layer: UnwrapArray, index: number): State { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 1acbe4d34fb6d..192f2749aea32 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -13,12 +13,11 @@ import { XYConfigPanel } from './xy_config_panel'; import { Visualization } from '../types'; import { State, PersistableState } from './types'; import { toExpression } from './to_expression'; -import { generateId } from '../id_generator'; export const xyVisualization: Visualization = { getSuggestions, - initialize(frame, state) { + initialize(_, state) { return ( state || { // seriesType: 'bar', From dcc243f65f43cab0742c4aaebac318577089c075 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Thu, 18 Jul 2019 17:15:34 -0400 Subject: [PATCH 22/67] Build basic suggestions --- .../dimension_panel/dimension_panel.test.tsx | 29 +- .../dimension_panel/popover_editor.tsx | 9 +- .../indexpattern_plugin/indexpattern.test.tsx | 67 ++-- .../indexpattern_plugin/indexpattern.tsx | 292 ++++++++++-------- 4 files changed, 233 insertions(+), 164 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index debe6c530cd0f..5b27484489e47 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -609,13 +609,11 @@ describe('IndexPatternDimensionPanel', () => { it('should select operation directly if only one field is possible', () => { const initialState = { - ...defaultProps.state, + ...state, indexPatterns: { 1: { - ...defaultProps.state.indexPatterns['1'], - fields: defaultProps.state.indexPatterns['1'].fields.filter( - field => field.name !== 'memory' - ), + ...state.indexPatterns['1'], + fields: state.indexPatterns['1'].fields.filter(field => field.name !== 'memory'), }, }, }; @@ -630,15 +628,20 @@ describe('IndexPatternDimensionPanel', () => { expect(setState).toHaveBeenCalledWith({ ...initialState, - columns: { - ...state.columns, - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'avg', - // Other parts of this don't matter for this test - }), + layers: { + first: { + ...initialState.layers.first, + columns: { + ...initialState.layers.first.columns, + col2: expect.objectContaining({ + sourceField: 'bytes', + operationType: 'avg', + // Other parts of this don't matter for this test + }), + }, + columnOrder: ['col1', 'col2'], + }, }, - columnOrder: ['col1', 'col2'], }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx index f1d9ebf155a31..de6259db5324c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/popover_editor.tsx @@ -95,7 +95,14 @@ export function PopoverEditor(props: PopoverEditorProps) { 'sourceField' ); if (possibleColumns.length === 1) { - setState(changeColumn(state, columnId, possibleColumns[0])); + setState( + changeColumn({ + state, + layerId, + columnId, + newColumn: possibleColumns[0], + }) + ); } else { setInvalidOperationType(operationType); } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index ddd9f9fdee435..663cafb5e0ba6 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -16,8 +16,10 @@ import { IndexPatternColumn, } from './indexpattern'; import { DatasourcePublicAPI, Operation, Datasource } from '../types'; +import { generateId } from '../id_generator'; jest.mock('./loader'); +jest.mock('../id_generator'); // chrome, notify, storage are used by ./plugin jest.mock('ui/chrome'); jest.mock('ui/notify'); @@ -242,6 +244,7 @@ describe('IndexPattern Data Source', () => { currentIndexPatternId: '1', layers: {}, }); + (generateId as jest.Mock).mockReturnValueOnce('suggestedLayer'); }); it('should apply a bucketed aggregation for a string field', () => { @@ -255,14 +258,18 @@ describe('IndexPattern Data Source', () => { expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toEqual( expect.objectContaining({ - columnOrder: ['col1', 'col2'], - columns: { - col1: expect.objectContaining({ - operationType: 'terms', - sourceField: 'source', - }), - col2: expect.objectContaining({ - operationType: 'count', + layers: { + suggestedLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + }), + col2: expect.objectContaining({ + operationType: 'count', + }), + }, }), }, }) @@ -279,6 +286,7 @@ describe('IndexPattern Data Source', () => { }), ], }); + expect(suggestions[0].layerId).toEqual('suggestedLayer'); }); it('should apply a bucketed aggregation for a date field', () => { @@ -292,14 +300,18 @@ describe('IndexPattern Data Source', () => { expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toEqual( expect.objectContaining({ - columnOrder: ['col1', 'col2'], - columns: { - col1: expect.objectContaining({ - operationType: 'date_histogram', - sourceField: 'timestamp', - }), - col2: expect.objectContaining({ - operationType: 'count', + layers: { + suggestedLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'date_histogram', + sourceField: 'timestamp', + }), + col2: expect.objectContaining({ + operationType: 'count', + }), + }, }), }, }) @@ -329,15 +341,19 @@ describe('IndexPattern Data Source', () => { expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toEqual( expect.objectContaining({ - columnOrder: ['col1', 'col2'], - columns: { - col1: expect.objectContaining({ - sourceField: 'timestamp', - operationType: 'date_histogram', - }), - col2: expect.objectContaining({ - sourceField: 'bytes', - operationType: 'min', + layers: { + suggestedLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'date_histogram', + sourceField: 'timestamp', + }), + col2: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + }), + }, }), }, }) @@ -475,6 +491,7 @@ describe('IndexPattern Data Source', () => { }, ], }, + layerId: 'first', }, ]); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index bfbad76b45236..791ff1203ff64 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -23,6 +23,7 @@ import { IndexPatternDimensionPanel } from './dimension_panel'; import { buildColumnForOperationType, getOperationTypesForField } from './operations'; import { IndexPatternDatasourcePluginPlugins } from './plugin'; import { IndexPatternDataPanel } from './datapanel'; +import { generateId } from '../id_generator'; import { Datasource, DataType } from '..'; export type OperationType = IndexPatternColumn['operationType']; @@ -259,13 +260,8 @@ export function getIndexPatternDatasource({ getPublicAPI(state, setState, layerId) { return { - // supportsLayers: true, - // supportsLayerJoin: true, - getTableSpec: () => { return state.layers[layerId].columnOrder.map(colId => ({ columnId: colId })); - // return state.columnOrder.map(colId => ({ columnId: colId })); - // return []; }, getOperationForColumnId: (columnId: string) => { const layer = Object.values(state.layers).find(l => @@ -290,7 +286,6 @@ export function getIndexPatternDatasource({ setState={newState => setState(newState)} dataPlugin={data} storage={storage} - // layer={props.layer || 0} layerId={props.layerId} {...props} /> @@ -330,107 +325,148 @@ export function getIndexPatternDatasource({ state, item ): Array> { - // const field: IndexPatternField = item as IndexPatternField; - - // if (Object.keys(state.columns).length) { - // // Not sure how to suggest multiple fields yet - // return []; - // } - - // const operations = getOperationTypesForField(field); - // const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); - - // if (hasBucket) { - // const countColumn = buildColumnForOperationType(1, 'count', state.columns); - - // // let column know about count column - // const column = buildColumnForOperationType( - // 0, - // hasBucket, - // { - // col2: countColumn, - // }, - // undefined, - // field - // ); - - // const suggestion: DatasourceSuggestion = { - // state: { - // ...state, - // columns: { - // col1: column, - // col2: countColumn, - // }, - // columnOrder: ['col1', 'col2'], - // }, - - // table: { - // columns: [ - // { - // columnId: 'col1', - // operation: columnToOperation(column), - // }, - // { - // columnId: 'col2', - // operation: columnToOperation(countColumn), - // }, - // ], - // isMultiRow: true, - // datasourceSuggestionId: 0, - // }, - // }; - - // return [suggestion]; - // } else if (state.indexPatterns[state.currentIndexPatternId].timeFieldName) { - // const currentIndexPattern = state.indexPatterns[state.currentIndexPatternId]; - // const dateField = currentIndexPattern.fields.find( - // f => f.name === currentIndexPattern.timeFieldName - // )!; - - // const column = buildColumnForOperationType( - // 0, - // operations[0], - // state.columns, - // undefined, - // field - // ); - - // const dateColumn = buildColumnForOperationType( - // 1, - // 'date_histogram', - // state.columns, - // undefined, - // dateField - // ); - - // const suggestion: DatasourceSuggestion = { - // state: { - // ...state, - // columns: { - // col1: dateColumn, - // col2: column, - // }, - // columnOrder: ['col1', 'col2'], - // }, - - // table: { - // columns: [ - // { - // columnId: 'col1', - // operation: columnToOperation(column), - // }, - // { - // columnId: 'col2', - // operation: columnToOperation(dateColumn), - // }, - // ], - // isMultiRow: true, - // datasourceSuggestionId: 0, - // }, - // }; - - // return [suggestion]; - // } + const layers = Object.keys(state.layers); + const field: IndexPatternField = item as IndexPatternField; + + let layerId; + let layer: IndexPatternLayer; + + if (layers.length === 0) { + layerId = generateId(); + layer = { + indexPatternId: state.currentIndexPatternId, + columnOrder: [], + columns: {}, + }; + } else if (state.layers[layers[0]].columnOrder.length) { + // Don't suggest when there are existing columns + return []; + } else { + layerId = layers[0]; + layer = state.layers[layerId]; + } + + const operations = getOperationTypesForField(field); + const hasBucket = operations.find(op => op === 'date_histogram' || op === 'terms'); + + if (hasBucket) { + const countColumn = buildColumnForOperationType({ + index: 1, + op: 'count', + columns: layer.columns, + indexPatternId: state.currentIndexPatternId, + layerId, + suggestedPriority: undefined, + }); + + // let column know about count column + const column = buildColumnForOperationType({ + index: 0, + layerId, + op: hasBucket, + indexPatternId: state.currentIndexPatternId, + columns: { + col2: countColumn, + }, + field, + suggestedPriority: undefined, + }); + + const suggestion: DatasourceSuggestion = { + state: { + ...state, + layers: { + [layerId]: { + indexPatternId: state.currentIndexPatternId, + columns: { + col1: column, + col2: countColumn, + }, + columnOrder: ['col1', 'col2'], + }, + }, + }, + + table: { + columns: [ + { + columnId: 'col1', + operation: columnToOperation(column), + }, + { + columnId: 'col2', + operation: columnToOperation(countColumn), + }, + ], + isMultiRow: true, + datasourceSuggestionId: 0, + }, + + layerId, + }; + + return [suggestion]; + } else if (state.indexPatterns[state.currentIndexPatternId].timeFieldName) { + const currentIndexPattern = state.indexPatterns[state.currentIndexPatternId]; + const dateField = currentIndexPattern.fields.find( + f => f.name === currentIndexPattern.timeFieldName + )!; + + const column = buildColumnForOperationType({ + index: 0, + op: operations[0], + columns: layer.columns, + suggestedPriority: undefined, + field, + indexPatternId: state.currentIndexPatternId, + layerId, + }); + + const dateColumn = buildColumnForOperationType({ + index: 1, + op: 'date_histogram', + columns: layer.columns, + suggestedPriority: undefined, + field: dateField, + indexPatternId: state.currentIndexPatternId, + layerId, + }); + + const suggestion: DatasourceSuggestion = { + state: { + ...state, + layers: { + [layerId]: { + ...layer, + columns: { + col1: dateColumn, + col2: column, + }, + columnOrder: ['col1', 'col2'], + }, + }, + }, + + table: { + columns: [ + { + columnId: 'col1', + operation: columnToOperation(column), + }, + { + columnId: 'col2', + operation: columnToOperation(dateColumn), + }, + ], + isMultiRow: true, + datasourceSuggestionId: 0, + }, + + layerId, + }; + + return [suggestion]; + } return []; }, @@ -438,24 +474,30 @@ export function getIndexPatternDatasource({ getDatasourceSuggestionsFromCurrentState( state ): Array> { - return []; - // if (!state.columnOrder.length) { - // return []; - // } - // return [ - // { - // state, - - // table: { - // columns: state.columnOrder.map(id => ({ - // columnId: id, - // operation: columnToOperation(state.columns[id]), - // })), - // isMultiRow: true, - // datasourceSuggestionId: 0, - // }, - // }, - // ]; + const layers = Object.entries(state.layers); + + return layers + .map(([layerId, layer], index) => { + if (layer.columnOrder.length === 0) { + return; + } + return { + state, + + table: { + columns: layer.columnOrder.map(id => ({ + columnId: id, + operation: columnToOperation(layer.columns[id]), + })), + isMultiRow: true, + datasourceSuggestionId: index, + }, + layerId, + }; + }) + .reduce((prev, current) => (current ? prev.concat([current]) : prev), [] as Array< + DatasourceSuggestion + >); }, }; From d9e101ae08b58050b1bf258386cc8607dd3de044 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Jul 2019 14:26:50 +0200 Subject: [PATCH 23/67] Restore save/load and fix typescript bugs --- x-pack/legacy/plugins/lens/mappings.json | 3 -- .../editor_frame/data_panel_wrapper.tsx | 3 +- .../editor_frame/editor_frame.tsx | 38 +++++++------- .../editor_frame_plugin/editor_frame/save.ts | 2 +- .../editor_frame/state_management.ts | 50 ++++++++++--------- .../lens/public/editor_frame_plugin/mocks.tsx | 1 - .../public/editor_frame_plugin/plugin.tsx | 4 +- .../public/persistence/saved_object_store.ts | 2 - x-pack/legacy/plugins/lens/public/types.ts | 1 - 9 files changed, 53 insertions(+), 51 deletions(-) diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json index 4c860a7171829..ec73f4ad32f87 100644 --- a/x-pack/legacy/plugins/lens/mappings.json +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -7,9 +7,6 @@ "visualizationType": { "type": "keyword" }, - "datasourceType": { - "type": "keyword" - }, "state": { "type": "text" } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx index 8a560e862d607..d0a17c75e1b51 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/data_panel_wrapper.tsx @@ -26,9 +26,10 @@ export const DataPanelWrapper = memo((props: DataPanelWrapperProps) => { props.dispatch({ type: 'UPDATE_DATASOURCE_STATE', newState, + datasourceId: props.activeDatasource!, }); }, - [props.dispatch] + [props.dispatch, props.activeDatasource] ); const datasourceProps: DatasourceDataPanelProps = { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index b2021c9d4c73d..de7c36e55bb8d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -27,7 +27,7 @@ export interface EditorFrameProps { visualizationMap: Record; // layerToDatasourceId: Record; redirectTo: (path: string) => void; - initialDatasourceId: string | null; + initialDatasourceIds: string[] | null; initialVisualizationId: string | null; ExpressionRenderer: ExpressionRenderer; onError: (e: { message: string }) => void; @@ -42,60 +42,58 @@ export function EditorFrame(props: EditorFrameProps) { // Initialize current datasource and all active datasources useEffect(() => { if (!allLoaded) { - Object.entries(state.datasourceMap).forEach(([datasourceId, datasource]) => { + Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { if (state.datasourceStates[datasourceId].isLoading) { datasource - .initialize(props.doc && props.doc.state.datasource) + .initialize(state.datasourceStates[datasourceId].state || undefined) .then(datasourceState => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', newState: datasourceState, + datasourceId, }); }) .catch(onError); } }); } - }, [props.doc, Object.keys(state.datasourceStates), state.activeDatasourceId]); + }, [allLoaded]); - const datasourceEntries: Array<[string, DatasourcePublicAPI]> = Object.keys( - state.datasourceMap - ).map(key => { - const ds = state.datasourceMap[key]; + const datasourcePublicAPIs: Array<[string, DatasourcePublicAPI]> = Object.entries( + props.datasourceMap + ).map(([key, ds]) => { return [ key, ds.getPublicAPI(state.datasourceStates[key].state, (newState: unknown) => { dispatch({ type: 'UPDATE_DATASOURCE_STATE', newState, + datasourceId: key, }); }), ]; }); - const layerToDatasourceId: Record = {}; const datasourceLayers: Record = {}; - datasourceEntries.forEach(([id, publicAPI]) => { + datasourcePublicAPIs.forEach(([id, publicAPI]) => { const stateWrapper = state.datasourceStates[id]; if (stateWrapper.isLoading) { return; } const dsState = stateWrapper.state; - const layers = state.datasourceMap[id].getLayers(dsState); + const layers = props.datasourceMap[id].getLayers(dsState); layers.forEach(layer => { - layerToDatasourceId[layer] = id; datasourceLayers[layer] = publicAPI; }); }); const framePublicAPI: FramePublicAPI = { - layerIdToDatasource: layerToDatasourceId, datasourceLayers, addNewLayer: () => { const newLayerId = generateId(); - const newState = state.datasourceMap[state.activeDatasourceId!].insertLayer( + const newState = props.datasourceMap[state.activeDatasourceId!].insertLayer( state.datasourceStates[state.activeDatasourceId!].state, newLayerId ); @@ -146,7 +144,7 @@ export function EditorFrame(props: EditorFrameProps) { ? props.visualizationMap[state.visualization.activeId] : undefined; - if (datasource) { + if (datasource && allLoaded) { return ( { if (datasource && visualization) { save({ - datasource, + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), dispatch, visualization, state, @@ -204,7 +208,7 @@ export function EditorFrame(props: EditorFrameProps) { >; datasourceStates: Record; activeDatasourceId: string | null; - layerIdToDatasource: FramePublicAPI['layerIdToDatasource']; + layerIdToDatasource: Record; } export type Action = @@ -43,6 +42,7 @@ export type Action = | { type: 'UPDATE_DATASOURCE_STATE'; newState: unknown; + datasourceId: string; } | { type: 'UPDATE_VISUALIZATION_STATE'; @@ -76,16 +76,18 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { return { saving: false, title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), - datasourceMap: props.datasourceMap, - datasourceStates: props.initialDatasourceId - ? { - [props.initialDatasourceId]: { - state: null, - isLoading: Boolean(props.initialDatasourceId), - }, - } + datasourceStates: props.doc + ? _.mapValues(props.doc.state.datasourceStates, state => ({ isLoading: true, state })) + : props.initialDatasourceIds + ? props.initialDatasourceIds.reduce( + (stateMap, datasourceId) => ({ + ...stateMap, + [datasourceId]: { state: null, isLoading: true }, + }), + {} + ) : {}, - activeDatasourceId: props.initialDatasourceId, + activeDatasourceId: props.initialDatasourceIds ? props.initialDatasourceIds[0] : null, visualization: { state: null, activeId: props.initialVisualizationId, @@ -109,17 +111,17 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, persistedId: action.doc.id, title: action.doc.title, - datasourceStates: action.doc.datasourceType - ? { - ...state.datasourceStates, - [action.doc.datasourceType]: { - isLoading: true, - state: action.doc.state.datasource, - }, - } - : state.datasourceStates, - activeDatasourceId: action.doc.datasourceType || null, - + datasourceStates: Object.entries(action.doc.state.datasourceStates).reduce( + (stateMap, [datasourceId, datasourceState]) => ({ + ...stateMap, + [datasourceId]: { + isLoading: true, + state: datasourceState, + }, + }), + {} + ), + activeDatasourceId: Object.keys(action.doc.state.datasourceStates)[0], visualization: { ...state.visualization, activeId: action.doc.visualizationType, @@ -163,7 +165,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, datasourceStates: { ...state.datasourceStates, - [state.activeDatasourceId!]: { + [action.datasourceId]: { state: action.newState, isLoading: false, }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index 6c3896ea477e7..ae3c1cf697111 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -57,7 +57,6 @@ export type FrameMock = jest.Mocked; export function createMockFramePublicAPI(): FrameMock { return { datasourceLayers: {}, - layerIdToDatasource: {}, addNewLayer: jest.fn(() => ''), }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 8763da31d6954..899cb158407a1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -217,7 +217,9 @@ export function InitializedEditor({ datasourceMap={datasources} visualizationMap={visualizations} // layerToDatasourceId={layerToDatasourceId} - initialDatasourceId={(doc && doc.datasourceType) || firstDatasourceId || null} + initialDatasourceIds={ + (doc && Object.keys(doc.state.datasourceStates)) || [firstDatasourceId] || null + } initialVisualizationId={(doc && doc.visualizationType) || firstVisualizationId || null} ExpressionRenderer={expressionRenderer} redirectTo={path => routeProps.history.push(path)} diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index b87d3e0f03168..ae9eb85c5f9b3 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -10,8 +10,6 @@ export interface Document { id?: string; type?: string; visualizationType: string | null; - activeDatasourceId: string | null; - // datasourceType: string | null; title: string; // The state is saved as a JSON string for now state: { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 34223175d1aaf..b95028955810d 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -176,7 +176,6 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; - layerIdToDatasource: Record; // Adds a new layer. This triggers a re-render addNewLayer: () => string; } From e35bc9fc5e95a7fc9deb3c0b1febed17774eabde Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Jul 2019 15:06:03 +0200 Subject: [PATCH 24/67] simplify init routine --- x-pack/legacy/plugins/lens/mappings.json | 3 +++ .../editor_frame/editor_frame.tsx | 3 +-- .../editor_frame_plugin/editor_frame/save.ts | 7 +------ .../editor_frame/state_management.ts | 20 +++++++++---------- .../public/editor_frame_plugin/plugin.tsx | 4 +--- 5 files changed, 15 insertions(+), 22 deletions(-) diff --git a/x-pack/legacy/plugins/lens/mappings.json b/x-pack/legacy/plugins/lens/mappings.json index ec73f4ad32f87..f7bf55fd76a67 100644 --- a/x-pack/legacy/plugins/lens/mappings.json +++ b/x-pack/legacy/plugins/lens/mappings.json @@ -7,6 +7,9 @@ "visualizationType": { "type": "keyword" }, + "activeDatasourceId": { + "type": "keyword" + }, "state": { "type": "text" } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index b5160127343ef..6a101fefb01c0 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -25,9 +25,8 @@ export interface EditorFrameProps { store: SavedObjectStore; datasourceMap: Record; visualizationMap: Record; - // layerToDatasourceId: Record; redirectTo: (path: string) => void; - initialDatasourceIds: string[] | null; + initialDatasourceId: string | null; initialVisualizationId: string | null; ExpressionRenderer: ExpressionRenderer; onError: (e: { message: string }) => void; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 9c5f2d5147f29..2e7b3111fc8c6 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -18,7 +18,6 @@ export interface Props { } export async function save({ - // datasource, activeDatasources, dispatch, redirectTo, @@ -39,13 +38,9 @@ export async function save({ title: state.title, type: 'lens', visualizationType: state.visualization.activeId, - // datasourceType: state.datasource.activeId, - // datasourceType: state.activeDatasourceId, - // activeDatasourceId: state.activeDatasourceId, + activeDatasourceId: state.activeDatasourceId!, state: { - // datasource: datasource.getPersistableState(state.datasource.state), datasourceStates, - // datasources: datasource.getPersistableState(state.datasource.state), visualization: visualization.getPersistableState(state.visualization.state), }, }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index f6e40550d432d..64a65569aebc0 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -73,16 +73,14 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), datasourceStates: props.doc ? _.mapValues(props.doc.state.datasourceStates, state => ({ isLoading: true, state })) - : props.initialDatasourceIds - ? props.initialDatasourceIds.reduce( - (stateMap, datasourceId) => ({ - ...stateMap, - [datasourceId]: { state: null, isLoading: true }, - }), - {} - ) - : {}, - activeDatasourceId: props.initialDatasourceIds ? props.initialDatasourceIds[0] : null, + : props.initialDatasourceId + ? { + [props.initialDatasourceId]: { + state: null, + isLoading: true + } + }: {}, + activeDatasourceId: props.initialDatasourceId ? props.initialDatasourceId : null, visualization: { state: null, activeId: props.initialVisualizationId, @@ -116,7 +114,7 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta }), {} ), - activeDatasourceId: Object.keys(action.doc.state.datasourceStates)[0], + activeDatasourceId: action.doc.activeDatasourceId, visualization: { ...state.visualization, activeId: action.doc.visualizationType, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index fd26b26141125..8495777587e93 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -215,9 +215,7 @@ export function InitializedEditor({ store={store} datasourceMap={datasources} visualizationMap={visualizations} - initialDatasourceIds={ - (doc && Object.keys(doc.state.datasourceStates)) || [firstDatasourceId] || null - } + initialDatasourceId={(doc && doc.activeDatasourceId) || firstDatasourceId || null} initialVisualizationId={(doc && doc.visualizationType) || firstVisualizationId || null} ExpressionRenderer={expressionRenderer} redirectTo={path => routeProps.history.push(path)} From cd90a9d8afa689ea3d46b5dd4e844375d4998445 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Jul 2019 15:21:44 +0200 Subject: [PATCH 25/67] fix save tests --- .../editor_frame/save.test.ts | 171 ++++++++---------- .../editor_frame_plugin/plugin.test.tsx | 2 - .../public/persistence/saved_object_store.ts | 1 + 3 files changed, 79 insertions(+), 95 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index 55faded176e51..fd93b005572fa 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -12,16 +12,21 @@ describe('save editor frame state', () => { const saveArgs: Props = { dispatch: jest.fn(), redirectTo: jest.fn(), - activeDatasources: {}, - // datasource: { getPersistableState: x => x }, + activeDatasources: { + indexpattern: { + getPersistableState: x => x, + }, + }, visualization: { getPersistableState: x => x }, state: { title: 'aaa', - // datasource: { activeId: '1', isLoading: false, state: {} }, - datasourceMap: {}, - datasourceStates: {}, + datasourceStates: { + indexpattern: { + state: 'hello', + isLoading: false, + }, + }, activeDatasourceId: 'indexpattern', - // layerIdToDatasource: {}, saving: false, visualization: { activeId: '2', state: {} }, }, @@ -40,20 +45,13 @@ describe('save editor frame state', () => { (action.type === 'SAVING' && action.isSaving && saved) || (action.type === 'SAVING' && !action.isSaving && !saved) ) { - throw new Error('Saving status was incorrectly set'); + throw new Error('Saving status was incorrectly set' + action.isSaving + ' ' + saved); } }); await save({ ...saveArgs, dispatch, - // state: { - // ...saveArgs.state, - // title: 'aaa', - // datasource: { activeId: '1', isLoading: false, state: {} }, - // saving: false, - // visualization: { activeId: '2', state: {} }, - // }, store: { async save() { saved = true; @@ -73,12 +71,6 @@ describe('save editor frame state', () => { save({ ...saveArgs, dispatch, - // state: { - // title: 'aaa', - // datasource: { activeId: '1', isLoading: false, state: {} }, - // saving: false, - // visualization: { activeId: '2', state: {} }, - // }, store: { async save() { throw new Error('aw shnap!'); @@ -99,41 +91,24 @@ describe('save editor frame state', () => { datasource.getPersistableState.mockImplementation(state => ({ stuff: `${state}_datasource_persisted`, })); + await save({ ...saveArgs, + activeDatasources: { + indexpattern: datasource, + }, store, - // state: { - // title: 'bbb', - // // datasource: { activeId: '1', isLoading: false, state: '2' }, - // datasourceStates - // saving: false, - // visualization: { activeId: '3', state: '4' }, - // }, - state: { title: 'bbb', - // datasource: { activeId: '1', isLoading: false, state: {} }, datasourceStates: { indexpattern: { - state: 'hello', + state: '2', isLoading: false, }, }, - datasourceMap: { - // indexpattern: createMockDatasource() - // getPersistableState(state) { - // return { - // stuff: `${state}_datsource_persisted`, - // }; - // }, - // }, - indexpattern: datasource, - }, - // datasourceStates: {}, activeDatasourceId: 'indexpattern', - // layerIdToDatasource: {}, saving: false, - visualization: { activeId: '2', state: {} }, + visualization: { activeId: '3', state: '4' }, }, visualization: { @@ -146,11 +121,9 @@ describe('save editor frame state', () => { }); expect(store.save).toHaveBeenCalledWith({ - // datasourceType: '1', activeDatasourceId: 'indexpattern', id: undefined, state: { - // datasource: { stuff: '2_datsource_persisted' }, datasourceStates: { indexpattern: { stuff: '2_datasource_persisted', @@ -164,52 +137,64 @@ describe('save editor frame state', () => { }); }); - // it('redirects to the edit screen if the id changes', async () => { - // const redirectTo = jest.fn(); - // const dispatch = jest.fn(); - // await save({ - // ...saveArgs, - // dispatch, - // redirectTo, - // state: { - // title: 'ccc', - // datasource: { activeId: '1', isLoading: false, state: {} }, - // saving: false, - // visualization: { activeId: '2', state: {} }, - // }, - // store: { - // async save() { - // return { id: 'bazinga' }; - // }, - // }, - // }); - - // expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PERSISTED_ID', id: 'bazinga' }); - // expect(redirectTo).toHaveBeenCalledWith('/edit/bazinga'); - // }); - - // it('does not redirect to the edit screen if the id does not change', async () => { - // const redirectTo = jest.fn(); - // const dispatch = jest.fn(); - // await save({ - // ...saveArgs, - // dispatch, - // redirectTo, - // state: { - // title: 'ddd', - // datasource: { activeId: '1', isLoading: false, state: {} }, - // persistedId: 'foo', - // saving: false, - // visualization: { activeId: '2', state: {} }, - // }, - // store: { - // async save() { - // return { id: 'foo' }; - // }, - // }, - // }); - - // expect(dispatch.mock.calls.some(({ type }) => type === 'UPDATE_PERSISTED_ID')).toBeFalsy(); - // expect(redirectTo).not.toHaveBeenCalled(); - // }); + it('redirects to the edit screen if the id changes', async () => { + const redirectTo = jest.fn(); + const dispatch = jest.fn(); + await save({ + ...saveArgs, + dispatch, + redirectTo, + state: { + title: 'ccc', + datasourceStates: { + indexpattern: { + state: {}, + isLoading: false, + }, + }, + activeDatasourceId: 'indexpattern', + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + return { id: 'bazinga' }; + }, + }, + }); + + expect(dispatch).toHaveBeenCalledWith({ type: 'UPDATE_PERSISTED_ID', id: 'bazinga' }); + expect(redirectTo).toHaveBeenCalledWith('/edit/bazinga'); + }); + + it('does not redirect to the edit screen if the id does not change', async () => { + const redirectTo = jest.fn(); + const dispatch = jest.fn(); + await save({ + ...saveArgs, + dispatch, + redirectTo, + state: { + title: 'ddd', + datasourceStates: { + indexpattern: { + state: {}, + isLoading: false, + }, + }, + activeDatasourceId: 'indexpattern', + persistedId: 'foo', + saving: false, + visualization: { activeId: '2', state: {} }, + }, + store: { + async save() { + return { id: 'foo' }; + }, + }, + }); + + expect(dispatch.mock.calls.some(({ type }) => type === 'UPDATE_PERSISTED_ID')).toBeFalsy(); + expect(redirectTo).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index 84582c215deee..134197bee4b27 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -76,7 +76,6 @@ describe('editor_frame plugin', () => { it('should load the document, if persistedId is defined', async () => { const doc: Document = { - // datasourceType: 'indexpattern', id: 'hoi', activeDatasourceId: 'indexpattern', state: { @@ -190,7 +189,6 @@ describe('editor_frame plugin', () => { const component = mount( ; From 9fac4904db5b0f5ea346efc2bc26799e6f7837b7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Jul 2019 15:25:26 +0200 Subject: [PATCH 26/67] fix persistence tests --- .../persistence/saved_object_store.test.ts | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts index 60cd67535a3c2..f46164d9abb9f 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.test.ts @@ -26,13 +26,11 @@ describe('LensStore', () => { const doc = await store.save({ title: 'Hello', visualizationType: 'bar', - // datasourceType: 'indexpattern', activeDatasourceId: 'indexpattern', state: { datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, - // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, }); @@ -41,27 +39,24 @@ describe('LensStore', () => { id: 'FOO', title: 'Hello', visualizationType: 'bar', - // datasourceType: 'indexpattern', - activeDatasourceId: 'indexPattern', + activeDatasourceId: 'indexpattern', state: { datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, - // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }, }); expect(client.create).toHaveBeenCalledTimes(1); expect(client.create).toHaveBeenCalledWith('lens', { - // datasourceType: 'indexpattern', title: 'Hello', visualizationType: 'bar', + activeDatasourceId: 'indexpattern', state: JSON.stringify({ datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, }, - // datasource: { type: 'index_pattern', indexPattern: '.kibana_test' }, visualization: { x: 'foo', y: 'baz' }, }), }); @@ -73,12 +68,10 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - // datasourceType: 'indexpattern', activeDatasourceId: 'indexpattern', state: { - // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, datasourceStates: { - indexpattern: { type: 'index_pattern', indexPattern: '.kibana_test' }, + indexpattern: { type: 'index_pattern', indexPattern: 'lotr' }, }, visualization: { gear: ['staff', 'pointy hat'] }, }, @@ -88,9 +81,8 @@ describe('LensStore', () => { id: 'Gandalf', title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - datasourceType: 'indexpattern', + activeDatasourceId: 'indexpattern', state: { - // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' }, }, @@ -102,9 +94,8 @@ describe('LensStore', () => { expect(client.update).toHaveBeenCalledWith('lens', 'Gandalf', { title: 'Even the very wise cannot see all ends.', visualizationType: 'line', - datasourceType: 'indexpattern', + activeDatasourceId: 'indexpattern', state: JSON.stringify({ - // datasource: { type: 'index_pattern', indexPattern: 'lotr' }, datasourceStates: { indexpattern: { type: 'index_pattern', indexPattern: 'lotr' }, }, From 5b997afa08f591e314310870e5f5511e7afd67a6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 19 Jul 2019 16:01:09 +0200 Subject: [PATCH 27/67] fix state management tests --- .../editor_frame/state_management.test.ts | 150 +++++++++++------- .../editor_frame/state_management.ts | 18 ++- 2 files changed, 102 insertions(+), 66 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index 5f1861269dc0e..b138100e99282 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -31,7 +31,7 @@ describe('editor_frame state management', () => { it('should store initial datasource and visualization', () => { const initialState = getInitialState(props); - expect(initialState.datasource.activeId).toEqual('testDatasource'); + expect(initialState.activeDatasourceId).toEqual('testDatasource'); expect(initialState.visualization.activeId).toEqual('testVis'); }); @@ -57,11 +57,13 @@ describe('editor_frame state management', () => { const newVisState = {}; const newState = reducer( { - datasource: { - activeId: 'testDatasource', - state: {}, - isLoading: false, + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'testDatasource', saving: false, title: 'aaa', visualization: { @@ -82,11 +84,13 @@ describe('editor_frame state management', () => { const newDatasourceState = {}; const newState = reducer( { - datasource: { - activeId: 'testDatasource', - state: {}, - isLoading: false, + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'testDatasource', saving: false, title: 'bbb', visualization: { @@ -97,10 +101,11 @@ describe('editor_frame state management', () => { { type: 'UPDATE_DATASOURCE_STATE', newState: newDatasourceState, + datasourceId: 'testDatasource', } ); - expect(newState.datasource.state).toBe(newDatasourceState); + expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState); }); it('should should switch active visualization', () => { @@ -108,11 +113,13 @@ describe('editor_frame state management', () => { const newVisState = {}; const newState = reducer( { - datasource: { - activeId: 'testDatasource', - state: {}, - isLoading: false, + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'testDatasource', saving: false, title: 'ccc', visualization: { @@ -136,11 +143,13 @@ describe('editor_frame state management', () => { const newDatasourceState = {}; const newState = reducer( { - datasource: { - activeId: 'testDatasource', - state: {}, - isLoading: false, + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'testDatasource', saving: false, title: 'ddd', visualization: { @@ -157,17 +166,20 @@ describe('editor_frame state management', () => { ); expect(newState.visualization.state).toBe(newVisState); - expect(newState.datasource.state).toBe(newDatasourceState); + expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState); }); - it('should should switch active datasource and purge visualization state', () => { + // TODO this behavior is wrong - multiple datasources can be active at once + it.skip('should should switch active datasource and purge visualization state', () => { const newState = reducer( { - datasource: { - activeId: 'testDatasource', - state: {}, - isLoading: false, + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'testDatasource', saving: false, title: 'eee', visualization: { @@ -183,18 +195,20 @@ describe('editor_frame state management', () => { expect(newState.visualization.state).toEqual(null); expect(newState.visualization.activeId).toBe(null); - expect(newState.datasource.activeId).toBe('testDatasource2'); - expect(newState.datasource.state).toBe(null); + expect(newState.activeDatasourceId).toBe('testDatasource2'); + expect(newState.datasourceStates.testDatasource.state).toBe(null); }); it('should mark as saving', () => { const newState = reducer( { - datasource: { - activeId: 'a', - state: {}, - isLoading: false, + datasourceStates: { + a: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'a', saving: false, title: 'fff', visualization: { @@ -214,11 +228,13 @@ describe('editor_frame state management', () => { it('should mark as saved', () => { const newState = reducer( { - datasource: { - activeId: 'a', - state: {}, - isLoading: false, + datasourceStates: { + a: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'a', saving: false, title: 'hhh', visualization: { @@ -238,11 +254,13 @@ describe('editor_frame state management', () => { it('should change the persisted id', () => { const newState = reducer( { - datasource: { - activeId: 'a', - state: {}, - isLoading: false, + datasourceStates: { + a: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'a', saving: false, title: 'iii', visualization: { @@ -262,11 +280,13 @@ describe('editor_frame state management', () => { it('should reset the state', () => { const newState = reducer( { - datasource: { - activeId: 'a', - state: {}, - isLoading: false, + datasourceStates: { + a: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'a', saving: false, title: 'jjj', visualization: { @@ -277,11 +297,13 @@ describe('editor_frame state management', () => { { type: 'RESET', state: { - datasource: { - activeId: 'z', - isLoading: false, - state: { hola: 'muchacho' }, + datasourceStates: { + z: { + isLoading: false, + state: { hola: 'muchacho' }, + }, }, + activeDatasourceId: 'z', persistedId: 'bar', saving: false, title: 'lll', @@ -294,11 +316,13 @@ describe('editor_frame state management', () => { ); expect(newState).toMatchObject({ - datasource: { - activeId: 'z', - isLoading: false, - state: { hola: 'muchacho' }, + datasourceStates: { + z: { + isLoading: false, + state: { hola: 'muchacho' }, + }, }, + activeDatasourceId: 'z', persistedId: 'bar', saving: false, visualization: { @@ -311,11 +335,13 @@ describe('editor_frame state management', () => { it('should load the state from the doc', () => { const newState = reducer( { - datasource: { - activeId: 'a', - state: {}, - isLoading: false, + datasourceStates: { + a: { + state: {}, + isLoading: false, + }, }, + activeDatasourceId: 'a', saving: false, title: 'mmm', visualization: { @@ -326,10 +352,10 @@ describe('editor_frame state management', () => { { type: 'VISUALIZATION_LOADED', doc: { - datasourceType: 'a', id: 'b', + activeDatasourceId: 'a', state: { - datasource: { foo: 'c' }, + datasourceStates: { a: { foo: 'c' } }, visualization: { bar: 'd' }, }, title: 'heyo!', @@ -340,11 +366,13 @@ describe('editor_frame state management', () => { ); expect(newState).toEqual({ - datasource: { - activeId: 'a', - isLoading: true, - state: { - foo: 'c', + activeDatasourceId: 'a', + datasourceStates: { + a: { + isLoading: true, + state: { + foo: 'c', + }, }, }, persistedId: 'b', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index 64a65569aebc0..bfdd822bf10d0 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -74,12 +74,13 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { datasourceStates: props.doc ? _.mapValues(props.doc.state.datasourceStates, state => ({ isLoading: true, state })) : props.initialDatasourceId - ? { - [props.initialDatasourceId]: { - state: null, - isLoading: true + ? { + [props.initialDatasourceId]: { + state: null, + isLoading: true, + }, } - }: {}, + : {}, activeDatasourceId: props.initialDatasourceId ? props.initialDatasourceId : null, visualization: { state: null, @@ -142,6 +143,13 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta case 'SWITCH_VISUALIZATION': return { ...state, + datasourceStates: + state.activeDatasourceId && action.datasourceState + ? { + ...state.datasourceStates, + [state.activeDatasourceId]: { state: action.datasourceState, isLoading: false }, + } + : state.datasourceStates, visualization: { ...state.visualization, activeId: action.newVisualizationId, From 26bc89c2d8aae7d59c0bc7dd74c036b4cf8bc68c Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 19 Jul 2019 13:57:34 -0400 Subject: [PATCH 28/67] Ensure the data table is aligned to the top --- .../lens/public/datatable_visualization_plugin/index.scss | 3 +++ x-pack/legacy/plugins/lens/public/index.scss | 1 + 2 files changed, 4 insertions(+) create mode 100644 x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.scss diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.scss b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.scss new file mode 100644 index 0000000000000..e36326d710f72 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/index.scss @@ -0,0 +1,3 @@ +.lnsDataTable { + align-self: flex-start; +} diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index 6a95e26dfbfd8..a512e066c9956 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -2,6 +2,7 @@ @import 'src/legacy/ui/public/styles/_styling_constants'; @import './xy_visualization_plugin/index'; +@import './datatable_visualization_plugin/index'; // @import './xy_visualization_plugin/xy_expression.scss'; @import './indexpattern_plugin/indexpattern'; @import './drag_drop/drag_drop.scss'; From cbcb341b6ba5cb2a59da1b889efb609d12faf442 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 19 Jul 2019 13:58:43 -0400 Subject: [PATCH 29/67] Add layer support to Lens datatable --- .../plugins/lens/public/app_plugin/plugin.tsx | 2 +- .../expression.tsx | 12 +- .../visualization.test.tsx | 148 +++++----- .../visualization.tsx | 257 ++++++++---------- .../lens/public/multi_column_editor/index.ts | 7 + .../multi_column_editor.tsx | 0 .../xy_config_panel.tsx | 2 +- 7 files changed, 208 insertions(+), 220 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts rename x-pack/legacy/plugins/lens/public/{xy_visualization_plugin => multi_column_editor}/multi_column_editor.tsx (100%) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx index 51f2193ef4691..a559c0a94465b 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -30,7 +30,7 @@ export class AppPlugin { editorFrame.registerDatasource('indexpattern', indexPattern); editorFrame.registerVisualization('xy', xyVisualization); - // editorFrame.registerVisualization('datatable', datatableVisualization); + editorFrame.registerVisualization('datatable', datatableVisualization); this.instance = editorFrame.createInstance({}); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index 44799bc7a403b..ddb6df8d766cf 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -125,17 +125,23 @@ export const datatableRenderer: RenderFunction = { }; function DatatableComponent(props: DatatableProps) { + // TODO: We should probalby change data.rows to be data.layers, since + // each row is actually an array of layer objects + const firstLayer = props.data.rows[0]; + const firstTable = firstLayer && (Object.values(firstLayer)[0] as KibanaDatatable); + return ( { + .map((field, index) => { return { - field: props.args.columns.columnIds[index], + field, name: props.args.columns.labels[index], }; }) .filter(({ field }) => !!field)} - items={props.data.rows} + items={firstTable ? firstTable.rows : []} /> ); } diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx index d43008f68c330..c927c0bd645fe 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx @@ -8,79 +8,79 @@ import React from 'react'; import { createMockDatasource } from '../editor_frame_plugin/mocks'; import { DatatableVisualizationState, - DatatableConfigPanel, datatableVisualization, + DataTableLayer, } from './visualization'; import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; -import { Operation, DataType } from '../types'; +import { Operation, DataType, FramePublicAPI } from '../types'; import { generateId } from '../id_generator'; jest.mock('../id_generator'); +function mockFrame(): FramePublicAPI { + return { + addNewLayer: () => 'aaa', + datasourceLayers: {}, + }; +} + describe('Datatable Visualization', () => { describe('#initialize', () => { it('should initialize from the empty state', () => { - const datasource = createMockDatasource(); (generateId as jest.Mock).mockReturnValueOnce('id'); - expect(datatableVisualization.initialize(datasource.publicAPIMock)).toEqual({ - columns: [{ id: 'id', label: '' }], + expect(datatableVisualization.initialize(mockFrame(), undefined)).toEqual({ + layers: [ + { + layerId: 'aaa', + columns: ['id'], + }, + ], }); }); it('should initialize from a persisted state', () => { - const datasource = createMockDatasource(); const expectedState: DatatableVisualizationState = { - columns: [{ id: 'saved', label: 'label' }], + layers: [ + { + layerId: 'foo', + columns: ['saved'], + }, + ], }; - expect(datatableVisualization.initialize(datasource.publicAPIMock, expectedState)).toEqual( - expectedState - ); + expect(datatableVisualization.initialize(mockFrame(), expectedState)).toEqual(expectedState); }); }); describe('#getPersistableState', () => { it('should persist the internal state', () => { const expectedState: DatatableVisualizationState = { - columns: [{ id: 'saved', label: 'label' }], + layers: [ + { + layerId: 'baz', + columns: ['a', 'b', 'c'], + }, + ], }; expect(datatableVisualization.getPersistableState(expectedState)).toEqual(expectedState); }); }); - describe('DatatableConfigPanel', () => { - it('should update the column label', () => { - const setState = jest.fn(); - const wrapper = mount( - {} }} - datasource={createMockDatasource().publicAPIMock} - setState={setState} - state={{ columns: [{ id: 'saved', label: 'label' }] }} - /> - ); - - const labelEditor = wrapper.find('[data-test-subj="lnsDatatable-columnLabel"]').at(1); - - act(() => { - labelEditor.simulate('change', { target: { value: 'New Label' } }); - }); - - expect(setState).toHaveBeenCalledWith({ - columns: [{ id: 'saved', label: 'New Label' }], - }); - }); - - it('should allow all operations to be shown', () => { + describe('DataTableLayer', () => { + it('allows all kinds of operations', () => { const setState = jest.fn(); const datasource = createMockDatasource(); + const layer = { layerId: 'a', columns: ['b', 'c'] }; mount( - {} }} - datasource={datasource.publicAPIMock} + frame={{ + addNewLayer: jest.fn(), + datasourceLayers: { a: datasource.publicAPIMock }, + }} + layer={layer} setState={setState} - state={{ columns: [{ id: 'saved', label: 'label' }] }} + state={{ layers: [layer] }} /> ); @@ -105,52 +105,72 @@ describe('Datatable Visualization', () => { ); }); - it('should remove a column', () => { + it('allows columns to be removed', () => { const setState = jest.fn(); - const wrapper = mount( - {} }} - datasource={createMockDatasource().publicAPIMock} + frame={{ + addNewLayer: jest.fn(), + datasourceLayers: { a: datasource.publicAPIMock }, + }} + layer={layer} setState={setState} - state={{ columns: [{ id: 'saved', label: '' }, { id: 'second', label: '' }] }} + state={{ layers: [layer] }} /> ); - act(() => { - wrapper - .find('[data-test-subj="lnsDatatable_dimensionPanelRemove_saved"]') - .first() - .simulate('click'); - }); + const onRemove = component + .find('[data-test-subj="datatable_multicolumnEditor"]') + .first() + .prop('onRemove') as (k: string) => {}; + + onRemove('b'); expect(setState).toHaveBeenCalledWith({ - columns: [{ id: 'second', label: '' }], + layers: [ + { + layerId: 'a', + columns: ['c'], + }, + ], }); }); - it('should be able to add more columns', () => { + it('allows columns to be added', () => { + (generateId as jest.Mock).mockReturnValueOnce('d'); const setState = jest.fn(); const datasource = createMockDatasource(); - const wrapper = mount( - {} }} - datasource={datasource.publicAPIMock} + frame={{ + addNewLayer: jest.fn(), + datasourceLayers: { a: datasource.publicAPIMock }, + }} + layer={layer} setState={setState} - state={{ columns: [{ id: 'saved', label: 'label' }] }} + state={{ layers: [layer] }} /> ); - (generateId as jest.Mock).mockReturnValueOnce('newId'); + const onAdd = component + .find('[data-test-subj="datatable_multicolumnEditor"]') + .first() + .prop('onAdd') as () => {}; - act(() => { - wrapper - .find('[data-test-subj="lnsDatatable_dimensionPanel_add"]') - .first() - .simulate('click'); - }); + onAdd(); expect(setState).toHaveBeenCalledWith({ - columns: [{ id: 'saved', label: 'label' }, { id: 'newId', label: '' }], + layers: [ + { + layerId: 'a', + columns: ['b', 'c', 'd'], + }, + ], }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index 7bd1dbedd1ac0..717ee7047be42 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -6,130 +6,71 @@ import React from 'react'; import { render } from 'react-dom'; -import { - EuiButtonIcon, - EuiForm, - EuiFieldText, - EuiFormRow, - EuiButton, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiForm, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; +import { MultiColumnEditor } from '../multi_column_editor'; import { SuggestionRequest, Visualization, VisualizationProps, VisualizationSuggestion, } from '../types'; -import { NativeRenderer } from '../native_renderer'; import { generateId } from '../id_generator'; -export interface DatatableVisualizationState { - columns: Array<{ - id: string; - label: string; - }>; +export interface LayerState { + layerId: string; + columns: string[]; } -export function DatatableConfigPanel(props: VisualizationProps) { - const { state, datasource, setState } = props; - - return ( - - {state.columns.map(({ id, label }, index) => { - const operation = datasource.getOperationForColumnId(id); - return ( - <> - - { - const newColumns = [...state.columns]; - newColumns[index] = { ...newColumns[index], label: e.target.value }; - setState({ - ...state, - columns: newColumns, - }); - }} - placeholder={ - operation - ? operation.label - : i18n.translate('xpack.lens.datatable.columnTitlePlaceholder', { - defaultMessage: 'Title', - }) - } - aria-label={i18n.translate('xpack.lens.datatable.columnTitlePlaceholder', { - defaultMessage: 'Title', - })} - /> - +export interface DatatableVisualizationState { + layers: LayerState[]; +} - - - - true, - }} - /> - +function newLayerState(layerId: string): LayerState { + return { + layerId, + columns: [generateId()], + }; +} - - { - datasource.removeColumnInTableSpec(id); - const newColumns = [...state.columns]; - newColumns.splice(index, 1); - setState({ - ...state, - columns: newColumns, - }); - }} - aria-label={i18n.translate('xpack.lens.datatable.removeColumnAriaLabel', { - defaultMessage: 'Remove', - })} - /> - - - - - ); - })} +function updateColumns( + state: DatatableVisualizationState, + layer: LayerState, + fn: (columns: string[]) => string[] +) { + const columns = fn(layer.columns); + const updatedLayer = { ...layer, columns }; + const layers = state.layers.map(l => (l.layerId === layer.layerId ? updatedLayer : l)); + return { ...state, layers }; +} -
- { - const newColumns = [...state.columns]; - newColumns.push({ - id: generateId(), - label: '', - }); - setState({ - ...state, - columns: newColumns, - }); - }} - iconType="plusInCircle" - /> -
-
+export function DataTableLayer({ + layer, + frame, + state, + setState, + dragDropContext, +}: { layer: LayerState } & VisualizationProps) { + const datasource = frame.datasourceLayers[layer.layerId]; + return ( + + true} + layerId={layer.layerId} + onAdd={() => setState(updateColumns(state, layer, columns => [...columns, generateId()]))} + onRemove={column => + setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) + } + testSubj="datatable_columns" + data-test-subj="datatable_multicolumnEditor" + /> + ); } @@ -137,19 +78,19 @@ export const datatableVisualization: Visualization< DatatableVisualizationState, DatatableVisualizationState > = { - initialize(datasource, state) { + initialize(frame, state) { + const layerId = Object.keys(frame.datasourceLayers)[0] || frame.addNewLayer(); return ( state || { - columns: [ - { - id: generateId(), - label: '', - }, - ], + layers: [newLayerState(layerId)], } ); }, + getLayerIds(state) { + return state.layers.map(l => l.layerId); + }, + getPersistableState: state => state, getSuggestions({ @@ -170,10 +111,12 @@ export const datatableVisualization: Visualization< score: 1, datasourceSuggestionId: table.datasourceSuggestionId, state: { - columns: table.columns.map(col => ({ - id: col.columnId, - label: col.operation.label, - })), + layers: [ + { + layerId: `${generateId()}`, + columns: table.columns.map(col => col.columnId), + }, + ], }, previewIcon: 'visTable', }; @@ -183,41 +126,53 @@ export const datatableVisualization: Visualization< renderConfigPanel: (domElement, props) => render( - + + {props.state.layers.map(layer => ( + + ))} + , domElement ), - toExpression: (state, datasource) => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_datatable', - arguments: { - columns: [ - { - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_datatable_columns', - arguments: { - columnIds: state.columns.map(({ id }) => id), - labels: state.columns.map(({ id, label }) => { - if (label) { - return label; - } - const operation = datasource.getOperationForColumnId(id); - return operation ? operation.label : ''; - }), + toExpression(state, frame) { + const layer = state.layers[0]; + const datasource = frame.datasourceLayers[layer.layerId]; + const operations = layer.columns + .map(columnId => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) + .filter(o => o.operation); + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_datatable', + arguments: { + columns: [ + { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_datatable_columns', + arguments: { + columnIds: operations.map(o => o.columnId), + labels: operations.map( + o => + o.operation!.label || + i18n.translate('xpack.lens.datatable.na', { + defaultMessage: 'N/A', + }) + ), + }, }, - }, - ], - }, - ], + ], + }, + ], + }, }, - }, - ], - }), + ], + }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts b/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts new file mode 100644 index 0000000000000..92bad0dc90766 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/index.ts @@ -0,0 +1,7 @@ +/* + * 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 './multi_column_editor'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx similarity index 100% rename from x-pack/legacy/plugins/lens/public/xy_visualization_plugin/multi_column_editor.tsx rename to x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 7d92cecbcec7a..dd6a2e2661d3b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -12,7 +12,7 @@ import { EuiButton, EuiButtonGroup, EuiForm, EuiFormRow, EuiPanel, IconType } fr import { State, SeriesType, LayerConfig } from './types'; import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; -import { MultiColumnEditor } from './multi_column_editor'; +import { MultiColumnEditor } from '../multi_column_editor'; import { generateId } from '../id_generator'; const chartTypeIcons: Array<{ id: SeriesType; label: string; iconType: IconType }> = [ From 91c6881813bad8ed9fe39fc0767466c61c970b7a Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 19 Jul 2019 14:51:27 -0400 Subject: [PATCH 30/67] Give xy chart a default layer initially --- .../xy_visualization.tsx | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 192f2749aea32..72898af850a26 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -13,37 +13,30 @@ import { XYConfigPanel } from './xy_config_panel'; import { Visualization } from '../types'; import { State, PersistableState } from './types'; import { toExpression } from './to_expression'; +import { generateId } from '../id_generator'; export const xyVisualization: Visualization = { getSuggestions, - initialize(_, state) { + initialize(frame, state) { return ( state || { - // seriesType: 'bar', title: 'Empty XY Chart', legend: { isVisible: true, position: Position.Right }, - // x: { - // accessor: generateId(), - // position: Position.Bottom, - // showGridlines: false, - // title: 'X', - // }, - // layers: [ - // { - // layerId: 'first', - // xAccessor: generateId(), - // seriesType: 'bar_stacked', - // accessors: [generateId()], - // position: Position.Left, - // showGridlines: false, - // title: 'Y', - // labels: [''], - // // splitSeriesAccessors: [generateId()], - // splitSeriesAccessors: [], - // }, - // ], - layers: [], + layers: [ + { + layerId: frame.addNewLayer(), + accessors: [generateId()], + datasourceId: '', + labels: [], + position: Position.Top, + seriesType: 'bar', + showGridlines: false, + splitSeriesAccessors: [generateId()], + title: '', + xAccessor: generateId(), + }, + ], } ); }, From e263e4cd82c536bff0f1ade06b25bbfb5cf0e25a Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 19 Jul 2019 14:51:44 -0400 Subject: [PATCH 31/67] Allow deletion of layers in xy charts --- .../xy_config_panel.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index dd6a2e2661d3b..17458fdf22394 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -8,7 +8,15 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { Position } from '@elastic/charts'; -import { EuiButton, EuiButtonGroup, EuiForm, EuiFormRow, EuiPanel, IconType } from '@elastic/eui'; +import { + EuiButton, + EuiButtonGroup, + EuiForm, + EuiFormRow, + EuiPanel, + IconType, + EuiButtonIcon, +} from '@elastic/eui'; import { State, SeriesType, LayerConfig } from './types'; import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; @@ -246,11 +254,19 @@ export function XYConfigPanel(props: VisualizationProps) { defaultMessage: 'Layer', })} > - + <> + { + setState({ ...state, layers: state.layers.filter(l => l !== layer) }); + }} + /> + + Date: Fri, 19 Jul 2019 15:20:46 -0400 Subject: [PATCH 32/67] xy: Make split accessor singular Remove commented code blocks --- .../__snapshots__/xy_expression.test.tsx.snap | 56 ++- .../xy_visualization_plugin/to_expression.ts | 35 +- .../public/xy_visualization_plugin/types.ts | 18 +- .../xy_config_panel.test.tsx | 77 +---- .../xy_config_panel.tsx | 326 ++---------------- .../xy_expression.test.tsx | 42 +-- .../xy_visualization_plugin/xy_expression.tsx | 101 +++--- .../xy_suggestions.test.ts | 38 +- .../xy_visualization_plugin/xy_suggestions.ts | 13 +- .../xy_visualization.test.ts | 36 +- .../xy_visualization.tsx | 2 +- 11 files changed, 169 insertions(+), 575 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index fb5fdfc183dea..a81c164dab608 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -38,9 +38,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={Array []} xAccessor="c" yAccessors={ @@ -91,9 +95,13 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={Array []} xAccessor="c" yAccessors={ @@ -144,9 +152,13 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={Array []} xAccessor="c" yAccessors={ @@ -197,9 +209,13 @@ exports[`xy_expression XYChart component it renders line 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={Array []} xAccessor="c" yAccessors={ @@ -250,9 +266,13 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={ Array [ "c", @@ -307,9 +327,13 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={ Array [ "c", @@ -364,9 +388,13 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = }, ] } - id="a,b,c" + id="a,b,c,d" key="0" - splitSeriesAccessors={Array []} + splitSeriesAccessors={ + Array [ + "d", + ] + } stackAccessors={ Array [ "c", diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 63c6d3749ab7d..903b26aefef92 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -8,13 +8,12 @@ import { Ast } from '@kbn/interpreter/common'; import { State } from './types'; import { FramePublicAPI } from '../types'; -// export const toExpression = (state: State, datasource: DatasourcePublicAPI): Ast => { export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => { const labels: Partial> = {}; if (!state || !state.layers.length) { return null; } - // const datasource = frame.datasourceLayers.first; + state.layers.forEach(layer => { const datasource = frame.datasourceLayers[layer.layerId]; if (!datasource) { @@ -28,7 +27,17 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => }); }); - return buildExpression(state, labels); + const stateWithValidAccessors = { + ...state, + layers: state.layers.map(layer => ({ + ...layer, + accessors: layer.accessors.filter(accessor => + Boolean(frame.datasourceLayers[layer.layerId].getOperationForColumnId(accessor)) + ), + })), + }; + + return buildExpression(stateWithValidAccessors, labels); }; export const buildExpression = ( @@ -56,24 +65,6 @@ export const buildExpression = ( ], }, ], - // x: [ - // { - // type: 'expression', - // chain: [ - // { - // type: 'function', - // function: 'lens_xy_xConfig', - // arguments: { - // title: [state.x.title], - // showGridlines: [state.x.showGridlines], - // position: [state.x.position], - // accessor: [state.x.accessor], - // hide: [Boolean(state.x.hide)], - // }, - // }, - // ], - // }, - // ], layers: state.layers.map(layer => ({ type: 'expression', chain: [ @@ -90,7 +81,7 @@ export const buildExpression = ( hide: [Boolean(layer.hide)], xAccessor: [layer.xAccessor], - splitSeriesAccessors: layer.splitSeriesAccessors, + splitAccessor: [layer.splitAccessor], seriesType: [layer.seriesType], labels: layer.accessors.map(accessor => { return columnLabels[accessor] || accessor; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 0765c384b897e..1ead6125d0561 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -152,10 +152,10 @@ export const layerConfig: ExpressionFunction< ], help: 'The type of chart to display.', }, - splitSeriesAccessors: { + splitAccessor: { types: ['string'], - help: 'The columns to split by', - multi: true, + help: 'The column to split by', + multi: false, }, accessors: { types: ['string'], @@ -188,31 +188,21 @@ export type SeriesType = export type LayerConfig = AxisConfig & { layerId: string; datasourceId: string; - xAccessor: string; - accessors: string[]; labels: string[]; seriesType: SeriesType; - splitSeriesAccessors: string[]; + splitAccessor: string; }; export interface XYArgs { - // seriesType: SeriesType; legend: LegendConfig; - // y: YConfig; - // x: XConfig; - // splitSeriesAccessors: string[]; layers: LayerConfig[]; } export interface XYState { - // seriesType: SeriesType; legend: LegendConfig; - // y: YState; - // x: XConfig; layers: LayerConfig[]; - // splitSeriesAccessors: string[]; } export type State = XYState; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index 20c5fb58f239c..8c3013b7e36d8 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -29,7 +29,7 @@ describe('XYConfigPanel', () => { seriesType: 'bar', layerId: 'first', datasourceId: '', - splitSeriesAccessors: [], + splitAccessor: 'baz', xAccessor: 'foo', position: Position.Bottom, showGridlines: true, @@ -52,7 +52,6 @@ describe('XYConfigPanel', () => { const component = mount( {}} state={testState()} @@ -86,10 +85,9 @@ describe('XYConfigPanel', () => { const component = mount( {}} - state={{ ...state, layers: [{ ...state.layers[0], splitSeriesAccessors: ['c'] }] }} + state={{ ...state, layers: [{ ...state.layers[0], splitAccessor: 'c' }] }} /> ); @@ -108,10 +106,8 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -151,7 +147,6 @@ describe('XYConfigPanel', () => { const component = mount( { const component = mount( { const component = mount( { }); test('the x dimension panel accepts any operations', () => { - // const datasource = { - // ...mockDatasource(), - // renderDimensionPanel: jest.fn(), - // }; const state = testState(); const component = mount( { const component = mount( ); @@ -302,7 +288,6 @@ describe('XYConfigPanel', () => { const component = mount( { }); test('the y dimension panel accepts numeric operations', () => { - // const datasource = { - // ...mockDatasource(), - // renderDimensionPanel: jest.fn(), - // }; const state = testState(); const component = mount( ); @@ -392,10 +371,8 @@ describe('XYConfigPanel', () => { const component = mount( ); @@ -407,54 +384,4 @@ describe('XYConfigPanel', () => { y: { accessors: ['a', 'b', 'c', 'zed'] }, }); }); - - test('allows adding split dimensions', () => { - (generateId as jest.Mock).mockReturnValueOnce('foo'); - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_splitSeriesDimensionPanel_add').onClick as Function)(); - - expect(setState).toHaveBeenCalledTimes(1); - expect(setState.mock.calls[0][0]).toMatchObject({ - splitSeriesAccessors: ['a', 'b', 'c', 'foo'], - }); - }); - - // test('allows toggling the y axis gridlines', () => { - // const toggleYGridlines = (showGridlines: boolean) => { - // const setState = jest.fn(); - // const state = testState(); - // const component = mount( - // - // ); - - // (testSubj(component, 'lnsXY_yShowGridlines').onChange as Function)(); - - // expect(setState).toHaveBeenCalledTimes(1); - // return setState.mock.calls[0][0]; - // }; - - // expect(toggleYGridlines(true)).toMatchObject({ - // y: { showGridlines: false }, - // }); - // expect(toggleYGridlines(false)).toMatchObject({ - // y: { showGridlines: true }, - // }); - // }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 17458fdf22394..d6d8edac3d58b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -98,7 +98,7 @@ function newLayerState(layerId: string): LayerConfig { showGridlines: false, position: Position.Left, labels: [''], - splitSeriesAccessors: [generateId()], + splitAccessor: generateId(), }; } @@ -107,145 +107,6 @@ export function XYConfigPanel(props: VisualizationProps) { return ( - {/* - - - // type.id.includes('stacked') && state.splitSeriesAccessors.length === 0 - // ? { ...type, isDisabled: true } - // : type - // )} - options={chartTypeIcons} - idSelected={state.seriesType} - onChange={seriesType => { - const isHorizontal = seriesType === 'horizontal_bar'; - setState({ - ...state, - seriesType: seriesType as SeriesType, - // x: { - // ...state.x, - // position: isHorizontal ? Position.Left : Position.Bottom, - // }, - // y: { - // ...state.y, - // position: isHorizontal ? Position.Bottom : Position.Left, - // }, - }); - }} - isIconOnly - /> - - */} - - {/* - - - setState({ - ...state, - legend: { ...state.legend, isVisible: !state.legend.isVisible }, - }) - } - /> - - - {state.legend.isVisible && ( - - - setState({ ...state, legend: { ...state.legend, position: position as Position } }) - } - isIconOnly - /> - - )} - */} - - {/* - */} - {/* - - setState({ ...state, x: { ...state.x, title: e.target.value } })} - aria-label={i18n.translate('xpack.lens.xyChart.xTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> - - */} - - {/* - operation.isBucketed, - layer: datasource.supportsLayers && datasource.supportsLayerJoin ? 'join' : 0, - }} - /> - - - - - setState({ ...state, x: { ...state.x, showGridlines: !state.x.showGridlines } }) - } - /> - - // - // - */} - {state.layers.map((layer, index) => ( @@ -281,11 +142,6 @@ export function XYConfigPanel(props: VisualizationProps) { name="chartType" className="eui-displayInlineBlock" data-test-subj="lnsXY_seriesType" - // options={chartTypeIcons.map(type => - // type.id.includes('stacked') && state.splitSeriesAccessors.length === 0 - // ? { ...type, isDisabled: true } - // : type - // )} options={chartTypeIcons} idSelected={layer.seriesType} onChange={seriesType => { @@ -302,70 +158,42 @@ export function XYConfigPanel(props: VisualizationProps) { defaultMessage: 'X Axis', })} > - <> - {/* - - setState({ ...state, x: { ...state.x, title: e.target.value } })} - aria-label={i18n.translate('xpack.lens.xyChart.xTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> - - */} - - - operation.isBucketed, - // layer: - // datasource.supportsLayers && datasource.supportsLayerJoin ? 'join' : index, - layerId: layer.layerId, - }} - /> - - - {/* - - - setState({ ...state, x: { ...state.x, showGridlines: !state.x.showGridlines } }) - } - /> - - */} - + operation.isBucketed, + layerId: layer.layerId, + }} + /> + operation.isBucketed, + layerId: layer.layerId, + }} + /> + + + @@ -374,7 +202,7 @@ export function XYConfigPanel(props: VisualizationProps) { state, { ...layer, - splitSeriesAccessors: [...layer.splitSeriesAccessors, generateId()], + accessors: [...layer.accessors, generateId()], }, index ) @@ -386,105 +214,17 @@ export function XYConfigPanel(props: VisualizationProps) { state, { ...layer, - splitSeriesAccessors: layer.splitSeriesAccessors.filter( - col => col !== accessor - ), + accessors: layer.accessors.filter(col => col !== accessor), }, index ) ) } - filterOperations={op => op.isBucketed && op.dataType !== 'date'} - suggestedPriority={0} - // layer={index} + filterOperations={op => !op.isBucketed && op.dataType === 'number'} + testSubj="yDimensionPanel" layerId={layer.layerId} - testSubj="splitSeriesDimensionPanel" /> - - - <> - {/* - - setState({ ...state, y: { ...state.y, title: e.target.value } })} - aria-label={i18n.translate('xpack.lens.xyChart.yTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> - - */} - - - - setState( - updateLayer( - state, - { - ...layer, - accessors: [...layer.accessors, generateId()], - }, - index - ) - ) - } - onRemove={accessor => - setState( - updateLayer( - state, - { - ...layer, - accessors: layer.accessors.filter(col => col !== accessor), - }, - index - ) - ) - } - filterOperations={op => !op.isBucketed && op.dataType === 'number'} - testSubj="yDimensionPanel" - // suggestedPriority={2} - // layer={index} - layerId={layer.layerId} - /> - - - {/* - - - setState({ ...state, y: { ...state.y, showGridlines: !state.y.showGridlines } }) - } - /> - - */} - - ))} diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index fc8014b118d06..beaf54e1592cd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -27,7 +27,6 @@ function sampleArgs() { }; const args: XYArgs = { - // seriesType: 'line', legend: { isVisible: false, position: Position.Top, @@ -44,24 +43,9 @@ function sampleArgs() { position: Position.Left, showGridlines: false, title: 'A and B', - - splitSeriesAccessors: [], + splitAccessor: 'd', }, ], - // y: { - // accessors: ['a', 'b'], - // labels: ['Label A', 'Label B'], - // position: Position.Left, - // showGridlines: false, - // title: 'A and B', - // }, - // x: { - // accessor: 'c', - // position: Position.Bottom, - // showGridlines: false, - // title: 'C', - // }, - // splitSeriesAccessors: [], }; return { data, args }; @@ -92,7 +76,7 @@ describe('xy_expression', () => { position: Position.Left, showGridlines: false, title: 'A and B', - splitSeriesAccessors: [], + splitAccessor: 'd', }; expect(layerConfig.fn(null, args, {})).toEqual({ @@ -130,8 +114,6 @@ describe('xy_expression', () => { test('it renders bar', () => { const { data, args } = sampleArgs(); - - // const component = shallow(); const component = shallow( { test('it renders area', () => { const { data, args } = sampleArgs(); - - // const component = shallow(); const component = shallow( { test('it renders horizontal bar', () => { const { data, args } = sampleArgs(); - - // const component = shallow( - // - // ); const component = shallow( { test('it renders stacked bar', () => { const { data, args } = sampleArgs(); - - // const component = shallow( - // - // ); const component = shallow( { test('it renders stacked area', () => { const { data, args } = sampleArgs(); - - // const component = shallow( - // - // ); const component = shallow( { test('it renders stacked horizontal bar', () => { const { data, args } = sampleArgs(); - - // const component = shallow( - // - // ); const component = shallow( { test('it remaps rows based on the labels', () => { const { data, args } = sampleArgs(); - - // const chart = shallow(); const chart = shallow( - {layers.map( - ({ splitSeriesAccessors, seriesType, labels, accessors, xAccessor, layerId }, index) => { - const seriesDataRow = data.rows.find(row => row[layerId]); - const seriesData = seriesDataRow ? seriesDataRow[layerId] : null; - - if (!seriesData) { - return; - } - - const idForCaching = accessors.concat([xAccessor], splitSeriesAccessors).join(','); - - const seriesProps = { - key: index, - splitSeriesAccessors, - stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], - id: getSpecId(idForCaching), - xAccessor, - yAccessors: labels, - data: (seriesData as KibanaDatatable).rows.map(row => { - const newRow: typeof row = {}; - - // Remap data to { 'Count of documents': 5 } - Object.keys(row).forEach(key => { - const labelIndex = accessors.indexOf(key); - if (labelIndex > -1) { - newRow[labels[labelIndex]] = row[key]; - } else { - newRow[key] = row[key]; - } - }); - return newRow; - }), - }; - - return seriesType === 'line' ? ( - - ) : seriesType === 'bar' || - seriesType === 'bar_stacked' || - seriesType === 'horizontal_bar' || - seriesType === 'horizontal_bar_stacked' ? ( - - ) : ( - - ); + {layers.map(({ splitAccessor, seriesType, labels, accessors, xAccessor, layerId }, index) => { + const seriesDataRow = data.rows.find(row => row[layerId]); + const seriesData = seriesDataRow ? seriesDataRow[layerId] : null; + + if (!seriesData) { + return; } - )} + + const idForCaching = accessors.concat([xAccessor, splitAccessor]).join(','); + + const seriesProps = { + key: index, + splitSeriesAccessors: [splitAccessor], + stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], + id: getSpecId(idForCaching), + xAccessor, + yAccessors: labels, + data: (seriesData as KibanaDatatable).rows.map(row => { + const newRow: typeof row = {}; + + Object.keys(row).forEach(key => { + const labelIndex = accessors.indexOf(key); + if (labelIndex > -1) { + newRow[labels[labelIndex]] = row[key]; + } else { + newRow[key] = row[key]; + } + }); + return newRow; + }), + }; + + return seriesType === 'line' ? ( + + ) : seriesType === 'bar' || + seriesType === 'bar_stacked' || + seriesType === 'horizontal_bar' || + seriesType === 'horizontal_bar_stacked' ? ( + + ) : ( + + ); + })} ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index f46a1733431d0..ef691ea3f4a07 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -52,14 +52,12 @@ describe('xy_suggestions', () => { // Helper that plucks out the important part of a suggestion for // most test assertions function suggestionSubset(suggestion: VisualizationSuggestion) { - return suggestion.state.layers.map( - ({ seriesType, splitSeriesAccessors, xAccessor, accessors }) => ({ - seriesType, - splitSeriesAccessors, - x: xAccessor, - y: accessors, - }) - ); + return suggestion.state.layers.map(({ seriesType, splitAccessor, xAccessor, accessors }) => ({ + seriesType, + splitAccessor, + x: xAccessor, + y: accessors, + })); } test('ignores invalid combinations', () => { @@ -106,9 +104,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar", - "splitSeriesAccessors": Array [ - "aaa", - ], + "splitAccessor": "aaa", "x": "date", "y": Array [ "bytes", @@ -134,9 +130,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "line", - "splitSeriesAccessors": Array [ - "product", - ], + "splitAccessor": "product", "x": "date", "y": Array [ "price", @@ -170,9 +164,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar", - "splitSeriesAccessors": Array [ - "bbb", - ], + "splitAccessor": "bbb", "x": "date", "y": Array [ "price", @@ -182,9 +174,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar", - "splitSeriesAccessors": Array [ - "ccc", - ], + "splitAccessor": "ccc", "x": "country", "y": Array [ "count", @@ -211,9 +201,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar", - "splitSeriesAccessors": Array [ - "ddd", - ], + "splitAccessor": "ddd", "x": "quantity", "y": Array [ "price", @@ -250,9 +238,7 @@ describe('xy_suggestions', () => { Array [ Object { "seriesType": "bar", - "splitSeriesAccessors": Array [ - "eee", - ], + "splitAccessor": "eee", "x": "mybool", "y": Array [ "num votes", diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index 59589907b8278..e256e5d2eb03d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -84,20 +84,13 @@ function getSuggestion( const title = `${yTitle} ${preposition} ${xTitle}`; const state: State = { legend: { isVisible: true, position: Position.Right }, - // seriesType: splitBy && isDate ? 'line' : 'bar', - // x: { - // accessor: xValue.columnId, - // position: Position.Bottom, - // showGridlines: false, - // title: xTitle, - // }, layers: [ { layerId: 'first', datasourceId: '', xAccessor: xValue.columnId, seriesType: splitBy && isDate ? 'line' : 'bar', - splitSeriesAccessors: splitBy && isDate ? [splitBy.columnId] : [generateId()], + splitAccessor: splitBy && isDate ? splitBy.columnId : generateId(), accessors: yValues.map(col => col.columnId), labels: [''], position: Position.Left, @@ -123,10 +116,6 @@ function getSuggestion( previewExpression: buildExpression( { ...state, - // x: { - // ...state.x, - // hide: true, - // }, layers: state.layers.map(layer => ({ ...layer, hide: true })), legend: { ...state.legend, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index ad06a447eba69..7593465e6473f 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -23,7 +23,7 @@ function exampleState(): State { datasourceId: '', labels: [''], seriesType: 'area', - splitSeriesAccessors: [], + splitAccessor: 'd', position: Position.Bottom, showGridlines: true, title: 'Baz', @@ -51,29 +51,27 @@ describe('xy_visualization', () => { expect(initialState).toMatchInlineSnapshot(` Object { + "layers": Array [ + Object { + "accessors": Array [ + "test-id1", + ], + "datasourceId": "", + "labels": Array [], + "layerId": "", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "splitAccessor": "test-id2", + "title": "", + "xAccessor": "test-id3", + }, + ], "legend": Object { "isVisible": true, "position": "right", }, - "seriesType": "bar", - "splitSeriesAccessors": Array [ - "test-id3", - ], "title": "Empty XY Chart", - "x": Object { - "accessor": "test-id1", - "position": "bottom", - "showGridlines": false, - "title": "X", - }, - "y": Object { - "accessors": Array [ - "test-id2", - ], - "position": "left", - "showGridlines": false, - "title": "Y", - }, } `); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 72898af850a26..8d12e18196b56 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -32,7 +32,7 @@ export const xyVisualization: Visualization = { position: Position.Top, seriesType: 'bar', showGridlines: false, - splitSeriesAccessors: [generateId()], + splitAccessor: generateId(), title: '', xAccessor: generateId(), }, From 844be816fcd243a583e60d945e136350f27199bc Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 19 Jul 2019 15:35:20 -0400 Subject: [PATCH 33/67] Change expression type for lens_merge_tables --- .../editor_frame/editor_frame.tsx | 1 - .../editor_frame_plugin/merge_tables.test.ts | 5 ++--- .../editor_frame_plugin/merge_tables.ts | 19 +++++++---------- x-pack/legacy/plugins/lens/public/types.ts | 5 +++++ .../xy_expression.test.tsx | 21 ++++++++----------- .../xy_visualization_plugin/xy_expression.tsx | 19 +++++++---------- 6 files changed, 31 insertions(+), 39 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 6a101fefb01c0..d67d707ee1532 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -60,7 +60,6 @@ export function EditorFrame(props: EditorFrameProps) { const datasourceLayers: Record = {}; Object.keys(props.datasourceMap).forEach(id => { - const stateWrapper = state.datasourceStates[id]; if (stateWrapper.isLoading) { return; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts index 8724a1d4fb560..2769f1e1201b4 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.test.ts @@ -28,9 +28,8 @@ describe('lens_merge_tables', () => { {} ) ).toEqual({ - columns: [{ id: 'first', name: '' }, { id: 'second', name: '' }], - rows: [{ first: sampleTable1, second: sampleTable2 }], - type: 'kibana_datatable', + tables: { first: sampleTable1, second: sampleTable2 }, + type: 'lens_multitable', }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts index 96264ff979b1e..c7747ace106fd 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/merge_tables.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { KibanaDatatable } from '../types'; +import { LensMultiTable, KibanaDatatable } from '../types'; interface MergeTables { layerIds: string[]; @@ -17,10 +17,10 @@ export const mergeTables: ExpressionFunction< 'lens_merge_tables', null, MergeTables, - KibanaDatatable + LensMultiTable > = { name: 'lens_merge_tables', - type: 'kibana_datatable', + type: 'lens_multitable', help: i18n.translate('xpack.lens.functions.mergeTables.help', { defaultMessage: 'A helper to merge any number of kibana tables into a single table', }), @@ -40,18 +40,13 @@ export const mergeTables: ExpressionFunction< types: ['null'], }, fn(_ctx, { layerIds, tables }: MergeTables) { - const row: Record = {}; - + const resultTables: Record = {}; tables.forEach((table, index) => { - row[layerIds[index]] = table; + resultTables[layerIds[index]] = table; }); return { - type: 'kibana_datatable', - columns: layerIds.map(layerId => ({ - id: layerId, - name: '', - })), - rows: [row], + type: 'lens_multitable', + tables: resultTables, }; }, }; diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 7f2fd056b2496..8e1c3190a7415 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -145,6 +145,11 @@ export interface Operation { // Extra meta-information like cardinality, color } +export interface LensMultiTable { + type: 'lens_multitable'; + tables: Record; +} + // This is a temporary type definition, to be replaced with // the official Kibana Datatable type definition. export interface KibanaDatatable { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index beaf54e1592cd..31644a6f936d2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -6,24 +6,21 @@ import { AreaSeries, BarSeries, Position, LineSeries, Settings } from '@elastic/charts'; import { xyChart, XYChart } from './xy_expression'; -import { KibanaDatatable } from '../types'; +import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerConfig } from './types'; function sampleArgs() { - const data: KibanaDatatable = { - type: 'kibana_datatable', - columns: [{ id: 'first', name: '' }], - rows: [ - { - first: { - type: 'kibana_datatable', - columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], - rows: [{ a: 1, b: 2, c: 3 }, { a: 1, b: 5, c: 4 }], - }, + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + first: { + type: 'kibana_datatable', + columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], + rows: [{ a: 1, b: 2, c: 3 }, { a: 1, b: 5, c: 4 }], }, - ], + }, }; const args: XYArgs = { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 0c3284f2db906..ea3c0e8f2afce 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -19,11 +19,11 @@ import { } from '@elastic/charts'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { XYArgs } from './types'; -import { KibanaDatatable } from '../types'; +import { LensMultiTable, KibanaDatatable } from '../types'; import { RenderFunction } from '../interpreter_types'; export interface XYChartProps { - data: KibanaDatatable; + data: LensMultiTable; args: XYArgs; } @@ -33,7 +33,7 @@ export interface XYRender { value: XYChartProps; } -export const xyChart: ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArgs, XYRender> = ({ +export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender> = ({ name: 'lens_xy_chart', type: 'render', help: 'An X/Y chart', @@ -51,7 +51,7 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArg context: { types: ['kibana_datatable'], }, - fn(data: KibanaDatatable, args: XYArgs) { + fn(data: LensMultiTable, args: XYArgs) { return { type: 'render', as: 'lens_xy_chart_renderer', @@ -62,10 +62,10 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArg }; }, // TODO the typings currently don't support custom type args. As soon as they do, this can be removed -} as unknown) as ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArgs, XYRender>; +} as unknown) as ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender>; export interface XYChartProps { - data: KibanaDatatable; + data: LensMultiTable; args: XYArgs; } @@ -109,10 +109,7 @@ export function XYChart({ data, args }: XYChartProps) { /> {layers.map(({ splitAccessor, seriesType, labels, accessors, xAccessor, layerId }, index) => { - const seriesDataRow = data.rows.find(row => row[layerId]); - const seriesData = seriesDataRow ? seriesDataRow[layerId] : null; - - if (!seriesData) { + if (!data.tables[layerId]) { return; } @@ -125,7 +122,7 @@ export function XYChart({ data, args }: XYChartProps) { id: getSpecId(idForCaching), xAccessor, yAccessors: labels, - data: (seriesData as KibanaDatatable).rows.map(row => { + data: data.tables[layerId].rows.map(row => { const newRow: typeof row = {}; Object.keys(row).forEach(key => { From 142bca2408b25bcd2c905e5bb0b347acb1afaeff Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 19 Jul 2019 15:39:17 -0400 Subject: [PATCH 34/67] Fix XY chart rendering expression --- .../lens/public/datatable_visualization_plugin/expression.tsx | 2 +- .../lens/public/xy_visualization_plugin/xy_expression.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index ddb6df8d766cf..dec497557e04f 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -56,7 +56,7 @@ export const datatable: ExpressionFunction< }, }, context: { - types: ['kibana_datatable'], + types: ['lens_multitable'], }, fn(data: KibanaDatatable, args: Args) { return { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index ea3c0e8f2afce..9cfc16d2a9796 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -19,7 +19,7 @@ import { } from '@elastic/charts'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { XYArgs } from './types'; -import { LensMultiTable, KibanaDatatable } from '../types'; +import { LensMultiTable } from '../types'; import { RenderFunction } from '../interpreter_types'; export interface XYChartProps { @@ -49,7 +49,7 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs }, }, context: { - types: ['kibana_datatable'], + types: ['lens_multitable'], }, fn(data: LensMultiTable, args: XYArgs) { return { From c10b6a1faa9a4a7eb328a8ac10c5af723165dc16 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 19 Jul 2019 15:53:57 -0400 Subject: [PATCH 35/67] Fix type errors relating to `layerId` in table suggestions --- .../editor_frame/config_panel_wrapper.tsx | 5 +---- .../editor_frame/editor_frame.test.tsx | 1 + .../editor_frame/suggestion_helpers.test.ts | 12 ++++++++++-- .../operation_definitions/count.tsx | 2 -- .../operation_definitions/date_histogram.tsx | 8 +------- .../operation_definitions/filter_ratio.tsx | 14 ++------------ .../operation_definitions/metrics.tsx | 9 --------- .../operation_definitions/terms.tsx | 8 -------- 8 files changed, 15 insertions(+), 44 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index 03e14a5b98e14..abe8819dc73ef 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useContext, memo } from 'react'; import { EuiSelect } from '@elastic/eui'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; -import { Visualization, DatasourcePublicAPI, FramePublicAPI } from '../../types'; +import { Visualization, FramePublicAPI } from '../../types'; import { DragContext } from '../../drag_drop'; interface ConfigPanelWrapperProps { @@ -16,7 +16,6 @@ interface ConfigPanelWrapperProps { visualizationMap: Record; activeVisualizationId: string | null; dispatch: (action: Action) => void; - // datasourcePublicAPI: DatasourcePublicAPI; framePublicAPI: FramePublicAPI; } @@ -71,7 +70,6 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config const newState = getSuggestedVisualizationState( props.framePublicAPI, props.visualizationMap[e.target.value] - // props.datasourcePublicAPI ); props.dispatch({ type: 'SWITCH_VISUALIZATION', @@ -87,7 +85,6 @@ export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: Config dragDropContext: context, state: props.visualizationState, setState: setVisualizationState, - // datasource: props.datasourcePublicAPI, frame: props.framePublicAPI, }} /> diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 73ed3330c3393..ad20cae73d49f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -30,6 +30,7 @@ function generateSuggestion(datasourceSuggestionId = 1, state = {}): DatasourceS datasourceSuggestionId: 1, isMultiRow: true, }, + layerId: 'first', }; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts index ac30bc2adae3e..34046688f3d51 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts @@ -8,9 +8,14 @@ import { getSuggestions } from './suggestion_helpers'; import { createMockVisualization } from '../mocks'; import { TableSuggestion } from '../../types'; -const generateSuggestion = (datasourceSuggestionId: number = 1, state = {}) => ({ +const generateSuggestion = ( + datasourceSuggestionId: number = 1, + state = {}, + layerId: string = 'first' +) => ({ state, table: { datasourceSuggestionId, columns: [], isMultiRow: false }, + layerId, }); describe('suggestion helpers', () => { @@ -136,7 +141,10 @@ describe('suggestion helpers', () => { const table1: TableSuggestion = { datasourceSuggestionId: 0, columns: [], isMultiRow: true }; const table2: TableSuggestion = { datasourceSuggestionId: 1, columns: [], isMultiRow: true }; getSuggestions( - [{ state: {}, table: table1 }, { state: {}, table: table2 }], + [ + { state: {}, table: table1, layerId: 'first' }, + { state: {}, table: table2, layerId: 'first' }, + ], { vis1: mockVisualization1, vis2: mockVisualization2, diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx index e754c43fba86f..50af9e6d96e95 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { CountIndexPatternColumn } from '../indexpattern'; -import { DimensionPriority, DimensionLayer } from '../../types'; import { OperationDefinition } from '../operations'; export const countOperation: OperationDefinition = { @@ -27,7 +26,6 @@ export const countOperation: OperationDefinition = { suggestedPriority, isBucketed: false, indexPatternId, - // layer, }; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx index fba90f415f49c..f47fc478ddd54 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/date_histogram.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiForm, EuiFormRow, EuiRange } from '@elastic/eui'; import { IndexPatternField, DateHistogramIndexPatternColumn } from '../indexpattern'; -import { DimensionLayer, DimensionPriority } from '../../types'; +import { DimensionPriority } from '../../types'; import { OperationDefinition } from '../operations'; import { updateColumnParam } from '../state_helpers'; @@ -46,17 +46,12 @@ export const dateHistogramOperation: OperationDefinition false, - buildColumn({ operationId, suggestedPriority, indexPatternId, columns, layerId }) { - // operationId: string, - // suggestedPriority: DimensionPriority | undefined, - // columns: Partial>, - // // layer: DimensionLayer - // layerId: string - // ): FilterRatioIndexPatternColumn { + buildColumn({ operationId, suggestedPriority, indexPatternId }) { return { operationId, label: i18n.translate('xpack.lens.indexPattern.filterRatio', { @@ -40,7 +31,6 @@ export const filterRatioOperation: OperationDefinition( @@ -31,13 +30,6 @@ function buildMetricOperation( ); }, buildColumn({ operationId, suggestedPriority, field, indexPatternId }): T { - // }: { - // operationId: string; - // suggestedPriority: DimensionPriority | undefined; - // layerId: string; - // columns: {}; - // field?: IndexPatternField; - // }): T { if (!field) { throw new Error(`Invariant: A ${type} operation can only be built with a field`); } @@ -50,7 +42,6 @@ function buildMetricOperation( sourceField: field ? field.name : '', isBucketed: false, indexPatternId, - // layer, } as T; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 72c74f1d809ee..181e29e932c66 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -46,13 +46,6 @@ export const termsOperation: OperationDefinition = { ); }, buildColumn({ operationId, suggestedPriority, columns, field, indexPatternId }) { - // }: { - // operationId: string; - // suggestedPriority: DimensionPriority | undefined; - // layerId: string; - // columns: Partial>; - // field?: IndexPatternField; - // }): TermsIndexPatternColumn { const existingMetricColumn = Object.entries(columns) .filter(([_columnId, column]) => column && isSortableByColumn(column)) .map(([id]) => id)[0]; @@ -66,7 +59,6 @@ export const termsOperation: OperationDefinition = { sourceField: field ? field.name : '', isBucketed: true, indexPatternId, - // layerId, params: { size: 5, orderBy: existingMetricColumn From 542557c9b2cfbd0d54408e62d89df5ef9b84d947 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Fri, 19 Jul 2019 17:53:12 -0400 Subject: [PATCH 36/67] Pass around tables for suggestions with associated layerIds --- .../editor_frame/config_panel_wrapper.tsx | 40 +-- .../editor_frame/editor_frame.test.tsx | 7 +- .../editor_frame/editor_frame.tsx | 28 +- .../editor_frame/suggestion_helpers.ts | 38 +-- .../editor_frame/suggestion_panel.test.tsx | 13 +- .../editor_frame/suggestion_panel.tsx | 33 ++- .../editor_frame/workspace_panel.test.tsx | 46 +--- .../editor_frame/workspace_panel.tsx | 7 +- x-pack/legacy/plugins/lens/public/types.ts | 1 + .../xy_visualization.test.ts.snap | 14 +- .../xy_config_panel.test.tsx | 251 ++++++++++-------- .../xy_config_panel.tsx | 3 + .../xy_visualization.test.ts | 18 +- 13 files changed, 276 insertions(+), 223 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index abe8819dc73ef..64bd7c544cd0f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -8,7 +8,7 @@ import React, { useMemo, useContext, memo } from 'react'; import { EuiSelect } from '@elastic/eui'; import { NativeRenderer } from '../../native_renderer'; import { Action } from './state_management'; -import { Visualization, FramePublicAPI } from '../../types'; +import { Visualization, FramePublicAPI, VisualizationSuggestion } from '../../types'; import { DragContext } from '../../drag_drop'; interface ConfigPanelWrapperProps { @@ -24,25 +24,33 @@ function getSuggestedVisualizationState( visualization: Visualization // datasource: DatasourcePublicAPI ) { - const suggestions = []; - // const suggestions = visualization.getSuggestions({ - // tables: [ - // { - // datasourceSuggestionId: 0, - // isMultiRow: true, - // columns: datasource.getTableSpec().map(col => ({ - // ...col, - // operation: datasource.getOperationForColumnId(col.columnId)!, - // })), - // }, - // ], - // }); + const datasources = Object.entries(frame.datasourceLayers); - if (!suggestions.length) { + let results: VisualizationSuggestion[] = []; + + datasources.forEach(([layerId, datasource]) => { + const suggestions = visualization.getSuggestions({ + layerId, + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: datasource.getTableSpec().map(col => ({ + ...col, + operation: datasource.getOperationForColumnId(col.columnId)!, + })), + }, + ], + }); + + results = results.concat(suggestions); + }); + + if (!results.length) { return visualization.initialize(frame); } - // return visualization.initialize(frame, suggestions[0].state); + return visualization.initialize(frame, results[0].state); } export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index ad20cae73d49f..06302c1b4307d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -153,7 +153,7 @@ describe('editor_frame', () => { expect(mockVisualization.initialize).toHaveBeenCalled(); }); - it('should pass the public datasource api into visualization initialize', async () => { + it('should pass the public frame api into visualization initialize', async () => { act(() => { mount( { await waitForPromises(); - expect(mockVisualization.initialize).toHaveBeenCalledWith(mockDatasource.publicAPIMock); + expect(mockVisualization.initialize).toHaveBeenCalledWith({ + datasourceLayers: {}, + addNewLayer: expect.any(Function), + }); }); it('should render data panel after initialization is complete', async () => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index d67d707ee1532..564659bc73355 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -36,13 +36,18 @@ export function EditorFrame(props: EditorFrameProps) { const [state, dispatch] = useReducer(reducer, props, getInitialState); const { onError } = props; - const allLoaded = Object.values(state.datasourceStates).every(({ isLoading }) => !isLoading); + const allLoaded = Object.values(state.datasourceStates).every( + ({ isLoading }) => typeof isLoading === 'boolean' && !isLoading + ); // Initialize current datasource and all active datasources useEffect(() => { if (!allLoaded) { Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { - if (state.datasourceStates[datasourceId].isLoading) { + if ( + !state.datasourceStates[datasourceId] || + state.datasourceStates[datasourceId].isLoading + ) { datasource .initialize(state.datasourceStates[datasourceId].state || undefined) .then(datasourceState => { @@ -61,7 +66,7 @@ export function EditorFrame(props: EditorFrameProps) { const datasourceLayers: Record = {}; Object.keys(props.datasourceMap).forEach(id => { const stateWrapper = state.datasourceStates[id]; - if (stateWrapper.isLoading) { + if (!stateWrapper || stateWrapper.isLoading) { return; } const dsState = stateWrapper.state; @@ -80,8 +85,6 @@ export function EditorFrame(props: EditorFrameProps) { layer ); - // layerToDatasourceId[layer] = id; - datasourceLayers[layer] = publicAPI; }); }); @@ -218,13 +221,16 @@ export function EditorFrame(props: EditorFrameProps) { } suggestionsPanel={ table); + // const datasourceTables = datasourceTableSuggestions.map(({ table }) => table); - return ( - Object.entries(visualizationMap) - .map(([visualizationId, visualization]) => { - return visualization - .getSuggestions({ - tables: datasourceTables, - state: visualizationId === activeVisualizationId ? visualizationState : undefined, - }) - .map(({ datasourceSuggestionId, ...suggestion }) => ({ - ...suggestion, - visualizationId, - datasourceState: datasourceTableSuggestions[datasourceSuggestionId].state, - })); - }) - // TODO why is flatMap not available here? - .reduce((globalList, currentList) => [...globalList, ...currentList], []) - .sort(({ score: scoreA }, { score: scoreB }) => scoreB - scoreA) - ); + return Object.entries(visualizationMap) + .map(([visualizationId, visualization]) => { + return visualization + .getSuggestions({ + // suggestions: datasourceTableSuggestions.map(({ table }) => table), + // suggestions: datasourceTableSuggestions, + tables: [], + layerId: '', + state: visualizationId === activeVisualizationId ? visualizationState : undefined, + }) + .map(({ datasourceSuggestionId, ...suggestion }) => ({ + ...suggestion, + visualizationId, + datasourceState: datasourceTableSuggestions[datasourceSuggestionId].state, + })); + }) + .reduce((globalList, currentList) => [...globalList, ...currentList], []) + .sort(({ score: scoreA }, { score: scoreB }) => scoreB - scoreA); } export function toSwitchAction(suggestion: Suggestion): Action { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index e3409a8eca7e9..d6e8874659f3c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -59,8 +59,17 @@ describe('suggestion_panel', () => { ] as Suggestion[]); defaultProps = { - activeDatasource: mockDatasource, - datasourceState: {}, + // activeDatasource: mockDatasource, + activeDatasourceId: 'mock', + datasourceMap: { + mock: mockDatasource, + }, + datasourceStates: { + mock: { + isLoading: false, + state: {}, + }, + }, activeVisualizationId: 'vis', visualizationMap: { vis: mockVisualization, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx index eb37a9d445bf5..7d25fa64b2f8f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.tsx @@ -17,8 +17,15 @@ import { prependDatasourceExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; export interface SuggestionPanelProps { - activeDatasource: Datasource; - datasourceState: unknown; + activeDatasourceId: string | null; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; activeVisualizationId: string | null; visualizationMap: Record; visualizationState: unknown; @@ -90,17 +97,21 @@ const SuggestionPreview = ({ export const SuggestionPanel = debouncedComponent(InnerSuggestionPanel, 2000); function InnerSuggestionPanel({ - activeDatasource, - datasourceState, + activeDatasourceId, + datasourceMap, + datasourceStates, activeVisualizationId, visualizationMap, visualizationState, dispatch, ExpressionRenderer: ExpressionRendererComponent, }: SuggestionPanelProps) { - const datasourceSuggestions = activeDatasource.getDatasourceSuggestionsFromCurrentState( - datasourceState - ); + if (!activeDatasourceId) { + return null; + } + const datasourceSuggestions = datasourceMap[ + activeDatasourceId + ].getDatasourceSuggestionsFromCurrentState(datasourceStates[activeDatasourceId].state); const suggestions = getSuggestions( datasourceSuggestions, @@ -119,12 +130,12 @@ function InnerSuggestionPanel({ /> - {/* TODO handle suggestions as discussed suggestions.map((suggestion, index) => { + {suggestions.map((suggestion, index) => { const previewExpression = suggestion.previewExpression ? prependDatasourceExpression( suggestion.previewExpression, - activeDatasource, - suggestion.datasourceState + datasourceMap, + datasourceStates ) : null; return ( @@ -136,7 +147,7 @@ function InnerSuggestionPanel({ key={`${suggestion.visualizationId}-${suggestion.title}`} /> ); - })*/} + })} ); } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 2f751d6d82622..62bdac8ba9ded 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -91,8 +91,6 @@ describe('workspace_panel', () => { datasourceStates={{}} datasourceMap={{}} framePublicAPI={createMockFramePublicAPI()} - // activeDatasource={{ ...mockDatasource, toExpression: () => null }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -109,16 +107,11 @@ describe('workspace_panel', () => { it('should render the resulting expression using the expression renderer', () => { const framePublicAPI = createMockFramePublicAPI(); - // framePublicAPI.layerIdToDatasource = { - // first: 'mock', - // }; framePublicAPI.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - // const mockDatasource = createMockDatasource(); mockDatasource.toExpression.mockReturnValue('datasource'); - // framePublicAPI.datasourceLayers instance = mount( { mock: mockDatasource, }} framePublicAPI={framePublicAPI} - // activeDatasource={{ - // ...mockDatasource, - // toExpression: () => 'datasource', - // }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -174,11 +162,6 @@ Object { datasourceStates={{}} datasourceMap={{}} framePublicAPI={createMockFramePublicAPI()} - // activeDatasource={{ - // ...mockDatasource, - // toExpression: () => 'datasource ||', - // }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -205,11 +188,6 @@ Object { datasourceStates={{}} datasourceMap={{}} framePublicAPI={createMockFramePublicAPI()} - // activeDatasource={{ - // ...mockDatasource, - // toExpression: () => 'datasource', - // }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -241,11 +219,6 @@ Object { datasourceStates={{}} datasourceMap={{}} framePublicAPI={createMockFramePublicAPI()} - // activeDatasource={{ - // ...mockDatasource, - // toExpression: () => 'datasource', - // }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -280,11 +253,6 @@ Object { datasourceStates={{}} datasourceMap={{}} framePublicAPI={createMockFramePublicAPI()} - // activeDatasource={{ - // ...mockDatasource, - // toExpression: () => 'datasource', - // }} - // datasourceState={{}} activeVisualizationId="vis" visualizationMap={{ vis: { ...mockVisualization, toExpression: () => 'vis' }, @@ -320,14 +288,20 @@ Object { beforeEach(() => { mockDispatch = jest.fn(); + // const mockDatasource = createMockDatasource(); instance = mount( { export interface SuggestionRequest { // It is up to the Visualization to rank these tables tables: TableSuggestion[]; + layerId: string; state?: T; // State is only passed if the visualization is active } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index e7a905b8656d4..88e0b9bcbb198 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -10,20 +10,14 @@ Object { "chain": Array [ Object { "arguments": Object { - "accessors": Array [ - "b", - "c", - ], + "accessors": Array [], "datasourceId": Array [ "", ], "hide": Array [ false, ], - "labels": Array [ - "b", - "c", - ], + "labels": Array [], "layerId": Array [ "first", ], @@ -36,7 +30,9 @@ Object { "showGridlines": Array [ true, ], - "splitSeriesAccessors": Array [], + "splitAccessor": Array [ + "d", + ], "title": Array [ "Baz", ], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index 8c3013b7e36d8..e1f8f41e3dcba 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -9,7 +9,7 @@ import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; import { XYConfigPanel } from './xy_config_panel'; -import { DatasourceDimensionPanelProps, Operation } from '../types'; +import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; import { State, SeriesType } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; @@ -21,6 +21,8 @@ jest.mock('../id_generator'); describe('XYConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; + let frame: FramePublicAPI; + function testState(): State { return { legend: { isVisible: true, position: Position.Right }, @@ -48,44 +50,62 @@ describe('XYConfigPanel', () => { .props(); } - test('disables stacked chart types without a split series', () => { - const component = mount( - {}} - state={testState()} - /> - ); - - const options = component - .find('[data-test-subj="lnsXY_seriesType"]') - .first() - .prop('options') as EuiButtonGroupProps['options']; - - expect(options.map(({ id }) => id)).toEqual([ - 'line', - 'area', - 'bar', - 'horizontal_bar', - 'area_stacked', - 'bar_stacked', - 'horizontal_bar_stacked', - ]); - - expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([ - 'area_stacked', - 'bar_stacked', - 'horizontal_bar_stacked', - ]); + // test('disables stacked chart types without a split series', () => { + // const frame = createMockFramePublicAPI(); + // frame.datasourceLayers = { + // first: createMockDatasource().publicAPIMock, + // }; + // const component = mount( + // {}} + // state={testState()} + // /> + // ); + + // const options = component + // .find('[data-test-subj="lnsXY_seriesType"]') + // .first() + // .prop('options') as EuiButtonGroupProps['options']; + + // expect(options.map(({ id }) => id)).toEqual([ + // 'line', + // 'area', + // 'bar', + // 'horizontal_bar', + // 'area_stacked', + // 'bar_stacked', + // 'horizontal_bar_stacked', + // ]); + + // expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([ + // 'area_stacked', + // 'bar_stacked', + // 'horizontal_bar_stacked', + // ]); + // }); + beforeEach(() => { + frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: createMockDatasource().publicAPIMock, + }; }); test('enables all stacked chart types when there is a split series', () => { const state = testState(); + // const frame = createMockFramePublicAPI(); + // frame.datasourceLayers = { + // first: createMockDatasource().publicAPIMock, + // }; const component = mount( {}} state={{ ...state, layers: [{ ...state.layers[0], splitAccessor: 'c' }] }} /> @@ -99,46 +119,46 @@ describe('XYConfigPanel', () => { expect(options.every(({ isDisabled }) => !isDisabled)).toEqual(true); }); - test('toggles axis position when going from horizontal bar to any other type', () => { - const changeSeriesType = (fromSeriesType: SeriesType, toSeriesType: SeriesType) => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_seriesType').onChange as Function)(toSeriesType); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(changeSeriesType('line', 'horizontal_bar')).toMatchObject({ - seriesType: 'horizontal_bar', - x: { position: Position.Left }, - y: { position: Position.Bottom }, - }); - expect(changeSeriesType('horizontal_bar', 'bar')).toMatchObject({ - seriesType: 'bar', - x: { position: Position.Bottom }, - y: { position: Position.Left }, - }); - expect(changeSeriesType('horizontal_bar', 'line')).toMatchObject({ - seriesType: 'line', - x: { position: Position.Bottom }, - y: { position: Position.Left }, - }); - expect(changeSeriesType('horizontal_bar', 'area')).toMatchObject({ - seriesType: 'area', - x: { position: Position.Bottom }, - y: { position: Position.Left }, - }); - }); + // test('toggles axis position when going from horizontal bar to any other type', () => { + // const changeSeriesType = (fromSeriesType: SeriesType, toSeriesType: SeriesType) => { + // const setState = jest.fn(); + // const state = testState(); + // const component = mount( + // + // ); + + // (testSubj(component, 'lnsXY_seriesType').onChange as Function)(toSeriesType); + + // expect(setState).toHaveBeenCalledTimes(1); + // return setState.mock.calls[0][0]; + // }; + + // expect(changeSeriesType('line', 'horizontal_bar')).toMatchObject({ + // seriesType: 'horizontal_bar', + // x: { position: Position.Left }, + // y: { position: Position.Bottom }, + // }); + // expect(changeSeriesType('horizontal_bar', 'bar')).toMatchObject({ + // seriesType: 'bar', + // x: { position: Position.Bottom }, + // y: { position: Position.Left }, + // }); + // expect(changeSeriesType('horizontal_bar', 'line')).toMatchObject({ + // seriesType: 'line', + // x: { position: Position.Bottom }, + // y: { position: Position.Left }, + // }); + // expect(changeSeriesType('horizontal_bar', 'area')).toMatchObject({ + // seriesType: 'area', + // x: { position: Position.Bottom }, + // y: { position: Position.Left }, + // }); + // }); test('allows toggling of legend visibility', () => { const toggleIsVisible = (isVisible: boolean) => { @@ -282,38 +302,38 @@ describe('XYConfigPanel', () => { }); }); - test('allows editing the y axis title', () => { - const testSetTitle = (title: string) => { - const setState = jest.fn(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_yTitle').onChange as Function)({ target: { value: title } }); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(testSetTitle('Hoi')).toMatchObject({ - y: { title: 'Hoi' }, - }); - expect(testSetTitle('There!')).toMatchObject({ - y: { title: 'There!' }, - }); - }); + // test('allows editing the y axis title', () => { + // const testSetTitle = (title: string) => { + // const setState = jest.fn(); + // const component = mount( + // + // ); + + // (testSubj(component, 'lnsXY_yTitle').onChange as Function)({ target: { value: title } }); + + // expect(setState).toHaveBeenCalledTimes(1); + // return setState.mock.calls[0][0]; + // }; + + // expect(testSetTitle('Hoi')).toMatchObject({ + // y: { title: 'Hoi' }, + // }); + // expect(testSetTitle('There!')).toMatchObject({ + // y: { title: 'There!' }, + // }); + // }); test('the y dimension panel accepts numeric operations', () => { const state = testState(); const component = mount( @@ -338,11 +358,11 @@ describe('XYConfigPanel', () => { }); test('allows removal of y dimensions', () => { - const frame = createMockFramePublicAPI(); - const datasourceMock = createMockDatasource().publicAPIMock; - frame.datasourceLayers = { - first: datasourceMock, - }; + // const frame = createMockFramePublicAPI(); + // const datasourceMock = createMockDatasource().publicAPIMock; + // frame.datasourceLayers = { + // first: datasourceMock, + // }; const setState = jest.fn(); const state = testState(); const component = mount( @@ -357,11 +377,19 @@ describe('XYConfigPanel', () => { (testSubj(component, 'lnsXY_yDimensionPanel_remove_b').onClick as Function)(); expect(setState).toHaveBeenCalledTimes(1); + // expect(setState.mock.calls[0][0]).toMatchObject({ + // y: { accessors: ['a', 'c'] }, + // }); expect(setState.mock.calls[0][0]).toMatchObject({ - y: { accessors: ['a', 'c'] }, + layers: [ + { + ...state.layers[0], + accessors: ['a', 'c'], + }, + ], }); - expect(datasourceMock.removeColumnInTableSpec).toHaveBeenCalledTimes(1); - expect(datasourceMock.removeColumnInTableSpec).toHaveBeenCalledWith('b'); + expect(frame.datasourceLayers.first.removeColumnInTableSpec).toHaveBeenCalledTimes(1); + expect(frame.datasourceLayers.first.removeColumnInTableSpec).toHaveBeenCalledWith('b'); }); test('allows adding y dimensions', () => { @@ -371,7 +399,7 @@ describe('XYConfigPanel', () => { const component = mount( @@ -380,8 +408,15 @@ describe('XYConfigPanel', () => { (testSubj(component, 'lnsXY_yDimensionPanel_add').onClick as Function)(); expect(setState).toHaveBeenCalledTimes(1); + // expect(setState.mock.calls[0][0]).toMatchObject({ + // y: { accessors: ['a', 'b', 'c', 'zed'] }, expect(setState.mock.calls[0][0]).toMatchObject({ - y: { accessors: ['a', 'b', 'c', 'zed'] }, + layers: [ + { + ...state.layers[0], + accessors: ['a', 'b', 'c', 'zed'], + }, + ], }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index d6d8edac3d58b..78fcc9c0de08e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -121,6 +121,9 @@ export function XYConfigPanel(props: VisualizationProps) { onClick={() => { setState({ ...state, layers: state.layers.filter(l => l !== layer) }); }} + aria-label={i18n.translate('xpack.lens.xyChart.removeLayer', { + defaultMessage: 'Remove layer', + })} /> { describe('#toExpression', () => { it('should map to a valid AST', () => { - expect( - xyVisualization.toExpression(exampleState(), createMockFramePublicAPI()) - ).toMatchSnapshot(); + const frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: createMockDatasource().publicAPIMock, + }; + expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); }); it('should default to labeling all columns with their column label', () => { @@ -107,10 +109,12 @@ describe('xy_visualization', () => { label: 'Second', } as Operation); - const expression = xyVisualization.toExpression( - exampleState(), - createMockFramePublicAPI() - )! as Ast; + const frame = createMockFramePublicAPI(); + frame.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + + const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledTimes(2); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); From c659084ad645b7ffce1b96962fd5d5714190d1e5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 11:15:50 +0200 Subject: [PATCH 37/67] fix tests in workspace panel --- .../editor_frame/workspace_panel.test.tsx | 199 +++++++++++++++--- .../editor_frame/workspace_panel.tsx | 2 +- 2 files changed, 172 insertions(+), 29 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 62bdac8ba9ded..debb5c380f50c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -19,6 +19,7 @@ import { InnerWorkspacePanel, WorkspacePanelProps } from './workspace_panel'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { DragDrop } from '../../drag_drop'; +import { Ast } from '@kbn/interpreter/common'; const waitForPromises = () => new Promise(resolve => setTimeout(resolve)); @@ -111,6 +112,7 @@ describe('workspace_panel', () => { first: mockDatasource.publicAPIMock, }; mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); instance = mount( { ); expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('should include data fetching for each layer in the expression', () => { + const mockDatasource2 = createMockDatasource(); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + second: mockDatasource2.publicAPIMock, + }; + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + + mockDatasource2.toExpression.mockReturnValue('datasource2'); + mockDatasource2.getLayers.mockReturnValue(['second']); + + instance = mount( + 'vis' }, + }} + visualizationState={{}} + dispatch={() => {}} + ExpressionRenderer={expressionRendererMock} + /> + ); + + expect( + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[0].arguments.layerIds + ).toEqual(['first', 'second']); + expect( + (instance.find(expressionRendererMock).prop('expression') as Ast).chain[0].arguments.tables + ).toMatchInlineSnapshot(` + Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, + ] + `); }); describe('expression failures', () => { it('should show an error message if the expression fails to parse', () => { + mockDatasource.toExpression.mockReturnValue('|||'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; + instance = mount( 'vis' }, @@ -177,6 +281,12 @@ Object { }); it('should show an error message if the expression fails to render', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; expressionRendererMock = jest.fn(({ onRenderFailure }) => { Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); return ; @@ -185,9 +295,16 @@ Object { instance = mount( 'vis' }, @@ -208,6 +325,12 @@ Object { }); it('should not attempt to run the expression again if it does not change', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; expressionRendererMock = jest.fn(({ onRenderFailure }) => { Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); return ; @@ -216,9 +339,16 @@ Object { instance = mount( 'vis' }, @@ -242,6 +372,12 @@ Object { }); it('should attempt to run the expression again if changes after an error', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource.getLayers.mockReturnValue(['first']); + const framePublicAPI = createMockFramePublicAPI(); + framePublicAPI.datasourceLayers = { + first: mockDatasource.publicAPIMock, + }; expressionRendererMock = jest.fn(({ onRenderFailure }) => { Promise.resolve().then(() => onRenderFailure!({ type: 'error' })); return ; @@ -250,9 +386,16 @@ Object { instance = mount( 'vis' }, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx index d1013af841a4c..84c3ac25503fc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.tsx @@ -104,7 +104,7 @@ export function InnerWorkspacePanel({ } catch (e) { setExpressionError(e.toString()); } - }, [activeVisualization, visualizationState, activeDatasourceId, datasourceStates]); + }, [activeVisualization, visualizationState, datasourceMap, datasourceStates]); useEffect(() => { // reset expression error if component attempts to run it again From 42302d68f2fd20d8a1cb1acf02dbb98728a03b67 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 14:39:37 +0200 Subject: [PATCH 38/67] fix editor_frame tests --- .../editor_frame/config_panel_wrapper.tsx | 2 +- .../editor_frame/editor_frame.test.tsx | 108 ++++++++++++------ .../editor_frame/editor_frame.tsx | 9 +- .../editor_frame/state_management.test.ts | 41 ++++++- .../editor_frame/state_management.ts | 8 +- .../editor_frame/suggestion_helpers.test.ts | 48 ++++---- .../editor_frame/suggestion_helpers.ts | 14 +-- .../editor_frame/suggestion_panel.test.tsx | 60 ++++++---- .../editor_frame/workspace_panel.test.tsx | 6 +- .../lens/public/editor_frame_plugin/mocks.tsx | 5 + .../editor_frame_plugin/plugin.test.tsx | 3 +- .../indexpattern_plugin/indexpattern.test.tsx | 6 +- .../indexpattern_plugin/indexpattern.tsx | 8 +- x-pack/legacy/plugins/lens/public/types.ts | 4 +- .../xy_config_panel.test.tsx | 2 +- .../xy_suggestions.test.ts | 23 +++- .../xy_visualization_plugin/xy_suggestions.ts | 34 ++++-- 17 files changed, 248 insertions(+), 133 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index 64bd7c544cd0f..d0d0c76987b45 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -30,7 +30,6 @@ function getSuggestedVisualizationState( datasources.forEach(([layerId, datasource]) => { const suggestions = visualization.getSuggestions({ - layerId, tables: [ { datasourceSuggestionId: 0, @@ -39,6 +38,7 @@ function getSuggestedVisualizationState( ...col, operation: datasource.getOperationForColumnId(col.columnId)!, })), + layerId, }, ], }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 06302c1b4307d..fd52e1c2b66fb 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -29,8 +29,8 @@ function generateSuggestion(datasourceSuggestionId = 1, state = {}): DatasourceS columns: [], datasourceSuggestionId: 1, isMultiRow: true, + layerId: 'first', }, - layerId: 'first', }; } @@ -247,6 +247,7 @@ describe('editor_frame', () => { }); it('should render the resulting expression using the expression renderer', async () => { + mockDatasource.getLayers.mockReturnValue(['first']); const instance = mount( { instance.update(); expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "vis", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); }); }); @@ -361,6 +378,7 @@ Object { }); it('should re-render config panel with updated datasource api after datasource state update', async () => { + mockDatasource.getLayers.mockReturnValue(['first']); mount( { it('should pass the datasource api to the visualization', async () => { + mockDatasource.getLayers.mockReturnValue(['first']); + mount( { const datasourceState = {}; mockDatasource.initialize.mockResolvedValue(datasourceState); + mockDatasource.getLayers.mockReturnValue(['first']); mount( { + mockDatasource.getLayers.mockReturnValue(['first']); mount( { setDatasourceState(updatedState); }); expect(mockDatasource.getPublicAPI).toHaveBeenLastCalledWith( updatedState, - expect.any(Function) + expect.any(Function), + 'first' ); }); }); describe('switching', () => { let instance: ReactWrapper; + beforeEach(async () => { + mockDatasource.getLayers.mockReturnValue(['first']); instance = mount( [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.5, state: {}, title: 'Suggestion2', previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.8, state: {}, title: 'Suggestion1', @@ -684,14 +716,14 @@ Object { ...mockVisualization, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.4, state: {}, title: 'Suggestion4', previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.45, state: {}, title: 'Suggestion3', @@ -736,7 +768,7 @@ Object { ...mockVisualization, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.8, state: suggestionVisState, title: 'Suggestion1', @@ -792,14 +824,14 @@ Object { ...mockVisualization, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.2, state: {}, title: 'Suggestion1', previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.8, state: suggestionVisState, title: 'Suggestion2', @@ -849,14 +881,14 @@ Object { ...mockVisualization, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.2, state: {}, title: 'Suggestion1', previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.6, state: {}, title: 'Suggestion2', @@ -868,7 +900,7 @@ Object { ...mockVisualization2, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.8, state: suggestionVisState, title: 'Suggestion3', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 564659bc73355..34b1946b8022e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -45,7 +45,7 @@ export function EditorFrame(props: EditorFrameProps) { if (!allLoaded) { Object.entries(props.datasourceMap).forEach(([datasourceId, datasource]) => { if ( - !state.datasourceStates[datasourceId] || + state.datasourceStates[datasourceId] && state.datasourceStates[datasourceId].isLoading ) { datasource @@ -198,7 +198,6 @@ export function EditorFrame(props: EditorFrameProps) { { expect(newState.datasourceStates.testDatasource.state).toBe(newDatasourceState); }); - // TODO this behavior is wrong - multiple datasources can be active at once - it.skip('should should switch active datasource and purge visualization state', () => { + it('should should switch active datasource and initialize new state', () => { const newState = reducer( { datasourceStates: { @@ -193,10 +192,40 @@ describe('editor_frame state management', () => { } ); - expect(newState.visualization.state).toEqual(null); - expect(newState.visualization.activeId).toBe(null); - expect(newState.activeDatasourceId).toBe('testDatasource2'); - expect(newState.datasourceStates.testDatasource.state).toBe(null); + expect(newState.activeDatasourceId).toEqual('testDatasource2'); + expect(newState.datasourceStates.testDatasource2.isLoading).toEqual(true); + }); + + it('not initialize already initialized datasource on switch', () => { + const datasource2State = {}; + const newState = reducer( + { + datasourceStates: { + testDatasource: { + state: {}, + isLoading: false, + }, + testDatasource2: { + state: datasource2State, + isLoading: false, + }, + }, + activeDatasourceId: 'testDatasource', + saving: false, + title: 'eee', + visualization: { + activeId: 'testVis', + state: {}, + }, + }, + { + type: 'SWITCH_DATASOURCE', + newDatasourceId: 'testDatasource2', + } + ); + + expect(newState.activeDatasourceId).toEqual('testDatasource2'); + expect(newState.datasourceStates.testDatasource2.state).toBe(datasource2State); }); it('should mark as saving', () => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index bfdd822bf10d0..c0ea024fe8d36 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -127,18 +127,12 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta ...state, datasourceStates: { ...state.datasourceStates, - [action.newDatasourceId]: { + [action.newDatasourceId]: state.datasourceStates[action.newDatasourceId] || { state: null, isLoading: true, }, }, activeDatasourceId: action.newDatasourceId, - visualization: { - ...state.visualization, - // purge visualization on datasource switch - state: null, - activeId: null, - }, }; case 'SWITCH_VISUALIZATION': return { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts index 34046688f3d51..a1408d9851398 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.test.ts @@ -6,16 +6,15 @@ import { getSuggestions } from './suggestion_helpers'; import { createMockVisualization } from '../mocks'; -import { TableSuggestion } from '../../types'; +import { TableSuggestion, DatasourceSuggestion } from '../../types'; const generateSuggestion = ( datasourceSuggestionId: number = 1, state = {}, layerId: string = 'first' -) => ({ +): DatasourceSuggestion => ({ state, - table: { datasourceSuggestionId, columns: [], isMultiRow: false }, - layerId, + table: { datasourceSuggestionId, columns: [], isMultiRow: false, layerId }, }); describe('suggestion helpers', () => { @@ -29,7 +28,7 @@ describe('suggestion helpers', () => { ...mockVisualization, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.5, title: 'Test', state: suggestedState, @@ -55,14 +54,14 @@ describe('suggestion helpers', () => { ...mockVisualization1, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.5, title: 'Test', state: {}, previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.5, title: 'Test2', state: {}, @@ -74,7 +73,7 @@ describe('suggestion helpers', () => { ...mockVisualization2, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.5, title: 'Test3', state: {}, @@ -99,14 +98,14 @@ describe('suggestion helpers', () => { ...mockVisualization1, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.2, title: 'Test', state: {}, previewIcon: 'empty', }, { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.8, title: 'Test2', state: {}, @@ -118,7 +117,7 @@ describe('suggestion helpers', () => { ...mockVisualization2, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.6, title: 'Test3', state: {}, @@ -138,13 +137,20 @@ describe('suggestion helpers', () => { it('should call all suggestion getters with all available data tables', () => { const mockVisualization1 = createMockVisualization(); const mockVisualization2 = createMockVisualization(); - const table1: TableSuggestion = { datasourceSuggestionId: 0, columns: [], isMultiRow: true }; - const table2: TableSuggestion = { datasourceSuggestionId: 1, columns: [], isMultiRow: true }; + const table1: TableSuggestion = { + datasourceSuggestionId: 0, + columns: [], + isMultiRow: true, + layerId: 'first', + }; + const table2: TableSuggestion = { + datasourceSuggestionId: 1, + columns: [], + isMultiRow: true, + layerId: 'first', + }; getSuggestions( - [ - { state: {}, table: table1, layerId: 'first' }, - { state: {}, table: table2, layerId: 'first' }, - ], + [{ state: {}, table: table1 }, { state: {}, table: table2 }], { vis1: mockVisualization1, vis2: mockVisualization2, @@ -164,20 +170,20 @@ describe('suggestion helpers', () => { const tableState1 = {}; const tableState2 = {}; const suggestions = getSuggestions( - [generateSuggestion(1, tableState1), generateSuggestion(1, tableState2)], + [generateSuggestion(1, tableState1), generateSuggestion(2, tableState2)], { vis1: { ...mockVisualization1, getSuggestions: () => [ { - datasourceSuggestionId: 0, + datasourceSuggestionId: 1, score: 0.3, title: 'Test', state: {}, previewIcon: 'empty', }, { - datasourceSuggestionId: 1, + datasourceSuggestionId: 2, score: 0.2, title: 'Test2', state: {}, @@ -189,7 +195,7 @@ describe('suggestion helpers', () => { ...mockVisualization2, getSuggestions: () => [ { - datasourceSuggestionId: 1, + datasourceSuggestionId: 2, score: 0.1, title: 'Test3', state: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts index b2a76c2485f0e..f2d1db4eb636c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_helpers.ts @@ -5,7 +5,7 @@ */ import { Ast } from '@kbn/interpreter/common'; -import { Visualization, DatasourceSuggestion } from '../../types'; +import { Visualization, DatasourceSuggestion, TableSuggestion } from '../../types'; import { Action } from './state_management'; export interface Suggestion { @@ -32,22 +32,22 @@ export function getSuggestions( activeVisualizationId: string | null, visualizationState: unknown ): Suggestion[] { - // const datasourceTables = datasourceTableSuggestions.map(({ table }) => table); + const datasourceTables: TableSuggestion[] = datasourceTableSuggestions.map(({ table }) => table); return Object.entries(visualizationMap) .map(([visualizationId, visualization]) => { return visualization .getSuggestions({ - // suggestions: datasourceTableSuggestions.map(({ table }) => table), - // suggestions: datasourceTableSuggestions, - tables: [], - layerId: '', + tables: datasourceTables, state: visualizationId === activeVisualizationId ? visualizationState : undefined, }) .map(({ datasourceSuggestionId, ...suggestion }) => ({ ...suggestion, visualizationId, - datasourceState: datasourceTableSuggestions[datasourceSuggestionId].state, + datasourceState: datasourceTableSuggestions.find( + datasourceSuggestion => + datasourceSuggestion.table.datasourceSuggestionId === datasourceSuggestionId + )!.state, })); }) .reduce((globalList, currentList) => [...globalList, ...currentList], []) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index d6e8874659f3c..d6911f15317ae 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -106,6 +106,7 @@ describe('suggestion_panel', () => { }); it('should render preview expression if there is one', () => { + mockDatasource.getLayers.mockReturnValue(['first']); (getSuggestions as jest.Mock).mockReturnValue([ { datasourceState: {}, @@ -135,30 +136,47 @@ describe('suggestion_panel', () => { (expressionRendererMock as jest.Mock).mock.calls[0][0].expression ); expect(passedExpression).toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object {}, - "function": "datasource_expression", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "test", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "expression", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource_expression", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "test", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "expression", + "type": "function", + }, + ], + "type": "expression", + } + `); }); it('should render render icon if there is no preview expression', () => { + mockDatasource.getLayers.mockReturnValue(['first']); (getSuggestions as jest.Mock).mockReturnValue([ { datasourceState: {}, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index debb5c380f50c..751d081806f2e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -460,13 +460,13 @@ describe('workspace_panel', () => { const expectedTable = { datasourceSuggestionId: 0, isMultiRow: true, + layerId: '1', columns: [], }; mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ { state: {}, table: expectedTable, - layerId: '1', }, ]); mockVisualization.getSuggestions.mockReturnValueOnce([ @@ -508,8 +508,8 @@ describe('workspace_panel', () => { datasourceSuggestionId: 0, isMultiRow: true, columns: [], + layerId: '1', }, - layerId: '1', }, { state: {}, @@ -517,8 +517,8 @@ describe('workspace_panel', () => { datasourceSuggestionId: 1, isMultiRow: true, columns: [], + layerId: '1', }, - layerId: '1', }, ]); mockVisualization.getSuggestions.mockReturnValueOnce([ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index cb503be73fb27..bb23678f0cd7c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -82,5 +82,10 @@ export function createMockDependencies() { run: jest.fn(_ => Promise.resolve({ type: 'render', as: 'test', value: undefined })), }, }, + interpreter: { + functionsRegistry: { + register: jest.fn(), + }, + }, } as unknown) as MockedDependencies; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx index 134197bee4b27..93ac1475b795e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.test.tsx @@ -19,7 +19,8 @@ jest.mock('ui/chrome', () => ({ getSavedObjectsClient: jest.fn(), })); -// mock away actual data plugin to prevent all of it being loaded +// mock away actual dependencies to prevent all of it being loaded +jest.mock('../../../../../../src/legacy/core_plugins/interpreter/public/registries', () => {}); jest.mock('../../../../../../src/legacy/core_plugins/data/public/setup', () => {}); function mockStore(): SavedObjectStore { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 663cafb5e0ba6..c2d786ea3a923 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -285,8 +285,8 @@ describe('IndexPattern Data Source', () => { columnId: 'col2', }), ], + layerId: 'suggestedLayer', }); - expect(suggestions[0].layerId).toEqual('suggestedLayer'); }); it('should apply a bucketed aggregation for a date field', () => { @@ -327,6 +327,7 @@ describe('IndexPattern Data Source', () => { columnId: 'col2', }), ], + layerId: 'suggestedLayer', }); }); @@ -369,6 +370,7 @@ describe('IndexPattern Data Source', () => { columnId: 'col2', }), ], + layerId: 'suggestedLayer', }); }); @@ -490,8 +492,8 @@ describe('IndexPattern Data Source', () => { }, }, ], + layerId: 'first', }, - layerId: 'first', }, ]); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 791ff1203ff64..ac59b3bd5818f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -400,9 +400,8 @@ export function getIndexPatternDatasource({ ], isMultiRow: true, datasourceSuggestionId: 0, + layerId, }, - - layerId, }; return [suggestion]; @@ -460,9 +459,8 @@ export function getIndexPatternDatasource({ ], isMultiRow: true, datasourceSuggestionId: 0, + layerId, }, - - layerId, }; return [suggestion]; @@ -491,8 +489,8 @@ export function getIndexPatternDatasource({ })), isMultiRow: true, datasourceSuggestionId: index, + layerId, }, - layerId, }; }) .reduce((prev, current) => (current ? prev.concat([current]) : prev), [] as Array< diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index f04da2d25d980..b4de805542b23 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -40,12 +40,12 @@ export interface TableSuggestion { datasourceSuggestionId: number; isMultiRow: boolean; columns: TableSuggestionColumn[]; + layerId: string; } export interface DatasourceSuggestion { state: T; table: TableSuggestion; - layerId: string; } /** @@ -160,7 +160,6 @@ export interface KibanaDatatable { export interface VisualizationProps { dragDropContext: DragContextState; - // datasource: DatasourcePublicAPI; frame: FramePublicAPI; state: T; setState: (newState: T) => void; @@ -169,7 +168,6 @@ export interface VisualizationProps { export interface SuggestionRequest { // It is up to the Visualization to rank these tables tables: TableSuggestion[]; - layerId: string; state?: T; // State is only passed if the visualization is active } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index e1f8f41e3dcba..04af0163a5ccf 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -10,7 +10,7 @@ import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps } from '@elastic/eui'; import { XYConfigPanel } from './xy_config_panel'; import { DatasourceDimensionPanelProps, Operation, FramePublicAPI } from '../types'; -import { State, SeriesType } from './types'; +import { State } from './types'; import { Position } from '@elastic/charts'; import { NativeRendererProps } from '../native_renderer'; import { generateId } from '../id_generator'; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index ef691ea3f4a07..a6b9495303bd2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -70,18 +70,30 @@ describe('xy_suggestions', () => { expect( getSuggestions({ tables: [ - { datasourceSuggestionId: 0, isMultiRow: true, columns: [dateCol('a')] }, + { + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [dateCol('a')], + layerId: 'first', + }, { datasourceSuggestionId: 1, isMultiRow: true, columns: [strCol('foo'), strCol('bar')], + layerId: 'first', }, { datasourceSuggestionId: 2, isMultiRow: false, columns: [strCol('foo'), numCol('bar')], + layerId: 'first', + }, + { + datasourceSuggestionId: 3, + isMultiRow: true, + columns: [unknownCol(), numCol('bar')], + layerId: 'first', }, - { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')] }, ], }) ).toEqual([]); @@ -95,6 +107,7 @@ describe('xy_suggestions', () => { datasourceSuggestionId: 0, isMultiRow: true, columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', }, ], }); @@ -121,6 +134,7 @@ describe('xy_suggestions', () => { datasourceSuggestionId: 1, isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', }, ], }); @@ -149,11 +163,13 @@ describe('xy_suggestions', () => { datasourceSuggestionId: 0, isMultiRow: true, columns: [numCol('price'), dateCol('date')], + layerId: 'first', }, { datasourceSuggestionId: 1, isMultiRow: true, columns: [numCol('count'), strCol('country')], + layerId: 'first', }, ], }); @@ -193,6 +209,7 @@ describe('xy_suggestions', () => { datasourceSuggestionId: 1, isMultiRow: true, columns: [numCol('quantity'), numCol('price')], + layerId: 'first', }, ], }); @@ -230,6 +247,7 @@ describe('xy_suggestions', () => { }, }, ], + layerId: 'first', }, ], }); @@ -255,6 +273,7 @@ describe('xy_suggestions', () => { datasourceSuggestionId: 0, isMultiRow: true, columns: [numCol('bytes'), dateCol('date')], + layerId: 'first', }, ], }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index e256e5d2eb03d..ae7a96f50e8c4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -42,10 +42,13 @@ export function getSuggestions( columns.some(col => col.operation.dataType === 'number') && !columns.some(col => !columnSortOrder.hasOwnProperty(col.operation.dataType)) ) - .map(table => getSuggestionForColumns(table)); + .map(table => getSuggestionForColumns(table, opts.state)); } -function getSuggestionForColumns(table: TableSuggestion): VisualizationSuggestion { +function getSuggestionForColumns( + table: TableSuggestion, + currentState?: State +): VisualizationSuggestion { const [buckets, values] = partition( prioritizeColumns(table.columns), col => col.operation.isBucketed @@ -53,10 +56,24 @@ function getSuggestionForColumns(table: TableSuggestion): VisualizationSuggestio if (buckets.length >= 1) { const [x, splitBy] = buckets; - return getSuggestion(table.datasourceSuggestionId, x, values, splitBy); + return getSuggestion( + table.datasourceSuggestionId, + table.layerId, + x, + values, + splitBy, + currentState + ); } else { const [x, ...yValues] = values; - return getSuggestion(table.datasourceSuggestionId, x, yValues); + return getSuggestion( + table.datasourceSuggestionId, + table.layerId, + x, + yValues, + undefined, + currentState + ); } } @@ -71,9 +88,11 @@ function prioritizeColumns(columns: TableSuggestionColumn[]) { function getSuggestion( datasourceSuggestionId: number, + layerId: string, xValue: TableSuggestionColumn, yValues: TableSuggestionColumn[], - splitBy?: TableSuggestionColumn + splitBy?: TableSuggestionColumn, + currentState?: State ): VisualizationSuggestion { const yTitle = yValues.map(col => col.operation.label).join(' & '); const xTitle = xValue.operation.label; @@ -83,10 +102,11 @@ function getSuggestion( const preposition = isDate ? 'over' : 'of'; const title = `${yTitle} ${preposition} ${xTitle}`; const state: State = { - legend: { isVisible: true, position: Position.Right }, + legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, layers: [ + ...(currentState ? currentState.layers.filter(layer => layer.layerId !== layerId) : []), { - layerId: 'first', + layerId, datasourceId: '', xAccessor: xValue.columnId, seriesType: splitBy && isDate ? 'line' : 'bar', From ad523b0edeffe1d95cae41db1c55523d50ec25f7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 15:21:38 +0200 Subject: [PATCH 39/67] fix review findings and use context handler for layer compatibility --- .../expressions/expression_renderer.tsx | 2 +- .../editor_frame/editor_frame.test.tsx | 5 ++ .../editor_frame/expression_helpers.ts | 1 + .../editor_frame/workspace_panel.test.tsx | 7 +- .../embeddable/embeddable.test.tsx | 88 ++++++------------- .../embeddable/expression_wrapper.tsx | 14 ++- 6 files changed, 44 insertions(+), 73 deletions(-) diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx index c1899aa9df9a2..c87ab2a7311b6 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -58,7 +58,7 @@ export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ } }); } - }, [expression, mountpoint.current]); + }, [expression, mountpoint.current, options.context, options.getInitialContext]); return (
{ expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` Object { "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, Object { "arguments": Object { "filters": Array [], diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts index 84a7ffa95f6c8..7f5c599bb9872 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/expression_helpers.ts @@ -53,6 +53,7 @@ export function prependKibanaContext( return { type: 'expression', chain: [ + { type: 'function', function: 'kibana', arguments: {} }, { type: 'function', function: 'kibana_context', diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index d629552be228f..619075fe87edc 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -123,6 +123,11 @@ describe('workspace_panel', () => { expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` Object { "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, Object { "arguments": Object { "filters": Array [], @@ -168,7 +173,7 @@ describe('workspace_panel', () => { /> ); - expect(instance.find('EuiFlexItem[data-test-subj="expression-failure"]')).toHaveLength(1); + expect(instance.find('[data-test-subj="expression-failure"]').first()).toBeTruthy(); expect(instance.find(expressionRendererMock)).toHaveLength(0); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx index 4564b7c1f8fb5..0c75d90adcd9a 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.test.tsx @@ -6,7 +6,7 @@ import { Embeddable } from './embeddable'; import { TimeRange } from 'ui/timefilter/time_history'; -import { Query } from 'src/legacy/core_plugins/data/public'; +import { Query, ExpressionRendererProps } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { Document } from '../../persistence'; import { act } from 'react-dom/test-utils'; @@ -26,9 +26,19 @@ const savedVis: Document = { }; describe('embeddable', () => { + let mountpoint: HTMLDivElement; + let expressionRenderer: jest.Mock; + + beforeEach(() => { + mountpoint = document.createElement('div'); + expressionRenderer = jest.fn(_props => null); + }); + + afterEach(() => { + mountpoint.remove(); + }); + it('should render expression with expression renderer', () => { - const mountpoint = document.createElement('div'); - const expressionRenderer = jest.fn(_props => null); const embeddable = new Embeddable( expressionRenderer, { @@ -41,39 +51,10 @@ describe('embeddable', () => { embeddable.render(mountpoint); expect(expressionRenderer).toHaveBeenCalledTimes(1); - expect(expressionRenderer.mock.calls[0][0]!.expression).toMatchInlineSnapshot(` - Object { - "chain": Array [ - Object { - "arguments": Object { - "filters": Array [], - "query": Array [], - "timeRange": Array [], - }, - "function": "kibana_context", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "my", - "type": "function", - }, - Object { - "arguments": Object {}, - "function": "expression", - "type": "function", - }, - ], - "type": "expression", - } - `); - mountpoint.remove(); + expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(savedVis.expression); }); it('should display error if expression renderering fails', () => { - const mountpoint = document.createElement('div'); - const expressionRenderer = jest.fn(_props => null); - const embeddable = new Embeddable( expressionRenderer, { @@ -86,17 +67,13 @@ describe('embeddable', () => { embeddable.render(mountpoint); act(() => { - expressionRenderer.mock.calls[0][0]!.onRenderFailure({}); + expressionRenderer.mock.calls[0][0]!.onRenderFailure!({ type: 'error' }); }); expect(mountpoint.innerHTML).toContain("Visualization couldn't be displayed"); - - mountpoint.remove(); }); - it('should prepend context to the expression chain', () => { - const mountpoint = document.createElement('div'); - const expressionRenderer = jest.fn(_props => null); + it('should re-render if new input is pushed', () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; @@ -108,21 +85,20 @@ describe('embeddable', () => { editable: true, savedVis, }, - { id: '123', timeRange, query, filters } + { id: '123' } ); embeddable.render(mountpoint); - expect(expressionRenderer.mock.calls[0][0]!.expression.chain[0].arguments).toEqual({ - timeRange: [JSON.stringify(timeRange)], - query: [JSON.stringify(query)], - filters: [JSON.stringify(filters)], + embeddable.updateInput({ + timeRange, + query, + filters, }); - mountpoint.remove(); + + expect(expressionRenderer).toHaveBeenCalledTimes(2); }); - it('should re-render if new input is pushed', () => { - const mountpoint = document.createElement('div'); - const expressionRenderer = jest.fn(_props => null); + it('should pass context in getInitialContext handler', () => { const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }]; @@ -134,29 +110,18 @@ describe('embeddable', () => { editable: true, savedVis, }, - { id: '123' } + { id: '123', timeRange, query, filters } ); embeddable.render(mountpoint); - embeddable.updateInput({ + expect(expressionRenderer.mock.calls[0][0].getInitialContext!()).toEqual({ timeRange, query, filters, }); - - expect(expressionRenderer).toHaveBeenCalledTimes(2); - - expect(expressionRenderer.mock.calls[1][0]!.expression.chain[0].arguments).toEqual({ - timeRange: [JSON.stringify(timeRange)], - query: [JSON.stringify(query)], - filters: [JSON.stringify(filters)], - }); - mountpoint.remove(); }); it('should not re-render if only change is in disabled filter', () => { - const mountpoint = document.createElement('div'); - const expressionRenderer = jest.fn(_props => null); const timeRange: TimeRange = { from: 'now-15d', to: 'now' }; const query: Query = { language: 'kquery', query: '' }; const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }]; @@ -179,6 +144,5 @@ describe('embeddable', () => { }); expect(expressionRenderer).toHaveBeenCalledTimes(1); - mountpoint.remove(); }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx index 7c883f3d7a0e5..d5ba7bcd39118 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/expression_wrapper.tsx @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -14,7 +14,6 @@ import { TimeRange } from 'ui/timefilter/time_history'; import { Query } from 'src/legacy/core_plugins/data/public'; import { Filter } from '@kbn/es-query'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { prependKibanaContext } from '../../editor_frame_plugin/editor_frame/expression_helpers'; export interface ExpressionWrapperProps { ExpressionRenderer: ExpressionRenderer; @@ -33,19 +32,15 @@ export function ExpressionWrapper({ context, }: ExpressionWrapperProps) { const [expressionError, setExpressionError] = useState(undefined); - const contextualizedExpression = useMemo(() => prependKibanaContext(expression, context), [ - expression, - context, - ]); useEffect(() => { // reset expression error if component attempts to run it again if (expressionError) { setExpressionError(undefined); } - }, [contextualizedExpression]); + }, [expression, context]); return ( - {contextualizedExpression === null || expressionError ? ( + {expression === '' || expressionError ? ( @@ -62,10 +57,11 @@ export function ExpressionWrapper({ ) : ( { setExpressionError(e); }} + getInitialContext={() => context} /> )} From a6db214b147e0ded8fd7e19cb80facb0c4e9b559 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 15:24:12 +0200 Subject: [PATCH 40/67] remove blank line --- .../lens/public/editor_frame_plugin/embeddable/embeddable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx index 3d623891ce32c..22519fae8f2df 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx @@ -72,7 +72,6 @@ export class Embeddable extends AbstractEmbeddable this.onContainerStateChanged(input)); this.onContainerStateChanged(initialInput); From 85a7fab37b34f1df88d92c36a2a7622991b5bc39 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 16:32:48 +0200 Subject: [PATCH 41/67] fix typing bug --- x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts index 41989427c3e0f..74b0ff434f4cb 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/loader.ts @@ -6,6 +6,7 @@ import { Chrome } from 'ui/chrome'; import { ToastNotifications } from 'ui/notify/toasts/toast_notifications'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { SavedObjectAttributes } from 'src/core/server/saved_objects'; import { IndexPatternField } from './indexpattern'; From 64b2f58264ff5565388e3943ff85be9aff200026 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Mon, 22 Jul 2019 11:35:14 -0400 Subject: [PATCH 42/67] Fix xy tests, skip inapplicable tests that will be implemented in a separate PR --- .../__snapshots__/xy_expression.test.tsx.snap | 2 +- .../xy_config_panel.test.tsx | 296 +++--------------- .../xy_config_panel.tsx | 3 +- .../xy_expression.test.tsx | 4 +- .../xy_suggestions.test.ts | 146 +++++---- .../xy_visualization.test.ts | 68 ++-- 6 files changed, 156 insertions(+), 363 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index a81c164dab608..ed614c665a9a7 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -23,7 +23,7 @@ exports[`xy_expression XYChart component it renders area 1`] = ` showGridLines={false} title="A and B" /> - { .props(); } - // test('disables stacked chart types without a split series', () => { - // const frame = createMockFramePublicAPI(); - // frame.datasourceLayers = { - // first: createMockDatasource().publicAPIMock, - // }; - // const component = mount( - // {}} - // state={testState()} - // /> - // ); - - // const options = component - // .find('[data-test-subj="lnsXY_seriesType"]') - // .first() - // .prop('options') as EuiButtonGroupProps['options']; - - // expect(options.map(({ id }) => id)).toEqual([ - // 'line', - // 'area', - // 'bar', - // 'horizontal_bar', - // 'area_stacked', - // 'bar_stacked', - // 'horizontal_bar_stacked', - // ]); - - // expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([ - // 'area_stacked', - // 'bar_stacked', - // 'horizontal_bar_stacked', - // ]); - // }); beforeEach(() => { frame = createMockFramePublicAPI(); frame.datasourceLayers = { @@ -92,22 +57,20 @@ describe('XYConfigPanel', () => { }; }); - test('enables all stacked chart types when there is a split series', () => { + test.skip('toggles axis position when going from horizontal bar to any other type', () => {}); + test.skip('allows toggling of legend visibility', () => {}); + test.skip('allows changing legend position', () => {}); + test.skip('allows toggling the y axis gridlines', () => {}); + test.skip('allows toggling the x axis gridlines', () => {}); + + test('enables stacked chart types even when there is no split series', () => { const state = testState(); - // const frame = createMockFramePublicAPI(); - // frame.datasourceLayers = { - // first: createMockDatasource().publicAPIMock, - // }; const component = mount( {}} - state={{ ...state, layers: [{ ...state.layers[0], splitAccessor: 'c' }] }} + setState={jest.fn()} + state={{ ...state, layers: [{ ...state.layers[0], xAccessor: 'shazm' }] }} /> ); @@ -116,141 +79,26 @@ describe('XYConfigPanel', () => { .first() .prop('options') as EuiButtonGroupProps['options']; - expect(options.every(({ isDisabled }) => !isDisabled)).toEqual(true); + expect(options.map(({ id }) => id)).toEqual([ + 'line', + 'area', + 'bar', + 'horizontal_bar', + 'area_stacked', + 'bar_stacked', + 'horizontal_bar_stacked', + ]); + + expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); - // test('toggles axis position when going from horizontal bar to any other type', () => { - // const changeSeriesType = (fromSeriesType: SeriesType, toSeriesType: SeriesType) => { - // const setState = jest.fn(); - // const state = testState(); - // const component = mount( - // - // ); - - // (testSubj(component, 'lnsXY_seriesType').onChange as Function)(toSeriesType); - - // expect(setState).toHaveBeenCalledTimes(1); - // return setState.mock.calls[0][0]; - // }; - - // expect(changeSeriesType('line', 'horizontal_bar')).toMatchObject({ - // seriesType: 'horizontal_bar', - // x: { position: Position.Left }, - // y: { position: Position.Bottom }, - // }); - // expect(changeSeriesType('horizontal_bar', 'bar')).toMatchObject({ - // seriesType: 'bar', - // x: { position: Position.Bottom }, - // y: { position: Position.Left }, - // }); - // expect(changeSeriesType('horizontal_bar', 'line')).toMatchObject({ - // seriesType: 'line', - // x: { position: Position.Bottom }, - // y: { position: Position.Left }, - // }); - // expect(changeSeriesType('horizontal_bar', 'area')).toMatchObject({ - // seriesType: 'area', - // x: { position: Position.Bottom }, - // y: { position: Position.Left }, - // }); - // }); - - test('allows toggling of legend visibility', () => { - const toggleIsVisible = (isVisible: boolean) => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_legendIsVisible').onChange as Function)(); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(toggleIsVisible(false)).toMatchObject({ - legend: { isVisible: true }, - }); - expect(toggleIsVisible(true)).toMatchObject({ - legend: { isVisible: false }, - }); - }); - - test('allows changing legend position', () => { - const testLegendPosition = (position: Position) => { - const setState = jest.fn(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_legendPosition').onChange as Function)(position); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(testLegendPosition(Position.Bottom)).toMatchObject({ - legend: { position: Position.Bottom }, - }); - expect(testLegendPosition(Position.Top)).toMatchObject({ - legend: { position: Position.Top }, - }); - expect(testLegendPosition(Position.Left)).toMatchObject({ - legend: { position: Position.Left }, - }); - expect(testLegendPosition(Position.Right)).toMatchObject({ - legend: { position: Position.Right }, - }); - }); - - test('allows editing the x axis title', () => { - const testSetTitle = (title: string) => { - const setState = jest.fn(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_xTitle').onChange as Function)({ target: { value: title } }); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(testSetTitle('Hoi')).toMatchObject({ - x: { title: 'Hoi' }, - }); - expect(testSetTitle('There!')).toMatchObject({ - x: { title: 'There!' }, - }); - }); - - test('the x dimension panel accepts any operations', () => { + test('the x dimension panel accepts only buckted operations', () => { + // TODO: this should eventually also accept raw operation const state = testState(); const component = mount( @@ -265,69 +113,23 @@ describe('XYConfigPanel', () => { isBucketed: false, label: 'bar', }; + const bucketedOps: Operation[] = [ + { ...exampleOperation, isBucketed: true, dataType: 'number' }, + { ...exampleOperation, isBucketed: true, dataType: 'string' }, + { ...exampleOperation, isBucketed: true, dataType: 'boolean' }, + { ...exampleOperation, isBucketed: true, dataType: 'date' }, + ]; const ops: Operation[] = [ + ...bucketedOps, { ...exampleOperation, dataType: 'number' }, { ...exampleOperation, dataType: 'string' }, { ...exampleOperation, dataType: 'boolean' }, { ...exampleOperation, dataType: 'date' }, ]; expect(columnId).toEqual('shazm'); - expect(ops.filter(filterOperations)).toEqual(ops); - }); - - test('allows toggling the x axis gridlines', () => { - const toggleXGridlines = (showGridlines: boolean) => { - const setState = jest.fn(); - const state = testState(); - const component = mount( - - ); - - (testSubj(component, 'lnsXY_xShowGridlines').onChange as Function)(); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(toggleXGridlines(true)).toMatchObject({ - x: { showGridlines: false }, - }); - expect(toggleXGridlines(false)).toMatchObject({ - x: { showGridlines: true }, - }); + expect(ops.filter(filterOperations)).toEqual(bucketedOps); }); - // test('allows editing the y axis title', () => { - // const testSetTitle = (title: string) => { - // const setState = jest.fn(); - // const component = mount( - // - // ); - - // (testSubj(component, 'lnsXY_yTitle').onChange as Function)({ target: { value: title } }); - - // expect(setState).toHaveBeenCalledTimes(1); - // return setState.mock.calls[0][0]; - // }; - - // expect(testSetTitle('Hoi')).toMatchObject({ - // y: { title: 'Hoi' }, - // }); - // expect(testSetTitle('There!')).toMatchObject({ - // y: { title: 'There!' }, - // }); - // }); - test('the y dimension panel accepts numeric operations', () => { const state = testState(); const component = mount( @@ -339,9 +141,11 @@ describe('XYConfigPanel', () => { /> ); - const panel = testSubj(component, 'lnsXY_yDimensionPanel_a'); - const nativeProps = (panel as NativeRendererProps).nativeProps; - const { filterOperations } = nativeProps; + const filterOperations = component + .find('[data-test-subj="lensXY_yDimensionPanel"]') + .first() + .prop('filterOperations') as (op: Operation) => boolean; + const exampleOperation: Operation = { dataType: 'number', id: 'foo', @@ -358,11 +162,6 @@ describe('XYConfigPanel', () => { }); test('allows removal of y dimensions', () => { - // const frame = createMockFramePublicAPI(); - // const datasourceMock = createMockDatasource().publicAPIMock; - // frame.datasourceLayers = { - // first: datasourceMock, - // }; const setState = jest.fn(); const state = testState(); const component = mount( @@ -374,12 +173,14 @@ describe('XYConfigPanel', () => { /> ); - (testSubj(component, 'lnsXY_yDimensionPanel_remove_b').onClick as Function)(); + const onRemove = component + .find('[data-test-subj="lensXY_yDimensionPanel"]') + .first() + .prop('onRemove') as (accessor: string) => {}; + + onRemove('b'); expect(setState).toHaveBeenCalledTimes(1); - // expect(setState.mock.calls[0][0]).toMatchObject({ - // y: { accessors: ['a', 'c'] }, - // }); expect(setState.mock.calls[0][0]).toMatchObject({ layers: [ { @@ -388,11 +189,9 @@ describe('XYConfigPanel', () => { }, ], }); - expect(frame.datasourceLayers.first.removeColumnInTableSpec).toHaveBeenCalledTimes(1); - expect(frame.datasourceLayers.first.removeColumnInTableSpec).toHaveBeenCalledWith('b'); }); - test('allows adding y dimensions', () => { + test('allows adding a y axis dimension', () => { (generateId as jest.Mock).mockReturnValueOnce('zed'); const setState = jest.fn(); const state = testState(); @@ -405,11 +204,14 @@ describe('XYConfigPanel', () => { /> ); - (testSubj(component, 'lnsXY_yDimensionPanel_add').onClick as Function)(); + const onAdd = component + .find('[data-test-subj="lensXY_yDimensionPanel"]') + .first() + .prop('onAdd') as () => {}; + + onAdd(); expect(setState).toHaveBeenCalledTimes(1); - // expect(setState.mock.calls[0][0]).toMatchObject({ - // y: { accessors: ['a', 'b', 'c', 'zed'] }, expect(setState.mock.calls[0][0]).toMatchObject({ layers: [ { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 78fcc9c0de08e..4659ff3dcda63 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -224,7 +224,8 @@ export function XYConfigPanel(props: VisualizationProps) { ) } filterOperations={op => !op.isBucketed && op.dataType === 'number'} - testSubj="yDimensionPanel" + data-test-subj="lensXY_yDimensionPanel" + testSubj="lensXY_yDimensionPanel" layerId={layer.layerId} /> diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index 31644a6f936d2..ed2001b65f4fd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -77,7 +77,7 @@ describe('xy_expression', () => { }; expect(layerConfig.fn(null, args, {})).toEqual({ - type: 'lens_xy_layerConfig', + type: 'lens_xy_layer', ...args, }); }); @@ -126,7 +126,7 @@ describe('xy_expression', () => { const component = shallow( ); expect(component).toMatchSnapshot(); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index a6b9495303bd2..4d99f54544426 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -7,7 +7,6 @@ import { getSuggestions } from './xy_suggestions'; import { TableSuggestionColumn, VisualizationSuggestion } from '../types'; import { State } from './types'; -import { Ast } from '@kbn/interpreter/target/common'; import { generateId } from '../id_generator'; jest.mock('../id_generator'); @@ -114,17 +113,17 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar", - "splitAccessor": "aaa", - "x": "date", - "y": Array [ - "bytes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "aaa", + "x": "date", + "y": Array [ + "bytes", + ], + }, + ] + `); }); test('suggests a split x y chart with date on x', () => { @@ -141,18 +140,18 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "line", - "splitAccessor": "product", - "x": "date", - "y": Array [ - "price", - "quantity", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "line", + "splitAccessor": "product", + "x": "date", + "y": Array [ + "price", + "quantity", + ], + }, + ] + `); }); test('supports multiple suggestions', () => { @@ -176,29 +175,29 @@ describe('xy_suggestions', () => { expect(rest).toHaveLength(0); expect([suggestionSubset(s1), suggestionSubset(s2)]).toMatchInlineSnapshot(` - Array [ - Array [ - Object { - "seriesType": "bar", - "splitAccessor": "bbb", - "x": "date", - "y": Array [ - "price", - ], - }, - ], - Array [ - Object { - "seriesType": "bar", - "splitAccessor": "ccc", - "x": "country", - "y": Array [ - "count", - ], - }, - ], - ] - `); + Array [ + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "bbb", + "x": "date", + "y": Array [ + "price", + ], + }, + ], + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "ccc", + "x": "country", + "y": Array [ + "count", + ], + }, + ], + ] + `); }); test('handles two numeric values', () => { @@ -215,17 +214,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar", - "splitAccessor": "ddd", - "x": "quantity", - "y": Array [ - "price", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "ddd", + "x": "quantity", + "y": Array [ + "price", + ], + }, + ] + `); }); test('handles unbucketed suggestions', () => { @@ -253,17 +252,17 @@ describe('xy_suggestions', () => { }); expect(suggestionSubset(suggestion)).toMatchInlineSnapshot(` - Array [ - Object { - "seriesType": "bar", - "splitAccessor": "eee", - "x": "mybool", - "y": Array [ - "num votes", - ], - }, - ] - `); + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "eee", + "x": "mybool", + "y": Array [ + "num votes", + ], + }, + ] + `); }); test('adds a preview expression with disabled axes and legend', () => { @@ -278,12 +277,9 @@ describe('xy_suggestions', () => { ], }); - const expression = suggestion.previewExpression! as Ast; + const expression = suggestion.previewExpression! as any; - expect( - (expression.chain[0].arguments.legend[0] as Ast).chain[0].arguments.isVisible[0] - ).toBeFalsy(); - expect((expression.chain[0].arguments.x[0] as Ast).chain[0].arguments.hide[0]).toBeTruthy(); - expect((expression.chain[0].arguments.y[0] as Ast).chain[0].arguments.hide[0]).toBeTruthy(); + expect(expression.chain[0].arguments.legend[0].chain[0].arguments.isVisible[0]).toBeFalsy(); + expect(expression.chain[0].arguments.layers[0].chain[0].arguments.hide[0]).toBeTruthy(); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 035af7a8ee5bf..538f2e9c6a232 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -6,7 +6,6 @@ import { xyVisualization } from './xy_visualization'; import { Position } from '@elastic/charts'; -import { Ast } from '@kbn/interpreter/target/common'; import { Operation } from '../types'; import { State } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; @@ -50,30 +49,30 @@ describe('xy_visualization', () => { expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); expect(initialState).toMatchInlineSnapshot(` - Object { - "layers": Array [ - Object { - "accessors": Array [ - "test-id1", - ], - "datasourceId": "", - "labels": Array [], - "layerId": "", - "position": "top", - "seriesType": "bar", - "showGridlines": false, - "splitAccessor": "test-id2", - "title": "", - "xAccessor": "test-id3", - }, - ], - "legend": Object { - "isVisible": true, - "position": "right", - }, - "title": "Empty XY Chart", - } - `); + Object { + "layers": Array [ + Object { + "accessors": Array [ + "test-id1", + ], + "datasourceId": "", + "labels": Array [], + "layerId": "", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "splitAccessor": "test-id2", + "title": "", + "xAccessor": "test-id3", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "title": "Empty XY Chart", + } + `); }); it('loads from persisted state', () => { @@ -101,27 +100,22 @@ describe('xy_visualization', () => { it('should default to labeling all columns with their column label', () => { const mockDatasource = createMockDatasource(); - mockDatasource.publicAPIMock.getOperationForColumnId - .mockReturnValueOnce({ - label: 'First', - } as Operation) - .mockReturnValueOnce({ - label: 'Second', - } as Operation); + mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { + return { label: `col_${col}` } as Operation; + }); const frame = createMockFramePublicAPI(); frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; - const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; + const expression = xyVisualization.toExpression(exampleState(), frame)! as any; - expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledTimes(2); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect((expression.chain[0].arguments.y[0] as Ast).chain[0].arguments.labels).toEqual([ - 'First', - 'Second', + expect(expression.chain[0].arguments.layers[0].chain[0].arguments.labels).toEqual([ + 'col_b', + 'col_c', ]); }); }); From f634f51c3637e861e392660d5bff65cd513d4ab3 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 22 Jul 2019 19:03:09 +0200 Subject: [PATCH 43/67] add some tests for multiple datasources and layers --- .../editor_frame/editor_frame.test.tsx | 253 +++++++++++++++++- .../editor_frame/editor_frame.tsx | 12 +- .../editor_frame/state_management.ts | 20 -- .../editor_frame/workspace_panel.test.tsx | 14 +- 4 files changed, 261 insertions(+), 38 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index fd52e1c2b66fb..35fba716f7d8d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -35,7 +35,7 @@ function generateSuggestion(datasourceSuggestionId = 1, state = {}): DatasourceS } describe('editor_frame', () => { - let mockVisualization: Visualization; + let mockVisualization: jest.Mocked; let mockDatasource: DatasourceMock; let mockVisualization2: jest.Mocked; @@ -106,7 +106,48 @@ describe('editor_frame', () => { expect(mockDatasource.initialize).not.toHaveBeenCalled(); }); - it('should not render something before datasource is initialized', () => { + it('should initialize all datasources with state from doc', () => { + const mockDatasource3 = createMockDatasource(); + const datasource1State = { datasource1: '' }; + const datasource2State = { datasource2: '' }; + + act(() => { + mount( + + ); + }); + + expect(mockDatasource.initialize).toHaveBeenCalledWith(datasource1State); + expect(mockDatasource2.initialize).toHaveBeenCalledWith(datasource2State); + expect(mockDatasource3.initialize).not.toHaveBeenCalled(); + }); + + it('should not render something before all datasources are initialized', () => { act(() => { mount( { }); }); + it('should add new layer on active datasource on frame api call', async () => { + const initialState = { datasource2: '' }; + mockDatasource2.initialize.mockReturnValue(Promise.resolve(initialState)); + act(() => { + mount( + + ); + }); + + await waitForPromises(); + + mockVisualization.initialize.mock.calls[0][0].addNewLayer(); + + expect(mockDatasource2.insertLayer).toHaveBeenCalledWith(initialState, expect.anything()); + }); + it('should render data panel after initialization is complete', async () => { const initialState = {}; let databaseInitialized: ({}) => void; @@ -270,6 +339,79 @@ describe('editor_frame', () => { instance.update(); + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "layerIds": Array [ + "first", + ], + "tables": Array [ + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource", + "type": "function", + }, + ], + "type": "expression", + }, + ], + }, + "function": "lens_merge_tables", + "type": "function", + }, + Object { + "arguments": Object {}, + "function": "vis", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + + it('should render individual expression for each given layer', async () => { + mockDatasource.toExpression.mockReturnValue('datasource'); + mockDatasource2.toExpression.mockReturnValueOnce('datasource2_1'); + mockDatasource2.toExpression.mockReturnValueOnce('datasource2_2'); + mockDatasource.getLayers.mockReturnValue(['first']); + mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + const instance = mount( + 'vis' }, + }} + datasourceMap={{ + testDatasource: mockDatasource, + testDatasource2: mockDatasource2, + }} + initialDatasourceId="testDatasource" + initialVisualizationId="testVis" + ExpressionRenderer={expressionRendererMock} + doc={{ + activeDatasourceId: 'testDatasource', + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, + }, + visualization: {}, + }, + }} + /> + ); + + await waitForPromises(); + + instance.update(); + expect(instance.find(expressionRendererMock).prop('expression')).toMatchInlineSnapshot(` Object { "chain": Array [ @@ -277,6 +419,8 @@ describe('editor_frame', () => { "arguments": Object { "layerIds": Array [ "first", + "second", + "third", ], "tables": Array [ Object { @@ -289,6 +433,26 @@ describe('editor_frame', () => { ], "type": "expression", }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2_1", + "type": "function", + }, + ], + "type": "expression", + }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2_2", + "type": "function", + }, + ], + "type": "expression", + }, ], }, "function": "lens_merge_tables", @@ -422,8 +586,9 @@ describe('editor_frame', () => { }); describe('datasource public api communication', () => { - it('should pass the datasource api to the visualization', async () => { + it('should pass the datasource api for each layer to the visualization', async () => { mockDatasource.getLayers.mockReturnValue(['first']); + mockDatasource2.getLayers.mockReturnValue(['second', 'third']); mount( { }} datasourceMap={{ testDatasource: mockDatasource, + testDatasource2: mockDatasource2, }} initialDatasourceId="testDatasource" initialVisualizationId="testVis" ExpressionRenderer={expressionRendererMock} + doc={{ + activeDatasourceId: 'testDatasource', + visualizationType: 'testVis', + title: '', + state: { + datasourceStates: { + testDatasource: {}, + testDatasource2: {}, + }, + visualization: {}, + }, + }} /> ); await waitForPromises(); - expect(mockVisualization.renderConfigPanel).toHaveBeenCalledWith( - expect.any(Element), - expect.objectContaining({ - frame: expect.objectContaining({ - datasourceLayers: { first: mockDatasource.publicAPIMock }, - }), - }) + expect(mockVisualization.renderConfigPanel).toHaveBeenCalled(); + + const datasourceLayers = + mockVisualization.renderConfigPanel.mock.calls[0][1].frame.datasourceLayers; + expect(datasourceLayers.first).toBe(mockDatasource.publicAPIMock); + expect(datasourceLayers.second).toBe(mockDatasource2.publicAPIMock); + expect(datasourceLayers.third).toBe(mockDatasource2.publicAPIMock); + }); + + it('should create a separate datasource public api for each layer', async () => { + mockDatasource.initialize.mockImplementation(initialState => Promise.resolve(initialState)); + mockDatasource.getLayers.mockReturnValue(['first']); + mockDatasource2.initialize.mockImplementation(initialState => Promise.resolve(initialState)); + mockDatasource2.getLayers.mockReturnValue(['second', 'third']); + + const datasource1State = { datasource1: '' }; + const datasource2State = { datasource2: '' }; + + mount( + + ); + + await waitForPromises(); + + expect(mockDatasource.getPublicAPI).toHaveBeenCalledWith( + datasource1State, + expect.anything(), + 'first' + ); + expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( + datasource2State, + expect.anything(), + 'second' + ); + expect(mockDatasource2.getPublicAPI).toHaveBeenCalledWith( + datasource2State, + expect.anything(), + 'third' ); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 34b1946b8022e..9b36824beab8f 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -100,9 +100,9 @@ export function EditorFrame(props: EditorFrameProps) { ); dispatch({ - type: 'CREATE_LAYER', - newLayerId, - newDatasourceState: newState, + type: 'UPDATE_DATASOURCE_STATE', + datasourceId: state.activeDatasourceId!, + newState, }); return newLayerId; @@ -136,7 +136,7 @@ export function EditorFrame(props: EditorFrameProps) { } }, [allLoaded, state.visualization.activeId, state.visualization.state]); - const datasource = + const activeDatasource = state.activeDatasourceId && !state.datasourceStates[state.activeDatasourceId].isLoading ? props.datasourceMap[state.activeDatasourceId] : undefined; @@ -145,14 +145,14 @@ export function EditorFrame(props: EditorFrameProps) { ? props.visualizationMap[state.visualization.activeId] : undefined; - if (datasource && allLoaded) { + if (allLoaded) { return ( { - if (datasource && visualization) { + if (activeDatasource && visualization) { save({ activeDatasources: Object.keys(state.datasourceStates).reduce( (datasourceMap, datasourceId) => ({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index c0ea024fe8d36..10f97e0036145 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -60,11 +60,6 @@ export type Action = | { type: 'SWITCH_DATASOURCE'; newDatasourceId: string; - } - | { - type: 'CREATE_LAYER'; - newLayerId: string; - newDatasourceState: unknown; }; export const getInitialState = (props: EditorFrameProps): EditorFrameState => { @@ -172,21 +167,6 @@ export const reducer = (state: EditorFrameState, action: Action): EditorFrameSta state: action.newState, }, }; - case 'CREATE_LAYER': - return { - ...state, - // layerIdToDatasource: { - // ...state.layerIdToDatasource, - // [action.newLayerId]: state.activeDatasourceId!, - // }, - datasourceStates: { - ...state.datasourceStates, - [state.activeDatasourceId!]: { - state: action.newDatasourceState, - isLoading: false, - }, - }, - }; default: return state; } diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 751d081806f2e..86d45a55304ad 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -183,7 +183,7 @@ describe('workspace_panel', () => { mockDatasource.getLayers.mockReturnValue(['first']); mockDatasource2.toExpression.mockReturnValue('datasource2'); - mockDatasource2.getLayers.mockReturnValue(['second']); + mockDatasource2.getLayers.mockReturnValue(['second', 'third']); instance = mount( { expect( (instance.find(expressionRendererMock).prop('expression') as Ast).chain[0].arguments.layerIds - ).toEqual(['first', 'second']); + ).toEqual(['first', 'second', 'third']); expect( (instance.find(expressionRendererMock).prop('expression') as Ast).chain[0].arguments.tables ).toMatchInlineSnapshot(` @@ -240,6 +240,16 @@ describe('workspace_panel', () => { ], "type": "expression", }, + Object { + "chain": Array [ + Object { + "arguments": Object {}, + "function": "datasource2", + "type": "function", + }, + ], + "type": "expression", + }, ] `); }); From 9fb5e9d3fbc62b240127bbb5862c0ffd63315e91 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 22 Jul 2019 14:48:03 -0400 Subject: [PATCH 44/67] Suggest that split series comes before X axis in XY chart --- .../public/xy_visualization_plugin/xy_config_panel.test.tsx | 2 +- .../lens/public/xy_visualization_plugin/xy_config_panel.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index ee0098422a688..d7716635edd0e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -92,7 +92,7 @@ describe('XYConfigPanel', () => { expect(options.filter(({ isDisabled }) => isDisabled).map(({ id }) => id)).toEqual([]); }); - test('the x dimension panel accepts only buckted operations', () => { + test('the x dimension panel accepts only bucketed operations', () => { // TODO: this should eventually also accept raw operation const state = testState(); const component = mount( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 4659ff3dcda63..7b2191decc8b2 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -168,6 +168,7 @@ export function XYConfigPanel(props: VisualizationProps) { columnId: layer.xAccessor, dragDropContext: props.dragDropContext, filterOperations: operation => operation.isBucketed, + suggestedPriority: 1, layerId: layer.layerId, }} /> @@ -185,6 +186,7 @@ export function XYConfigPanel(props: VisualizationProps) { columnId: layer.splitAccessor, dragDropContext: props.dragDropContext, filterOperations: operation => operation.isBucketed, + suggestedPriority: 0, layerId: layer.layerId, }} /> From 19d9a50a01b3416f8c2ac784250c58272b030af6 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Mon, 22 Jul 2019 14:56:34 -0400 Subject: [PATCH 45/67] Get datatable suggestion working --- .../datatable_visualization_plugin/expression.tsx | 12 +++--------- .../datatable_visualization_plugin/visualization.tsx | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index dec497557e04f..16600a4f7411e 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -9,7 +9,7 @@ import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { EuiBasicTable } from '@elastic/eui'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { KibanaDatatable } from '../types'; +import { KibanaDatatable, LensMultiTable } from '../types'; import { RenderFunction } from '../interpreter_types'; export interface DatatableColumns { @@ -22,7 +22,7 @@ interface Args { } export interface DatatableProps { - data: KibanaDatatable; + data: LensMultiTable; args: Args; } @@ -106,11 +106,6 @@ export const datatableColumns: ExpressionFunction< }, }; -export interface DatatableProps { - data: KibanaDatatable; - args: Args; -} - export const datatableRenderer: RenderFunction = { name: 'lens_datatable_renderer', displayName: i18n.translate('xpack.lens.datatable.visualizationName', { @@ -127,8 +122,7 @@ export const datatableRenderer: RenderFunction = { function DatatableComponent(props: DatatableProps) { // TODO: We should probalby change data.rows to be data.layers, since // each row is actually an array of layer objects - const firstLayer = props.data.rows[0]; - const firstTable = firstLayer && (Object.values(firstLayer)[0] as KibanaDatatable); + const [firstTable] = Object.values(props.data.tables); return ( col.columnId), }, ], From ddab6e4ba0a81e2ed09176aa3943081dbb3b9437 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Mon, 22 Jul 2019 15:20:18 -0400 Subject: [PATCH 46/67] Adjust how xy axis labels are computed --- .../__snapshots__/xy_expression.test.tsx.snap | 98 ++++++++----------- .../xy_visualization.test.ts.snap | 7 +- .../xy_visualization_plugin/to_expression.ts | 47 +++++---- .../public/xy_visualization_plugin/types.ts | 6 -- .../xy_config_panel.test.tsx | 1 - .../xy_config_panel.tsx | 1 - .../xy_expression.test.tsx | 20 ---- .../xy_visualization_plugin/xy_expression.tsx | 48 +++++---- .../xy_visualization_plugin/xy_suggestions.ts | 10 +- .../xy_visualization.test.ts | 8 +- .../xy_visualization.tsx | 1 - 11 files changed, 105 insertions(+), 142 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index ed614c665a9a7..4cc9faf56652d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -15,25 +15,23 @@ exports[`xy_expression XYChart component it renders area 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -72,25 +70,23 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -129,25 +125,23 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -186,25 +180,23 @@ exports[`xy_expression XYChart component it renders line 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -243,25 +235,23 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -304,25 +294,23 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` id="x" position="bottom" showGridLines={false} - title="X" /> @@ -365,25 +353,23 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = id="x" position="bottom" showGridLines={false} - title="X" /> diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index 88e0b9bcbb198..4bc072f9180bc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -17,7 +17,6 @@ Object { "hide": Array [ false, ], - "labels": Array [], "layerId": Array [ "first", ], @@ -66,6 +65,12 @@ Object { "type": "expression", }, ], + "xTitle": Array [ + "x", + ], + "yTitle": Array [ + "y", + ], }, "function": "lens_xy_chart", "type": "function", diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 903b26aefef92..bd813ff2ad257 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -5,28 +5,36 @@ */ import { Ast } from '@kbn/interpreter/common'; -import { State } from './types'; +import { State, LayerConfig } from './types'; import { FramePublicAPI } from '../types'; +function xyTitles(layer: LayerConfig, frame: FramePublicAPI) { + const defaults = { + xTitle: 'x', + yTitle: 'y', + }; + + if (!layer || !layer.accessors.length) { + return defaults; + } + const datasource = frame.datasourceLayers[layer.layerId]; + if (!datasource) { + return defaults; + } + const x = datasource.getOperationForColumnId(layer.xAccessor); + const y = datasource.getOperationForColumnId(layer.accessors[0]); + + return { + xTitle: x ? x.label : defaults.xTitle, + yTitle: y ? y.label : defaults.yTitle, + }; +} + export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => { - const labels: Partial> = {}; if (!state || !state.layers.length) { return null; } - state.layers.forEach(layer => { - const datasource = frame.datasourceLayers[layer.layerId]; - if (!datasource) { - return; - } - layer.accessors.forEach(columnId => { - const operation = datasource.getOperationForColumnId(columnId); - if (operation && operation.label) { - labels[columnId] = operation.label; - } - }); - }); - const stateWithValidAccessors = { ...state, layers: state.layers.map(layer => ({ @@ -37,12 +45,12 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => })), }; - return buildExpression(stateWithValidAccessors, labels); + return buildExpression(stateWithValidAccessors, xyTitles(state.layers[0], frame)); }; export const buildExpression = ( state: State, - columnLabels: Partial> + { xTitle, yTitle }: { xTitle: string; yTitle: string } ): Ast => ({ type: 'expression', chain: [ @@ -50,6 +58,8 @@ export const buildExpression = ( type: 'function', function: 'lens_xy_chart', arguments: { + xTitle: [xTitle], + yTitle: [yTitle], legend: [ { type: 'expression', @@ -83,9 +93,6 @@ export const buildExpression = ( xAccessor: [layer.xAccessor], splitAccessor: [layer.splitAccessor], seriesType: [layer.seriesType], - labels: layer.accessors.map(accessor => { - return columnLabels[accessor] || accessor; - }), accessors: layer.accessors, }, }, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index 1ead6125d0561..f474fd2b0ad5d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -162,11 +162,6 @@ export const layerConfig: ExpressionFunction< help: 'The columns to display on the y axis.', multi: true, }, - labels: { - types: ['string'], - help: '', - multi: true, - }, }, fn: function fn(_context: unknown, args: LayerConfig) { return { @@ -190,7 +185,6 @@ export type LayerConfig = AxisConfig & { datasourceId: string; xAccessor: string; accessors: string[]; - labels: string[]; seriesType: SeriesType; splitAccessor: string; }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index ee0098422a688..5e8b726c9841c 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -37,7 +37,6 @@ describe('XYConfigPanel', () => { showGridlines: true, title: 'X', accessors: ['bar'], - labels: [''], }, ], }; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 4659ff3dcda63..a850b99ecc50f 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -97,7 +97,6 @@ function newLayerState(layerId: string): LayerConfig { title: '', showGridlines: false, position: Position.Left, - labels: [''], splitAccessor: generateId(), }; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index ed2001b65f4fd..31b19960aab85 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -36,7 +36,6 @@ function sampleArgs() { xAccessor: 'c', accessors: ['a', 'b'], - labels: ['Label A', 'Label B'], position: Position.Left, showGridlines: false, title: 'A and B', @@ -69,7 +68,6 @@ describe('xy_expression', () => { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - labels: ['Label A', 'Label B'], position: Position.Left, showGridlines: false, title: 'A and B', @@ -184,23 +182,5 @@ describe('xy_expression', () => { expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); expect(component.find(Settings).prop('rotation')).toEqual(90); }); - - test('it remaps rows based on the labels', () => { - const { data, args } = sampleArgs(); - const chart = shallow( - - ); - const barSeries = chart.find(BarSeries); - - expect(barSeries.prop('yAccessors')).toEqual(['Label A', 'Label B']); - expect(barSeries.prop('data')[0]).toEqual({ - 'Label A': 1, - 'Label B': 2, - c: 3, - }); - }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 9cfc16d2a9796..520b2a67b77fd 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -22,9 +22,14 @@ import { XYArgs } from './types'; import { LensMultiTable } from '../types'; import { RenderFunction } from '../interpreter_types'; +type ExpressionArgs = XYArgs & { + xTitle: string; + yTitle: string; +}; + export interface XYChartProps { data: LensMultiTable; - args: XYArgs; + args: ExpressionArgs; } export interface XYRender { @@ -33,11 +38,24 @@ export interface XYRender { value: XYChartProps; } -export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender> = ({ +export const xyChart: ExpressionFunction< + 'lens_xy_chart', + LensMultiTable, + ExpressionArgs, + XYRender +> = ({ name: 'lens_xy_chart', type: 'render', help: 'An X/Y chart', args: { + xTitle: { + types: ['string'], + help: 'X axis title', + }, + yTitle: { + types: ['string'], + help: 'Y axis title', + }, legend: { types: ['lens_xy_legendConfig'], help: 'Configure the chart legend.', @@ -51,7 +69,7 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs context: { types: ['lens_multitable'], }, - fn(data: LensMultiTable, args: XYArgs) { + fn(data: LensMultiTable, args: ExpressionArgs) { return { type: 'render', as: 'lens_xy_chart_renderer', @@ -66,7 +84,7 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs export interface XYChartProps { data: LensMultiTable; - args: XYArgs; + args: ExpressionArgs; } export const xyChartRenderer: RenderFunction = { @@ -95,7 +113,7 @@ export function XYChart({ data, args }: XYChartProps) { @@ -103,12 +121,12 @@ export function XYChart({ data, args }: XYChartProps) { - {layers.map(({ splitAccessor, seriesType, labels, accessors, xAccessor, layerId }, index) => { + {layers.map(({ splitAccessor, seriesType, accessors, xAccessor, layerId }, index) => { if (!data.tables[layerId]) { return; } @@ -121,20 +139,8 @@ export function XYChart({ data, args }: XYChartProps) { stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], id: getSpecId(idForCaching), xAccessor, - yAccessors: labels, - data: data.tables[layerId].rows.map(row => { - const newRow: typeof row = {}; - - Object.keys(row).forEach(key => { - const labelIndex = accessors.indexOf(key); - if (labelIndex > -1) { - newRow[labels[labelIndex]] = row[key]; - } else { - newRow[key] = row[key]; - } - }); - return newRow; - }), + yAccessors: accessors, + data: data.tables[layerId].rows, }; return seriesType === 'line' ? ( diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index ae7a96f50e8c4..b9fe1a77fca14 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -112,7 +112,6 @@ function getSuggestion( seriesType: splitBy && isDate ? 'line' : 'bar', splitAccessor: splitBy && isDate ? splitBy.columnId : generateId(), accessors: yValues.map(col => col.columnId), - labels: [''], position: Position.Left, showGridlines: false, title: yTitle, @@ -120,13 +119,6 @@ function getSuggestion( ], }; - const labels: Partial> = {}; - yValues.forEach(({ columnId, operation: { label } }) => { - if (label) { - labels[columnId] = label; - } - }); - return { title, score: 1, @@ -142,7 +134,7 @@ function getSuggestion( isVisible: false, }, }, - labels + { xTitle, yTitle } ), }; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 538f2e9c6a232..fd251156ae06d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -20,7 +20,6 @@ function exampleState(): State { { layerId: 'first', datasourceId: '', - labels: [''], seriesType: 'area', splitAccessor: 'd', position: Position.Bottom, @@ -56,7 +55,6 @@ describe('xy_visualization', () => { "test-id1", ], "datasourceId": "", - "labels": Array [], "layerId": "", "position": "top", "seriesType": "bar", @@ -113,10 +111,8 @@ describe('xy_visualization', () => { expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); - expect(expression.chain[0].arguments.layers[0].chain[0].arguments.labels).toEqual([ - 'col_b', - 'col_c', - ]); + expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); + expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 8d12e18196b56..59e811de4fc02 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -28,7 +28,6 @@ export const xyVisualization: Visualization = { layerId: frame.addNewLayer(), accessors: [generateId()], datasourceId: '', - labels: [], position: Position.Top, seriesType: 'bar', showGridlines: false, From 3f25c939d7a69e5d7c32d7a0adc233ce3950cd8a Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 22 Jul 2019 17:11:41 -0400 Subject: [PATCH 47/67] Datasource suggestions should handle layers and have tests --- .../dimension_panel/dimension_panel.test.tsx | 4 +- .../indexpattern_plugin/indexpattern.test.tsx | 312 +++++++++++++++++- .../indexpattern_plugin/indexpattern.tsx | 56 +++- 3 files changed, 358 insertions(+), 14 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx index 5b27484489e47..e19e635b82bfd 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.test.tsx @@ -135,8 +135,8 @@ describe('IndexPatternDimensionPanel', () => { wrapper .find('[data-test-subj="indexPattern-configure-dimension"]') .first() - .text() - ).toEqual('Configure dimension'); + .prop('iconType') + ).toEqual('plusInCircle'); }); it('should pass the right arguments to getPotentialColumns', async () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index c2d786ea3a923..3eb94f47b6eb5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -236,7 +236,7 @@ describe('IndexPattern Data Source', () => { }); describe('#getDatasourceSuggestionsForField', () => { - describe('with no previous selections', () => { + describe('with no layer', () => { let initialState: IndexPatternPrivateState; beforeEach(async () => { @@ -411,7 +411,187 @@ describe('IndexPattern Data Source', () => { }); }); - describe('with a prior column', () => { + describe('with a previous empty layer', () => { + let initialState: IndexPatternPrivateState; + + beforeEach(async () => { + initialState = await indexPatternDatasource.initialize({ + currentIndexPatternId: '1', + layers: { + previousLayer: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + }, + }); + }); + + it('should apply a bucketed aggregation for a string field', () => { + const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, { + name: 'source', + type: 'string', + aggregatable: true, + searchable: true, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0].state).toEqual( + expect.objectContaining({ + layers: { + previousLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'terms', + sourceField: 'source', + }), + col2: expect.objectContaining({ + operationType: 'count', + }), + }, + }), + }, + }) + ); + expect(suggestions[0].table).toEqual({ + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'col1', + }), + expect.objectContaining({ + columnId: 'col2', + }), + ], + layerId: 'previousLayer', + }); + }); + + it('should apply a bucketed aggregation for a date field', () => { + const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0].state).toEqual( + expect.objectContaining({ + layers: { + previousLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'date_histogram', + sourceField: 'timestamp', + }), + col2: expect.objectContaining({ + operationType: 'count', + }), + }, + }), + }, + }) + ); + expect(suggestions[0].table).toEqual({ + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'col1', + }), + expect.objectContaining({ + columnId: 'col2', + }), + ], + layerId: 'previousLayer', + }); + }); + + it('should select a metric for a number field', () => { + const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0].state).toEqual( + expect.objectContaining({ + layers: { + previousLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'date_histogram', + sourceField: 'timestamp', + }), + col2: expect.objectContaining({ + operationType: 'min', + sourceField: 'bytes', + }), + }, + }), + }, + }) + ); + expect(suggestions[0].table).toEqual({ + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'col1', + }), + expect.objectContaining({ + columnId: 'col2', + }), + ], + layerId: 'previousLayer', + }); + }); + + it('should not make any suggestions for a number without a time field', async () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: '1', + indexPatterns: { + 1: { + id: '1', + title: 'no timefield', + fields: [ + { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + }, + }, + layers: { + previousLayer: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, + }; + + const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(state, { + name: 'bytes', + type: 'number', + aggregatable: true, + searchable: true, + }); + + expect(suggestions).toHaveLength(0); + }); + }); + + describe('with a prior layer that contains configuration', () => { let initialState: IndexPatternPrivateState; beforeEach(async () => { @@ -451,6 +631,79 @@ describe('IndexPattern Data Source', () => { ).toHaveLength(0); }); }); + + describe('finding the layer that is using the current index pattern', () => { + let initialState: IndexPatternPrivateState; + + beforeEach(async () => { + initialState = await indexPatternDatasource.initialize({ + currentIndexPatternId: '1', + layers: { + previousLayer: { + indexPatternId: '1', + columns: {}, + columnOrder: [], + }, + currentLayer: { + indexPatternId: '2', + columns: {}, + columnOrder: [], + }, + }, + }); + }); + + it('suggests on the layer that matches by indexPatternId', () => { + const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, { + name: 'timestamp', + type: 'date', + aggregatable: true, + searchable: true, + aggregationRestrictions: { + date_histogram: { + agg: 'date_histogram', + fixed_interval: '1d', + delay: '7d', + time_zone: 'UTC', + }, + }, + }); + + expect(suggestions).toHaveLength(1); + expect(suggestions[0].state).toEqual( + expect.objectContaining({ + layers: { + previousLayer: initialState.layers.previousLayer, + currentLayer: expect.objectContaining({ + columnOrder: ['col1', 'col2'], + columns: { + col1: expect.objectContaining({ + operationType: 'date_histogram', + sourceField: 'timestamp', + }), + col2: expect.objectContaining({ + operationType: 'count', + }), + }, + }), + }, + }) + ); + expect(suggestions[0].table).toEqual({ + datasourceSuggestionId: 0, + isMultiRow: true, + columns: [ + expect.objectContaining({ + columnId: 'col1', + }), + expect.objectContaining({ + columnId: 'col2', + }), + ], + layerId: 'currentLayer', + }); + }); + }); }); describe('#getDatasourceSuggestionsFromCurrentState', () => { @@ -499,6 +752,61 @@ describe('IndexPattern Data Source', () => { }); }); + describe('#insertLayer', () => { + it('should insert an empty layer into the previous state', () => { + const state = { + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + second: { + indexPatternId: '2', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }; + expect(indexPatternDatasource.insertLayer(state, 'newLayer')).toEqual({ + ...state, + layers: { + ...state.layers, + newLayer: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + }, + }); + }); + }); + + describe('#getLayers', () => { + it('should list the current layers', () => { + expect( + indexPatternDatasource.getLayers({ + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + second: { + indexPatternId: '2', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }) + ).toEqual(['first', 'second']); + }); + }); + describe('#getPublicAPI', () => { let publicAPI: DatasourcePublicAPI; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index ac59b3bd5818f..77925a0524296 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -191,6 +191,35 @@ function removeProperty(prop: string, object: Record): Record + _.isEqual(f, field) + ); + + if (isMatchingActive !== -1) { + return state.currentIndexPatternId; + } + } + + const matchingIndexPattern = Object.values(state.indexPatterns).find(indexPattern => { + if (indexPattern.id === state.currentIndexPatternId) { + return; + } + + const hasMatch = indexPattern.fields.findIndex(f => _.isEqual(f, field)); + + if (hasMatch !== -1) { + return indexPattern.id; + } + }); + + return matchingIndexPattern ? matchingIndexPattern.id : null; +} + export function getIndexPatternDatasource({ chrome, toastNotifications, @@ -272,11 +301,6 @@ export function getIndexPatternDatasource({ return columnToOperation(layer.columns[columnId]); } return null; - - // if (!state.columns[columnId]) { - // return null; - // } - // return columnToOperation(state.columns[columnId]); }, renderDimensionPanel: (domElement: Element, props: DatasourceDimensionPanelProps) => { render( @@ -328,22 +352,32 @@ export function getIndexPatternDatasource({ const layers = Object.keys(state.layers); const field: IndexPatternField = item as IndexPatternField; + const indexPatternId = getIndexPatternIdFromField(state, field); + let layerId; let layer: IndexPatternLayer; - if (layers.length === 0) { + if (indexPatternId) { + layerId = layers.find(id => state.layers[id].indexPatternId === indexPatternId); + } + + if (!layerId) { + // The field we're suggesting on might not match any existing layer. This will always add + // a new layer if possible, but that might not be desirable if the layers are too complicated + // already layerId = generateId(); layer = { indexPatternId: state.currentIndexPatternId, columnOrder: [], columns: {}, }; - } else if (state.layers[layers[0]].columnOrder.length) { - // Don't suggest when there are existing columns - return []; } else { - layerId = layers[0]; layer = state.layers[layerId]; + if (layer.columnOrder.length) { + // We aren't suggesting ways of using the field to replace the existing user configuration + // This is a good place for future extension + return []; + } } const operations = getOperationTypesForField(field); @@ -376,6 +410,7 @@ export function getIndexPatternDatasource({ state: { ...state, layers: { + ...state.layers, [layerId]: { indexPatternId: state.currentIndexPatternId, columns: { @@ -435,6 +470,7 @@ export function getIndexPatternDatasource({ state: { ...state, layers: { + ...state.layers, [layerId]: { ...layer, columns: { From 0ac2c318f4e14a4e773d70ab5ec223404370f083 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 22 Jul 2019 17:27:54 -0400 Subject: [PATCH 48/67] Fix linting in XY chart and remove commented code --- .../editor_frame/config_panel_wrapper.tsx | 6 +----- .../editor_frame_plugin/editor_frame/save.ts | 1 - .../lens/public/editor_frame_plugin/plugin.tsx | 9 +-------- x-pack/legacy/plugins/lens/public/index.scss | 2 +- .../indexpattern_plugin/state_helpers.ts | 1 - .../public/persistence/saved_object_store.ts | 1 - x-pack/legacy/plugins/lens/public/types.ts | 6 ------ .../public/xy_visualization_plugin/plugin.tsx | 1 - .../public/xy_visualization_plugin/types.ts | 2 ++ .../xy_expression.test.tsx | 2 ++ .../xy_visualization_plugin/xy_expression.tsx | 18 ++++-------------- 11 files changed, 11 insertions(+), 38 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx index d0d0c76987b45..2bca6c670c770 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/config_panel_wrapper.tsx @@ -19,11 +19,7 @@ interface ConfigPanelWrapperProps { framePublicAPI: FramePublicAPI; } -function getSuggestedVisualizationState( - frame: FramePublicAPI, - visualization: Visualization - // datasource: DatasourcePublicAPI -) { +function getSuggestedVisualizationState(frame: FramePublicAPI, visualization: Visualization) { const datasources = Object.entries(frame.datasourceLayers); let results: VisualizationSuggestion[] = []; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts index 2e7b3111fc8c6..723a193750e10 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.ts @@ -8,7 +8,6 @@ import { Action, EditorFrameState } from './state_management'; import { Document } from '../../persistence/saved_object_store'; export interface Props { - // datasource: { getPersistableState: (state: unknown) => unknown }; activeDatasources: Record unknown }>; dispatch: (value: Action) => void; redirectTo: (path: string) => void; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx index 8495777587e93..359ac72bbd379 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/plugin.tsx @@ -24,7 +24,6 @@ import { EditorFrameSetup, EditorFrameInstance, ErrorCallback, - // DatasourcePublicAPI, } from '../types'; import { EditorFrame } from './editor_frame'; import { SavedObjectIndexStore, SavedObjectStore, Document } from '../persistence'; @@ -60,7 +59,6 @@ interface RenderProps extends InitializationResult { onError: ErrorCallback; datasources: Record; visualizations: Record; - // layerToDatasourceId: Record; // Maps layer ID to datasource ID expressionRenderer: ExpressionRenderer; } @@ -70,7 +68,6 @@ export class EditorFramePlugin { private ExpressionRenderer: ExpressionRenderer | null = null; private readonly datasources: Record = {}; private readonly visualizations: Record = {}; - // private readonly layerToDatasourceId: Record = {}; private createInstance(): EditorFrameInstance { let domElement: Element; @@ -127,10 +124,7 @@ export class EditorFramePlugin { }; } - public setup( - _core: CoreSetup | null, - plugins: EditorFrameSetupPlugins // { interpreter, data, storage, toastNotifications: toast }: IndexPatternDatasourcePluginPlugins - ): EditorFrameSetup { + public setup(_core: CoreSetup | null, plugins: EditorFrameSetupPlugins): EditorFrameSetup { plugins.interpreter.functionsRegistry.register(() => mergeTables); this.ExpressionRenderer = plugins.data.expressions.ExpressionRenderer; @@ -198,7 +192,6 @@ export function InitializedEditor({ store, datasources, visualizations, - // layerToDatasourceId, expressionRenderer, }: RenderProps) { const firstDatasourceId = Object.keys(datasources)[0]; diff --git a/x-pack/legacy/plugins/lens/public/index.scss b/x-pack/legacy/plugins/lens/public/index.scss index a512e066c9956..97f8c32aa633f 100644 --- a/x-pack/legacy/plugins/lens/public/index.scss +++ b/x-pack/legacy/plugins/lens/public/index.scss @@ -3,7 +3,7 @@ @import './xy_visualization_plugin/index'; @import './datatable_visualization_plugin/index'; -// @import './xy_visualization_plugin/xy_expression.scss'; +@import './xy_visualization_plugin/xy_expression.scss'; @import './indexpattern_plugin/indexpattern'; @import './drag_drop/drag_drop.scss'; @import './editor_frame_plugin/editor_frame/index'; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts index 4663945d7cb8b..9f95b3726ea3a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/state_helpers.ts @@ -75,7 +75,6 @@ export function changeColumn({ layerId, columnId, newColumn, - // extra, keepParams, }: { state: IndexPatternPrivateState; diff --git a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts index e4e7e3b2856ed..d90f1d4514174 100644 --- a/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts +++ b/x-pack/legacy/plugins/lens/public/persistence/saved_object_store.ts @@ -15,7 +15,6 @@ export interface Document { // The state is saved as a JSON string for now state: { datasourceStates: Record; - // datasource: unknown; visualization: unknown; }; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b4de805542b23..22fb0780c6b3d 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -27,13 +27,9 @@ export interface EditorFrameSetup { // Hints the default nesting to the data source. 0 is the highest priority export type DimensionPriority = 0 | 1 | 2; -// 0 is the default layer, layers are joined sequentially but there can only be one 'join' layer -export type DimensionLayer = 'join' | number; - export interface TableSuggestionColumn { columnId: string; operation: Operation; - layer?: DimensionLayer; } export interface TableSuggestion { @@ -93,7 +89,6 @@ export interface DatasourcePublicAPI { export interface TableSpecColumn { // Column IDs are the keys for internal state in data sources and visualizations columnId: string; - layer?: DimensionLayer; } // TableSpec is managed by visualizations @@ -188,7 +183,6 @@ export interface FramePublicAPI { export interface Visualization { // For initializing from saved object - // initialize: (frame: FramePublicAPI, datasource: DatasourcePublicAPI, state?: P) => T; initialize: (frame: FramePublicAPI, state?: P) => T; getPersistableState: (state: T) => P; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx index 390b469e61510..b16c26f6dee45 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/plugin.tsx @@ -25,7 +25,6 @@ class XyVisualizationPlugin { setup(_core: CoreSetup | null, { interpreter }: XyVisualizationPluginSetupPlugins) { interpreter.functionsRegistry.register(() => legendConfig); interpreter.functionsRegistry.register(() => xConfig); - // interpreter.functionsRegistry.register(() => yConfig); interpreter.functionsRegistry.register(() => layerConfig); interpreter.functionsRegistry.register(() => xyChart); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index f474fd2b0ad5d..fa38a1b694c2e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -190,6 +190,8 @@ export type LayerConfig = AxisConfig & { }; export interface XYArgs { + xTitle: string; + yTitle: string; legend: LegendConfig; layers: LayerConfig[]; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index 31b19960aab85..deec67744de93 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -24,6 +24,8 @@ function sampleArgs() { }; const args: XYArgs = { + xTitle: '', + yTitle: '', legend: { isVisible: false, position: Position.Top, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index 520b2a67b77fd..e10a5433de165 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -22,14 +22,9 @@ import { XYArgs } from './types'; import { LensMultiTable } from '../types'; import { RenderFunction } from '../interpreter_types'; -type ExpressionArgs = XYArgs & { - xTitle: string; - yTitle: string; -}; - export interface XYChartProps { data: LensMultiTable; - args: ExpressionArgs; + args: XYArgs; } export interface XYRender { @@ -38,12 +33,7 @@ export interface XYRender { value: XYChartProps; } -export const xyChart: ExpressionFunction< - 'lens_xy_chart', - LensMultiTable, - ExpressionArgs, - XYRender -> = ({ +export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs, XYRender> = ({ name: 'lens_xy_chart', type: 'render', help: 'An X/Y chart', @@ -69,7 +59,7 @@ export const xyChart: ExpressionFunction< context: { types: ['lens_multitable'], }, - fn(data: LensMultiTable, args: ExpressionArgs) { + fn(data: LensMultiTable, args: XYArgs) { return { type: 'render', as: 'lens_xy_chart_renderer', @@ -84,7 +74,7 @@ export const xyChart: ExpressionFunction< export interface XYChartProps { data: LensMultiTable; - args: ExpressionArgs; + args: XYArgs; } export const xyChartRenderer: RenderFunction = { From 61f232b388907fd84428a2e507a6667bf13bff08 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Mon, 22 Jul 2019 17:30:14 -0400 Subject: [PATCH 49/67] Update snapshots from earlier change --- .../__snapshots__/xy_expression.test.tsx.snap | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index 4cc9faf56652d..a7895146ff4bc 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -15,11 +15,13 @@ exports[`xy_expression XYChart component it renders area 1`] = ` id="x" position="bottom" showGridLines={false} + title="" /> Date: Tue, 23 Jul 2019 09:41:11 +0200 Subject: [PATCH 50/67] add comments and check whether reload is actually necessary --- .../embeddable/embeddable.tsx | 18 ++++++++++++------ .../embeddable/embeddable_factory.ts | 5 +++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx index 22519fae8f2df..318cff621a9d9 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/embeddable/embeddable.tsx @@ -65,6 +65,9 @@ export class Embeddable extends AbstractEmbeddable Date: Tue, 23 Jul 2019 12:18:48 -0400 Subject: [PATCH 51/67] Fix linting errors --- .../xy_suggestions.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts index 4d99f54544426..8ae338702f6b3 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.test.ts @@ -5,9 +5,10 @@ */ import { getSuggestions } from './xy_suggestions'; -import { TableSuggestionColumn, VisualizationSuggestion } from '../types'; +import { TableSuggestionColumn, VisualizationSuggestion, DataType } from '../types'; import { State } from './types'; import { generateId } from '../id_generator'; +import { Ast } from '@kbn/interpreter/target/common'; jest.mock('../id_generator'); @@ -62,8 +63,7 @@ describe('xy_suggestions', () => { test('ignores invalid combinations', () => { const unknownCol = () => { const str = strCol('foo'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { ...str, operation: { ...str.operation, dataType: 'wonkies' } } as any; + return { ...str, operation: { ...str.operation, dataType: 'wonkies' as DataType } }; }; expect( @@ -277,9 +277,13 @@ describe('xy_suggestions', () => { ], }); - const expression = suggestion.previewExpression! as any; + const expression = suggestion.previewExpression! as Ast; - expect(expression.chain[0].arguments.legend[0].chain[0].arguments.isVisible[0]).toBeFalsy(); - expect(expression.chain[0].arguments.layers[0].chain[0].arguments.hide[0]).toBeTruthy(); + expect( + (expression.chain[0].arguments.legend[0] as Ast).chain[0].arguments.isVisible[0] + ).toBeFalsy(); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.hide[0] + ).toBeTruthy(); }); }); From b629292b866e4517f553f38474ecd193c1de55ea Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 23 Jul 2019 12:22:56 -0400 Subject: [PATCH 52/67] More cleanup --- .../public/datatable_visualization_plugin/expression.tsx | 2 -- .../datatable_visualization_plugin/visualization.tsx | 4 ---- .../editor_frame_plugin/editor_frame/state_management.ts | 1 - .../editor_frame/suggestion_panel.test.tsx | 1 - .../plugins/lens/public/editor_frame_plugin/mocks.tsx | 1 - .../dimension_panel/dimension_panel.tsx | 1 - .../public/multi_column_editor/multi_column_editor.tsx | 1 - x-pack/legacy/plugins/lens/public/types.ts | 3 --- .../public/xy_visualization_plugin/xy_config_panel.tsx | 9 --------- .../xy_visualization_plugin/xy_visualization.test.ts | 3 ++- .../public/xy_visualization_plugin/xy_visualization.tsx | 2 -- 11 files changed, 2 insertions(+), 26 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx index 16600a4f7411e..076bb1c20ac10 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/expression.tsx @@ -120,8 +120,6 @@ export const datatableRenderer: RenderFunction = { }; function DatatableComponent(props: DatatableProps) { - // TODO: We should probalby change data.rows to be data.layers, since - // each row is actually an array of layer objects const [firstTable] = Object.values(props.data.tables); return ( diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index 44505e9a567fb..6578f8c6ce7c1 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -87,10 +87,6 @@ export const datatableVisualization: Visualization< ); }, - getLayerIds(state) { - return state.layers.map(l => l.layerId); - }, - getPersistableState: state => state, getSuggestions({ diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index 10f97e0036145..2507bf462279e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -81,7 +81,6 @@ export const getInitialState = (props: EditorFrameProps): EditorFrameState => { state: null, activeId: props.initialVisualizationId, }, - // layerIdToDatasource: {}, }; }; diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx index d6911f15317ae..effadb27ddb43 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/suggestion_panel.test.tsx @@ -59,7 +59,6 @@ describe('suggestion_panel', () => { ] as Suggestion[]); defaultProps = { - // activeDatasource: mockDatasource, activeDatasourceId: 'mock', datasourceMap: { mock: mockDatasource, diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index bb23678f0cd7c..c7f2092b1eca2 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -16,7 +16,6 @@ export function createMockVisualization(): jest.Mocked { initialize: jest.fn((_frame, _state?) => ({})), renderConfigPanel: jest.fn(), toExpression: jest.fn((_state, _frame) => null), - getLayerIds: jest.fn(_state => []), }; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 933051afff3e0..271e19b8bf86a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -30,7 +30,6 @@ export type IndexPatternDimensionPanelProps = DatasourceDimensionPanelProps & { dragDropContext: DragContextState; dataPlugin: DataSetup; storage: Storage; - // layer: DimensionLayer; layerId: string; }; diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx index 30f376d43a05e..cda1932a35589 100644 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx @@ -18,7 +18,6 @@ interface Props { filterOperations: (op: Operation) => boolean; suggestedPriority?: 0 | 1 | 2 | undefined; testSubj: string; - // layer: number; layerId: string; } diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 22fb0780c6b3d..4ea2e08985e38 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -61,7 +61,6 @@ export interface Datasource { renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; - // toExpression: (state: T) => Ast | string | null; toExpression: (state: T, layerId: string) => Ast | string | null; getDatasourceSuggestionsForField: (state: T, field: unknown) => Array>; @@ -194,6 +193,4 @@ export interface Visualization { // The frame will call this function on all visualizations when the table changes, or when // rendering additional ways of using the data getSuggestions: (options: SuggestionRequest) => Array>; - - getLayerIds: (state: T) => string[]; } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index 7bcba3b9a822e..a2ff3b06d65f0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -248,15 +248,6 @@ export function XYConfigPanel(props: VisualizationProps) { - - - - - - ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index fd251156ae06d..cdd20f4726714 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -10,6 +10,7 @@ import { Operation } from '../types'; import { State } from './types'; import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; +import { Ast } from '@kbn/interpreter/target/common'; jest.mock('../id_generator'); @@ -107,7 +108,7 @@ describe('xy_visualization', () => { first: mockDatasource.publicAPIMock, }; - const expression = xyVisualization.toExpression(exampleState(), frame)! as any; + const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 59e811de4fc02..4b6f5d5f31acf 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -42,8 +42,6 @@ export const xyVisualization: Visualization = { getPersistableState: state => state, - getLayerIds: state => state.layers.map(({ layerId }) => layerId), - renderConfigPanel: (domElement, props) => render( From a221e3926197cde714025859ff21cb487af5ced7 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 23 Jul 2019 12:49:56 -0400 Subject: [PATCH 53/67] Remove commented code --- .../editor_frame/workspace_panel.test.tsx | 1 - .../dimension_panel/dimension_panel.test.tsx | 35 ------------------- .../operation_definitions/terms.tsx | 1 - 3 files changed, 37 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx index 86d45a55304ad..85054d8f7c702 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/workspace_panel.test.tsx @@ -441,7 +441,6 @@ describe('workspace_panel', () => { beforeEach(() => { mockDispatch = jest.fn(); - // const mockDatasource = createMockDatasource(); instance = mount( { it('should pass the right arguments to getPotentialColumns', async () => { wrapper = shallow(); - // expect(getPotentialColumns as jest.Mock).toHaveBeenCalledWith({ expect(getPotentialColumns).toHaveBeenCalledWith({ fields: state.indexPatterns[state.currentIndexPatternId].fields, suggestedPriority: 1, @@ -806,26 +805,7 @@ describe('IndexPatternDimensionPanel', () => { describe('drag and drop', () => { function dragDropState(): IndexPatternPrivateState { - // return {0 - // ...state, - // // currentIndexPatternId: 'foo', - // // indexPatterns: { - // // foo: { - // // id: 'foo', - // // title: 'Foo pattern', - // // fields: [ - // // { - // // aggregatable: true, - // // name: 'bar', - // // searchable: true, - // // type: 'number', - // // }, - // // ], - // // }, - // // }, - // }; return { - // indexPatterns: expectedIndexPatterns, indexPatterns: { foo: { id: 'foo', @@ -864,21 +844,6 @@ describe('IndexPatternDimensionPanel', () => { }, }, }; - - // setState = jest.fn(); - - // dragDropContext = createMockedDragDropContext(); - - // defaultProps = { - // dragDropContext, - // state, - // setState, - // columnId: 'col1', - // layerId: 'first', - // filterOperations: () => true, - // dataPlugin: data, - // storage: localStorage, - // }; } it('is not droppable if no drag is happening', () => { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx index 181e29e932c66..a3705526f4474 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/terms.tsx @@ -103,7 +103,6 @@ export const termsOperation: OperationDefinition = { return currentColumn; }, paramEditor: ({ state, setState, columnId: currentColumnId, layerId }) => { - // const currentColumn = state.columns[currentColumnId] as TermsIndexPatternColumn; const currentColumn = state.layers[layerId].columns[currentColumnId] as TermsIndexPatternColumn; const SEPARATOR = '$$$'; function toValue(orderBy: TermsIndexPatternColumn['params']['orderBy']) { From 765e57b3bc4252f2262fca6d89e880ae385d31b1 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 23 Jul 2019 13:04:30 -0400 Subject: [PATCH 54/67] Test the multi-column editor --- .../multi_column_editor.test.tsx | 71 +++++++++++++++++++ .../multi_column_editor.tsx | 3 +- 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx new file mode 100644 index 0000000000000..012c27d3ce3ff --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.test.tsx @@ -0,0 +1,71 @@ +/* + * 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 { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { MultiColumnEditor } from './multi_column_editor'; +import { mount } from 'enzyme'; + +jest.useFakeTimers(); + +describe('MultiColumnEditor', () => { + it('should add a trailing accessor if the accessor list is empty', () => { + const onAdd = jest.fn(); + mount( + true} + layerId="foo" + onAdd={onAdd} + onRemove={jest.fn()} + testSubj="bar" + /> + ); + + expect(onAdd).toHaveBeenCalledTimes(0); + + jest.runAllTimers(); + + expect(onAdd).toHaveBeenCalledTimes(1); + }); + + it('should add a trailing accessor if the last accessor is configured', () => { + const onAdd = jest.fn(); + mount( + true} + layerId="foo" + onAdd={onAdd} + onRemove={jest.fn()} + testSubj="bar" + /> + ); + + expect(onAdd).toHaveBeenCalledTimes(0); + + jest.runAllTimers(); + + expect(onAdd).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx index 30f376d43a05e..2e46f43d79622 100644 --- a/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx +++ b/x-pack/legacy/plugins/lens/public/multi_column_editor/multi_column_editor.tsx @@ -18,7 +18,6 @@ interface Props { filterOperations: (op: Operation) => boolean; suggestedPriority?: 0 | 1 | 2 | undefined; testSubj: string; - // layer: number; layerId: string; } @@ -36,7 +35,7 @@ export function MultiColumnEditor({ const lastOperation = datasource.getOperationForColumnId(accessors[accessors.length - 1]); useEffect(() => { - if (lastOperation !== null) { + if (accessors.length === 0 || lastOperation !== null) { setTimeout(onAdd); } }, [lastOperation]); From 616d24ce7fa682d5d305919ea3a9d5ff1b30d822 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 23 Jul 2019 13:44:40 -0400 Subject: [PATCH 55/67] XY Visualization does not need to track datasourceId --- .../lens/public/xy_visualization_plugin/to_expression.ts | 1 - .../plugins/lens/public/xy_visualization_plugin/types.ts | 5 ----- .../public/xy_visualization_plugin/xy_config_panel.test.tsx | 1 - .../public/xy_visualization_plugin/xy_expression.test.tsx | 2 -- .../lens/public/xy_visualization_plugin/xy_suggestions.ts | 1 - .../public/xy_visualization_plugin/xy_visualization.test.ts | 1 - .../lens/public/xy_visualization_plugin/xy_visualization.tsx | 1 - 7 files changed, 12 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index bd813ff2ad257..2a29e84ce51cf 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -83,7 +83,6 @@ export const buildExpression = ( function: 'lens_xy_layer', arguments: { layerId: [layer.layerId], - datasourceId: [layer.datasourceId], title: [layer.title], showGridlines: [layer.showGridlines], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index fa38a1b694c2e..e50adf138ee3e 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -131,10 +131,6 @@ export const layerConfig: ExpressionFunction< types: ['string'], help: '', }, - datasourceId: { - types: ['string'], - help: '', - }, xAccessor: { types: ['string'], help: '', @@ -182,7 +178,6 @@ export type SeriesType = export type LayerConfig = AxisConfig & { layerId: string; - datasourceId: string; xAccessor: string; accessors: string[]; seriesType: SeriesType; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index 858ff3d670e1f..630ac00ce42ad 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -30,7 +30,6 @@ describe('XYConfigPanel', () => { { seriesType: 'bar', layerId: 'first', - datasourceId: '', splitAccessor: 'baz', xAccessor: 'foo', position: Position.Bottom, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index deec67744de93..ed1144f2e08b9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -33,7 +33,6 @@ function sampleArgs() { layers: [ { layerId: 'first', - datasourceId: 'indexpattern', seriesType: 'line', xAccessor: 'c', @@ -66,7 +65,6 @@ describe('xy_expression', () => { test('layerConfig produces the correct arguments', () => { const args: LayerConfig = { layerId: 'first', - datasourceId: 'indexpattern', seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts index b9fe1a77fca14..eeffcfd0036fe 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_suggestions.ts @@ -107,7 +107,6 @@ function getSuggestion( ...(currentState ? currentState.layers.filter(layer => layer.layerId !== layerId) : []), { layerId, - datasourceId: '', xAccessor: xValue.columnId, seriesType: splitBy && isDate ? 'line' : 'bar', splitAccessor: splitBy && isDate ? splitBy.columnId : generateId(), diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index cdd20f4726714..9df122d862add 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -20,7 +20,6 @@ function exampleState(): State { layers: [ { layerId: 'first', - datasourceId: '', seriesType: 'area', splitAccessor: 'd', position: Position.Bottom, diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx index 4b6f5d5f31acf..35825dbc228db 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.tsx @@ -27,7 +27,6 @@ export const xyVisualization: Visualization = { { layerId: frame.addNewLayer(), accessors: [generateId()], - datasourceId: '', position: Position.Top, seriesType: 'bar', showGridlines: false, From fdb1197ced8c54c5e814106ee136a13587453f75 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 23 Jul 2019 13:53:31 -0400 Subject: [PATCH 56/67] Fix various comments --- .../public/editor_frame_plugin/editor_frame/editor_frame.tsx | 2 +- .../indexpattern_plugin/dimension_panel/dimension_panel.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 9b36824beab8f..239afb5f95a49 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -123,7 +123,7 @@ export function EditorFrame(props: EditorFrameProps) { } }, [props.doc]); - // Initialize visualization as soon as all datasource is ready + // Initialize visualization as soon as all datasources are ready useEffect(() => { if (allLoaded && state.visualization.state === null && state.visualization.activeId !== null) { const initialVisualizationState = props.visualizationMap[ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx index 933051afff3e0..d4cd4710ca952 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/dimension_panel/dimension_panel.tsx @@ -86,7 +86,6 @@ export const IndexPatternDimensionPanel = memo(function IndexPatternDimensionPan changeColumn({ state: props.state, layerId, - // layers: props.state.layers, columnId: props.columnId, newColumn: column, }) From e432c86560d22ab0f2b89583a52b0ad8ebe6fac9 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 23 Jul 2019 14:11:29 -0400 Subject: [PATCH 57/67] Remove unused xy prop Add datasource header to datatable config --- .../visualization.tsx | 47 ++++++++++++------- .../xy_visualization.test.ts.snap | 3 -- .../xy_config_panel.tsx | 1 - .../xy_visualization.test.ts | 45 +++++++++--------- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx index 6578f8c6ce7c1..279aa4e7baeac 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { render } from 'react-dom'; -import { EuiForm, EuiFormRow } from '@elastic/eui'; +import { EuiForm, EuiFormRow, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n/react'; import { MultiColumnEditor } from '../multi_column_editor'; @@ -17,6 +17,7 @@ import { VisualizationSuggestion, } from '../types'; import { generateId } from '../id_generator'; +import { NativeRenderer } from '../native_renderer'; export interface LayerState { layerId: string; @@ -54,23 +55,33 @@ export function DataTableLayer({ }: { layer: LayerState } & VisualizationProps) { const datasource = frame.datasourceLayers[layer.layerId]; return ( - - true} - layerId={layer.layerId} - onAdd={() => setState(updateColumns(state, layer, columns => [...columns, generateId()]))} - onRemove={column => - setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) - } - testSubj="datatable_columns" - data-test-subj="datatable_multicolumnEditor" - /> - + + <> + + + true} + layerId={layer.layerId} + onAdd={() => + setState(updateColumns(state, layer, columns => [...columns, generateId()])) + } + onRemove={column => + setState(updateColumns(state, layer, columns => columns.filter(c => c !== column))) + } + testSubj="datatable_columns" + data-test-subj="datatable_multicolumnEditor" + /> + + + ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index 4bc072f9180bc..05d38f68f481b 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -11,9 +11,6 @@ Object { Object { "arguments": Object { "accessors": Array [], - "datasourceId": Array [ - "", - ], "hide": Array [ false, ], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index a2ff3b06d65f0..a3f404921f0b0 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -90,7 +90,6 @@ function updateLayer(state: State, layer: UnwrapArray, index: n function newLayerState(layerId: string): LayerConfig { return { layerId, - datasourceId: 'indexpattern', // TODO: Don't hard code xAccessor: generateId(), seriesType: 'bar_stacked', accessors: [generateId()], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 9df122d862add..86b3aef5fd10d 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -48,29 +48,28 @@ describe('xy_visualization', () => { expect(initialState.layers[0].xAccessor).not.toEqual(initialState.layers[0].accessors[0]); expect(initialState).toMatchInlineSnapshot(` - Object { - "layers": Array [ - Object { - "accessors": Array [ - "test-id1", - ], - "datasourceId": "", - "layerId": "", - "position": "top", - "seriesType": "bar", - "showGridlines": false, - "splitAccessor": "test-id2", - "title": "", - "xAccessor": "test-id3", - }, - ], - "legend": Object { - "isVisible": true, - "position": "right", - }, - "title": "Empty XY Chart", - } - `); + Object { + "layers": Array [ + Object { + "accessors": Array [ + "test-id1", + ], + "layerId": "", + "position": "top", + "seriesType": "bar", + "showGridlines": false, + "splitAccessor": "test-id2", + "title": "", + "xAccessor": "test-id3", + }, + ], + "legend": Object { + "isVisible": true, + "position": "right", + }, + "title": "Empty XY Chart", + } + `); }); it('loads from persisted state', () => { From be5ad16688fb94867ec43239271803c1a30c35a9 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 23 Jul 2019 15:53:58 -0400 Subject: [PATCH 58/67] Use operation labels for XY chart --- .../__snapshots__/xy_expression.test.tsx.snap | 112 +++++++++--------- .../xy_visualization.test.ts.snap | 12 +- .../xy_visualization_plugin/to_expression.ts | 80 ++++++++----- .../public/xy_visualization_plugin/types.ts | 18 ++- .../xy_expression.test.tsx | 19 ++- .../xy_visualization_plugin/xy_expression.tsx | 74 +++++++----- .../xy_visualization.test.ts | 31 +++-- 7 files changed, 218 insertions(+), 128 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap index a7895146ff4bc..5eda0db6d6c40 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_expression.test.tsx.snap @@ -27,30 +27,30 @@ exports[`xy_expression XYChart component it renders area 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={Array []} xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -84,30 +84,30 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={Array []} xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -141,30 +141,30 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={Array []} xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -198,30 +198,30 @@ exports[`xy_expression XYChart component it renders line 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={Array []} xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -255,22 +255,22 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={ @@ -281,8 +281,8 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -316,22 +316,22 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={ @@ -342,8 +342,8 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> @@ -377,22 +377,22 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = data={ Array [ Object { - "a": 1, - "b": 2, + "Label A": 1, + "Label B": 2, "c": 3, }, Object { - "a": 1, - "b": 5, + "Label A": 1, + "Label B": 5, "c": 4, }, ] } - id="a,b,c,d" + id="Label D" key="0" splitSeriesAccessors={ Array [ - "d", + "Label D", ] } stackAccessors={ @@ -403,8 +403,8 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = xAccessor="c" yAccessors={ Array [ - "a", - "b", + "Label A", + "Label B", ] } /> diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap index 05d38f68f481b..8d4433421d8c9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/__snapshots__/xy_visualization.test.ts.snap @@ -10,7 +10,13 @@ Object { "chain": Array [ Object { "arguments": Object { - "accessors": Array [], + "accessors": Array [ + "b", + "c", + ], + "columnToLabel": Array [ + "{\\"b\\":\\"col_b\\",\\"c\\":\\"col_c\\",\\"d\\":\\"col_d\\"}", + ], "hide": Array [ false, ], @@ -63,10 +69,10 @@ Object { }, ], "xTitle": Array [ - "x", + "col_a", ], "yTitle": Array [ - "y", + "col_b", ], }, "function": "lens_xy_chart", diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts index 2a29e84ce51cf..cec80c9dbed96 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/to_expression.ts @@ -37,20 +37,31 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => const stateWithValidAccessors = { ...state, - layers: state.layers.map(layer => ({ - ...layer, - accessors: layer.accessors.filter(accessor => - Boolean(frame.datasourceLayers[layer.layerId].getOperationForColumnId(accessor)) - ), - })), + layers: state.layers.map(layer => { + const datasource = frame.datasourceLayers[layer.layerId]; + + const newLayer = { ...layer }; + + if (!datasource.getOperationForColumnId(layer.splitAccessor)) { + delete newLayer.splitAccessor; + } + + return { + ...newLayer, + accessors: layer.accessors.filter(accessor => + Boolean(datasource.getOperationForColumnId(accessor)) + ), + }; + }), }; - return buildExpression(stateWithValidAccessors, xyTitles(state.layers[0], frame)); + return buildExpression(stateWithValidAccessors, xyTitles(state.layers[0], frame), frame); }; export const buildExpression = ( state: State, - { xTitle, yTitle }: { xTitle: string; yTitle: string } + { xTitle, yTitle }: { xTitle: string; yTitle: string }, + frame?: FramePublicAPI ): Ast => ({ type: 'expression', chain: [ @@ -75,28 +86,43 @@ export const buildExpression = ( ], }, ], - layers: state.layers.map(layer => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_xy_layer', - arguments: { - layerId: [layer.layerId], + layers: state.layers.map(layer => { + const columnToLabel: Record = {}; + + if (frame) { + const datasource = frame.datasourceLayers[layer.layerId]; + layer.accessors.concat([layer.splitAccessor]).forEach(accessor => { + const operation = datasource.getOperationForColumnId(accessor); + if (operation && operation.label) { + columnToLabel[accessor] = operation.label; + } + }); + } + + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_xy_layer', + arguments: { + layerId: [layer.layerId], - title: [layer.title], - showGridlines: [layer.showGridlines], - position: [layer.position], - hide: [Boolean(layer.hide)], + title: [layer.title], + showGridlines: [layer.showGridlines], + position: [layer.position], + hide: [Boolean(layer.hide)], - xAccessor: [layer.xAccessor], - splitAccessor: [layer.splitAccessor], - seriesType: [layer.seriesType], - accessors: layer.accessors, + xAccessor: [layer.xAccessor], + splitAccessor: [layer.splitAccessor], + seriesType: [layer.seriesType], + accessors: layer.accessors, + columnToLabel: [JSON.stringify(columnToLabel)], + }, }, - }, - ], - })), + ], + }; + }), }, }, ], diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts index e50adf138ee3e..90579747e6862 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/types.ts @@ -110,12 +110,12 @@ export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConf }, }; -type LayerConfigResult = LayerConfig & { type: 'lens_xy_layer' }; +type LayerConfigResult = LayerArgs & { type: 'lens_xy_layer' }; export const layerConfig: ExpressionFunction< 'lens_xy_layer', null, - LayerConfig, + LayerArgs, LayerConfigResult > = { name: 'lens_xy_layer', @@ -158,8 +158,12 @@ export const layerConfig: ExpressionFunction< help: 'The columns to display on the y axis.', multi: true, }, + columnToLabel: { + types: ['string'], + help: 'JSON key-value pairs of column ID to label', + }, }, - fn: function fn(_context: unknown, args: LayerConfig) { + fn: function fn(_context: unknown, args: LayerArgs) { return { type: 'lens_xy_layer', ...args, @@ -184,13 +188,19 @@ export type LayerConfig = AxisConfig & { splitAccessor: string; }; +export type LayerArgs = LayerConfig & { + columnToLabel?: string; // Actually a JSON key-value pair +}; + +// Arguments to XY chart expression, with computed properties export interface XYArgs { xTitle: string; yTitle: string; legend: LegendConfig; - layers: LayerConfig[]; + layers: LayerArgs[]; } +// Persisted parts of the state export interface XYState { legend: LegendConfig; layers: LayerConfig[]; diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx index ed1144f2e08b9..aaa748cf0bb12 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.test.tsx @@ -34,13 +34,13 @@ function sampleArgs() { { layerId: 'first', seriesType: 'line', - xAccessor: 'c', accessors: ['a', 'b'], position: Position.Left, showGridlines: false, title: 'A and B', splitAccessor: 'd', + columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', }, ], }; @@ -182,5 +182,22 @@ describe('xy_expression', () => { expect(component.find(BarSeries).prop('stackAccessors')).toHaveLength(1); expect(component.find(Settings).prop('rotation')).toEqual(90); }); + + test('it rewrites the rows based on provided labels', () => { + const { data, args } = sampleArgs(); + + const component = shallow(); + expect(component.find(LineSeries).prop('data')).toEqual([ + { 'Label A': 1, 'Label B': 2, c: 3 }, + { 'Label A': 1, 'Label B': 5, c: 4 }, + ]); + }); + + test('it uses labels as Y accessors', () => { + const { data, args } = sampleArgs(); + + const component = shallow(); + expect(component.find(LineSeries).prop('yAccessors')).toEqual(['Label A', 'Label B']); + }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index f8d5310328143..f65c8c990b976 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -142,34 +142,54 @@ export function XYChart({ data, args }: XYChartProps) { hide={layers[0].hide} /> - {layers.map(({ splitAccessor, seriesType, accessors, xAccessor, layerId }, index) => { - if (!data.tables[layerId]) { - return; + {layers.map( + ({ splitAccessor, seriesType, accessors, xAccessor, layerId, columnToLabel }, index) => { + if (!data.tables[layerId]) { + return; + } + + const columnToLabelMap = columnToLabel ? JSON.parse(columnToLabel) : {}; + + const rows = data.tables[layerId].rows.map(row => { + const newRow: typeof row = {}; + + // Remap data to { 'Count of documents': 5 } + Object.keys(row).forEach(key => { + if (columnToLabelMap[key]) { + newRow[columnToLabelMap[key]] = row[key]; + } else { + newRow[key] = row[key]; + } + }); + return newRow; + }); + + const splitAccessorLabel = columnToLabelMap[splitAccessor]; + const yAccessors = accessors.map(accessor => columnToLabelMap[accessor] || accessor); + const idForLegend = splitAccessorLabel || yAccessors; + + const seriesProps = { + key: index, + splitSeriesAccessors: [splitAccessorLabel || splitAccessor], + stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], + id: getSpecId(idForLegend), + xAccessor, + yAccessors, + data: rows, + }; + + return seriesType === 'line' ? ( + + ) : seriesType === 'bar' || + seriesType === 'bar_stacked' || + seriesType === 'horizontal_bar' || + seriesType === 'horizontal_bar_stacked' ? ( + + ) : ( + + ); } - - const idForCaching = accessors.concat([xAccessor, splitAccessor]).join(','); - - const seriesProps = { - key: index, - splitSeriesAccessors: [splitAccessor], - stackAccessors: seriesType.includes('stacked') ? [xAccessor] : [], - id: getSpecId(idForCaching), - xAccessor, - yAccessors: accessors, - data: data.tables[layerId].rows, - }; - - return seriesType === 'line' ? ( - - ) : seriesType === 'bar' || - seriesType === 'bar_stacked' || - seriesType === 'horizontal_bar' || - seriesType === 'horizontal_bar_stacked' ? ( - - ) : ( - - ); - })} + )} ); } diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts index 86b3aef5fd10d..060f645717ee4 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_visualization.test.ts @@ -86,32 +86,43 @@ describe('xy_visualization', () => { }); describe('#toExpression', () => { - it('should map to a valid AST', () => { - const frame = createMockFramePublicAPI(); - frame.datasourceLayers = { - first: createMockDatasource().publicAPIMock, - }; - expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); - }); + let mockDatasource: ReturnType; + let frame: ReturnType; - it('should default to labeling all columns with their column label', () => { - const mockDatasource = createMockDatasource(); + beforeEach(() => { + frame = createMockFramePublicAPI(); + mockDatasource = createMockDatasource(); mockDatasource.publicAPIMock.getOperationForColumnId.mockImplementation(col => { return { label: `col_${col}` } as Operation; }); - const frame = createMockFramePublicAPI(); frame.datasourceLayers = { first: mockDatasource.publicAPIMock, }; + }); + + it('should map to a valid AST', () => { + expect(xyVisualization.toExpression(exampleState(), frame)).toMatchSnapshot(); + }); + it('should default to labeling all columns with their column label', () => { const expression = xyVisualization.toExpression(exampleState(), frame)! as Ast; expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b'); expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c'); + expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d'); expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']); expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']); + expect( + (expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel + ).toEqual([ + JSON.stringify({ + b: 'col_b', + c: 'col_c', + d: 'col_d', + }), + ]); }); }); }); From 6533e904bfff243e33f0bb0959d39260a1bd7a0d Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 23 Jul 2019 16:36:04 -0400 Subject: [PATCH 59/67] Adding and removing layers is reflected in the datasource --- .../visualization.test.tsx | 4 ++ .../editor_frame/editor_frame.test.tsx | 1 + .../editor_frame/editor_frame.tsx | 11 ++++ .../lens/public/editor_frame_plugin/mocks.tsx | 2 + .../indexpattern_plugin/indexpattern.test.tsx | 31 ++++++++++ .../indexpattern_plugin/indexpattern.tsx | 9 +++ x-pack/legacy/plugins/lens/public/types.ts | 4 +- .../xy_config_panel.test.tsx | 60 +++++++++++++++++++ .../xy_config_panel.tsx | 2 + 9 files changed, 123 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx index c927c0bd645fe..62df18c391445 100644 --- a/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx +++ b/x-pack/legacy/plugins/lens/public/datatable_visualization_plugin/visualization.test.tsx @@ -20,6 +20,7 @@ jest.mock('../id_generator'); function mockFrame(): FramePublicAPI { return { addNewLayer: () => 'aaa', + removeLayer: () => {}, datasourceLayers: {}, }; } @@ -76,6 +77,7 @@ describe('Datatable Visualization', () => { dragDropContext={{ dragging: undefined, setDragging: () => {} }} frame={{ addNewLayer: jest.fn(), + removeLayer: jest.fn(), datasourceLayers: { a: datasource.publicAPIMock }, }} layer={layer} @@ -114,6 +116,7 @@ describe('Datatable Visualization', () => { dragDropContext={{ dragging: undefined, setDragging: () => {} }} frame={{ addNewLayer: jest.fn(), + removeLayer: jest.fn(), datasourceLayers: { a: datasource.publicAPIMock }, }} layer={layer} @@ -149,6 +152,7 @@ describe('Datatable Visualization', () => { dragDropContext={{ dragging: undefined, setDragging: () => {} }} frame={{ addNewLayer: jest.fn(), + removeLayer: jest.fn(), datasourceLayers: { a: datasource.publicAPIMock }, }} layer={layer} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx index 35fba716f7d8d..8dbf80d9afc74 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.test.tsx @@ -219,6 +219,7 @@ describe('editor_frame', () => { expect(mockVisualization.initialize).toHaveBeenCalledWith({ datasourceLayers: {}, addNewLayer: expect.any(Function), + removeLayer: expect.any(Function), }); }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 239afb5f95a49..388cb5b58e8b3 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -107,6 +107,17 @@ export function EditorFrame(props: EditorFrameProps) { return newLayerId; }, + removeLayer: (layerId: string) => { + const newState = props.datasourceMap[state.activeDatasourceId!].removeLayer( + state.datasourceStates[state.activeDatasourceId!].state, + layerId + ); + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + datasourceId: state.activeDatasourceId!, + newState, + }); + }, }; useEffect(() => { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx index c7f2092b1eca2..225b9802f113d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/mocks.tsx @@ -43,6 +43,7 @@ export function createMockDatasource(): DatasourceMock { renderDataPanel: jest.fn(), toExpression: jest.fn((_frame, _state) => null), insertLayer: jest.fn((_state, _newLayerId) => {}), + removeLayer: jest.fn((_state, _layerId) => {}), getLayers: jest.fn(_state => []), // this is an additional property which doesn't exist on real datasources @@ -57,6 +58,7 @@ export function createMockFramePublicAPI(): FrameMock { return { datasourceLayers: {}, addNewLayer: jest.fn(() => ''), + removeLayer: jest.fn(), }; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx index 3eb94f47b6eb5..7dc6b36612dfb 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx @@ -784,6 +784,37 @@ describe('IndexPattern Data Source', () => { }); }); + describe('#removeLayer', () => { + it('should remove a layer', () => { + const state = { + indexPatterns: expectedIndexPatterns, + layers: { + first: { + indexPatternId: '1', + columnOrder: [], + columns: {}, + }, + second: { + indexPatternId: '2', + columnOrder: [], + columns: {}, + }, + }, + currentIndexPatternId: '1', + }; + expect(indexPatternDatasource.removeLayer(state, 'first')).toEqual({ + ...state, + layers: { + second: { + indexPatternId: '2', + columnOrder: [], + columns: {}, + }, + }, + }); + }); + }); + describe('#getLayers', () => { it('should list the current layers', () => { expect( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx index 77925a0524296..67639346a3ad1 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -269,6 +269,15 @@ export function getIndexPatternDatasource({ }; }, + removeLayer(state: IndexPatternPrivateState, layerId: string) { + const newLayers = { ...state.layers }; + delete newLayers[layerId]; + return { + ...state, + layers: newLayers, + }; + }, + getLayers(state: IndexPatternPrivateState) { return Object.keys(state.layers); }, diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 4ea2e08985e38..3c75377991b3e 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -57,6 +57,7 @@ export interface Datasource { getPersistableState: (state: T) => P; insertLayer: (state: T, newLayerId: string) => T; + removeLayer: (state: T, layerId: string) => T; getLayers: (state: T) => string[]; renderDataPanel: (domElement: Element, props: DatasourceDataPanelProps) => void; @@ -176,8 +177,9 @@ export interface VisualizationSuggestion { export interface FramePublicAPI { datasourceLayers: Record; - // Adds a new layer. This triggers a re-render + // Adds a new layer. This has a side effect of updating the datasource state addNewLayer: () => string; + removeLayer: (layerId: string) => void; } export interface Visualization { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx index 630ac00ce42ad..15fcec52cbfca 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.test.tsx @@ -219,4 +219,64 @@ describe('XYConfigPanel', () => { ], }); }); + + describe('layers', () => { + it('adds layers', () => { + frame.addNewLayer = jest.fn().mockReturnValue('newLayerId'); + (generateId as jest.Mock).mockReturnValue('accessor'); + const setState = jest.fn(); + const state = testState(); + const component = mount( + + ); + + component + .find('[data-test-subj="lnsXY_layer_add"]') + .first() + .simulate('click'); + + expect(frame.addNewLayer).toHaveBeenCalled(); + expect(setState).toHaveBeenCalledTimes(1); + expect(generateId).toHaveBeenCalledTimes(4); + expect(setState.mock.calls[0][0]).toMatchObject({ + layers: [ + ...state.layers, + expect.objectContaining({ + layerId: 'newLayerId', + xAccessor: 'accessor', + accessors: ['accessor'], + splitAccessor: 'accessor', + }), + ], + }); + }); + it('removes layers', () => { + const setState = jest.fn(); + const state = testState(); + const component = mount( + + ); + + component + .find('[data-test-subj="lnsXY_layer_remove"]') + .first() + .simulate('click'); + + expect(frame.removeLayer).toHaveBeenCalled(); + expect(setState).toHaveBeenCalledTimes(1); + expect(setState.mock.calls[0][0]).toMatchObject({ + layers: [], + }); + }); + }); }); diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx index a3f404921f0b0..14d1523df6ec9 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_config_panel.tsx @@ -116,7 +116,9 @@ export function XYConfigPanel(props: VisualizationProps) { <> { + frame.removeLayer(layer.layerId); setState({ ...state, layers: state.layers.filter(l => l !== layer) }); }} aria-label={i18n.translate('xpack.lens.xyChart.removeLayer', { From 74e0ec971b23c6ab0f7ce0e7c58e9ce15f265686 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jul 2019 10:50:20 +0200 Subject: [PATCH 60/67] rewrote datasource state init --- .../editor_frame/state_management.test.ts | 41 +++++++++++++++++++ .../editor_frame/state_management.ts | 25 ++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts index 37e5db016d627..b7b44bbb16688 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.test.ts @@ -43,6 +43,47 @@ describe('editor_frame state management', () => { expect(props.visualizationMap.testVis.initialize).not.toHaveBeenCalled(); }); + it('should prefill state if doc is passed in', () => { + const initialState = getInitialState({ + ...props, + doc: { + activeDatasourceId: 'testDatasource', + state: { + datasourceStates: { + testDatasource: { internalState1: '' }, + testDatasource2: { internalState2: '' }, + }, + visualization: {}, + }, + title: '', + visualizationType: 'testVis', + }, + }); + + expect(initialState.datasourceStates).toMatchInlineSnapshot(` + Object { + "testDatasource": Object { + "isLoading": true, + "state": Object { + "internalState1": "", + }, + }, + "testDatasource2": Object { + "isLoading": true, + "state": Object { + "internalState2": "", + }, + }, + } + `); + expect(initialState.visualization).toMatchInlineSnapshot(` + Object { + "activeId": "testVis", + "state": null, + } + `); + }); + it('should not set active id if no initial visualization is passed in', () => { const initialState = getInitialState({ ...props, initialVisualizationId: null }); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts index 2507bf462279e..004bbc45194a1 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/state_management.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; import { EditorFrameProps } from '../editor_frame'; import { Document } from '../../persistence/saved_object_store'; @@ -63,19 +62,23 @@ export type Action = }; export const getInitialState = (props: EditorFrameProps): EditorFrameState => { + const datasourceStates: EditorFrameState['datasourceStates'] = {}; + + if (props.doc) { + Object.entries(props.doc.state.datasourceStates).forEach(([datasourceId, state]) => { + datasourceStates[datasourceId] = { isLoading: true, state }; + }); + } else if (props.initialDatasourceId) { + datasourceStates[props.initialDatasourceId] = { + state: null, + isLoading: true, + }; + } + return { saving: false, title: i18n.translate('xpack.lens.chartTitle', { defaultMessage: 'New visualization' }), - datasourceStates: props.doc - ? _.mapValues(props.doc.state.datasourceStates, state => ({ isLoading: true, state })) - : props.initialDatasourceId - ? { - [props.initialDatasourceId]: { - state: null, - isLoading: true, - }, - } - : {}, + datasourceStates, activeDatasourceId: props.initialDatasourceId ? props.initialDatasourceId : null, visualization: { state: null, From 116d49b7b22a5481c217ae9b494d622d038b0a75 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jul 2019 11:18:52 +0200 Subject: [PATCH 61/67] clean up editor_frame frame api implementation --- .../editor_frame/editor_frame.tsx | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 388cb5b58e8b3..981fd1fddfdef 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -64,30 +64,29 @@ export function EditorFrame(props: EditorFrameProps) { }, [allLoaded]); const datasourceLayers: Record = {}; - Object.keys(props.datasourceMap).forEach(id => { - const stateWrapper = state.datasourceStates[id]; - if (!stateWrapper || stateWrapper.isLoading) { - return; - } - const dsState = stateWrapper.state; - const layers = props.datasourceMap[id].getLayers(dsState); + Object.keys(props.datasourceMap) + .filter(id => state.datasourceStates[id] && !state.datasourceStates[id].isLoading) + .forEach(id => { + const datasourceState = state.datasourceStates[id].state; + const datasource = props.datasourceMap[id]; - layers.forEach(layer => { - const publicAPI = props.datasourceMap[id].getPublicAPI( - dsState, - (newState: unknown) => { - dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - datasourceId: id, - newState, - }); - }, - layer - ); + const layers = datasource.getLayers(datasourceState); + layers.forEach(layer => { + const publicAPI = props.datasourceMap[id].getPublicAPI( + datasourceState, + (newState: unknown) => { + dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + datasourceId: id, + newState, + }); + }, + layer + ); - datasourceLayers[layer] = publicAPI; + datasourceLayers[layer] = publicAPI; + }); }); - }); const framePublicAPI: FramePublicAPI = { datasourceLayers, @@ -112,6 +111,7 @@ export function EditorFrame(props: EditorFrameProps) { state.datasourceStates[state.activeDatasourceId!].state, layerId ); + dispatch({ type: 'UPDATE_DATASOURCE_STATE', datasourceId: state.activeDatasourceId!, From 7704e9765b59a9b1bdc348080a8ad68d0776552e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jul 2019 11:26:08 +0200 Subject: [PATCH 62/67] clean up editor frame --- .../editor_frame/editor_frame.tsx | 132 ++++++++---------- 1 file changed, 57 insertions(+), 75 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index 981fd1fddfdef..cdc0f9c19e21e 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -156,56 +156,54 @@ export function EditorFrame(props: EditorFrameProps) { ? props.visualizationMap[state.visualization.activeId] : undefined; - if (allLoaded) { - return ( - - { - if (activeDatasource && visualization) { - save({ - activeDatasources: Object.keys(state.datasourceStates).reduce( - (datasourceMap, datasourceId) => ({ - ...datasourceMap, - [datasourceId]: props.datasourceMap[datasourceId], - }), - {} - ), - dispatch, - visualization, - state, - redirectTo: props.redirectTo, - store: props.store, - }).catch(onError); - } - }} - disabled={state.saving || !state.activeDatasourceId || !state.visualization.activeId} - > - {i18n.translate('xpack.lens.editorFrame.Save', { - defaultMessage: 'Save', - })} - - - } - dataPanel={ - - } - configPanel={ + return ( + + { + if (activeDatasource && visualization) { + save({ + activeDatasources: Object.keys(state.datasourceStates).reduce( + (datasourceMap, datasourceId) => ({ + ...datasourceMap, + [datasourceId]: props.datasourceMap[datasourceId], + }), + {} + ), + dispatch, + visualization, + state, + redirectTo: props.redirectTo, + store: props.store, + }).catch(onError); + } + }} + disabled={state.saving || !state.activeDatasourceId || !state.visualization.activeId} + > + {i18n.translate('xpack.lens.editorFrame.Save', { + defaultMessage: 'Save', + })} + + + } + dataPanel={ + + } + configPanel={ + allLoaded && ( - } - workspacePanel={ + ) + } + workspacePanel={ + allLoaded && ( - } - suggestionsPanel={ + ) + } + suggestionsPanel={ + allLoaded && ( - } - /> - ); - } - - return ( - + ) } /> ); From c42d38382ac5ef76b17acc0ea98f3d97ba6683ae Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jul 2019 18:07:51 +0200 Subject: [PATCH 63/67] fix linting problems --- .../lens/public/editor_frame_plugin/editor_frame/save.test.ts | 4 ++-- .../lens/public/xy_visualization_plugin/xy_expression.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts index 095d00a6848a0..4e9b2b8015bba 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/save.test.ts @@ -37,8 +37,8 @@ describe('save editor frame state', () => { addNewLayer: jest.fn(), removeLayer: jest.fn(), datasourceLayers: { - first: mockDatasource.publicAPIMock - } + first: mockDatasource.publicAPIMock, + }, }, store: { async save() { diff --git a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx index b79bf61c59ceb..28268185209ea 100644 --- a/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/xy_visualization_plugin/xy_expression.tsx @@ -176,7 +176,9 @@ export function XYChart({ data, args }: XYChartProps) { id: getSpecId(idForLegend), xAccessor, yAccessors, - xScaleType: (typeof rows[0][xAccessor] === 'string' ? ScaleType.Ordinal : ScaleType.Linear) as ScaleType.Linear | ScaleType.Ordinal | ScaleType.Time, + xScaleType: (typeof rows[0][xAccessor] === 'string' + ? ScaleType.Ordinal + : ScaleType.Linear) as ScaleType.Linear | ScaleType.Ordinal | ScaleType.Time, yScaleType: ScaleType.Linear as ScaleType.Linear, data: rows, }; From 38d200a96223b18c9ec31bae776dc893d919015c Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 24 Jul 2019 18:25:00 +0200 Subject: [PATCH 64/67] fix typing errors --- .../public/editor_frame_plugin/editor_frame/editor_frame.tsx | 4 +++- .../public/editor_frame_plugin/embeddable/embeddable.test.tsx | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx index cdc0f9c19e21e..12ba3ff6e3192 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/editor_frame.tsx @@ -162,7 +162,7 @@ export function EditorFrame(props: EditorFrameProps) {