From 33802fd436805337e8b00a5e48fec86b6dba44d3 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 19 Jun 2019 16:23:13 -0400 Subject: [PATCH 01/46] Lens basic metric visualization --- .../plugins/lens/public/app_plugin/plugin.tsx | 4 + .../metric_visualization_plugin/index.ts | 7 ++ .../metric_config_panel.test.tsx | 103 ++++++++++++++++++ .../metric_config_panel.tsx | 54 +++++++++ .../metric_expression.test.tsx | 75 +++++++++++++ .../metric_expression.tsx | 97 +++++++++++++++++ .../metric_suggestions.test.ts | 91 ++++++++++++++++ .../metric_suggestions.ts | 40 +++++++ .../metric_visualization.test.ts | 76 +++++++++++++ .../metric_visualization.tsx | 50 +++++++++ .../metric_visualization_plugin/plugin.tsx | 71 ++++++++++++ .../metric_visualization_plugin/types.ts | 13 +++ 12 files changed, 681 insertions(+) create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/index.ts create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/types.ts 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 857cee9adbc64..6d2de3ac92034 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { editorFrameSetup, editorFrameStop } from '../editor_frame_plugin'; import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; +import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; import { App } from './app'; import { EditorFrameInstance } from '../types'; @@ -21,10 +22,12 @@ export class AppPlugin { // entry point to the app we have no choice until the new platform is ready const indexPattern = indexPatternDatasourceSetup(); const xyVisualization = xyVisualizationSetup(); + const metricVisualization = metricVisualizationSetup(); const editorFrame = editorFrameSetup(); editorFrame.registerDatasource('indexpattern', indexPattern); editorFrame.registerVisualization('xy', xyVisualization); + editorFrame.registerVisualization('metric', metricVisualization); this.instance = editorFrame.createInstance({}); @@ -39,6 +42,7 @@ export class AppPlugin { // TODO this will be handled by the plugin platform itself indexPatternDatasourceStop(); xyVisualizationStop(); + metricVisualizationStop(); editorFrameStop(); } } diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/index.ts b/x-pack/plugins/lens/public/metric_visualization_plugin/index.ts new file mode 100644 index 0000000000000..f75dce9b7507f --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/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 './plugin'; diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx new file mode 100644 index 0000000000000..956edeb84bc8c --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx @@ -0,0 +1,103 @@ +/* + * 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 { ReactWrapper } from 'enzyme'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { MetricConfigPanel } from './metric_config_panel'; +import { DatasourcePublicAPI, DatasourceDimensionPanelProps, Operation } from '../types'; +import { State } from './types'; +import { NativeRendererProps } from '../native_renderer'; + +describe('MetricConfigPanel', () => { + const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; + + function mockDatasource(): DatasourcePublicAPI { + return { + duplicateColumn: () => [], + getOperationForColumnId: () => null, + generateColumnId: () => 'TESTID', + getTableSpec: () => [], + moveColumnTo: () => {}, + removeColumnInTableSpec: () => [], + renderDimensionPanel: () => {}, + }; + } + + function testState(): State { + return { + title: 'Test Metric', + accessor: 'foo', + }; + } + + function testSubj(component: ReactWrapper, subj: string) { + return component + .find(`[data-test-subj="${subj}"]`) + .first() + .props(); + } + + test('allows editing the chart title', () => { + const testSetTitle = (title: string) => { + const setState = jest.fn(); + const component = mount( + + ); + + (testSubj(component, 'lnsMetric_title').onChange as Function)({ target: { value: title } }); + + expect(setState).toHaveBeenCalledTimes(1); + return setState.mock.calls[0][0]; + }; + + expect(testSetTitle('Hoi')).toMatchObject({ + title: 'Hoi', + }); + expect(testSetTitle('There!')).toMatchObject({ + title: 'There!', + }); + }); + + test('the value dimension panel only accepts singular numeric operations', () => { + const datasource = { + ...mockDatasource(), + renderDimensionPanel: jest.fn(), + }; + const state = testState(); + const component = mount( + + ); + + const panel = testSubj(component, 'lns_metric_valueDimensionPanel'); + const nativeProps = (panel as NativeRendererProps).nativeProps; + const { columnId, filterOperations } = nativeProps; + const exampleOperation: Operation = { + dataType: 'number', + id: 'foo', + isBucketed: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(columnId).toEqual('shazm'); + expect(ops.filter(filterOperations)).toEqual([{ ...exampleOperation, dataType: 'number' }]); + }); +}); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx new file mode 100644 index 0000000000000..082bf519326c1 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx @@ -0,0 +1,54 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui'; +import { State } from './types'; +import { VisualizationProps, Operation } from '../types'; +import { NativeRenderer } from '../native_renderer'; + +export function MetricConfigPanel(props: VisualizationProps) { + const { state, datasource, setState } = props; + + return ( + + + setState({ ...state, title: e.target.value })} + aria-label={i18n.translate('xpack.lens.metric.chartTitleAriaLabel', { + defaultMessage: 'Title', + })} + /> + + + + !op.isBucketed && op.dataType === 'number', + }} + /> + + + ); +} diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx new file mode 100644 index 0000000000000..8eca60f63d5d0 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { metricChart, MetricChart } from './metric_expression'; +import { KibanaDatatable } from '../types'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { MetricConfig } 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 }], + }; + + const args: MetricConfig = { + title: 'My fanci metric chart', + accessor: 'a', + }; + + return { data, args }; +} + +describe('metric_expression', () => { + describe('metricChart', () => { + test('it renders with the specified data and args', () => { + const { data, args } = sampleArgs(); + + expect(metricChart.fn(data, args, {})).toEqual({ + type: 'render', + as: 'lens_metric_chart_renderer', + value: { data, args }, + }); + }); + }); + + describe('MetricChart component', () => { + test('it renders the title and value', () => { + const { data, args } = sampleArgs(); + + expect(shallow()).toMatchInlineSnapshot(` + + +
+ 1 +
+ + My fanci metric chart + +
+
+`); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx new file mode 100644 index 0000000000000..eb558131774d8 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -0,0 +1,97 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { MetricConfig } from './types'; +import { KibanaDatatable } from '../types'; +import { RenderFunction } from './plugin'; + +export interface MetricChartProps { + data: KibanaDatatable; + args: MetricConfig; +} + +export interface MetricRender { + type: 'render'; + as: 'lens_metric_chart_renderer'; + value: MetricChartProps; +} + +export const metricChart: ExpressionFunction< + 'lens_metric_chart', + KibanaDatatable, + MetricConfig, + MetricRender +> = ({ + name: 'lens_metric_chart', + type: 'render', + help: 'A metric chart', + args: { + title: { + types: ['string'], + help: 'The chart title.', + }, + accessor: { + types: ['string'], + help: 'The column whose value is being displayed', + }, + }, + context: { + types: ['kibana_datatable'], + }, + fn(data: KibanaDatatable, args: MetricChartProps) { + return { + type: 'render', + as: 'lens_metric_chart_renderer', + value: { + data, + args, + }, + }; + }, + // TODO the typings currently don't support custom type args. As soon as they do, this can be removed +} as unknown) as ExpressionFunction< + 'lens_metric_chart', + KibanaDatatable, + MetricConfig, + MetricRender +>; + +export interface MetricChartProps { + data: KibanaDatatable; + args: MetricConfig; +} + +export const metricChartRenderer: RenderFunction = { + name: 'lens_metric_chart_renderer', + displayName: 'Metric Chart', + help: 'Metric Chart Renderer', + validate: () => {}, + reuseDomNode: true, + render: async (domNode: Element, config: MetricChartProps, _handlers: unknown) => { + ReactDOM.render(, domNode); + }, +}; + +export function MetricChart({ data, args }: MetricChartProps) { + const { title, accessor } = args; + const row = data.rows[0] as { [k: string]: number }; + // TODO: Use field formatters here... + const value = Number(Number(row[accessor]).toFixed(3)).toString(); + + return ( + + + {/* TODO: Auto-scale the text on resizes */} +
{value}
+ {title} +
+
+ ); +} diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts new file mode 100644 index 0000000000000..1294746a91214 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { getSuggestions } from './metric_suggestions'; +import { TableColumn } from '../types'; + +describe('metric_suggestions', () => { + function numCol(columnId: string): TableColumn { + return { + columnId, + operation: { + dataType: 'number', + id: `avg_${columnId}`, + label: `Avg ${columnId}`, + isBucketed: false, + }, + }; + } + + function strCol(columnId: string): TableColumn { + return { + columnId, + operation: { + dataType: 'string', + id: `terms_${columnId}`, + label: `Top 5 ${columnId}`, + isBucketed: true, + }, + }; + } + + function dateCol(columnId: string): TableColumn { + return { + columnId, + operation: { + dataType: 'date', + id: `date_histogram_${columnId}`, + isBucketed: true, + label: `${columnId} histogram`, + }, + }; + } + + 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; + }; + + expect( + getSuggestions({ + tables: [ + { datasourceSuggestionId: 0, isMultiRow: true, columns: [dateCol('a')] }, + { datasourceSuggestionId: 1, isMultiRow: true, columns: [strCol('foo'), strCol('bar')] }, + { datasourceSuggestionId: 2, isMultiRow: true, columns: [numCol('bar')] }, + { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')] }, + { datasourceSuggestionId: 4, isMultiRow: false, columns: [numCol('bar'), numCol('baz')] }, + ], + }) + ).toEqual([]); + }); + + test('suggests a basic metric chart', () => { + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 0, + isMultiRow: false, + columns: [numCol('bytes')], + }, + ], + }); + + expect(rest).toHaveLength(0); + expect(suggestion).toMatchInlineSnapshot(` +Object { + "datasourceSuggestionId": 0, + "score": 1, + "state": Object { + "accessor": "bytes", + "title": "Avg bytes", + }, + "title": "Avg bytes", +} +`); + }); +}); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts new file mode 100644 index 0000000000000..3e5898d180ad4 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -0,0 +1,40 @@ +/* + * 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 { SuggestionRequest, VisualizationSuggestion, TableSuggestion } from '../types'; +import { State } from './types'; + +/** + * Generate suggestions for the metric chart. + * + * @param opts + */ +export function getSuggestions( + opts: SuggestionRequest +): Array> { + return opts.tables + .filter( + ({ isMultiRow, columns }) => + // We only render metric charts for single-row queries. We require a single, numeric column. + !isMultiRow && columns.length === 1 && columns[0].operation.dataType === 'number' + ) + .map(table => getSuggestion(table)); +} + +function getSuggestion(table: TableSuggestion): VisualizationSuggestion { + const col = table.columns[0]; + const title = col.operation.label; + + return { + title, + score: 1, + datasourceSuggestionId: table.datasourceSuggestionId, + state: { + title, + accessor: col.columnId, + }, + }; +} diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts new file mode 100644 index 0000000000000..2deac417b28c2 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts @@ -0,0 +1,76 @@ +/* + * 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 { metricVisualization } from './metric_visualization'; +import { DatasourcePublicAPI } from '../types'; +import { State } from './types'; +import { createMockDatasource } from '../editor_frame_plugin/mocks'; + +function exampleState(): State { + return { + title: 'Foo', + accessor: 'a', + }; +} + +describe('metric_visualization', () => { + describe('#initialize', () => { + it('loads default state', () => { + const mockDatasource = createMockDatasource(); + mockDatasource.publicAPIMock.generateColumnId + .mockReturnValue('test-id1') + .mockReturnValueOnce('test-id2'); + const initialState = metricVisualization.initialize(mockDatasource.publicAPIMock); + + expect(initialState.accessor).toBeDefined(); + expect(initialState.title).toBeDefined(); + + expect(initialState).toMatchInlineSnapshot(` +Object { + "accessor": "test-id2", + "title": "Empty Metric Chart", +} +`); + }); + + it('loads from persisted state', () => { + expect( + metricVisualization.initialize(createMockDatasource().publicAPIMock, exampleState()) + ).toEqual(exampleState()); + }); + }); + + describe('#getPersistableState', () => { + it('persists the state as given', () => { + expect(metricVisualization.getPersistableState(exampleState())).toEqual(exampleState()); + }); + }); + + describe('#toExpression', () => { + it('should map to a valid AST', () => { + expect(metricVisualization.toExpression(exampleState(), {} as DatasourcePublicAPI)) + .toMatchInlineSnapshot(` +Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + "a", + ], + "title": Array [ + "Foo", + ], + }, + "function": "lens_metric_chart", + "type": "function", + }, + ], + "type": "expression", +} +`); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx new file mode 100644 index 0000000000000..9f4b232f3fe04 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render } from 'react-dom'; +import { I18nProvider } from '@kbn/i18n/react'; +import { getSuggestions } from './metric_suggestions'; +import { MetricConfigPanel } from './metric_config_panel'; +import { Visualization } from '../types'; +import { State, PersistableState } from './types'; + +export const metricVisualization: Visualization = { + getSuggestions, + + initialize(datasource, state) { + return ( + state || { + title: 'Empty Metric Chart', + accessor: datasource.generateColumnId(), + } + ); + }, + + getPersistableState: state => state, + + renderConfigPanel: (domElement, props) => + render( + + + , + domElement + ), + + toExpression: state => ({ + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_metric_chart', + arguments: { + title: [state.title], + accessor: [state.accessor], + }, + }, + ], + }), +}; diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx new file mode 100644 index 0000000000000..1028fbf4ec8b6 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/plugin.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 { Registry } from '@kbn/interpreter/target/common'; +import { CoreSetup } from 'src/core/public'; +import { metricVisualization } from './metric_visualization'; +import { + renderersRegistry, + functionsRegistry, + // @ts-ignore untyped dependency +} from '../../../../../src/legacy/core_plugins/interpreter/public/registries'; +import { ExpressionFunction } from '../../../../../src/legacy/core_plugins/interpreter/public'; +import { metricChart, metricChartRenderer } from './metric_expression'; + +// 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 +// are available +interface RenderHandlers { + done: () => void; + onDestroy: (fn: () => void) => void; +} + +export interface RenderFunction { + name: string; + displayName: string; + help: string; + validate: () => void; + reuseDomNode: boolean; + render: (domNode: Element, data: T, handlers: RenderHandlers) => void; +} + +export interface InterpreterSetup { + renderersRegistry: Registry; + functionsRegistry: Registry< + ExpressionFunction, + ExpressionFunction + >; +} + +export interface MetricVisualizationPluginSetupPlugins { + interpreter: InterpreterSetup; +} + +class MetricVisualizationPlugin { + constructor() {} + + setup(_core: CoreSetup | null, { interpreter }: MetricVisualizationPluginSetupPlugins) { + interpreter.functionsRegistry.register(() => metricChart); + + interpreter.renderersRegistry.register(() => metricChartRenderer as RenderFunction); + + return metricVisualization; + } + + stop() {} +} + +const plugin = new MetricVisualizationPlugin(); + +export const metricVisualizationSetup = () => + plugin.setup(null, { + interpreter: { + renderersRegistry, + functionsRegistry, + }, + }); + +export const metricVisualizationStop = () => plugin.stop(); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/plugins/lens/public/metric_visualization_plugin/types.ts new file mode 100644 index 0000000000000..42f93089896c8 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface MetricConfig { + accessor: string; + title: string; +} + +export type State = MetricConfig; +export type PersistableState = MetricConfig; From f5c809a14860563e0f00d9cd955e77f8611ebc84 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Mon, 24 Jun 2019 14:33:10 -0400 Subject: [PATCH 02/46] Fix merge issues, localize expression help text --- .../metric_visualization_plugin/index.ts | 0 .../metric_config_panel.test.tsx | 8 +--- .../metric_config_panel.tsx | 0 .../metric_expression.test.tsx | 0 .../metric_expression.tsx | 2 +- .../metric_suggestions.test.ts | 0 .../metric_suggestions.ts | 0 .../metric_visualization.test.ts | 0 .../metric_visualization.tsx | 0 .../metric_visualization_plugin/plugin.tsx | 2 +- .../metric_visualization_plugin/types.ts | 0 .../public/xy_visualization_plugin/types.ts | 29 +++++++++++---- .../xy_visualization_plugin/xy_expression.tsx | 37 ++++++++++++++----- 13 files changed, 54 insertions(+), 24 deletions(-) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/index.ts (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx (93%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx (97%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/plugin.tsx (95%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/types.ts (100%) diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/index.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx similarity index 93% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx index 956edeb84bc8c..0c38c86b51c43 100644 --- a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx @@ -11,19 +11,15 @@ import { MetricConfigPanel } from './metric_config_panel'; import { DatasourcePublicAPI, DatasourceDimensionPanelProps, Operation } from '../types'; import { State } from './types'; import { NativeRendererProps } from '../native_renderer'; +import { createMockDatasource } from '../editor_frame_plugin/mocks'; describe('MetricConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; function mockDatasource(): DatasourcePublicAPI { return { - duplicateColumn: () => [], - getOperationForColumnId: () => null, + ...createMockDatasource().publicAPIMock, generateColumnId: () => 'TESTID', - getTableSpec: () => [], - moveColumnTo: () => {}, - removeColumnInTableSpec: () => [], - renderDimensionPanel: () => {}, }; } diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx similarity index 97% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index eb558131774d8..638c33a28e47c 100644 --- a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -81,7 +81,7 @@ export const metricChartRenderer: RenderFunction = { export function MetricChart({ data, args }: MetricChartProps) { const { title, accessor } = args; - const row = data.rows[0] as { [k: string]: number }; + const row = data.rows[0]; // TODO: Use field formatters here... const value = Number(Number(row[accessor]).toFixed(3)).toString(); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx similarity index 95% rename from x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx index 1028fbf4ec8b6..4344dc0e21b3b 100644 --- a/x-pack/plugins/lens/public/metric_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx @@ -12,7 +12,7 @@ import { functionsRegistry, // @ts-ignore untyped dependency } from '../../../../../src/legacy/core_plugins/interpreter/public/registries'; -import { ExpressionFunction } from '../../../../../src/legacy/core_plugins/interpreter/public'; +import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public'; import { metricChart, metricChartRenderer } from './metric_expression'; // TODO these are intermediary types because interpreter is not typed yet diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/types.ts rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts 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 2f5d587ff892c..5f7773e6c3ad8 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 @@ -5,6 +5,7 @@ */ import { Position } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; import { ExpressionFunction, ArgumentType, @@ -33,12 +34,16 @@ export const legendConfig: ExpressionFunction< args: { isVisible: { types: ['boolean'], - help: 'Specifies whether or not the legend is visible.', + help: i18n.translate('xpack.lens.xyChart.isVisible.help', { + defaultMessage: 'Specifies whether or not the legend is visible.', + }), }, position: { types: ['string'], options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: 'Specifies the legend position.', + help: i18n.translate('xpack.lens.xyChart.position.help', { + defaultMessage: 'Specifies the legend position.', + }), }, }, fn: function fn(_context: unknown, args: LegendConfig) { @@ -58,16 +63,22 @@ interface AxisConfig { const axisConfig: { [key in keyof AxisConfig]: ArgumentType } = { title: { types: ['string'], - help: 'The axis title', + help: i18n.translate('xpack.lens.xyChart.title.help', { + defaultMessage: 'The axis title', + }), }, showGridlines: { types: ['boolean'], - help: 'Show / hide axis grid lines.', + help: i18n.translate('xpack.lens.xyChart.showGridlines.help', { + defaultMessage: 'Show / hide axis grid lines.', + }), }, position: { types: ['string'], options: [Position.Top, Position.Right, Position.Bottom, Position.Left], - help: 'The position of the axis', + help: i18n.translate('xpack.lens.xyChart.axisPosition.help', { + defaultMessage: 'The position of the axis', + }), }, }; @@ -89,7 +100,9 @@ export const yConfig: ExpressionFunction<'lens_xy_yConfig', null, YConfig, YConf ...axisConfig, accessors: { types: ['string'], - help: 'The columns to display on the y axis.', + help: i18n.translate('xpack.lens.xyChart.accessors.help', { + defaultMessage: 'The columns to display on the y axis.', + }), multi: true, }, }, @@ -119,7 +132,9 @@ export const xConfig: ExpressionFunction<'lens_xy_xConfig', null, XConfig, XConf ...axisConfig, accessor: { types: ['string'], - help: 'The column to display on the x axis.', + help: i18n.translate('xpack.lens.xyChart.accessor.help', { + defaultMessage: 'The column to display on the x axis.', + }), }, }, fn: function fn(_context: unknown, args: XConfig) { 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 9b2b9290b54f1..0f436ed291aa3 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 @@ -17,6 +17,7 @@ import { BarSeries, } from '@elastic/charts'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; +import { i18n } from '@kbn/i18n'; import { XYArgs } from './types'; import { KibanaDatatable } from '../types'; import { RenderFunction } from './plugin'; @@ -35,38 +36,54 @@ export interface XYRender { export const xyChart: ExpressionFunction<'lens_xy_chart', KibanaDatatable, XYArgs, XYRender> = ({ name: 'lens_xy_chart', type: 'render', - help: 'An X/Y chart', + help: i18n.translate('xpack.lens.xyChart.help', { + defaultMessage: 'An X/Y chart', + }), args: { seriesType: { types: ['string'], options: ['bar', 'line', 'area'], - help: 'The type of chart to display.', + help: i18n.translate('xpack.lens.xyChart.seriesType.help', { + defaultMessage: 'The type of chart to display.', + }), }, title: { types: ['string'], - help: 'The char title.', + help: i18n.translate('xpack.lens.xyChart.chartTitle.help', { + defaultMessage: 'The chart title.', + }), }, legend: { types: ['lens_xy_legendConfig'], - help: 'Configure the chart legend.', + help: i18n.translate('xpack.lens.xyChart.legend.help', { + defaultMessage: 'Configure the chart legend.', + }), }, y: { types: ['lens_xy_yConfig'], - help: 'The y axis configuration', + help: i18n.translate('xpack.lens.xyChart.yConfig.help', { + defaultMessage: 'The y axis configuration', + }), }, x: { types: ['lens_xy_xConfig'], - help: 'The x axis configuration', + help: i18n.translate('xpack.lens.xyChart.xConfig.help', { + defaultMessage: 'The x axis configuration', + }), }, splitSeriesAccessors: { types: ['string'], multi: true, - help: 'The columns used to split the series.', + help: i18n.translate('xpack.lens.xyChart.splitSeriesAccessors.help', { + defaultMessage: 'The columns used to split the series.', + }), }, stackAccessors: { types: ['string'], multi: true, - help: 'The columns used to stack the series.', + help: i18n.translate('xpack.lens.xyChart.stackAccessors.help', { + defaultMessage: 'The columns used to stack the series.', + }), }, }, context: { @@ -93,7 +110,9 @@ export interface XYChartProps { export const xyChartRenderer: RenderFunction = { name: 'lens_xy_chart_renderer', displayName: 'XY Chart', - help: 'X/Y Chart Renderer', + help: i18n.translate('xpack.lens.xyChart.renderer.help', { + defaultMessage: 'X/Y Chart Renderer', + }), validate: () => {}, reuseDomNode: true, render: async (domNode: Element, config: XYChartProps, _handlers: unknown) => { From c159b4ebd79c4e96de8ae6f7d003e8dd1359f4c1 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 20 Jun 2019 13:12:05 -0400 Subject: [PATCH 03/46] Add auto-scaling to the lens metric visualization --- .../metric_expression.tsx | 16 +-- .../auto_scale.test.tsx | 59 +++++++++++ .../auto_scale.tsx | 100 ++++++++++++++++++ 3 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx create mode 100644 x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index 638c33a28e47c..e7c33837d34a9 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -11,6 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { MetricConfig } from './types'; import { KibanaDatatable } from '../types'; import { RenderFunction } from './plugin'; +import { AutoScale } from './auto_scale'; export interface MetricChartProps { data: KibanaDatatable; @@ -86,12 +87,13 @@ export function MetricChart({ data, args }: MetricChartProps) { const value = Number(Number(row[accessor]).toFixed(3)).toString(); return ( - - - {/* TODO: Auto-scale the text on resizes */} -
{value}
- {title} -
-
+ + + +
{value}
+ {title} +
+
+
); } diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx new file mode 100644 index 0000000000000..c373203056ae1 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { computeScale, AutoScale } from './auto_scale'; +import { render } from 'enzyme'; + +const mockElement = (clientWidth = 100, clientHeight = 200) => ({ + clientHeight, + clientWidth, +}); + +describe('AutoScale', () => { + describe('computeScale', () => { + it('is 1 if any element is null', () => { + expect(computeScale(null, null)).toBe(1); + expect(computeScale(mockElement(), null)).toBe(1); + expect(computeScale(null, mockElement())).toBe(1); + }); + + it('is never over 1', () => { + expect(computeScale(mockElement(2000, 2000), mockElement(1000, 1000))).toBe(1); + }); + + it('is the lesser of the x or y scale', () => { + expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.25); + expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.5); + }); + }); + + describe('AutoScale', () => { + it('renders', () => { + expect( + render( + +

Hoi!

+
+ ) + ).toMatchInlineSnapshot(` +
+
+

+ Hoi! +

+
+
+`); + }); + }); +}); diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx new file mode 100644 index 0000000000000..fa7a04c2b3a51 --- /dev/null +++ b/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -0,0 +1,100 @@ +/* + * 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'; + +interface Props { + children: React.ReactNode | React.ReactNode[]; +} + +interface State { + scale: number; +} + +export class AutoScale extends React.Component { + private child: Element | null = null; + private parent: Element | null = null; + + constructor(props: Props) { + super(props); + + this.state = { scale: 1 }; + } + + setParent(el: Element | null) { + if (this.parent !== el) { + this.parent = el; + setTimeout(() => this.scale()); + } + } + + setChild(el: Element | null) { + if (this.child !== el) { + this.child = el; + setTimeout(() => this.scale()); + } + } + + scale() { + const scale = computeScale(this.parent, this.child); + + // Prevent an infinite render loop + if (this.state.scale !== scale) { + this.setState({ scale }); + } + } + + render() { + const { children } = this.props; + const { scale } = this.state; + + return ( +
this.setParent(el)} + style={{ + display: 'flex', + justifyContent: 'center', + position: 'relative', + }} + > +
this.setChild(el)} + style={{ + transform: `scale(${scale})`, + position: 'relative', + }} + > + {children} +
+
+ ); + } +} + +interface ClientDimensionable { + clientWidth: number; + clientHeight: number; +} + +/** + * computeScale computes the ratio by which the child needs to shrink in order + * to fit into the parent. This function is only exported for testing purposes. + */ +export function computeScale( + parent: ClientDimensionable | null, + child: ClientDimensionable | null +) { + if (!parent || !child) { + return 1; + } + + const scaleX = parent.clientWidth / child.clientWidth; + const scaleY = parent.clientHeight / child.clientHeight; + + return Math.min(1, Math.min(scaleX, scaleY)); +} From 356e9b6ef0141b22a3bf1407f5dad51cd261d81d Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 20 Jun 2019 13:18:25 -0400 Subject: [PATCH 04/46] Fix unit tests broken by autoscale --- .../metric_expression.test.tsx | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index 8eca60f63d5d0..bd0f8ba80bb40 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -43,32 +43,34 @@ describe('metric_expression', () => { const { data, args } = sampleArgs(); expect(shallow()).toMatchInlineSnapshot(` - - + -
- 1 -
- - My fanci metric chart - -
-
+
+ 1 +
+ + My fanci metric chart + + + + `); }); }); From 6aea5dcb57d6587b29e45f7420bf587cdc37a2ba Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Mon, 24 Jun 2019 14:43:27 -0400 Subject: [PATCH 05/46] Move autoscale to the new Lens folder --- .../lens/public/metric_visualization_plugin/auto_scale.test.tsx | 0 .../lens/public/metric_visualization_plugin/auto_scale.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx (100%) rename x-pack/{ => legacy}/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx (100%) diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx diff --git a/x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx similarity index 100% rename from x-pack/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx rename to x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx From 2dc623fbaafd339b3d28437722531248777ec7d5 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 17 Jul 2019 14:21:44 -0400 Subject: [PATCH 06/46] Add metric preview icon --- .../metric_suggestions.test.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts index 1294746a91214..6feadddf8d9a9 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -77,15 +77,16 @@ describe('metric_suggestions', () => { expect(rest).toHaveLength(0); expect(suggestion).toMatchInlineSnapshot(` -Object { - "datasourceSuggestionId": 0, - "score": 1, - "state": Object { - "accessor": "bytes", - "title": "Avg bytes", - }, - "title": "Avg bytes", -} -`); + Object { + "datasourceSuggestionId": 0, + "previewIcon": "visMetric", + "score": 1, + "state": Object { + "accessor": "bytes", + "title": "Avg bytes", + }, + "title": "Avg bytes", + } + `); }); }); From bae8a6cc97bcf8b420548d290be6c9af6e207cae Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 17 Jul 2019 14:33:38 -0400 Subject: [PATCH 07/46] Fix metric vis tests --- .../metric_visualization.test.ts | 50 ++++++++++--------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts index 6abf703613a55..62c5c44107766 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts @@ -10,6 +10,8 @@ import { State } from './types'; import { createMockDatasource } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; +jest.mock('../id_generator'); + function exampleState(): State { return { title: 'Foo', @@ -21,18 +23,18 @@ describe('metric_visualization', () => { describe('#initialize', () => { it('loads default state', () => { const mockDatasource = createMockDatasource(); - (generateId as jest.Mock).mockReturnValueOnce('test-id1').mockReturnValueOnce('test-id2'); + (generateId as jest.Mock).mockReturnValueOnce('test-id1'); const initialState = metricVisualization.initialize(mockDatasource.publicAPIMock); expect(initialState.accessor).toBeDefined(); expect(initialState.title).toBeDefined(); expect(initialState).toMatchInlineSnapshot(` -Object { - "accessor": "test-id2", - "title": "Empty Metric Chart", -} -`); + Object { + "accessor": "test-id1", + "title": "Empty Metric Chart", + } + `); }); it('loads from persisted state', () => { @@ -52,24 +54,24 @@ Object { it('should map to a valid AST', () => { expect(metricVisualization.toExpression(exampleState(), {} as DatasourcePublicAPI)) .toMatchInlineSnapshot(` -Object { - "chain": Array [ - Object { - "arguments": Object { - "accessor": Array [ - "a", - ], - "title": Array [ - "Foo", - ], - }, - "function": "lens_metric_chart", - "type": "function", - }, - ], - "type": "expression", -} -`); + Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + "a", + ], + "title": Array [ + "Foo", + ], + }, + "function": "lens_metric_chart", + "type": "function", + }, + ], + "type": "expression", + } + `); }); }); }); From 4ee8f632de55808ade8438a4a69efeecf000b53e Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 17 Jul 2019 14:39:37 -0400 Subject: [PATCH 08/46] Fix metric plugin imports --- .../plugins/lens/public/metric_visualization_plugin/plugin.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx index 4344dc0e21b3b..8e55c988ade30 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx @@ -10,8 +10,7 @@ import { metricVisualization } from './metric_visualization'; import { renderersRegistry, functionsRegistry, - // @ts-ignore untyped dependency -} from '../../../../../src/legacy/core_plugins/interpreter/public/registries'; +} from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public'; import { metricChart, metricChartRenderer } from './metric_expression'; From 218cf00faa31e7c5fdf7c5fb97204066c27aea70 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 17 Jul 2019 15:30:59 -0400 Subject: [PATCH 09/46] Use the operation label as the metric title --- .../metric_config_panel.test.tsx | 27 ------------------ .../metric_config_panel.tsx | 22 ++------------- .../metric_suggestions.test.ts | 1 - .../metric_suggestions.ts | 1 - .../metric_visualization.test.ts | 23 +++++++++------ .../metric_visualization.tsx | 28 ++++++++++--------- .../metric_visualization_plugin/types.ts | 1 - 7 files changed, 32 insertions(+), 71 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx index 14ee0ab048b57..a755cbc690e45 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx @@ -22,7 +22,6 @@ describe('MetricConfigPanel', () => { function testState(): State { return { - title: 'Test Metric', accessor: 'foo', }; } @@ -34,32 +33,6 @@ describe('MetricConfigPanel', () => { .props(); } - test('allows editing the chart title', () => { - const testSetTitle = (title: string) => { - const setState = jest.fn(); - const component = mount( - - ); - - (testSubj(component, 'lnsMetric_title').onChange as Function)({ target: { value: title } }); - - expect(setState).toHaveBeenCalledTimes(1); - return setState.mock.calls[0][0]; - }; - - expect(testSetTitle('Hoi')).toMatchObject({ - title: 'Hoi', - }); - expect(testSetTitle('There!')).toMatchObject({ - title: 'There!', - }); - }); - test('the value dimension panel only accepts singular numeric operations', () => { const datasource = { ...mockDatasource(), diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx index 082bf519326c1..aa60d79f4ebb1 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx @@ -6,34 +6,16 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFieldText, EuiForm, EuiFormRow } from '@elastic/eui'; +import { EuiForm, EuiFormRow } from '@elastic/eui'; import { State } from './types'; import { VisualizationProps, Operation } from '../types'; import { NativeRenderer } from '../native_renderer'; export function MetricConfigPanel(props: VisualizationProps) { - const { state, datasource, setState } = props; + const { state, datasource } = props; return ( - - setState({ ...state, title: e.target.value })} - aria-label={i18n.translate('xpack.lens.metric.chartTitleAriaLabel', { - defaultMessage: 'Title', - })} - /> - - { "score": 1, "state": Object { "accessor": "bytes", - "title": "Avg bytes", }, "title": "Avg bytes", } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index 8d2c05cdbbdcc..d99f0fbeaa114 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -34,7 +34,6 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion { datasourceSuggestionId: table.datasourceSuggestionId, previewIcon: 'visMetric', state: { - title, accessor: col.columnId, }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts index 62c5c44107766..409eee06185bb 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts @@ -5,16 +5,15 @@ */ import { metricVisualization } from './metric_visualization'; -import { DatasourcePublicAPI } from '../types'; import { State } from './types'; import { createMockDatasource } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; +import { DatasourcePublicAPI } from '../types'; jest.mock('../id_generator'); function exampleState(): State { return { - title: 'Foo', accessor: 'a', }; } @@ -27,12 +26,9 @@ describe('metric_visualization', () => { const initialState = metricVisualization.initialize(mockDatasource.publicAPIMock); expect(initialState.accessor).toBeDefined(); - expect(initialState.title).toBeDefined(); - expect(initialState).toMatchInlineSnapshot(` Object { "accessor": "test-id1", - "title": "Empty Metric Chart", } `); }); @@ -52,8 +48,19 @@ describe('metric_visualization', () => { describe('#toExpression', () => { it('should map to a valid AST', () => { - expect(metricVisualization.toExpression(exampleState(), {} as DatasourcePublicAPI)) - .toMatchInlineSnapshot(` + const datasource: DatasourcePublicAPI = { + ...createMockDatasource().publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + label: 'shazm', + }; + }, + }; + + expect(metricVisualization.toExpression(exampleState(), datasource)).toMatchInlineSnapshot(` Object { "chain": Array [ Object { @@ -62,7 +69,7 @@ describe('metric_visualization', () => { "a", ], "title": Array [ - "Foo", + "shazm", ], }, "function": "lens_metric_chart", diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx index 60f006398b086..d871d30080891 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx @@ -19,7 +19,6 @@ export const metricVisualization: Visualization = { initialize(_, state) { return ( state || { - title: 'Empty Metric Chart', accessor: generateId(), } ); @@ -35,17 +34,20 @@ export const metricVisualization: Visualization = { domElement ), - toExpression: state => ({ - type: 'expression', - chain: [ - { - type: 'function', - function: 'lens_metric_chart', - arguments: { - title: [state.title], - accessor: [state.accessor], + toExpression(state, datasource) { + const operation = datasource.getOperationForColumnId(state.accessor); + return { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_metric_chart', + arguments: { + title: [(operation && operation.label) || ''], + accessor: [state.accessor], + }, }, - }, - ], - }), + ], + }; + }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts index 42f93089896c8..cd02d36577262 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts @@ -6,7 +6,6 @@ export interface MetricConfig { accessor: string; - title: string; } export type State = MetricConfig; From 59e19d9ed6ead96c2b21c31ba96d398be2e705b1 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Thu, 8 Aug 2019 20:44:31 -0400 Subject: [PATCH 10/46] Add metric suggestions, fix tests --- .../plugins/lens/public/app_plugin/plugin.tsx | 4 +- .../editor_frame/editor_frame.test.tsx | 7 +- .../editor_frame/suggestion_helpers.test.ts | 5 +- .../editor_frame/suggestion_helpers.ts | 41 ++-- .../indexpattern_plugin/indexpattern.test.tsx | 2 +- ...stions.tsx => indexpattern_suggestions.ts} | 205 ++++++++++++++++-- .../metric_config_panel.test.tsx | 15 +- .../metric_config_panel.tsx | 11 +- .../metric_expression.test.tsx | 76 ++++--- .../metric_expression.tsx | 19 +- .../metric_suggestions.test.ts | 40 ++-- .../metric_suggestions.ts | 1 + .../metric_visualization.test.ts | 31 ++- .../metric_visualization.tsx | 31 ++- .../metric_visualization_plugin/types.ts | 10 +- 15 files changed, 362 insertions(+), 136 deletions(-) rename x-pack/legacy/plugins/lens/public/indexpattern_plugin/{indexpattern_suggestions.tsx => indexpattern_suggestions.ts} (62%) 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 7502346d42dec..59dcdda6cd3a4 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; -import chrome, { Chrome } from 'ui/chrome'; +import chrome from 'ui/chrome'; import { localStorage } from 'ui/storage/storage_service'; import { QueryBar } from '../../../../../../src/legacy/core_plugins/data/public/query'; import { editorFrameSetup, editorFrameStop } from '../editor_frame_plugin'; @@ -38,9 +38,9 @@ export class AppPlugin { const store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient()); editorFrame.registerDatasource('indexpattern', indexPattern); - editorFrame.registerVisualization(metricVisualization); editorFrame.registerVisualization(xyVisualization); editorFrame.registerVisualization(datatableVisualization); + editorFrame.registerVisualization(metricVisualization); this.instance = editorFrame.createInstance({}); 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 7d9ff8918ab29..01ba2f36cf1c7 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 @@ -1120,12 +1120,7 @@ describe('editor_frame', () => { // TODO why is this necessary? instance.update(); const suggestions = instance.find('[data-test-subj="suggestion-title"]'); - expect(suggestions.map(el => el.text())).toEqual([ - 'Suggestion1', - 'Suggestion2', - 'Suggestion3', - 'Suggestion4', - ]); + expect(suggestions.map(el => el.text())).toEqual(['Suggestion1', 'Suggestion3']); }); it('should switch to suggested visualization', async () => { 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 a1408d9851398..9d5317f05a4f9 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 @@ -85,7 +85,8 @@ describe('suggestion helpers', () => { 'vis1', {} ); - expect(suggestions).toHaveLength(3); + expect(suggestions).toHaveLength(2); + expect(suggestions.map(s => s.visualizationId)).toEqual(['vis2', 'vis1']); }); it('should rank the visualizations by score', () => { @@ -129,9 +130,9 @@ describe('suggestion helpers', () => { 'vis1', {} ); + expect(suggestions.length).toEqual(2); expect(suggestions[0].score).toBe(0.8); expect(suggestions[1].score).toBe(0.6); - expect(suggestions[2].score).toBe(0.2); }); it('should call all suggestion getters with all available data tables', () => { 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 f2d1db4eb636c..d8e7a676058e7 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,6 +5,7 @@ */ import { Ast } from '@kbn/interpreter/common'; +import _ from 'lodash'; import { Visualization, DatasourceSuggestion, TableSuggestion } from '../../types'; import { Action } from './state_management'; @@ -34,24 +35,28 @@ export function getSuggestions( ): Suggestion[] { const datasourceTables: TableSuggestion[] = 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.find( - datasourceSuggestion => - datasourceSuggestion.table.datasourceSuggestionId === datasourceSuggestionId - )!.state, - })); - }) - .reduce((globalList, currentList) => [...globalList, ...currentList], []) - .sort(({ score: scoreA }, { score: scoreB }) => scoreB - scoreA); + const suggestions = Object.entries(visualizationMap).map(([visualizationId, visualization]) => { + return visualization + .getSuggestions({ + tables: datasourceTables, + state: visualizationId === activeVisualizationId ? visualizationState : undefined, + }) + .map(({ datasourceSuggestionId, ...suggestion }) => ({ + ...suggestion, + visualizationId, + datasourceState: datasourceTableSuggestions.find( + datasourceSuggestion => + datasourceSuggestion.table.datasourceSuggestionId === datasourceSuggestionId + )!.state, + })); + }); + + return _(suggestions) + .flatten() + .sortBy(({ score }) => score) + .reverse() + .uniq(({ visualizationId }) => visualizationId) + .value() as Suggestion[]; } export function toSwitchAction(suggestion: Suggestion): Action { 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 b7d761b42030a..adda7217c97a2 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 @@ -960,7 +960,7 @@ describe('IndexPattern Data Source', () => { indexPatterns: expectedIndexPatterns, }, table: { - datasourceSuggestionId: 0, + datasourceSuggestionId: 3, isMultiRow: true, columns: [ { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts similarity index 62% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 01373967d072f..9030d66e51b18 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -13,10 +13,147 @@ import { IndexPatternLayer, IndexPatternPrivateState, IndexPattern, + IndexPatternColumn, } from './indexpattern'; import { buildColumn, getOperationTypesForField } from './operations'; import { hasField } from './utils'; +// A layer structure that makes suggestion generation a bit cleaner +interface Layer { + id: string; + columns: IndexPatternColumn[]; + indexPatternId: string; + columnMap: Record; +} + +// The options passed into the suggestion generation functions. +interface SuggestionContext { + indexPatterns: Record; + layers: Layer[]; + datasourceSuggestionId: number; +} + +function isMetric(column: IndexPatternColumn) { + return !column.isBucketed && column.dataType === 'number'; +} + +// Generate a single metric suggestion, if there isn't one already in the layers +function suggestMetric({ + indexPatterns, + layers, + datasourceSuggestionId, +}: SuggestionContext): DatasourceSuggestion | undefined { + // If we already have something like a metric, we don't need + // to suggest a new one. + if (layers.some(l => l.columns.length === 1 && isMetric(Object.values(l.columns)[0]))) { + return; + } + + const layer = layers.find(l => !!l.columns.some(isMetric)); + + if (!layer) { + return; + } + + const column = layer.columns.find(isMetric)!; + const columnId = 'metric'; + + return { + table: { + layerId: layer.id, + datasourceSuggestionId, + columns: [ + { + columnId, + operation: columnToOperation(column), + }, + ], + isMultiRow: false, + }, + state: { + currentIndexPatternId: layer.indexPatternId, + indexPatterns, + layers: { + [layer.id]: { + columnOrder: [columnId], + columns: { metric: column }, + indexPatternId: layer.indexPatternId, + }, + }, + }, + }; +} + +// Generate a histogram suggestion, if there isn't one already in the layers +function suggestHistogram({ + indexPatterns, + layers, + datasourceSuggestionId, +}: SuggestionContext): DatasourceSuggestion | undefined { + // If we already have something like a histogram, we don't need + // to suggest a new one. + if (layers.some(l => l.columns.length > 1 && l.columns.some(c => c.isBucketed))) { + return; + } + + const findDateField = (l: Layer) => + indexPatterns[l.indexPatternId].fields.find(f => f.aggregatable && f.type === 'date'); + + const layer = layers.find(l => l.columns.find(isMetric) && findDateField(l)); + + if (!layer) { + return; + } + + const column = layer.columns.find(isMetric)!; + const bucketableField = findDateField(layer); + + if (!column || !bucketableField) { + return; + } + + const bucketColumn = buildColumn({ + op: getOperationTypesForField(bucketableField)[0], + columns: layer.columnMap, + layerId: layer.id, + indexPattern: indexPatterns[layer.indexPatternId], + suggestedPriority: undefined, + field: bucketableField, + }); + + return { + table: { + datasourceSuggestionId, + columns: [ + { + columnId: 'histogram', + operation: columnToOperation(bucketColumn), + }, + { + columnId: 'metric', + operation: columnToOperation(column), + }, + ], + isMultiRow: true, + layerId: layer.id, + }, + state: { + indexPatterns, + currentIndexPatternId: layer.indexPatternId, + layers: { + [layer.id]: { + columnOrder: ['histogram', 'metric'], + columns: { + histogram: bucketColumn, + metric: column, + }, + indexPatternId: layer.indexPatternId, + }, + }, + }, + }; +} + function buildSuggestion({ state, updatedLayer, @@ -78,7 +215,7 @@ export function getDatasourceSuggestionsForField( } } -function getBucketOperation(field: IndexPatternField) { +function getBucketOperationTypes(field: IndexPatternField) { return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); } @@ -90,7 +227,7 @@ function getExistingLayerSuggestionsForField( const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; const operations = getOperationTypesForField(field); - const usableAsBucketOperation = getBucketOperation(field); + const usableAsBucketOperation = getBucketOperationTypes(field); const fieldInUse = Object.values(layer.columns).some( column => hasField(column) && column.sourceField === field.name ); @@ -157,7 +294,7 @@ function addFieldAsBucketOperation( indexPattern: IndexPattern, field: IndexPatternField ) { - const applicableBucketOperation = getBucketOperation(field); + const applicableBucketOperation = getBucketOperationTypes(field); const newColumn = buildColumn({ op: applicableBucketOperation, columns: layer.columns, @@ -208,7 +345,7 @@ function getEmptyLayerSuggestionsForField( ) { const indexPattern = state.indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; - if (getBucketOperation(field)) { + if (getBucketOperationTypes(field)) { newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(layerId, indexPattern, field); @@ -240,7 +377,7 @@ function createNewLayerWithBucketAggregation( // let column know about count column const column = buildColumn({ layerId, - op: getBucketOperation(field), + op: getBucketOperationTypes(field), indexPattern, columns: { col2: countColumn, @@ -295,20 +432,46 @@ function createNewLayerWithMetricAggregation( }; } -export function getDatasourceSuggestionsFromCurrentState( - state: IndexPatternPrivateState -): Array> { - const layers = Object.entries(state.layers); - - return layers - .map(([layerId, layer], index) => { - if (layer.columnOrder.length === 0) { - return; - } - - return buildSuggestion({ state, layerId, isMultiRow: true, datasourceSuggestionId: index }); - }) - .reduce((prev, current) => (current ? prev.concat([current]) : prev), [] as Array< - DatasourceSuggestion - >); +export function getDatasourceSuggestionsFromCurrentState(state: IndexPatternPrivateState) { + const layers = Object.entries(state.layers).map(([id, layer]) => ({ + id, + indexPatternId: layer.indexPatternId, + columnMap: layer.columns, + columns: Object.values(layer.columns), + })); + const [firstLayer] = layers.filter(({ columns }) => columns.length > 0); + const suggestions: Array> = []; + + const histogramSuggestion = suggestHistogram({ + layers, + indexPatterns: state.indexPatterns, + datasourceSuggestionId: 1, + }); + + const metricSuggestion = suggestMetric({ + layers, + indexPatterns: state.indexPatterns, + datasourceSuggestionId: 2, + }); + + if (firstLayer) { + suggestions.push( + buildSuggestion({ + state, + layerId: firstLayer.id, + isMultiRow: true, + datasourceSuggestionId: 3, + }) + ); + } + + if (histogramSuggestion) { + suggestions.push(histogramSuggestion); + } + + if (metricSuggestion) { + suggestions.push(metricSuggestion); + } + + return suggestions; } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx index a755cbc690e45..ff2e55ac83dcc 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx @@ -8,10 +8,10 @@ import React from 'react'; import { ReactWrapper } from 'enzyme'; import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { MetricConfigPanel } from './metric_config_panel'; -import { DatasourcePublicAPI, DatasourceDimensionPanelProps, Operation } from '../types'; +import { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; import { State } from './types'; import { NativeRendererProps } from '../native_renderer'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; describe('MetricConfigPanel', () => { const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; @@ -23,6 +23,7 @@ describe('MetricConfigPanel', () => { function testState(): State { return { accessor: 'foo', + layerId: 'bar', }; } @@ -34,16 +35,15 @@ describe('MetricConfigPanel', () => { } test('the value dimension panel only accepts singular numeric operations', () => { - const datasource = { - ...mockDatasource(), - renderDimensionPanel: jest.fn(), - }; const state = testState(); const component = mount( ); @@ -53,7 +53,6 @@ describe('MetricConfigPanel', () => { const { columnId, filterOperations } = nativeProps; const exampleOperation: Operation = { dataType: 'number', - id: 'foo', isBucketed: false, label: 'bar', }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx index aa60d79f4ebb1..335dbb27556d8 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx @@ -8,11 +8,13 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow } from '@elastic/eui'; import { State } from './types'; -import { VisualizationProps, Operation } from '../types'; +import { VisualizationProps } from '../types'; import { NativeRenderer } from '../native_renderer'; export function MetricConfigPanel(props: VisualizationProps) { - const { state, datasource } = props; + const { state, frame } = props; + const [datasource] = Object.values(frame.datasourceLayers); + const [layerId] = Object.keys(frame.datasourceLayers); return ( @@ -22,12 +24,13 @@ export function MetricConfigPanel(props: VisualizationProps) { })} > !op.isBucketed && op.dataType === 'number', + filterOperations: op => !op.isBucketed && op.dataType === 'number', }} /> diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index bd0f8ba80bb40..825b08d676bbe 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -5,21 +5,27 @@ */ import { metricChart, MetricChart } from './metric_expression'; -import { KibanaDatatable } from '../types'; +import { LensMultiTable } from '../types'; import React from 'react'; import { shallow } from 'enzyme'; import { MetricConfig } 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 }], + const data: LensMultiTable = { + type: 'lens_multitable', + tables: { + l1: { + type: 'kibana_datatable', + columns: [{ id: 'a', name: 'a' }, { id: 'b', name: 'b' }, { id: 'c', name: 'c' }], + rows: [{ a: 10110, b: 2, c: 3 }], + }, + }, }; const args: MetricConfig = { - title: 'My fanci metric chart', accessor: 'a', + layerId: 'l1', + title: 'My fanci metric chart', }; return { data, args }; @@ -43,35 +49,35 @@ describe('metric_expression', () => { const { data, args } = sampleArgs(); expect(shallow()).toMatchInlineSnapshot(` - - - -
- 1 -
- - My fanci metric chart - -
-
-
-`); + + + +
+ 10110 +
+ + My fanci metric chart + +
+
+
+ `); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index e7c33837d34a9..cc0f65efa9c0a 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -9,12 +9,12 @@ import ReactDOM from 'react-dom'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { MetricConfig } from './types'; -import { KibanaDatatable } from '../types'; +import { LensMultiTable } from '../types'; import { RenderFunction } from './plugin'; import { AutoScale } from './auto_scale'; export interface MetricChartProps { - data: KibanaDatatable; + data: LensMultiTable; args: MetricConfig; } @@ -26,7 +26,7 @@ export interface MetricRender { export const metricChart: ExpressionFunction< 'lens_metric_chart', - KibanaDatatable, + LensMultiTable, MetricConfig, MetricRender > = ({ @@ -44,9 +44,9 @@ export const metricChart: ExpressionFunction< }, }, context: { - types: ['kibana_datatable'], + types: ['lens_multitable'], }, - fn(data: KibanaDatatable, args: MetricChartProps) { + fn(data: LensMultiTable, args: MetricChartProps) { return { type: 'render', as: 'lens_metric_chart_renderer', @@ -59,16 +59,11 @@ export const metricChart: ExpressionFunction< // TODO the typings currently don't support custom type args. As soon as they do, this can be removed } as unknown) as ExpressionFunction< 'lens_metric_chart', - KibanaDatatable, + LensMultiTable, MetricConfig, MetricRender >; -export interface MetricChartProps { - data: KibanaDatatable; - args: MetricConfig; -} - export const metricChartRenderer: RenderFunction = { name: 'lens_metric_chart_renderer', displayName: 'Metric Chart', @@ -82,7 +77,7 @@ export const metricChartRenderer: RenderFunction = { export function MetricChart({ data, args }: MetricChartProps) { const { title, accessor } = args; - const row = data.rows[0]; + const [row] = Object.values(data.tables)[0].rows; // TODO: Use field formatters here... const value = Number(Number(row[accessor]).toFixed(3)).toString(); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts index f964775c5ae5e..49b3e56c2d2ca 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -5,39 +5,36 @@ */ import { getSuggestions } from './metric_suggestions'; -import { TableColumn } from '../types'; +import { TableSuggestionColumn } from '..'; describe('metric_suggestions', () => { - function numCol(columnId: string): TableColumn { + function numCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { dataType: 'number', - id: `avg_${columnId}`, label: `Avg ${columnId}`, isBucketed: false, }, }; } - function strCol(columnId: string): TableColumn { + function strCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { dataType: 'string', - id: `terms_${columnId}`, label: `Top 5 ${columnId}`, isBucketed: true, }, }; } - function dateCol(columnId: string): TableColumn { + function dateCol(columnId: string): TableSuggestionColumn { return { columnId, operation: { dataType: 'date', - id: `date_histogram_${columnId}`, isBucketed: true, label: `${columnId} histogram`, }, @@ -54,11 +51,26 @@ describe('metric_suggestions', () => { expect( getSuggestions({ tables: [ - { datasourceSuggestionId: 0, isMultiRow: true, columns: [dateCol('a')] }, - { datasourceSuggestionId: 1, isMultiRow: true, columns: [strCol('foo'), strCol('bar')] }, - { datasourceSuggestionId: 2, isMultiRow: true, columns: [numCol('bar')] }, - { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')] }, - { datasourceSuggestionId: 4, isMultiRow: false, columns: [numCol('bar'), numCol('baz')] }, + { columns: [dateCol('a')], datasourceSuggestionId: 0, isMultiRow: true, layerId: 'l1' }, + { + columns: [strCol('foo'), strCol('bar')], + datasourceSuggestionId: 1, + isMultiRow: true, + layerId: 'l1', + }, + { layerId: 'l1', datasourceSuggestionId: 2, isMultiRow: true, columns: [numCol('bar')] }, + { + columns: [unknownCol(), numCol('bar')], + datasourceSuggestionId: 3, + isMultiRow: true, + layerId: 'l1', + }, + { + columns: [numCol('bar'), numCol('baz')], + datasourceSuggestionId: 4, + isMultiRow: false, + layerId: 'l1', + }, ], }) ).toEqual([]); @@ -68,9 +80,10 @@ describe('metric_suggestions', () => { const [suggestion, ...rest] = getSuggestions({ tables: [ { + columns: [numCol('bytes')], datasourceSuggestionId: 0, isMultiRow: false, - columns: [numCol('bytes')], + layerId: 'l1', }, ], }); @@ -83,6 +96,7 @@ describe('metric_suggestions', () => { "score": 1, "state": Object { "accessor": "bytes", + "layerId": "l1", }, "title": "Avg bytes", } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index d99f0fbeaa114..136a1f07f3707 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -34,6 +34,7 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion { datasourceSuggestionId: table.datasourceSuggestionId, previewIcon: 'visMetric', state: { + layerId: table.layerId, accessor: col.columnId, }, }; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts index 409eee06185bb..b6de912089c4b 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts @@ -6,37 +6,47 @@ import { metricVisualization } from './metric_visualization'; import { State } from './types'; -import { createMockDatasource } from '../editor_frame_plugin/mocks'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; import { generateId } from '../id_generator'; -import { DatasourcePublicAPI } from '../types'; +import { DatasourcePublicAPI, FramePublicAPI } from '../types'; jest.mock('../id_generator'); function exampleState(): State { return { accessor: 'a', + layerId: 'l1', + }; +} + +function mockFrame(): FramePublicAPI { + return { + ...createMockFramePublicAPI(), + addNewLayer: () => 'l42', + datasourceLayers: { + l1: createMockDatasource().publicAPIMock, + l42: createMockDatasource().publicAPIMock, + }, }; } describe('metric_visualization', () => { describe('#initialize', () => { it('loads default state', () => { - const mockDatasource = createMockDatasource(); (generateId as jest.Mock).mockReturnValueOnce('test-id1'); - const initialState = metricVisualization.initialize(mockDatasource.publicAPIMock); + const initialState = metricVisualization.initialize(mockFrame()); expect(initialState.accessor).toBeDefined(); expect(initialState).toMatchInlineSnapshot(` Object { "accessor": "test-id1", + "layerId": "l42", } `); }); it('loads from persisted state', () => { - expect( - metricVisualization.initialize(createMockDatasource().publicAPIMock, exampleState()) - ).toEqual(exampleState()); + expect(metricVisualization.initialize(mockFrame(), exampleState())).toEqual(exampleState()); }); }); @@ -60,7 +70,12 @@ describe('metric_visualization', () => { }, }; - expect(metricVisualization.toExpression(exampleState(), datasource)).toMatchInlineSnapshot(` + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(` Object { "chain": Array [ Object { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx index d871d30080891..f178b2bd4fe5e 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { getSuggestions } from './metric_suggestions'; import { MetricConfigPanel } from './metric_config_panel'; import { Visualization } from '../types'; @@ -14,11 +15,33 @@ import { State, PersistableState } from './types'; import { generateId } from '../id_generator'; export const metricVisualization: Visualization = { + id: 'lnsMetric', + + visualizationTypes: [ + { + id: 'lnsMetric', + icon: 'visMetric', + label: i18n.translate('xpack.lens.metric.label', { + defaultMessage: 'Metric', + }), + }, + ], + + getDescription() { + return { + icon: 'visMetric', + label: i18n.translate('xpack.lens.metric.label', { + defaultMessage: 'Metric', + }), + }; + }, + getSuggestions, - initialize(_, state) { + initialize(frame, state) { return ( state || { + layerId: frame.addNewLayer(), accessor: generateId(), } ); @@ -34,8 +57,10 @@ export const metricVisualization: Visualization = { domElement ), - toExpression(state, datasource) { - const operation = datasource.getOperationForColumnId(state.accessor); + toExpression(state, frame) { + const [datasource] = Object.values(frame.datasourceLayers); + const operation = datasource && datasource.getOperationForColumnId(state.accessor); + return { type: 'expression', chain: [ diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts index cd02d36577262..89d41552639c4 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts @@ -4,9 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface MetricConfig { +export interface State { + layerId: string; accessor: string; } -export type State = MetricConfig; -export type PersistableState = MetricConfig; +export interface MetricConfig extends State { + title: string; +} + +export type PersistableState = State; From eda40df9f73500c21e7cd58e41cee8d8dc73cea9 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 9 Aug 2019 14:03:57 -0400 Subject: [PATCH 11/46] Back out suggestion changes, in lieu of Joe's work --- .../editor_frame/editor_frame.test.tsx | 7 +- .../editor_frame/suggestion_helpers.test.ts | 6 +- .../editor_frame/suggestion_helpers.ts | 9 +- ...xpattern.test.tsx => indexpattern.test.ts} | 2 +- .../indexpattern_suggestions.ts | 205 ++---------------- 5 files changed, 34 insertions(+), 195 deletions(-) rename x-pack/legacy/plugins/lens/public/indexpattern_plugin/{indexpattern.test.tsx => indexpattern.test.ts} (99%) 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 01ba2f36cf1c7..7d9ff8918ab29 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 @@ -1120,7 +1120,12 @@ describe('editor_frame', () => { // TODO why is this necessary? instance.update(); const suggestions = instance.find('[data-test-subj="suggestion-title"]'); - expect(suggestions.map(el => el.text())).toEqual(['Suggestion1', 'Suggestion3']); + expect(suggestions.map(el => el.text())).toEqual([ + 'Suggestion1', + 'Suggestion2', + 'Suggestion3', + 'Suggestion4', + ]); }); it('should switch to suggested visualization', async () => { 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 9d5317f05a4f9..9d509404eef2c 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 @@ -85,8 +85,7 @@ describe('suggestion helpers', () => { 'vis1', {} ); - expect(suggestions).toHaveLength(2); - expect(suggestions.map(s => s.visualizationId)).toEqual(['vis2', 'vis1']); + expect(suggestions).toHaveLength(3); }); it('should rank the visualizations by score', () => { @@ -130,9 +129,10 @@ describe('suggestion helpers', () => { 'vis1', {} ); - expect(suggestions.length).toEqual(2); + expect(suggestions.length).toEqual(3); expect(suggestions[0].score).toBe(0.8); expect(suggestions[1].score).toBe(0.6); + expect(suggestions[2].score).toBe(0.2); }); it('should call all suggestion getters with all available data tables', () => { 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 d8e7a676058e7..c58bf4dc59e7e 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 @@ -51,12 +51,9 @@ export function getSuggestions( })); }); - return _(suggestions) - .flatten() - .sortBy(({ score }) => score) - .reverse() - .uniq(({ visualizationId }) => visualizationId) - .value() as Suggestion[]; + return _.flatten(suggestions).sort((a, b) => { + return b.score - a.score; + }); } export function toSwitchAction(suggestion: Suggestion): Action { 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.ts similarity index 99% rename from x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx rename to x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index adda7217c97a2..b7d761b42030a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -960,7 +960,7 @@ describe('IndexPattern Data Source', () => { indexPatterns: expectedIndexPatterns, }, table: { - datasourceSuggestionId: 3, + datasourceSuggestionId: 0, isMultiRow: true, columns: [ { diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 9030d66e51b18..01373967d072f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -13,147 +13,10 @@ import { IndexPatternLayer, IndexPatternPrivateState, IndexPattern, - IndexPatternColumn, } from './indexpattern'; import { buildColumn, getOperationTypesForField } from './operations'; import { hasField } from './utils'; -// A layer structure that makes suggestion generation a bit cleaner -interface Layer { - id: string; - columns: IndexPatternColumn[]; - indexPatternId: string; - columnMap: Record; -} - -// The options passed into the suggestion generation functions. -interface SuggestionContext { - indexPatterns: Record; - layers: Layer[]; - datasourceSuggestionId: number; -} - -function isMetric(column: IndexPatternColumn) { - return !column.isBucketed && column.dataType === 'number'; -} - -// Generate a single metric suggestion, if there isn't one already in the layers -function suggestMetric({ - indexPatterns, - layers, - datasourceSuggestionId, -}: SuggestionContext): DatasourceSuggestion | undefined { - // If we already have something like a metric, we don't need - // to suggest a new one. - if (layers.some(l => l.columns.length === 1 && isMetric(Object.values(l.columns)[0]))) { - return; - } - - const layer = layers.find(l => !!l.columns.some(isMetric)); - - if (!layer) { - return; - } - - const column = layer.columns.find(isMetric)!; - const columnId = 'metric'; - - return { - table: { - layerId: layer.id, - datasourceSuggestionId, - columns: [ - { - columnId, - operation: columnToOperation(column), - }, - ], - isMultiRow: false, - }, - state: { - currentIndexPatternId: layer.indexPatternId, - indexPatterns, - layers: { - [layer.id]: { - columnOrder: [columnId], - columns: { metric: column }, - indexPatternId: layer.indexPatternId, - }, - }, - }, - }; -} - -// Generate a histogram suggestion, if there isn't one already in the layers -function suggestHistogram({ - indexPatterns, - layers, - datasourceSuggestionId, -}: SuggestionContext): DatasourceSuggestion | undefined { - // If we already have something like a histogram, we don't need - // to suggest a new one. - if (layers.some(l => l.columns.length > 1 && l.columns.some(c => c.isBucketed))) { - return; - } - - const findDateField = (l: Layer) => - indexPatterns[l.indexPatternId].fields.find(f => f.aggregatable && f.type === 'date'); - - const layer = layers.find(l => l.columns.find(isMetric) && findDateField(l)); - - if (!layer) { - return; - } - - const column = layer.columns.find(isMetric)!; - const bucketableField = findDateField(layer); - - if (!column || !bucketableField) { - return; - } - - const bucketColumn = buildColumn({ - op: getOperationTypesForField(bucketableField)[0], - columns: layer.columnMap, - layerId: layer.id, - indexPattern: indexPatterns[layer.indexPatternId], - suggestedPriority: undefined, - field: bucketableField, - }); - - return { - table: { - datasourceSuggestionId, - columns: [ - { - columnId: 'histogram', - operation: columnToOperation(bucketColumn), - }, - { - columnId: 'metric', - operation: columnToOperation(column), - }, - ], - isMultiRow: true, - layerId: layer.id, - }, - state: { - indexPatterns, - currentIndexPatternId: layer.indexPatternId, - layers: { - [layer.id]: { - columnOrder: ['histogram', 'metric'], - columns: { - histogram: bucketColumn, - metric: column, - }, - indexPatternId: layer.indexPatternId, - }, - }, - }, - }; -} - function buildSuggestion({ state, updatedLayer, @@ -215,7 +78,7 @@ export function getDatasourceSuggestionsForField( } } -function getBucketOperationTypes(field: IndexPatternField) { +function getBucketOperation(field: IndexPatternField) { return getOperationTypesForField(field).find(op => op === 'date_histogram' || op === 'terms'); } @@ -227,7 +90,7 @@ function getExistingLayerSuggestionsForField( const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; const operations = getOperationTypesForField(field); - const usableAsBucketOperation = getBucketOperationTypes(field); + const usableAsBucketOperation = getBucketOperation(field); const fieldInUse = Object.values(layer.columns).some( column => hasField(column) && column.sourceField === field.name ); @@ -294,7 +157,7 @@ function addFieldAsBucketOperation( indexPattern: IndexPattern, field: IndexPatternField ) { - const applicableBucketOperation = getBucketOperationTypes(field); + const applicableBucketOperation = getBucketOperation(field); const newColumn = buildColumn({ op: applicableBucketOperation, columns: layer.columns, @@ -345,7 +208,7 @@ function getEmptyLayerSuggestionsForField( ) { const indexPattern = state.indexPatterns[indexPatternId]; let newLayer: IndexPatternLayer | undefined; - if (getBucketOperationTypes(field)) { + if (getBucketOperation(field)) { newLayer = createNewLayerWithBucketAggregation(layerId, indexPattern, field); } else if (indexPattern.timeFieldName && getOperationTypesForField(field).length > 0) { newLayer = createNewLayerWithMetricAggregation(layerId, indexPattern, field); @@ -377,7 +240,7 @@ function createNewLayerWithBucketAggregation( // let column know about count column const column = buildColumn({ layerId, - op: getBucketOperationTypes(field), + op: getBucketOperation(field), indexPattern, columns: { col2: countColumn, @@ -432,46 +295,20 @@ function createNewLayerWithMetricAggregation( }; } -export function getDatasourceSuggestionsFromCurrentState(state: IndexPatternPrivateState) { - const layers = Object.entries(state.layers).map(([id, layer]) => ({ - id, - indexPatternId: layer.indexPatternId, - columnMap: layer.columns, - columns: Object.values(layer.columns), - })); - const [firstLayer] = layers.filter(({ columns }) => columns.length > 0); - const suggestions: Array> = []; - - const histogramSuggestion = suggestHistogram({ - layers, - indexPatterns: state.indexPatterns, - datasourceSuggestionId: 1, - }); - - const metricSuggestion = suggestMetric({ - layers, - indexPatterns: state.indexPatterns, - datasourceSuggestionId: 2, - }); - - if (firstLayer) { - suggestions.push( - buildSuggestion({ - state, - layerId: firstLayer.id, - isMultiRow: true, - datasourceSuggestionId: 3, - }) - ); - } - - if (histogramSuggestion) { - suggestions.push(histogramSuggestion); - } - - if (metricSuggestion) { - suggestions.push(metricSuggestion); - } - - return suggestions; +export function getDatasourceSuggestionsFromCurrentState( + state: IndexPatternPrivateState +): Array> { + const layers = Object.entries(state.layers); + + return layers + .map(([layerId, layer], index) => { + if (layer.columnOrder.length === 0) { + return; + } + + return buildSuggestion({ state, layerId, isMultiRow: true, datasourceSuggestionId: index }); + }) + .reduce((prev, current) => (current ? prev.concat([current]) : prev), [] as Array< + DatasourceSuggestion + >); } From 085210c1741dd87d20d66bc09913c096a07a47a7 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 13 Aug 2019 14:44:43 -0400 Subject: [PATCH 12/46] Fix metric autoscale logic --- .../lens/public/metric_visualization_plugin/auto_scale.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index fa7a04c2b3a51..4f813093db419 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -59,6 +59,7 @@ export class AutoScale extends React.Component { display: 'flex', justifyContent: 'center', position: 'relative', + maxWidth: '100%', }} >
Date: Tue, 13 Aug 2019 15:08:17 -0400 Subject: [PATCH 13/46] Register metric as an embeddable --- x-pack/legacy/plugins/lens/public/register_embeddable.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/legacy/plugins/lens/public/register_embeddable.ts b/x-pack/legacy/plugins/lens/public/register_embeddable.ts index 0364b1a4eb65b..e488f8e3d9aa3 100644 --- a/x-pack/legacy/plugins/lens/public/register_embeddable.ts +++ b/x-pack/legacy/plugins/lens/public/register_embeddable.ts @@ -8,10 +8,12 @@ import { indexPatternDatasourceSetup } from './indexpattern_plugin'; import { xyVisualizationSetup } from './xy_visualization_plugin'; import { editorFrameSetup } from './editor_frame_plugin'; import { datatableVisualizationSetup } from './datatable_visualization_plugin'; +import { metricVisualizationSetup } from './metric_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(); +metricVisualizationSetup(); editorFrameSetup(); From 8436275546090c8e86ce3f19bf23757338d6e138 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 13 Aug 2019 15:09:02 -0400 Subject: [PATCH 14/46] Fix metric autoscale flicker --- .../auto_scale.test.tsx | 32 +++++++++---------- .../auto_scale.tsx | 23 +++++++------ 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx index c373203056ae1..f5ad9249f600b 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx @@ -26,8 +26,8 @@ describe('AutoScale', () => { }); it('is the lesser of the x or y scale', () => { - expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.25); - expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.5); + expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.246); + expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.496); }); }); @@ -40,20 +40,20 @@ describe('AutoScale', () => { ) ).toMatchInlineSnapshot(` -
-
-

- Hoi! -

-
-
-`); +
+
+

+ Hoi! +

+
+
+ `); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index 4f813093db419..38b1f42dbd29f 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -21,22 +21,25 @@ export class AutoScale extends React.Component { constructor(props: Props) { super(props); - this.state = { scale: 1 }; + // An initial scale of 0 means we always redraw + // at least once, which is sub-optimal, but it + // prevents an annoying flicker. + this.state = { scale: 0 }; } - setParent(el: Element | null) { + setParent = (el: Element | null) => { if (this.parent !== el) { this.parent = el; setTimeout(() => this.scale()); } - } + }; - setChild(el: Element | null) { + setChild = (el: Element | null) => { if (this.child !== el) { this.child = el; setTimeout(() => this.scale()); } - } + }; scale() { const scale = computeScale(this.parent, this.child); @@ -54,7 +57,7 @@ export class AutoScale extends React.Component { return (
this.setParent(el)} + ref={this.setParent} style={{ display: 'flex', justifyContent: 'center', @@ -64,7 +67,7 @@ export class AutoScale extends React.Component { >
this.setChild(el)} + ref={this.setChild} style={{ transform: `scale(${scale})`, position: 'relative', @@ -94,8 +97,10 @@ export function computeScale( return 1; } - const scaleX = parent.clientWidth / child.clientWidth; - const scaleY = parent.clientHeight / child.clientHeight; + const marginSize = 16; + const labelSize = 16; + const scaleX = (parent.clientWidth - marginSize) / child.clientWidth; + const scaleY = (parent.clientHeight - marginSize - labelSize) / child.clientHeight; return Math.min(1, Math.min(scaleX, scaleY)); } From 4cbf3c84eb142acb2bcc3b30c89b0b29d395e1cd Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 13 Aug 2019 15:09:34 -0400 Subject: [PATCH 15/46] Render mini metric in suggestions panel --- .../metric_suggestions.test.ts | 17 +++++++++++++++++ .../metric_suggestions.ts | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts index 49b3e56c2d2ca..120d0369bc7ea 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -92,6 +92,23 @@ describe('metric_suggestions', () => { expect(suggestion).toMatchInlineSnapshot(` Object { "datasourceSuggestionId": 0, + "previewExpression": Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + "bytes", + ], + "title": Array [ + "", + ], + }, + "function": "lens_metric_chart", + "type": "function", + }, + ], + "type": "expression", + }, "previewIcon": "visMetric", "score": 1, "state": Object { diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index 136a1f07f3707..85981be00c3a0 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -33,6 +33,19 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion { score: 1, datasourceSuggestionId: table.datasourceSuggestionId, previewIcon: 'visMetric', + previewExpression: { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_metric_chart', + arguments: { + title: [''], + accessor: [col.columnId], + }, + }, + ], + }, state: { layerId: table.layerId, accessor: col.columnId, From 5065a146999948febf47aa551c6fa5fe4ffd40fd Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Tue, 13 Aug 2019 15:09:50 -0400 Subject: [PATCH 16/46] Cache the metric filterOperations function --- .../metric_visualization_plugin/metric_config_panel.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx index 335dbb27556d8..b80235e69ba05 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiForm, EuiFormRow } from '@elastic/eui'; import { State } from './types'; -import { VisualizationProps } from '../types'; +import { VisualizationProps, OperationMetadata } from '../types'; import { NativeRenderer } from '../native_renderer'; +const isMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; + export function MetricConfigPanel(props: VisualizationProps) { const { state, frame } = props; const [datasource] = Object.values(frame.datasourceLayers); @@ -30,7 +32,7 @@ export function MetricConfigPanel(props: VisualizationProps) { layerId, columnId: state.accessor, dragDropContext: props.dragDropContext, - filterOperations: op => !op.isBucketed && op.dataType === 'number', + filterOperations: isMetric, }} /> From b11021b2b4d4a8934de3330cc9f4351d4842010d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 14 Aug 2019 12:53:38 +0200 Subject: [PATCH 17/46] fix auto scaling edge cases --- .../public/metric_visualization_plugin/auto_scale.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index 38b1f42dbd29f..a7267e6be1ede 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -61,8 +61,10 @@ export class AutoScale extends React.Component { style={{ display: 'flex', justifyContent: 'center', + alignItems: 'center', position: 'relative', maxWidth: '100%', + maxHeight: '100%', }} >
{ ref={this.setChild} style={{ transform: `scale(${scale})`, - position: 'relative', }} > {children} @@ -97,10 +98,8 @@ export function computeScale( return 1; } - const marginSize = 16; - const labelSize = 16; - const scaleX = (parent.clientWidth - marginSize) / child.clientWidth; - const scaleY = (parent.clientHeight - marginSize - labelSize) / child.clientHeight; + const scaleX = (parent.clientWidth) / child.clientWidth; + const scaleY = (parent.clientHeight) / child.clientHeight; return Math.min(1, Math.min(scaleX, scaleY)); } From e46f93591177e15b30a73d2ce09a51261f652d3b Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Wed, 14 Aug 2019 16:57:55 -0400 Subject: [PATCH 18/46] Modify auto-scale to handle resize events --- .../auto_scale.test.tsx | 10 +-- .../auto_scale.tsx | 82 +++++++++++-------- .../metric_expression.test.tsx | 54 ++++++------ .../metric_expression.tsx | 21 +++-- 4 files changed, 92 insertions(+), 75 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx index f5ad9249f600b..9be06ab6fd297 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx @@ -26,8 +26,8 @@ describe('AutoScale', () => { }); it('is the lesser of the x or y scale', () => { - expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.246); - expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.496); + expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.25); + expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.5); }); }); @@ -41,12 +41,10 @@ describe('AutoScale', () => { ) ).toMatchInlineSnapshot(`

Hoi! diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index a7267e6be1ede..b19e6800aa414 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -5,8 +5,10 @@ */ import React from 'react'; +import _ from 'lodash'; +import { EuiResizeObserver } from '@elastic/eui'; -interface Props { +interface Props extends React.HTMLAttributes { children: React.ReactNode | React.ReactNode[]; } @@ -17,10 +19,20 @@ interface State { export class AutoScale extends React.Component { private child: Element | null = null; private parent: Element | null = null; + private scale: () => void; constructor(props: Props) { super(props); + this.scale = _.throttle(() => { + const scale = computeScale(this.parent, this.child); + + // Prevent an infinite render loop + if (this.state.scale !== scale) { + this.setState({ scale }); + } + }); + // An initial scale of 0 means we always redraw // at least once, which is sub-optimal, but it // prevents an annoying flicker. @@ -41,42 +53,44 @@ export class AutoScale extends React.Component { } }; - scale() { - const scale = computeScale(this.parent, this.child); - - // Prevent an infinite render loop - if (this.state.scale !== scale) { - this.setState({ scale }); - } - } - render() { const { children } = this.props; const { scale } = this.state; + const style = this.props.style || {}; return ( -
-
- {children} -
-
+ + {resizeRef => ( +
{ + this.setParent(el); + resizeRef(el); + }} + style={{ + ...style, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + maxWidth: '100%', + maxHeight: '100%', + }} + > +
+ {children} +
+
+ )} +
); } } @@ -98,8 +112,8 @@ export function computeScale( return 1; } - const scaleX = (parent.clientWidth) / child.clientWidth; - const scaleY = (parent.clientHeight) / child.clientHeight; + const scaleX = parent.clientWidth / child.clientWidth; + const scaleY = parent.clientHeight / child.clientHeight; return Math.min(1, Math.min(scaleX, scaleY)); } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index 825b08d676bbe..81f8a0fbce210 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -49,34 +49,34 @@ describe('metric_expression', () => { const { data, args } = sampleArgs(); expect(shallow()).toMatchInlineSnapshot(` - - + - -
- 10110 -
- - My fanci metric chart - -
-
-
+ 10110 + + + My fanci metric chart + +

`); }); }); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index cc0f65efa9c0a..e14c6749fff55 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -82,13 +82,18 @@ export function MetricChart({ data, args }: MetricChartProps) { const value = Number(Number(row[accessor]).toFixed(3)).toString(); return ( - - - -
{value}
- {title} -
-
-
+
+ {value} + {title} +
); } From 43ffe2386abf43717a8558a4e08d0ad3f0dfcde7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 15 Aug 2019 14:46:14 +0200 Subject: [PATCH 19/46] use format hints in metric vis --- .../metric_expression.test.tsx | 3 +- .../metric_expression.tsx | 31 ++++++++++++++----- .../metric_visualization_plugin/plugin.tsx | 20 ++++++++++-- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index 81f8a0fbce210..b8c646be91e52 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -48,7 +48,8 @@ describe('metric_expression', () => { test('it renders the title and value', () => { const { data, args } = sampleArgs(); - expect(shallow()).toMatchInlineSnapshot(` + expect(shallow( x} />)) + .toMatchInlineSnapshot(`
; -export const metricChartRenderer: RenderFunction = { +export const getMetricChartRenderer = ( + formatFactory: FormatFactory +): RenderFunction => ({ name: 'lens_metric_chart_renderer', displayName: 'Metric Chart', help: 'Metric Chart Renderer', validate: () => {}, reuseDomNode: true, render: async (domNode: Element, config: MetricChartProps, _handlers: unknown) => { - ReactDOM.render(, domNode); + ReactDOM.render(, domNode); }, -}; +}); -export function MetricChart({ data, args }: MetricChartProps) { +export function MetricChart({ + data, + args, + formatFactory, +}: MetricChartProps & { formatFactory: FormatFactory }) { const { title, accessor } = args; - const [row] = Object.values(data.tables)[0].rows; - // TODO: Use field formatters here... - const value = Number(Number(row[accessor]).toFixed(3)).toString(); + let value = '-'; + const firstTable = Object.values(data.tables)[0]; + + if (firstTable) { + const column = firstTable.columns[0]; + const row = firstTable.rows[0]; + if (row[accessor]) { + value = + column && column.formatHint + ? formatFactory(column.formatHint).convert(row[accessor]) + : Number(Number(row[accessor]).toFixed(3)).toString(); + } + } return (
metricChart); - interpreter.renderersRegistry.register(() => metricChartRenderer as RenderFunction); + interpreter.renderersRegistry.register( + () => getMetricChartRenderer(fieldFormat.formatFactory) as RenderFunction + ); return metricVisualization; } @@ -65,6 +76,9 @@ export const metricVisualizationSetup = () => renderersRegistry, functionsRegistry, }, + fieldFormat: { + formatFactory: getFormat, + }, }); export const metricVisualizationStop = () => plugin.stop(); From 981c577144d9080ce1ba71404c8dee9019912f48 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 15 Aug 2019 18:53:31 +0200 Subject: [PATCH 20/46] start cleaning up suggestions --- .../operation_definitions/metrics.tsx | 66 +++++++++++-------- .../public/indexpattern_plugin/operations.ts | 12 +++- .../xy_visualization_plugin/xy_suggestions.ts | 61 +++++++++++++---- 3 files changed, 95 insertions(+), 44 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx index 41313b7cbc073..f57cf9f0d2180 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx @@ -14,12 +14,19 @@ import { } from '../indexpattern'; import { OperationDefinition } from '../operations'; -function buildMetricOperation( - type: T['operationType'], - displayName: string, - ofName: (name: string) => string -) { +function buildMetricOperation({ + type, + displayName, + ofName, + priority, +}: { + type: T['operationType']; + displayName: string; + ofName: (name: string) => string; + priority?: number; +}) { const operationDefinition: OperationDefinition = { + priority, type, displayName, getPossibleOperationsForDocument: () => [], @@ -74,50 +81,51 @@ function buildMetricOperation( return operationDefinition; } -export const minOperation = buildMetricOperation( - 'min', - i18n.translate('xpack.lens.indexPattern.min', { +export const minOperation = buildMetricOperation({ + type: 'min', + displayName: i18n.translate('xpack.lens.indexPattern.min', { defaultMessage: 'Minimum', }), - name => + ofName: name => i18n.translate('xpack.lens.indexPattern.minOf', { defaultMessage: 'Minimum of {name}', values: { name }, - }) -); + }), +}); -export const maxOperation = buildMetricOperation( - 'max', - i18n.translate('xpack.lens.indexPattern.max', { +export const maxOperation = buildMetricOperation({ + type: 'max', + displayName: i18n.translate('xpack.lens.indexPattern.max', { defaultMessage: 'Maximum', }), - name => + ofName: name => i18n.translate('xpack.lens.indexPattern.maxOf', { defaultMessage: 'Maximum of {name}', values: { name }, - }) -); + }), +}); -export const averageOperation = buildMetricOperation( - 'avg', - i18n.translate('xpack.lens.indexPattern.avg', { +export const averageOperation = buildMetricOperation({ + type: 'avg', + displayName: i18n.translate('xpack.lens.indexPattern.avg', { defaultMessage: 'Average', }), - name => + ofName: name => i18n.translate('xpack.lens.indexPattern.avgOf', { defaultMessage: 'Average of {name}', values: { name }, - }) -); + }), +}); -export const sumOperation = buildMetricOperation( - 'sum', - i18n.translate('xpack.lens.indexPattern.sum', { +export const sumOperation = buildMetricOperation({ + type: 'sum', + priority: 1, + displayName: i18n.translate('xpack.lens.indexPattern.sum', { defaultMessage: 'Sum', }), - name => + ofName: name => i18n.translate('xpack.lens.indexPattern.sumOf', { defaultMessage: 'Sum of {name}', values: { name }, - }) -); + }), +}); 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 25821f505fe08..372d559caa83b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -70,6 +70,7 @@ export interface ParamEditorProps { export interface OperationDefinition { type: C['operationType']; displayName: string; + priority?: number; getPossibleOperationsForDocument: (indexPattern: IndexPattern) => OperationMetadata[]; getPossibleOperationsForField: (field: IndexPatternField) => OperationMetadata[]; buildColumn: (arg: { @@ -161,6 +162,13 @@ export function getAvailableOperationsByMetadata(indexPattern: IndexPattern) { return Object.values(operationByMetadata); } +function getDefinition(findFunction: (definition: PossibleOperationDefinition) => boolean) { + const candidates = operationDefinitions.filter(findFunction); + return candidates.reduce((a, b) => + (a.priority || Number.NEGATIVE_INFINITY) > (b.priority || Number.NEGATIVE_INFINITY) ? a : b + ); +} + export function buildColumn({ op, columns, @@ -183,11 +191,11 @@ export function buildColumn({ if (op) { operationDefinition = operationDefinitionMap[op]; } else if (asDocumentOperation) { - operationDefinition = operationDefinitions.find( + operationDefinition = getDefinition( definition => definition.getPossibleOperationsForDocument(indexPattern).length !== 0 )!; } else if (field) { - operationDefinition = operationDefinitions.find( + operationDefinition = getDefinition( definition => definition.getPossibleOperationsForField(field).length !== 0 )!; } 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 82ee5b6ae1b7d..7bfcf16041d1d 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 @@ -5,15 +5,16 @@ */ import { i18n } from '@kbn/i18n'; -import { partition } from 'lodash'; +import { partition, isEqual } from 'lodash'; import { Position } from '@elastic/charts'; +import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { SuggestionRequest, VisualizationSuggestion, TableSuggestionColumn, TableSuggestion, } from '../types'; -import { State, SeriesType } from './types'; +import { State, SeriesType, LayerConfig } from './types'; import { generateId } from '../id_generator'; import { buildExpression } from './to_expression'; @@ -24,6 +25,21 @@ const columnSortOrder = { number: 3, }; +function getIconForSeries(type: SeriesType): EuiIconType { + switch (type) { + case 'area': + case 'area_stacked': + return 'visArea'; + case 'bar': + case 'bar_stacked': + return 'visBarVertical'; + case 'line': + return 'visLine'; + default: + throw new Error('unknown series type'); + } +} + /** * Generate suggestions for the xy chart. * @@ -123,30 +139,30 @@ function getSuggestion( }); const seriesType: SeriesType = (currentState && currentState.preferredSeriesType) || (splitBy && isDate ? 'line' : 'bar'); + const newLayer = { + layerId, + seriesType, + xAccessor: xValue.columnId, + splitAccessor: splitBy ? splitBy.columnId : generateId(), + accessors: yValues.map(col => col.columnId), + title: yTitle, + }; const state: State = { isHorizontal: false, legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, preferredSeriesType: seriesType, layers: [ ...(currentState ? currentState.layers.filter(layer => layer.layerId !== layerId) : []), - { - layerId, - seriesType, - xAccessor: xValue.columnId, - splitAccessor: splitBy ? splitBy.columnId : generateId(), - accessors: yValues.map(col => col.columnId), - title: yTitle, - }, + newLayer, ], }; - - return { + const suggestion = { title, // chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score score: ((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3, datasourceSuggestionId, state, - previewIcon: isDate ? 'visLine' : 'visBar', + previewIcon: getIconForSeries(seriesType), previewExpression: buildExpression( { ...state, @@ -159,4 +175,23 @@ function getSuggestion( { xTitle, yTitle } ), }; + + // if current state is using the same data, suggest same chart with different series type + const oldLayer = currentState && currentState.layers.find(layer => layer.layerId === layerId); + if (oldLayer && isSameData(newLayer, oldLayer, Boolean(splitBy))) { + const currentSeriesType = oldLayer.seriesType; + const suggestedSeriesType = currentSeriesType === 'area' ? 'bar' : 'area'; + suggestion.title = `${suggestedSeriesType} chart`; + newLayer.seriesType = suggestedSeriesType; + } + + return suggestion; +} + +function isSameData(newLayer: LayerConfig, oldLayer: LayerConfig, splitBy: boolean) { + return ( + newLayer.xAccessor === oldLayer.xAccessor && + isEqual(newLayer.accessors, oldLayer.accessors) && + (!splitBy || newLayer.splitAccessor === oldLayer.splitAccessor) + ); } From 7e1c55066768a28151fcdf74089334ec8aa34f7a Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 16 Aug 2019 14:25:41 -0400 Subject: [PATCH 21/46] Tweak metric to only scale down so far, and scale both the number and the label. --- .../public/metric_visualization_plugin/auto_scale.tsx | 10 +++++----- .../metric_visualization_plugin/metric_expression.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index b19e6800aa414..365e88d440f2a 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -74,16 +74,13 @@ export class AutoScale extends React.Component { alignItems: 'center', maxWidth: '100%', maxHeight: '100%', + overflow: 'hidden', }} >
{children} @@ -108,6 +105,9 @@ export function computeScale( parent: ClientDimensionable | null, child: ClientDimensionable | null ) { + const MAX_SCALE = 1; + const MIN_SCALE = 0.3; + if (!parent || !child) { return 1; } @@ -115,5 +115,5 @@ export function computeScale( const scaleX = parent.clientWidth / child.clientWidth; const scaleY = parent.clientHeight / child.clientHeight; - return Math.min(1, Math.min(scaleX, scaleY)); + return Math.max(Math.min(MAX_SCALE, Math.min(scaleX, scaleY)), MIN_SCALE); } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index e14c6749fff55..6a677042172ef 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -90,10 +90,13 @@ export function MetricChart({ data, args }: MetricChartProps) { alignItems: 'center', maxWidth: '100%', maxHeight: '100%', + textAlign: 'center', }} > - {value} - {title} + +
{value}
+
{title}
+
); } From 859aa995669ca67da9a5b4634fa99a374fd357a0 Mon Sep 17 00:00:00 2001 From: Christopher Davies Date: Fri, 16 Aug 2019 14:30:21 -0400 Subject: [PATCH 22/46] Fix lens metric tests --- .../auto_scale.test.tsx | 10 ++++-- .../metric_expression.test.tsx | 33 +++++++++++-------- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx index 9be06ab6fd297..60008d1237d82 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx @@ -25,8 +25,12 @@ describe('AutoScale', () => { expect(computeScale(mockElement(2000, 2000), mockElement(1000, 1000))).toBe(1); }); + it('is never under 0.3', () => { + expect(computeScale(mockElement(2000, 1000), mockElement(1000, 10000))).toBe(0.3); + }); + it('is the lesser of the x or y scale', () => { - expect(computeScale(mockElement(1000, 2000), mockElement(3000, 8000))).toBe(0.25); + expect(computeScale(mockElement(2000, 2000), mockElement(3000, 5000))).toBe(0.4); expect(computeScale(mockElement(2000, 3000), mockElement(4000, 3200))).toBe(0.5); }); }); @@ -41,10 +45,10 @@ describe('AutoScale', () => { ) ).toMatchInlineSnapshot(`

Hoi! diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx index 81f8a0fbce210..f8ce2a61e9bdf 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -58,24 +58,31 @@ describe('metric_expression', () => { "justifyContent": "center", "maxHeight": "100%", "maxWidth": "100%", + "textAlign": "center", } } > - +
- 10110 + > + 10110 +
+
+ My fanci metric chart +
- - My fanci metric chart -

`); }); From c85345e5c663b190a36b156bf378b2b3540b8006 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 19 Aug 2019 05:37:45 +0200 Subject: [PATCH 23/46] start adding more suggestions --- .../indexpattern_suggestions.tsx | 52 ++++++++++++++++++- .../xy_visualization_plugin/xy_suggestions.ts | 1 + 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx index 1b5007cbe5558..90494135aa630 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.tsx @@ -308,6 +308,9 @@ export function getDatasourceSuggestionsFromCurrentState( const onlyMetric = layer.columnOrder.every(columnId => !layer.columns[columnId].isBucketed); const onlyBucket = layer.columnOrder.every(columnId => layer.columns[columnId].isBucketed); + const timeDimension = layer.columnOrder.find( + columnId => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType + ); if (onlyMetric || onlyBucket) { // intermediary chart, don't try to suggest reduced versions return buildSuggestion({ @@ -318,7 +321,39 @@ export function getDatasourceSuggestionsFromCurrentState( }); } - return createSimplifiedTableSuggestions(state, layerId); + const simplifiedSuggestions = createSimplifiedTableSuggestions(state, layerId); + + const indexPattern = state.indexPatterns[layer.indexPatternId]; + if (!timeDimension && indexPattern.timeFieldName) { + const newId = generateId(); + const availableBucketedColumns = layer.columnOrder.filter( + columnId => layer.columns[columnId].isBucketed + ); + const availableMetricColumns = layer.columnOrder.filter( + columnId => !layer.columns[columnId].isBucketed + ); + const timeColumn = buildColumn({ + layerId, + op: 'date_histogram', + indexPattern, + columns: layer.columns, + field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName), + suggestedPriority: undefined, + }); + const updatedLayer = buildLayerByColumnOrder( + { ...layer, columns: { ...layer.columns, [newId]: timeColumn } }, + [...availableBucketedColumns, newId, ...availableMetricColumns] + ); + const timedSuggestion = buildSuggestion({ + state, + layerId, + isMultiRow: true, + updatedLayer, + }); + return [...simplifiedSuggestions, timedSuggestion]; + } + + return simplifiedSuggestions; }) ).map( (suggestion, index): DatasourceSuggestion => ({ @@ -355,7 +390,20 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer return allMetricsSuggestion; } }) - ).map(updatedLayer => buildSuggestion({ state, layerId, isMultiRow: true, updatedLayer })); + ) + .concat( + availableMetricColumns.map(columnId => { + return buildLayerByColumnOrder(layer, [columnId]); + }) + ) + .map(updatedLayer => + buildSuggestion({ + state, + layerId, + isMultiRow: updatedLayer.columnOrder.length > 1, + updatedLayer, + }) + ); } function buildLayerByColumnOrder(layer: IndexPatternLayer, columnOrder: string[]) { 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 7bfcf16041d1d..9c1460e696043 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 @@ -181,6 +181,7 @@ function getSuggestion( if (oldLayer && isSameData(newLayer, oldLayer, Boolean(splitBy))) { const currentSeriesType = oldLayer.seriesType; const suggestedSeriesType = currentSeriesType === 'area' ? 'bar' : 'area'; + // TODO i18n suggestion.title = `${suggestedSeriesType} chart`; newLayer.seriesType = suggestedSeriesType; } From 837c76ce7aeb2898227de8b2ca8af71ac1bace82 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 19 Aug 2019 05:47:29 +0200 Subject: [PATCH 24/46] remove unused imports --- x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx | 2 +- .../public/metric_visualization_plugin/metric_expression.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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 d1fd22809fc87..07bd55cbd4e93 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/plugin.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { I18nProvider, FormattedMessage } from '@kbn/i18n/react'; import { HashRouter, Switch, Route, RouteComponentProps } from 'react-router-dom'; -import chrome, { Chrome } from 'ui/chrome'; +import chrome from 'ui/chrome'; import { Storage } from 'ui/storage'; import { editorFrameSetup, editorFrameStop } from '../editor_frame_plugin'; import { indexPatternDatasourceSetup, indexPatternDatasourceStop } from '../indexpattern_plugin'; diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx index 238bcd86d87fc..daff873feb18c 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -7,7 +7,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; -import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; import { MetricConfig } from './types'; import { LensMultiTable } from '../types'; From e5ebcf85c83bc1d61ec0dabec8e3f6c790fd3b13 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 19 Aug 2019 12:03:15 +0200 Subject: [PATCH 25/46] work on suggestions --- .../indexpattern_suggestions.ts | 19 ++++++++++++++++--- .../metric_suggestions.ts | 1 + 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 90494135aa630..3bb0fd1ca1a5f 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -48,7 +48,7 @@ function buildSuggestion({ columnId, operation: columnToOperation(columns[columnId]), })), - isMultiRow: isMultiRow || true, + isMultiRow: typeof isMultiRow === 'undefined' || isMultiRow, datasourceSuggestionId: datasourceSuggestionId || 0, layerId, }, @@ -306,12 +306,13 @@ export function getDatasourceSuggestionsFromCurrentState( return []; } + const indexPattern = state.indexPatterns[layer.indexPatternId]; const onlyMetric = layer.columnOrder.every(columnId => !layer.columns[columnId].isBucketed); const onlyBucket = layer.columnOrder.every(columnId => layer.columns[columnId].isBucketed); const timeDimension = layer.columnOrder.find( columnId => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType ); - if (onlyMetric || onlyBucket) { + if (onlyBucket) { // intermediary chart, don't try to suggest reduced versions return buildSuggestion({ state, @@ -321,9 +322,21 @@ export function getDatasourceSuggestionsFromCurrentState( }); } + if (onlyMetric) { + if (!timeDimension && indexPattern.timeFieldName) { + // TODO suggestion metric over time + } + // suggest only metric + return buildSuggestion({ + state, + layerId, + isMultiRow: false, + datasourceSuggestionId: index, + }); + } + const simplifiedSuggestions = createSimplifiedTableSuggestions(state, layerId); - const indexPattern = state.indexPatterns[layer.indexPatternId]; if (!timeDimension && indexPattern.timeFieldName) { const newId = generateId(); const availableBucketedColumns = layer.columnOrder.filter( diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index 85981be00c3a0..94d9db841bfbe 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -21,6 +21,7 @@ export function getSuggestions( // We only render metric charts for single-row queries. We require a single, numeric column. !isMultiRow && columns.length === 1 && columns[0].operation.dataType === 'number' ) + .filter(({ columns }) => !opts.state || opts.state.accessor !== columns[0].columnId) .map(table => getSuggestion(table)); } From 400c35267f99384d05282d7db1632d0be9ca42f5 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Aug 2019 12:02:36 +0200 Subject: [PATCH 26/46] work more on suggestions --- .../visualization.tsx | 73 +++++++++++-------- .../editor_frame/suggestion_panel.tsx | 14 +++- .../indexpattern_suggestions.ts | 66 ++++++++++++++--- .../auto_scale.tsx | 6 +- .../metric_suggestions.ts | 19 +++-- x-pack/legacy/plugins/lens/public/types.ts | 4 + .../xy_visualization_plugin/xy_suggestions.ts | 62 +++++++++------- 7 files changed, 164 insertions(+), 80 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 d89a9c0276972..5eda1aa059755 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 @@ -127,42 +127,53 @@ export const datatableVisualization: Visualization< getSuggestions({ tables, + state, }: SuggestionRequest): Array< VisualizationSuggestion > { const maxColumnCount = Math.max.apply(undefined, tables.map(table => table.columns.length)); - return tables.map(table => { - const title = i18n.translate('xpack.lens.datatable.visualizationOf', { - defaultMessage: 'Table: {operations}', - values: { - operations: table.columns - .map(col => col.operation.label) - .join( - i18n.translate('xpack.lens.datatable.conjunctionSign', { - defaultMessage: ' & ', - description: - 'A character that can be used for conjunction of multiple enumarated items. Make sure to include spaces around it if needed.', - }) - ), - }, - }); - - return { - title, - // largest possible table will have a score of 0.2, less columns reduce score - score: (table.columns.length / maxColumnCount) * 0.2, - datasourceSuggestionId: table.datasourceSuggestionId, - state: { - layers: [ - { - layerId: table.layerId, - columns: table.columns.map(col => col.columnId), + return ( + tables + // don't suggest current table if visualization is active + .filter(({ changeType }) => !state || changeType !== 'unchanged') + .map(table => { + const title = + table.changeType === 'unchanged' + ? i18n.translate('xpack.lens.datatable.suggestionLabel', { defaultMessage: 'Table' }) + : i18n.translate('xpack.lens.datatable.visualizationOf', { + defaultMessage: 'Table: {operations}', + values: { + operations: + table.label || + table.columns + .map(col => col.operation.label) + .join( + i18n.translate('xpack.lens.datatable.conjunctionSign', { + defaultMessage: ' & ', + description: + 'A character that can be used for conjunction of multiple enumarated items. Make sure to include spaces around it if needed.', + }) + ), + }, + }); + + return { + title, + // largest possible table will have a score of 0.2, less columns reduce score + score: (table.columns.length / maxColumnCount) * 0.2, + datasourceSuggestionId: table.datasourceSuggestionId, + state: { + layers: [ + { + layerId: table.layerId, + columns: table.columns.map(col => col.columnId), + }, + ], }, - ], - }, - previewIcon: 'visTable', - }; - }); + previewIcon: 'visTable', + }; + }) + ); }, renderConfigPanel: (domElement, props) => 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 e2403245a22b8..15052764d93e8 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 @@ -16,7 +16,7 @@ import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins import { prependDatasourceExpression } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; -const MAX_SUGGESTIONS_DISPLAYED = 3; +const MAX_SUGGESTIONS_DISPLAYED = 5; export interface SuggestionPanelProps { activeDatasourceId: string | null; @@ -139,12 +139,20 @@ function InnerSuggestionPanel({
- {suggestions.map(suggestion => { + {suggestions.map((suggestion: Suggestion) => { const previewExpression = suggestion.previewExpression ? prependDatasourceExpression( suggestion.previewExpression, datasourceMap, - datasourceStates + suggestion.datasourceId + ? { + ...datasourceStates, + [suggestion.datasourceId]: { + isLoading: false, + state: suggestion.datasourceState, + }, + } + : datasourceStates ) : null; return ( diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 3bb0fd1ca1a5f..803bbf99edb3e 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -6,7 +6,7 @@ import _ from 'lodash'; import { generateId } from '../id_generator'; -import { DatasourceSuggestion } from '../types'; +import { DatasourceSuggestion, TableChangeType, TableSuggestion } from '../types'; import { columnToOperation, IndexPatternField, @@ -23,12 +23,16 @@ function buildSuggestion({ layerId, isMultiRow, datasourceSuggestionId, + label, + changeType, }: { state: IndexPatternPrivateState; layerId: string; + changeType: TableChangeType; updatedLayer?: IndexPatternLayer; isMultiRow?: boolean; datasourceSuggestionId?: number; + label?: string; }) { const columnOrder = (updatedLayer || state.layers[layerId]).columnOrder; const columns = (updatedLayer || state.layers[layerId]).columns; @@ -51,6 +55,8 @@ function buildSuggestion({ isMultiRow: typeof isMultiRow === 'undefined' || isMultiRow, datasourceSuggestionId: datasourceSuggestionId || 0, layerId, + changeType, + label, }, }; } @@ -106,6 +112,7 @@ function getExistingLayerSuggestionsForField( state, updatedLayer, layerId, + changeType: 'extended', }), ] : []; @@ -219,6 +226,7 @@ function getEmptyLayerSuggestionsForField( state, updatedLayer: newLayer, layerId, + changeType: 'initial', }), ] : []; @@ -237,13 +245,16 @@ function createNewLayerWithBucketAggregation( suggestedPriority: undefined, }); + const col1 = generateId(); + const col2 = generateId(); + // let column know about count column const column = buildColumn({ layerId, op: getBucketOperation(field), indexPattern, columns: { - col2: countColumn, + [col2]: countColumn, }, field, suggestedPriority: undefined, @@ -252,10 +263,10 @@ function createNewLayerWithBucketAggregation( return { indexPatternId: indexPattern.id, columns: { - col1: column, - col2: countColumn, + [col1]: column, + [col2]: countColumn, }, - columnOrder: ['col1', 'col2'], + columnOrder: [col1, col2], }; } @@ -285,13 +296,16 @@ function createNewLayerWithMetricAggregation( layerId, }); + const col1 = generateId(); + const col2 = generateId(); + return { indexPatternId: indexPattern.id, columns: { - col1: dateColumn, - col2: column, + [col1]: dateColumn, + [col2]: column, }, - columnOrder: ['col1', 'col2'], + columnOrder: [col1, col2], }; } @@ -310,7 +324,8 @@ export function getDatasourceSuggestionsFromCurrentState( const onlyMetric = layer.columnOrder.every(columnId => !layer.columns[columnId].isBucketed); const onlyBucket = layer.columnOrder.every(columnId => layer.columns[columnId].isBucketed); const timeDimension = layer.columnOrder.find( - columnId => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType + columnId => + layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); if (onlyBucket) { // intermediary chart, don't try to suggest reduced versions @@ -319,12 +334,35 @@ export function getDatasourceSuggestionsFromCurrentState( layerId, isMultiRow: false, datasourceSuggestionId: index, + changeType: 'unchanged', }); } if (onlyMetric) { - if (!timeDimension && indexPattern.timeFieldName) { - // TODO suggestion metric over time + if (indexPattern.timeFieldName) { + const newId = generateId(); + const timeColumn = buildColumn({ + layerId, + op: 'date_histogram', + indexPattern, + columns: layer.columns, + field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName), + suggestedPriority: undefined, + }); + const updatedLayer = buildLayerByColumnOrder( + { ...layer, columns: { ...layer.columns, [newId]: timeColumn } }, + [newId, ...layer.columnOrder] + ); + return buildSuggestion({ + state, + layerId, + isMultiRow: true, + updatedLayer, + datasourceSuggestionId: index, + // TODO i18n + label: 'over time', + changeType: 'extended', + }); } // suggest only metric return buildSuggestion({ @@ -332,6 +370,7 @@ export function getDatasourceSuggestionsFromCurrentState( layerId, isMultiRow: false, datasourceSuggestionId: index, + changeType: 'unchanged', }); } @@ -362,6 +401,9 @@ export function getDatasourceSuggestionsFromCurrentState( layerId, isMultiRow: true, updatedLayer, + // TODO i18n + label: 'over time', + changeType: 'extended', }); return [...simplifiedSuggestions, timedSuggestion]; } @@ -415,6 +457,8 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer layerId, isMultiRow: updatedLayer.columnOrder.length > 1, updatedLayer, + changeType: + layer.columnOrder.length === updatedLayer.columnOrder.length ? 'unchanged' : 'reduced', }) ); } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx index 365e88d440f2a..9f4a7c4674243 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -64,8 +64,10 @@ export class AutoScale extends React.Component {
{ - this.setParent(el); - resizeRef(el); + if (el !== null) { + this.setParent(el); + resizeRef(el); + } }} style={{ ...style, diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index 94d9db841bfbe..460e2f55cafdd 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -15,14 +15,17 @@ import { State } from './types'; export function getSuggestions( opts: SuggestionRequest ): Array> { - return opts.tables - .filter( - ({ isMultiRow, columns }) => - // We only render metric charts for single-row queries. We require a single, numeric column. - !isMultiRow && columns.length === 1 && columns[0].operation.dataType === 'number' - ) - .filter(({ columns }) => !opts.state || opts.state.accessor !== columns[0].columnId) - .map(table => getSuggestion(table)); + return ( + opts.tables + .filter( + ({ isMultiRow, columns }) => + // We only render metric charts for single-row queries. We require a single, numeric column. + !isMultiRow && columns.length === 1 && columns[0].operation.dataType === 'number' + ) + // don't suggest current table if visualization is active + .filter(({ changeType }) => !opts.state || changeType !== 'unchanged') + .map(table => getSuggestion(table)) + ); } function getSuggestion(table: TableSuggestion): VisualizationSuggestion { diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 3f448f00a8d75..06ac3e646cdd1 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -52,8 +52,12 @@ export interface TableSuggestion { isMultiRow: boolean; columns: TableSuggestionColumn[]; layerId: string; + label?: string; + changeType: TableChangeType; } +export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended'; + export interface DatasourceSuggestion { state: T; table: TableSuggestion; 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 c7fcbce1fbc0c..d4a78c489e5d9 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 @@ -14,6 +14,7 @@ import { TableSuggestionColumn, TableSuggestion, OperationMetadata, + TableChangeType, } from '../types'; import { State, SeriesType, LayerConfig } from './types'; import { generateId } from '../id_generator'; @@ -78,20 +79,24 @@ function getSuggestionForColumns( return getSuggestion( table.datasourceSuggestionId, table.layerId, + table.changeType, x, values, splitBy, - currentState + currentState, + table.label ); } else if (buckets.length === 0) { const [x, ...yValues] = values; return getSuggestion( table.datasourceSuggestionId, table.layerId, + table.changeType, x, yValues, undefined, - currentState + currentState, + table.label ); } } @@ -108,10 +113,12 @@ function prioritizeColumns(columns: TableSuggestionColumn[]) { function getSuggestion( datasourceSuggestionId: number, layerId: string, + changeType: TableChangeType, xValue: TableSuggestionColumn, yValues: TableSuggestionColumn[], splitBy?: TableSuggestionColumn, - currentState?: State + currentState?: State, + tableLabel?: string ): VisualizationSuggestion { const yTitle = yValues .map(col => col.operation.label) @@ -125,7 +132,7 @@ function getSuggestion( const xTitle = xValue.operation.label; const isDate = xValue.operation.dataType === 'date'; - const title = isDate + let title = isDate ? i18n.translate('xpack.lens.xySuggestions.dateSuggestion', { defaultMessage: '{yTitle} over {xTitle}', description: @@ -138,9 +145,18 @@ function getSuggestion( 'Chart description for a value of some groups, like "Top URLs of top 5 countries"', values: { xTitle, yTitle }, }); + + if (currentState && changeType === 'extended' && tableLabel) { + title = tableLabel; + } + const oldLayer = currentState && currentState.layers.find(layer => layer.layerId === layerId); const seriesType: SeriesType = - (currentState && currentState.preferredSeriesType) || (splitBy && isDate ? 'line' : 'bar'); - const newLayer = { + (oldLayer && oldLayer.seriesType) || + (currentState && currentState.preferredSeriesType) || + (splitBy && isDate ? 'line' : 'bar'); + let isHorizontal = currentState ? currentState.isHorizontal : false; + let newLayer = { + ...(oldLayer || {}), layerId, seriesType, xAccessor: xValue.columnId, @@ -148,8 +164,22 @@ function getSuggestion( accessors: yValues.map(col => col.columnId), title: yTitle, }; + // if current state is using the same data, suggest same chart with different series type + if (oldLayer && changeType === 'unchanged') { + if (xValue.operation.scale && xValue.operation.scale !== 'ordinal') { + const currentSeriesType = oldLayer.seriesType; + const newSeriesType = currentSeriesType === 'area' ? 'bar' : 'area'; + // TODO i18n + title = `${newSeriesType} chart`; + newLayer = { ...newLayer, seriesType: newSeriesType }; + } else { + // todo i18n + title = 'Flip'; + isHorizontal = !isHorizontal; + } + } const state: State = { - isHorizontal: false, + isHorizontal, legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, preferredSeriesType: seriesType, layers: [ @@ -188,23 +218,5 @@ function getSuggestion( ), }; - // if current state is using the same data, suggest same chart with different series type - const oldLayer = currentState && currentState.layers.find(layer => layer.layerId === layerId); - if (oldLayer && isSameData(newLayer, oldLayer, Boolean(splitBy))) { - const currentSeriesType = oldLayer.seriesType; - const suggestedSeriesType = currentSeriesType === 'area' ? 'bar' : 'area'; - // TODO i18n - suggestion.title = `${suggestedSeriesType} chart`; - newLayer.seriesType = suggestedSeriesType; - } - return suggestion; } - -function isSameData(newLayer: LayerConfig, oldLayer: LayerConfig, splitBy: boolean) { - return ( - newLayer.xAccessor === oldLayer.xAccessor && - isEqual(newLayer.accessors, oldLayer.accessors) && - (!splitBy || newLayer.splitAccessor === oldLayer.splitAccessor) - ); -} From 8d9cab5dda49e3863d5c96da19e24322f41500ea Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Aug 2019 13:05:42 +0200 Subject: [PATCH 27/46] work more on suggestions --- .../visualization.tsx | 6 +- .../editor_frame/suggestion_panel.tsx | 55 +++++++++----- .../indexpattern_suggestions.ts | 72 +++++++++++++++++-- .../operation_definitions/metrics.tsx | 3 +- .../public/indexpattern_plugin/operations.ts | 3 + .../metric_suggestions.ts | 4 +- .../xy_visualization_plugin/xy_suggestions.ts | 31 ++++---- 7 files changed, 130 insertions(+), 44 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 5eda1aa059755..0102bbe013cf2 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 @@ -139,9 +139,11 @@ export const datatableVisualization: Visualization< .map(table => { const title = table.changeType === 'unchanged' - ? i18n.translate('xpack.lens.datatable.suggestionLabel', { defaultMessage: 'Table' }) + ? i18n.translate('xpack.lens.datatable.suggestionLabel', { + defaultMessage: 'As table', + }) : i18n.translate('xpack.lens.datatable.visualizationOf', { - defaultMessage: 'Table: {operations}', + defaultMessage: 'Table {operations}', values: { operations: table.label || 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 15052764d93e8..71de25358e87e 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 @@ -13,7 +13,7 @@ import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; import { getSuggestions, Suggestion, switchToSuggestion } from './suggestion_helpers'; import { ExpressionRenderer } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { prependDatasourceExpression } from './expression_helpers'; +import { prependDatasourceExpression, prependKibanaContext } from './expression_helpers'; import { debouncedComponent } from '../../debounced_component'; const MAX_SUGGESTIONS_DISPLAYED = 5; @@ -140,21 +140,12 @@ function InnerSuggestionPanel({
{suggestions.map((suggestion: Suggestion) => { - const previewExpression = suggestion.previewExpression - ? prependDatasourceExpression( - suggestion.previewExpression, - datasourceMap, - suggestion.datasourceId - ? { - ...datasourceStates, - [suggestion.datasourceId]: { - isLoading: false, - state: suggestion.datasourceState, - }, - } - : datasourceStates - ) - : null; + const previewExpression = preparePreviewExpression( + suggestion, + datasourceMap, + datasourceStates, + frame + ); return ( ); } +function preparePreviewExpression( + suggestion: Suggestion, + datasourceMap: Record>, + datasourceStates: Record, + framePublicAPI: FramePublicAPI +) { + if (!suggestion.previewExpression) return null; + + const expressionWithDatasource = prependDatasourceExpression( + suggestion.previewExpression, + datasourceMap, + suggestion.datasourceId + ? { + ...datasourceStates, + [suggestion.datasourceId]: { + isLoading: false, + state: suggestion.datasourceState, + }, + } + : datasourceStates + ); + + const expressionContext = { + query: framePublicAPI.query, + timeRange: { + from: framePublicAPI.dateRange.fromDate, + to: framePublicAPI.dateRange.toDate, + }, + }; + + return prependKibanaContext(expressionWithDatasource, expressionContext); +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 803bbf99edb3e..f6569e14303f5 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -13,8 +13,10 @@ import { IndexPatternLayer, IndexPatternPrivateState, IndexPattern, + OperationType, + IndexPatternColumn, } from './indexpattern'; -import { buildColumn, getOperationTypesForField } from './operations'; +import { buildColumn, getOperationTypesForField, operationDefinitionMap } from './operations'; import { hasField } from './utils'; function buildSuggestion({ @@ -420,6 +422,7 @@ export function getDatasourceSuggestionsFromCurrentState( function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) { const layer = state.layers[layerId]; + const indexPattern = state.indexPatterns[layer.indexPatternId]; const availableBucketedColumns = layer.columnOrder.filter( columnId => layer.columns[columnId].isBucketed @@ -448,22 +451,77 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer ) .concat( availableMetricColumns.map(columnId => { - return buildLayerByColumnOrder(layer, [columnId]); + let column = layer.columns[columnId]; + // if field based, suggest different metric + if (hasField(column)) { + const field = indexPattern.fields.find( + ({ name }) => hasField(column) && column.sourceField === name + )!; + const alternativeMetricOperations = getOperationTypesForField(field).filter( + operationType => operationType !== column.operationType + ); + if (alternativeMetricOperations.length > 0) { + column = buildColumn({ + op: alternativeMetricOperations[0], + columns: layer.columns, + indexPattern, + layerId, + field, + suggestedPriority: undefined, + }); + } + return buildLayerByColumnOrder( + { + ...layer, + columns: { + [columnId]: column, + }, + }, + [columnId] + ); + } else { + return buildLayerByColumnOrder(layer, [columnId]); + } }) ) - .map(updatedLayer => - buildSuggestion({ + .map(updatedLayer => { + return buildSuggestion({ state, layerId, isMultiRow: updatedLayer.columnOrder.length > 1, updatedLayer, changeType: layer.columnOrder.length === updatedLayer.columnOrder.length ? 'unchanged' : 'reduced', - }) - ); + label: + updatedLayer.columnOrder.length === 1 + ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) + : getBucketSuggestionTitle(updatedLayer), + }); + }); } -function buildLayerByColumnOrder(layer: IndexPatternLayer, columnOrder: string[]) { +function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean) { + const { operationType, label } = Object.values(layer.columns)[0]; + // TODO i18n + if (onlyMetric) { + return `${operationDefinitionMap[operationType].displayName} overall`; + } else { + return `${label} overall`; + } +} + +function getBucketSuggestionTitle(layer: IndexPatternLayer) { + const { operationType } = layer.columns[ + layer.columnOrder.find(columnId => layer.columns[columnId].isBucketed)! + ]; + // TODO i18n + return `By ${operationDefinitionMap[operationType].displayName}`; +} + +function buildLayerByColumnOrder( + layer: IndexPatternLayer, + columnOrder: string[] +): IndexPatternLayer { return { ...layer, columns: _.pick(layer.columns, columnOrder), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx index 9b3df5ed53730..4f28cafaaed29 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx @@ -109,6 +109,7 @@ export const maxOperation = buildMetricOperation({ export const averageOperation = buildMetricOperation({ type: 'avg', + priority: 1, displayName: i18n.translate('xpack.lens.indexPattern.avg', { defaultMessage: 'Average', }), @@ -121,7 +122,7 @@ export const averageOperation = buildMetricOperation({ export const sumOperation = buildMetricOperation({ type: 'sum', - priority: 1, + priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.sum', { defaultMessage: 'Sum', }), 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 5e6dbe01f84e3..66e2680bb7b96 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations.ts @@ -117,6 +117,9 @@ export function getOperationTypesForField(field: IndexPatternField) { .filter( operationDefinition => operationDefinition.getPossibleOperationsForField(field).length > 0 ) + .sort( + (a, b) => (b.priority || Number.NEGATIVE_INFINITY) - (a.priority || Number.NEGATIVE_INFINITY) + ) .map(({ type }) => type); } diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts index 460e2f55cafdd..9fc57fa3937d0 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -30,11 +30,11 @@ export function getSuggestions( function getSuggestion(table: TableSuggestion): VisualizationSuggestion { const col = table.columns[0]; - const title = col.operation.label; + const title = table.label || col.operation.label; return { title, - score: 1, + score: 0.5, datasourceSuggestionId: table.datasourceSuggestionId, previewIcon: 'visMetric', previewExpression: { 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 d4a78c489e5d9..1d34e874d85f0 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 @@ -132,23 +132,22 @@ function getSuggestion( const xTitle = xValue.operation.label; const isDate = xValue.operation.dataType === 'date'; - let title = isDate - ? i18n.translate('xpack.lens.xySuggestions.dateSuggestion', { - defaultMessage: '{yTitle} over {xTitle}', - description: - 'Chart description for charts over time, like "Transfered bytes over log.timestamp"', - values: { xTitle, yTitle }, - }) - : i18n.translate('xpack.lens.xySuggestions.nonDateSuggestion', { - defaultMessage: '{yTitle} of {xTitle}', - description: - 'Chart description for a value of some groups, like "Top URLs of top 5 countries"', - values: { xTitle, yTitle }, - }); + let title = + tableLabel || + (isDate + ? i18n.translate('xpack.lens.xySuggestions.dateSuggestion', { + defaultMessage: '{yTitle} over {xTitle}', + description: + 'Chart description for charts over time, like "Transfered bytes over log.timestamp"', + values: { xTitle, yTitle }, + }) + : i18n.translate('xpack.lens.xySuggestions.nonDateSuggestion', { + defaultMessage: '{yTitle} of {xTitle}', + description: + 'Chart description for a value of some groups, like "Top URLs of top 5 countries"', + values: { xTitle, yTitle }, + })); - if (currentState && changeType === 'extended' && tableLabel) { - title = tableLabel; - } const oldLayer = currentState && currentState.layers.find(layer => layer.layerId === layerId); const seriesType: SeriesType = (oldLayer && oldLayer.seriesType) || From 40b99f36c3d2874529d26fe4034788118f2399f6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 20 Aug 2019 13:15:38 +0200 Subject: [PATCH 28/46] work more on suggestions --- .../datatable_visualization_plugin/visualization.tsx | 2 ++ .../editor_frame/suggestion_helpers.ts | 1 + .../editor_frame/suggestion_panel.tsx | 4 +++- .../indexpattern_plugin/indexpattern_suggestions.ts | 10 +--------- x-pack/legacy/plugins/lens/public/types.ts | 1 + .../public/xy_visualization_plugin/xy_suggestions.ts | 2 ++ 6 files changed, 10 insertions(+), 10 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 0102bbe013cf2..934a91021ef07 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 @@ -173,6 +173,8 @@ export const datatableVisualization: Visualization< ], }, previewIcon: 'visTable', + // dont show suggestions for reduced versions + hide: table.changeType === 'reduced', }; }) ); 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 5d0894fd20e8b..27e7db20efdae 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 @@ -20,6 +20,7 @@ export interface Suggestion { visualizationState: unknown; previewExpression?: Ast | string; previewIcon: string; + hide?: boolean; } /** 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 71de25358e87e..65c880232d0a0 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 @@ -122,7 +122,9 @@ function InnerSuggestionPanel({ visualizationMap, activeVisualizationId, visualizationState, - }).slice(0, MAX_SUGGESTIONS_DISPLAYED); + }) + .filter(suggestion => !suggestion.hide) + .slice(0, MAX_SUGGESTIONS_DISPLAYED); if (suggestions.length === 0) { return null; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index f6569e14303f5..8d6c32214c67c 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -495,7 +495,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer label: updatedLayer.columnOrder.length === 1 ? getMetricSuggestionTitle(updatedLayer, availableMetricColumns.length === 1) - : getBucketSuggestionTitle(updatedLayer), + : undefined, }); }); } @@ -510,14 +510,6 @@ function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean) } } -function getBucketSuggestionTitle(layer: IndexPatternLayer) { - const { operationType } = layer.columns[ - layer.columnOrder.find(columnId => layer.columns[columnId].isBucketed)! - ]; - // TODO i18n - return `By ${operationDefinitionMap[operationType].displayName}`; -} - function buildLayerByColumnOrder( layer: IndexPatternLayer, columnOrder: string[] diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 06ac3e646cdd1..20e72d6fd1a37 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -186,6 +186,7 @@ export interface SuggestionRequest { export interface VisualizationSuggestion { score: number; + hide?: boolean; title: string; state: T; datasourceSuggestionId: number; 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 1d34e874d85f0..b7b84f9b914e2 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 @@ -198,6 +198,8 @@ function getSuggestion( title, // chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score score: ((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3, + // don't advertise chart of same type but with less data + hide: currentState && changeType === 'reduced', datasourceSuggestionId, state, previewIcon: getIconForSeries(seriesType), From 2c975bdbb9fc899f5ef8a8be87f5842df1b70cdc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 21 Aug 2019 16:45:04 +0200 Subject: [PATCH 29/46] clean up tests and add new ones --- .../editor_frame/chart_switch.test.tsx | 3 + .../editor_frame/chart_switch.tsx | 4 + .../editor_frame/editor_frame.test.tsx | 24 +- .../editor_frame/suggestion_helpers.test.ts | 10 +- .../editor_frame/suggestion_helpers.ts | 4 +- .../editor_frame/suggestion_panel.test.tsx | 17 + .../editor_frame/workspace_panel.test.tsx | 10 +- .../indexpattern_suggestions.test.tsx | 419 +++++++++++++++++- .../indexpattern_suggestions.ts | 176 ++++---- .../operation_definitions/count.tsx | 1 + .../operation_definitions/filter_ratio.tsx | 1 + .../operation_definitions/metrics.tsx | 4 +- .../indexpattern_plugin/operations.test.ts | 2 +- .../metric_suggestions.test.ts | 22 +- x-pack/legacy/plugins/lens/public/types.ts | 71 +++ .../xy_visualization_plugin/to_expression.ts | 8 +- .../xy_suggestions.test.ts | 161 ++++++- .../xy_visualization_plugin/xy_suggestions.ts | 195 +++++--- 18 files changed, 948 insertions(+), 184 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx index a3d9f02c9def3..5624553d92ddf 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx @@ -94,6 +94,7 @@ describe('chart_switch', () => { datasourceSuggestionId: 0, isMultiRow: true, layerId: 'a', + changeType: 'unchanged', }, }, ]); @@ -219,6 +220,7 @@ describe('chart_switch', () => { datasourceSuggestionId: 0, layerId: 'first', isMultiRow: true, + changeType: 'unchanged', }, }, ]); @@ -447,6 +449,7 @@ describe('chart_switch', () => { datasourceSuggestionId: 0, layerId: 'a', isMultiRow: true, + changeType: 'unchanged', }, }, ]); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx index daed3adf9cd46..d4ae425c66792 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx @@ -112,6 +112,10 @@ export function ChartSwitch(props: Props) { visualizationMap: { [visualizationId]: newVisualization }, activeVisualizationId: props.visualizationId, visualizationState: props.visualizationState, + }).filter(suggestion => { + // don't use extended versions of current data table on switching between visualizations + // to avoid confusing the user. + return suggestion.changeType !== 'extended'; })[0]; let dataLoss: VisualizationSelection['dataLoss']; 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 3904c2c114543..c7d1eb543d10d 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 @@ -32,6 +32,7 @@ function generateSuggestion(datasourceSuggestionId = 1, state = {}): DatasourceS datasourceSuggestionId, isMultiRow: true, layerId: 'first', + changeType: 'unchanged', }, }; } @@ -907,6 +908,7 @@ describe('editor_frame', () => { datasourceSuggestionId: 0, isMultiRow: true, layerId: 'first', + changeType: 'unchanged', }, }, ]); @@ -1068,7 +1070,7 @@ describe('editor_frame', () => { expect(mockVisualization2.getSuggestions).toHaveBeenCalled(); }); - it('should display top 3 suggestions in descending order', async () => { + it('should display top 5 suggestions in descending order', async () => { const instance = mount( { testVis: { ...mockVisualization, getSuggestions: () => [ + { + datasourceSuggestionId: 0, + score: 0.1, + state: {}, + title: 'Suggestion6', + previewIcon: 'empty', + }, { datasourceSuggestionId: 0, score: 0.5, state: {}, + title: 'Suggestion3', + previewIcon: 'empty', + }, + { + datasourceSuggestionId: 0, + score: 0.7, + state: {}, title: 'Suggestion2', previewIcon: 'empty', }, @@ -1099,14 +1115,14 @@ describe('editor_frame', () => { datasourceSuggestionId: 0, score: 0.4, state: {}, - title: 'Suggestion4', + title: 'Suggestion5', previewIcon: 'empty', }, { datasourceSuggestionId: 0, score: 0.45, state: {}, - title: 'Suggestion3', + title: 'Suggestion4', previewIcon: 'empty', }, ], @@ -1133,7 +1149,7 @@ describe('editor_frame', () => { .find('[data-test-subj="lnsSuggestion"]') .find(EuiPanel) .map(el => el.parents(EuiToolTip).prop('content')) - ).toEqual(['Suggestion1', 'Suggestion2', 'Suggestion3']); + ).toEqual(['Suggestion1', 'Suggestion2', 'Suggestion3', 'Suggestion4', 'Suggestion5']); }); it('should switch to suggested visualization', async () => { 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 ddfb79cb07a18..cb4a5bf79e817 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 @@ -14,7 +14,13 @@ const generateSuggestion = ( layerId: string = 'first' ): DatasourceSuggestion => ({ state, - table: { datasourceSuggestionId, columns: [], isMultiRow: false, layerId }, + table: { + datasourceSuggestionId, + columns: [], + isMultiRow: false, + layerId, + changeType: 'unchanged', + }, }); let datasourceMap: Record; @@ -233,12 +239,14 @@ describe('suggestion helpers', () => { columns: [], isMultiRow: true, layerId: 'first', + changeType: 'unchanged', }; const table2: TableSuggestion = { datasourceSuggestionId: 1, columns: [], isMultiRow: true, layerId: 'first', + changeType: 'unchanged', }; datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([ { state: {}, table: table1 }, 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 27e7db20efdae..b3aac25fd1fcf 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 @@ -6,7 +6,7 @@ import _ from 'lodash'; import { Ast } from '@kbn/interpreter/common'; -import { Visualization, Datasource, FramePublicAPI } from '../../types'; +import { Visualization, Datasource, FramePublicAPI, TableChangeType } from '../../types'; import { Action } from './state_management'; export interface Suggestion { @@ -21,6 +21,7 @@ export interface Suggestion { previewExpression?: Ast | string; previewIcon: string; hide?: boolean; + changeType: TableChangeType; } /** @@ -101,6 +102,7 @@ export function getSuggestions({ datasourceState: datasourceSuggestion.state, datasourceId: datasourceSuggestion.datasourceId, columns: datasourceSuggestion.table.columns.length, + changeType: datasourceSuggestion.table.changeType, }; }); }) 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 66436941dc499..84b4f24bf0f9c 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 @@ -156,6 +156,23 @@ describe('suggestion_panel', () => { expect(passedExpression).toMatchInlineSnapshot(` Object { "chain": Array [ + Object { + "arguments": Object {}, + "function": "kibana", + "type": "function", + }, + Object { + "arguments": Object { + "query": Array [ + "{\\"query\\":\\"\\",\\"language\\":\\"lucene\\"}", + ], + "timeRange": Array [ + "{\\"from\\":\\"now-7d\\",\\"to\\":\\"now\\"}", + ], + }, + "function": "kibana_context", + "type": "function", + }, Object { "arguments": Object { "layerIds": Array [ 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 c24836415c0f4..04f173c75eab2 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 @@ -7,7 +7,7 @@ import React from 'react'; import { ExpressionRendererProps } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { Visualization, FramePublicAPI } from '../../types'; +import { Visualization, FramePublicAPI, TableSuggestion } from '../../types'; import { createMockVisualization, createMockDatasource, @@ -548,11 +548,12 @@ describe('workspace_panel', () => { } it('should immediately transition if exactly one suggestion is returned', () => { - const expectedTable = { + const expectedTable: TableSuggestion = { datasourceSuggestionId: 0, isMultiRow: true, layerId: '1', columns: [], + changeType: 'unchanged', }; mockDatasource.getDatasourceSuggestionsForField.mockReturnValueOnce([ { @@ -597,6 +598,7 @@ describe('workspace_panel', () => { isMultiRow: true, layerId: '1', columns: [], + changeType: 'unchanged', }, }, ]); @@ -624,6 +626,7 @@ describe('workspace_panel', () => { isMultiRow: true, layerId: '1', columns: [], + changeType: 'unchanged', }, }, ]); @@ -651,6 +654,7 @@ describe('workspace_panel', () => { isMultiRow: true, layerId: '1', columns: [], + changeType: 'unchanged', }, }, ]); @@ -681,6 +685,7 @@ describe('workspace_panel', () => { isMultiRow: true, columns: [], layerId: '1', + changeType: 'unchanged', }, }, { @@ -690,6 +695,7 @@ describe('workspace_panel', () => { isMultiRow: true, columns: [], layerId: '1', + changeType: 'unchanged', }, }, ]); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index d86d88ed6ff02..130efb3daa3ca 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -174,11 +174,14 @@ describe('IndexPattern Data Source suggestions', () => { let initialState: IndexPatternPrivateState; beforeEach(async () => { + jest.resetAllMocks(); initialState = await indexPatternDatasource.initialize({ currentIndexPatternId: '1', layers: {}, }); (generateId as jest.Mock).mockReturnValueOnce('suggestedLayer'); + (generateId as jest.Mock).mockReturnValueOnce('col1'); + (generateId as jest.Mock).mockReturnValueOnce('col2'); }); it('should apply a bucketed aggregation for a string field', () => { @@ -186,7 +189,6 @@ describe('IndexPattern Data Source suggestions', () => { field: { name: 'source', type: 'string', aggregatable: true, searchable: true }, indexPatternId: '1', }); - expect(suggestions).toHaveLength(1); expect(suggestions[0].state).toEqual( expect.objectContaining({ @@ -207,6 +209,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -247,6 +251,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -279,7 +285,7 @@ describe('IndexPattern Data Source suggestions', () => { sourceField: 'timestamp', }), col2: expect.objectContaining({ - operationType: 'min', + operationType: 'avg', sourceField: 'bytes', }), }, @@ -288,6 +294,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -341,6 +349,7 @@ describe('IndexPattern Data Source suggestions', () => { let initialState: IndexPatternPrivateState; beforeEach(async () => { + jest.resetAllMocks(); initialState = await indexPatternDatasource.initialize({ currentIndexPatternId: '1', layers: { @@ -351,6 +360,8 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }); + (generateId as jest.Mock).mockReturnValueOnce('col1'); + (generateId as jest.Mock).mockReturnValueOnce('col2'); }); it('should apply a bucketed aggregation for a string field', () => { @@ -379,6 +390,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -419,6 +432,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -451,7 +466,7 @@ describe('IndexPattern Data Source suggestions', () => { sourceField: 'timestamp', }), col2: expect.objectContaining({ - operationType: 'min', + operationType: 'avg', sourceField: 'bytes', }), }, @@ -460,6 +475,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -542,8 +559,8 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, sourceField: 'bytes', - label: 'Min of bytes', - operationType: 'min', + label: 'Avg of bytes', + operationType: 'avg', }, }, columnOrder: ['col1', 'col2'], @@ -575,8 +592,8 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, sourceField: 'bytes', - label: 'Min of bytes', - operationType: 'min', + label: 'Avg of bytes', + operationType: 'avg', }, }, }, @@ -633,6 +650,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'extended', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -701,7 +720,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { ...initialState.layers.currentLayer.columns, newId: expect.objectContaining({ - operationType: 'min', + operationType: 'avg', sourceField: 'memory', }), }, @@ -727,7 +746,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { ...initialState.layers.currentLayer.columns, newId: expect.objectContaining({ - operationType: 'max', + operationType: 'sum', sourceField: 'bytes', }), }, @@ -742,6 +761,7 @@ describe('IndexPattern Data Source suggestions', () => { let initialState: IndexPatternPrivateState; beforeEach(async () => { + jest.resetAllMocks(); initialState = await indexPatternDatasource.initialize({ currentIndexPatternId: '1', layers: { @@ -757,6 +777,8 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }); + (generateId as jest.Mock).mockReturnValueOnce('col1'); + (generateId as jest.Mock).mockReturnValueOnce('col2'); }); it('suggests on the layer that matches by indexPatternId', () => { @@ -799,6 +821,8 @@ describe('IndexPattern Data Source suggestions', () => { }) ); expect(suggestions[0].table).toEqual({ + changeType: 'initial', + label: undefined, datasourceSuggestionId: 0, isMultiRow: true, columns: [ @@ -882,7 +906,7 @@ describe('IndexPattern Data Source suggestions', () => { columns: { col1: { label: 'My Op 2', - dataType: 'number', + dataType: 'string', isBucketed: true, // Private @@ -903,6 +927,8 @@ describe('IndexPattern Data Source suggestions', () => { table: { datasourceSuggestionId: 0, isMultiRow: true, + changeType: 'unchanged', + label: undefined, columns: [ { columnId: 'col1', @@ -910,6 +936,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + scale: undefined, }, }, ], @@ -920,13 +947,16 @@ describe('IndexPattern Data Source suggestions', () => { table: { datasourceSuggestionId: 1, isMultiRow: true, + changeType: 'unchanged', + label: undefined, columns: [ { columnId: 'col1', operation: { label: 'My Op 2', - dataType: 'number', + dataType: 'string', isBucketed: true, + scale: undefined, }, }, ], @@ -936,9 +966,230 @@ describe('IndexPattern Data Source suggestions', () => { ]); }); - it('returns simplified versions of table with more than 2 columns', async () => { + it('returns a metric over time for single metric tables', async () => { + jest.resetAllMocks(); + (generateId as jest.Mock).mockReturnValueOnce('col2'); + const state = await indexPatternDatasource.initialize({ + ...persistedState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'op', + scale: 'ratio', + }, + }, + }, + }, + }); + expect(indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state)).toEqual([ + expect.objectContaining({ + table: { + datasourceSuggestionId: 0, + isMultiRow: true, + changeType: 'extended', + label: 'Over time', + columns: [ + { + columnId: 'col2', + operation: { + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + scale: 'interval', + }, + }, + { + columnId: 'col1', + operation: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }, + }, + ], + layerId: 'first', + }, + }), + expect.objectContaining({ + table: { + datasourceSuggestionId: 1, + isMultiRow: false, + changeType: 'unchanged', + label: undefined, + columns: [ + { + columnId: 'col1', + operation: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }, + }, + ], + layerId: 'first', + }, + }), + ]); + }); + + it('adds date histogram over default time field for tables without time dimension', async () => { + jest.resetAllMocks(); + (generateId as jest.Mock).mockReturnValueOnce('newCol'); + const state = await indexPatternDatasource.initialize({ + ...persistedState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1', 'col2'], + columns: { + col1: { + label: 'My Terms', + dataType: 'string', + isBucketed: true, + operationType: 'terms', + sourceField: 'source', + scale: 'ordinal', + params: { + orderBy: { type: 'alphabetical' }, + orderDirection: 'asc', + size: 5, + }, + }, + col2: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'bytes', + scale: 'ratio', + }, + }, + }, + }, + }); + expect(indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state)[2]).toEqual( + expect.objectContaining({ + table: { + datasourceSuggestionId: 2, + isMultiRow: true, + changeType: 'extended', + label: 'Over time', + columns: [ + { + columnId: 'col1', + operation: { + label: 'My Terms', + dataType: 'string', + isBucketed: true, + scale: 'ordinal', + }, + }, + { + columnId: 'newCol', + operation: { + label: 'Date Histogram of timestamp', + dataType: 'date', + isBucketed: true, + scale: 'interval', + }, + }, + { + columnId: 'col2', + operation: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + scale: 'ratio', + }, + }, + ], + layerId: 'first', + }, + }) + ); + }); + + it('does not create an over time suggestion if there is no default time field', async () => { + jest.resetAllMocks(); + (generateId as jest.Mock).mockReturnValueOnce('newCol'); const state = await indexPatternDatasource.initialize({ ...persistedState, + layers: { + first: { + indexPatternId: '1', + columnOrder: ['col1'], + columns: { + col1: { + label: 'My Op', + dataType: 'number', + isBucketed: false, + operationType: 'avg', + sourceField: 'bytes', + scale: 'ratio', + }, + }, + }, + }, + }); + // the single suggestion is the current unchanged state of the data table + expect( + indexPatternDatasource.getDatasourceSuggestionsFromCurrentState({ + ...state, + indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } }, + }).length + ).toEqual(1); + }); + + it('returns simplified versions of table with more than 2 columns', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: '1', + indexPatterns: { + 1: { + id: '1', + title: 'my-fake-index-pattern', + fields: [ + { + name: 'field1', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field3', + type: 'string', + aggregatable: true, + searchable: true, + }, + { + name: 'field4', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field5', + type: 'number', + aggregatable: true, + searchable: true, + }, + ], + }, + }, layers: { first: { ...persistedState.layers.first, @@ -1002,31 +1253,150 @@ describe('IndexPattern Data Source suggestions', () => { columnOrder: ['col1', 'col2', 'col3', 'col4', 'col5'], }, }, - }); + }; const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state); // 1 bucket col, 2 metric cols - validateTable(suggestions[0], ['col1', 'col4', 'col5'], 1); + isTableWithBucketColumns(suggestions[0], ['col1', 'col4', 'col5'], 1); // 1 bucket col, 1 metric col - validateTable(suggestions[1], ['col1', 'col4'], 1); + isTableWithBucketColumns(suggestions[1], ['col1', 'col4'], 1); // 2 bucket cols, 2 metric cols - validateTable(suggestions[2], ['col1', 'col2', 'col4', 'col5'], 2); + isTableWithBucketColumns(suggestions[2], ['col1', 'col2', 'col4', 'col5'], 2); // 2 bucket cols, 1 metric col - validateTable(suggestions[3], ['col1', 'col2', 'col4'], 2); + isTableWithBucketColumns(suggestions[3], ['col1', 'col2', 'col4'], 2); // 3 bucket cols, 2 metric cols - validateTable(suggestions[4], ['col1', 'col2', 'col3', 'col4', 'col5'], 3); + isTableWithBucketColumns(suggestions[4], ['col1', 'col2', 'col3', 'col4', 'col5'], 3); // 3 bucket cols, 1 metric col - validateTable(suggestions[5], ['col1', 'col2', 'col3', 'col4'], 3); + isTableWithBucketColumns(suggestions[5], ['col1', 'col2', 'col3', 'col4'], 3); + + // first metric col + isTableWithMetricColumns(suggestions[6], ['col4']); + + // second metric col + isTableWithMetricColumns(suggestions[7], ['col5']); + + expect(suggestions.length).toBe(8); + }); + + it('returns uses a different operation on field based columns for only metric suggestions', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: '1', + indexPatterns: { + 1: { + id: '1', + title: 'my-fake-index-pattern', + fields: [ + { + name: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ], + }, + }, + layers: { + first: { + ...persistedState.layers.first, + columns: { + col1: { + label: 'Date histogram', + dataType: 'date', + isBucketed: true, + + operationType: 'date_histogram', + sourceField: 'field2', + params: { + interval: 'd', + }, + }, + col2: { + label: 'Average of field1', + dataType: 'number', + isBucketed: false, + + operationType: 'avg', + sourceField: 'field1', + }, + }, + columnOrder: ['col1', 'col2'], + }, + }, + }; + + const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state); + expect(suggestions[1].table.columns[0].operation.label).toBe('Sum of field1'); + }); + + it('reuses document based column for only metric suggestions', () => { + const state: IndexPatternPrivateState = { + currentIndexPatternId: '1', + indexPatterns: { + 1: { + id: '1', + title: 'my-fake-index-pattern', + fields: [ + { + name: 'field1', + type: 'number', + aggregatable: true, + searchable: true, + }, + { + name: 'field2', + type: 'date', + aggregatable: true, + searchable: true, + }, + ], + }, + }, + layers: { + first: { + ...persistedState.layers.first, + columns: { + col1: { + label: 'Date histogram', + dataType: 'date', + isBucketed: true, + + operationType: 'date_histogram', + sourceField: 'field2', + params: { + interval: 'd', + }, + }, + col2: { + label: 'Count', + dataType: 'number', + isBucketed: false, + + operationType: 'count', + }, + }, + columnOrder: ['col1', 'col2'], + }, + }, + }; + + const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state); + expect(suggestions[1].table.columns[0].operation.label).toBe('Count'); }); }); }); -function validateTable( +function isTableWithBucketColumns( suggestion: DatasourceSuggestion, columnIds: string[], numBuckets: number @@ -1036,3 +1406,12 @@ function validateTable( suggestion.table.columns.slice(0, numBuckets).every(column => column.operation.isBucketed) ).toBeTruthy(); } + +function isTableWithMetricColumns( + suggestion: DatasourceSuggestion, + columnIds: string[] +) { + expect(suggestion.table.isMultiRow).toEqual(false); + expect(suggestion.table.columns.map(column => column.columnId)).toEqual(columnIds); + expect(suggestion.table.columns.every(column => !column.operation.isBucketed)).toBeTruthy(); +} diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 8d6c32214c67c..228be62a2fdae 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; +import _, { partition } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; -import { DatasourceSuggestion, TableChangeType, TableSuggestion } from '../types'; +import { DatasourceSuggestion, TableChangeType } from '../types'; import { columnToOperation, IndexPatternField, IndexPatternLayer, IndexPatternPrivateState, IndexPattern, - OperationType, - IndexPatternColumn, } from './indexpattern'; import { buildColumn, getOperationTypesForField, operationDefinitionMap } from './operations'; import { hasField } from './utils'; @@ -35,7 +34,7 @@ function buildSuggestion({ isMultiRow?: boolean; datasourceSuggestionId?: number; label?: string; -}) { +}): DatasourceSuggestion { const columnOrder = (updatedLayer || state.layers[layerId]).columnOrder; const columns = (updatedLayer || state.layers[layerId]).columns; return { @@ -77,6 +76,9 @@ export function getDatasourceSuggestionsForField( // already return getEmptyLayerSuggestionsForField(state, generateId(), indexPatternId, field); } else { + // The field we're suggesting on matches an existing layer. In this case we find the layer with + // the fewest configured columns and try to add the field to this table. If this layer does not + // contain any layers yet, behave as if there is no layer. const mostEmptyLayerId = _.min(layerIds, layerId => state.layers[layerId].columnOrder.length); if (state.layers[mostEmptyLayerId].columnOrder.length === 0) { return getEmptyLayerSuggestionsForField(state, mostEmptyLayerId, indexPatternId, field); @@ -318,10 +320,6 @@ export function getDatasourceSuggestionsFromCurrentState( Object.entries(state.layers || {}) .filter(([_id, layer]) => layer.columnOrder.length) .map(([layerId, layer], index) => { - if (layer.columnOrder.length === 0) { - return []; - } - const indexPattern = state.indexPatterns[layer.indexPatternId]; const onlyMetric = layer.columnOrder.every(columnId => !layer.columns[columnId].isBucketed); const onlyBucket = layer.columnOrder.every(columnId => layer.columns[columnId].isBucketed); @@ -329,88 +327,45 @@ export function getDatasourceSuggestionsFromCurrentState( columnId => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); - if (onlyBucket) { - // intermediary chart, don't try to suggest reduced versions - return buildSuggestion({ - state, - layerId, - isMultiRow: false, - datasourceSuggestionId: index, - changeType: 'unchanged', - }); - } - if (onlyMetric) { - if (indexPattern.timeFieldName) { - const newId = generateId(); - const timeColumn = buildColumn({ - layerId, - op: 'date_histogram', - indexPattern, - columns: layer.columns, - field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName), - suggestedPriority: undefined, - }); - const updatedLayer = buildLayerByColumnOrder( - { ...layer, columns: { ...layer.columns, [newId]: timeColumn } }, - [newId, ...layer.columnOrder] - ); - return buildSuggestion({ + const suggestions: Array> = []; + if (onlyBucket) { + // intermediary chart without metric, don't try to suggest reduced versions + suggestions.push( + buildSuggestion({ state, layerId, isMultiRow: true, - updatedLayer, datasourceSuggestionId: index, - // TODO i18n - label: 'over time', - changeType: 'extended', - }); + changeType: 'unchanged', + }) + ); + } else if (onlyMetric) { + if (indexPattern.timeFieldName) { + // suggest current metric over time if there is a default time field + suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId)); } // suggest only metric - return buildSuggestion({ - state, - layerId, - isMultiRow: false, - datasourceSuggestionId: index, - changeType: 'unchanged', - }); - } - - const simplifiedSuggestions = createSimplifiedTableSuggestions(state, layerId); - - if (!timeDimension && indexPattern.timeFieldName) { - const newId = generateId(); - const availableBucketedColumns = layer.columnOrder.filter( - columnId => layer.columns[columnId].isBucketed - ); - const availableMetricColumns = layer.columnOrder.filter( - columnId => !layer.columns[columnId].isBucketed - ); - const timeColumn = buildColumn({ - layerId, - op: 'date_histogram', - indexPattern, - columns: layer.columns, - field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName), - suggestedPriority: undefined, - }); - const updatedLayer = buildLayerByColumnOrder( - { ...layer, columns: { ...layer.columns, [newId]: timeColumn } }, - [...availableBucketedColumns, newId, ...availableMetricColumns] + suggestions.push( + buildSuggestion({ + state, + layerId, + isMultiRow: false, + datasourceSuggestionId: index, + changeType: 'unchanged', + }) ); - const timedSuggestion = buildSuggestion({ - state, - layerId, - isMultiRow: true, - updatedLayer, - // TODO i18n - label: 'over time', - changeType: 'extended', - }); - return [...simplifiedSuggestions, timedSuggestion]; + } else { + suggestions.push(...createSimplifiedTableSuggestions(state, layerId)); + + if (!timeDimension && indexPattern.timeFieldName) { + // suggest current configuration over time if there is a default time field + // and no time dimension yet + suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId)); + } } - return simplifiedSuggestions; + return suggestions; }) ).map( (suggestion, index): DatasourceSuggestion => ({ @@ -420,19 +375,47 @@ export function getDatasourceSuggestionsFromCurrentState( ); } +function createSuggestionWithDefaultDateHistogram( + state: IndexPatternPrivateState, + layerId: string +) { + const layer = state.layers[layerId]; + const indexPattern = state.indexPatterns[layer.indexPatternId]; + const newId = generateId(); + const [buckets, metrics] = separateBucketColumns(layer); + const timeColumn = buildColumn({ + layerId, + op: 'date_histogram', + indexPattern, + columns: layer.columns, + field: indexPattern.fields.find(({ name }) => name === indexPattern.timeFieldName), + suggestedPriority: undefined, + }); + const updatedLayer = buildLayerByColumnOrder( + { ...layer, columns: { ...layer.columns, [newId]: timeColumn } }, + [...buckets, newId, ...metrics] + ); + return buildSuggestion({ + state, + layerId, + isMultiRow: true, + updatedLayer, + label: i18n.translate('xpack.lens.indexpattern.suggestions.overTimeLabel', { + defaultMessage: 'Over time', + }), + changeType: 'extended', + }); +} + function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) { const layer = state.layers[layerId]; const indexPattern = state.indexPatterns[layer.indexPatternId]; - const availableBucketedColumns = layer.columnOrder.filter( - columnId => layer.columns[columnId].isBucketed - ); - const availableMetricColumns = layer.columnOrder.filter( - columnId => !layer.columns[columnId].isBucketed - ); + const [availableBucketedColumns, availableMetricColumns] = separateBucketColumns(layer); return _.flatten( availableBucketedColumns.map((_col, index) => { + // build suggestions with less buckets const bucketedColumns = availableBucketedColumns.slice(0, index + 1); const allMetricsSuggestion = buildLayerByColumnOrder(layer, [ ...bucketedColumns, @@ -451,6 +434,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer ) .concat( availableMetricColumns.map(columnId => { + // build suggestions with only metrics let column = layer.columns[columnId]; // if field based, suggest different metric if (hasField(column)) { @@ -502,12 +486,18 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer function getMetricSuggestionTitle(layer: IndexPatternLayer, onlyMetric: boolean) { const { operationType, label } = Object.values(layer.columns)[0]; - // TODO i18n - if (onlyMetric) { - return `${operationDefinitionMap[operationType].displayName} overall`; - } else { - return `${label} overall`; - } + return i18n.translate('xpack.lens.indexpattern.suggestions.overallLabel', { + defaultMessage: '{operation} overall', + values: { + operation: onlyMetric ? operationDefinitionMap[operationType].displayName : label, + }, + description: + 'Title of a suggested chart containing only a single numerical metric calculated over all available data', + }); +} + +function separateBucketColumns(layer: IndexPatternLayer) { + return partition(layer.columnOrder, columnId => layer.columns[columnId].isBucketed); } function buildLayerByColumnOrder( 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 0cb4838faa12c..3e48968e81fdf 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 @@ -10,6 +10,7 @@ import { OperationDefinition } from '../operations'; export const countOperation: OperationDefinition = { type: 'count', + priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.count', { defaultMessage: 'Count', }), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx index 3f4e7c3dc407d..8af227b84b8de 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx @@ -18,6 +18,7 @@ import { updateColumnParam } from '../state_helpers'; export const filterRatioOperation: OperationDefinition = { type: 'filter_ratio', + priority: 1, displayName: i18n.translate('xpack.lens.indexPattern.filterRatio', { defaultMessage: 'Filter Ratio', }), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx index 4f28cafaaed29..b3e8818c2d2db 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/metrics.tsx @@ -109,7 +109,7 @@ export const maxOperation = buildMetricOperation({ export const averageOperation = buildMetricOperation({ type: 'avg', - priority: 1, + priority: 2, displayName: i18n.translate('xpack.lens.indexPattern.avg', { defaultMessage: 'Average', }), @@ -122,7 +122,7 @@ export const averageOperation = buildMetricOperation({ export const sumOperation = buildMetricOperation({ type: 'sum', - priority: 2, + priority: 1, displayName: i18n.translate('xpack.lens.indexPattern.sum', { defaultMessage: 'Sum', }), 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 d4353f52c4a67..50436eb75b8f6 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 @@ -210,7 +210,7 @@ describe('getOperationTypesForField', () => { suggestedPriority: 0, field, }) as MinIndexPatternColumn; - expect(column.operationType).toEqual('min'); + expect(column.operationType).toEqual('avg'); expect(column.sourceField).toEqual(field.name); }); diff --git a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts index 120d0369bc7ea..a28e8dc7300f1 100644 --- a/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -51,25 +51,40 @@ describe('metric_suggestions', () => { expect( getSuggestions({ tables: [ - { columns: [dateCol('a')], datasourceSuggestionId: 0, isMultiRow: true, layerId: 'l1' }, + { + columns: [dateCol('a')], + datasourceSuggestionId: 0, + isMultiRow: true, + layerId: 'l1', + changeType: 'unchanged', + }, { columns: [strCol('foo'), strCol('bar')], datasourceSuggestionId: 1, isMultiRow: true, layerId: 'l1', + changeType: 'unchanged', + }, + { + layerId: 'l1', + datasourceSuggestionId: 2, + isMultiRow: true, + columns: [numCol('bar')], + changeType: 'unchanged', }, - { layerId: 'l1', datasourceSuggestionId: 2, isMultiRow: true, columns: [numCol('bar')] }, { columns: [unknownCol(), numCol('bar')], datasourceSuggestionId: 3, isMultiRow: true, layerId: 'l1', + changeType: 'unchanged', }, { columns: [numCol('bar'), numCol('baz')], datasourceSuggestionId: 4, isMultiRow: false, layerId: 'l1', + changeType: 'unchanged', }, ], }) @@ -84,6 +99,7 @@ describe('metric_suggestions', () => { datasourceSuggestionId: 0, isMultiRow: false, layerId: 'l1', + changeType: 'unchanged', }, ], }); @@ -110,7 +126,7 @@ describe('metric_suggestions', () => { "type": "expression", }, "previewIcon": "visMetric", - "score": 1, + "score": 0.5, "state": Object { "accessor": "bytes", "layerId": "l1", diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 2ba5ec1e1fa20..6e2997d610201 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -47,15 +47,52 @@ export interface TableSuggestionColumn { operation: Operation; } +/** + * A possible table a datasource can create. This object is passed to the visualization + * which tries to build a meaningful visualization given the shape of the table. If this + * is possible, the visualization returns a `VisualizationSuggestion` object + */ export interface TableSuggestion { + /** + * The id of this table. This id has to be included in the `VisualizationSuggestion` to map + * the visualization to the right table as there can be multiple tables in a single `SuggestionRequest`. + */ datasourceSuggestionId: number; + /** + * Flag indicating whether the table will include more than one column. + * This is not the case for example for a single metric aggregation + * */ isMultiRow: boolean; + /** + * The columns of the table. Each column has to be mapped to a dimension in a chart. If a visualization + * can't use all columns of a suggestion, it should not return a `VisualizationSuggestion` based on it + * because there would be unreferenced columns + */ columns: TableSuggestionColumn[]; + /** + * The layer this table will replace. This is only relevant if the visualization this suggestion is passed + * is currently active and has multiple layers configured. If this suggestion is applied, the table of this + * layer will be replaced by the columns specified in this suggestion + */ layerId: string; + /** + * A label describing the table. This can be used to provide a title for the `VisualizationSuggestion`, + * but the visualization can also decide to overwrite it. + */ label?: string; + /** + * The change type indicates what was changed in this table compared to the currently active table of this layer. + */ changeType: TableChangeType; } +/** + * Indicates what was changed in this table compared to the currently active table of this layer. + * * `initial` means the layer associated with this table does not exist in the current configuration + * * `unchanged` means the table is the same in the currently active configuration + * * `reduced` means the table is a reduced version of the currently active table (some columns dropped, but not all of them) + * * `extended` means the table is an extended version of the currently active table (added one or multiple additional columns) + */ export type TableChangeType = 'initial' | 'unchanged' | 'reduced' | 'extended'; export interface DatasourceSuggestion { @@ -186,13 +223,47 @@ export interface SuggestionRequest { state?: T; // State is only passed if the visualization is active } +/** + * A possible configuration of a given visualization. It is based on a `TableSuggestion`. + * Suggestion might be shown in the UI to be chosen by the user directly, but they are + * also applied directly under some circumstances (dragging in the first field from the data + * panel or switching to another visualization in the chart switcher). + */ export interface VisualizationSuggestion { + /** + * The score of a suggestion should indicate how valuable the suggestion is. It is used + * to rank multiple suggestions of multiple visualizations. The number should be between 0 and 1 + */ score: number; + /** + * Flag indicating whether this suggestion should not be advertised to the user. It is still + * considered in scenarios where the available suggestion with the highest suggestion is applied + * directly. + */ hide?: boolean; + /** + * Descriptive title of the suggestion. Should be as short as possible. This title is shown if + * the suggestion is advertised to the user and will also show either the `previewExpression` or + * the `previewIcon` + */ title: string; + /** + * The new state of the visualization if this suggestion is applied. + */ state: T; + /** + * The id of the `TableSuggestion` object this visualization suggestion is based on. + * This is used to switch the datasource configuration to the right table. + */ datasourceSuggestionId: number; + /** + * The expression of the preview of the chart rendered if the suggestion is advertised to the user. + * If there is no expression provided, the preview icon is used. + */ previewExpression?: Ast | string; + /** + * An EUI icon type shown instead of the preview expression. + */ previewIcon: 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 e4e0e57b7926a..ccd8132a4a8b5 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 @@ -70,9 +70,9 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => return buildExpression( stateWithValidAccessors, - xyTitles(state.layers[0], frame), metadata, - frame + frame, + xyTitles(state.layers[0], frame) ); }; @@ -103,9 +103,9 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S export const buildExpression = ( state: State, - { xTitle, yTitle }: { xTitle: string; yTitle: string }, metadata: Record>, - frame?: FramePublicAPI + frame?: FramePublicAPI, + { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' } ): Ast => ({ type: 'expression', chain: [ 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 4005a51280595..123aa87e0fd2b 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 @@ -6,7 +6,7 @@ import { getSuggestions } from './xy_suggestions'; import { TableSuggestionColumn, VisualizationSuggestion, DataType } from '../types'; -import { State } from './types'; +import { State, XYState } from './types'; import { generateId } from '../id_generator'; import { Ast } from '@kbn/interpreter/target/common'; @@ -20,6 +20,7 @@ describe('xy_suggestions', () => { dataType: 'number', label: `Avg ${columnId}`, isBucketed: false, + scale: 'ratio', }, }; } @@ -31,6 +32,7 @@ describe('xy_suggestions', () => { dataType: 'string', label: `Top 5 ${columnId}`, isBucketed: true, + scale: 'ordinal', }, }; } @@ -42,6 +44,7 @@ describe('xy_suggestions', () => { dataType: 'date', isBucketed: true, label: `${columnId} histogram`, + scale: 'interval', }, }; } @@ -71,24 +74,28 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [dateCol('a')], layerId: 'first', + changeType: 'unchanged', }, { datasourceSuggestionId: 1, isMultiRow: true, columns: [strCol('foo'), strCol('bar')], layerId: 'first', + changeType: 'unchanged', }, { datasourceSuggestionId: 2, isMultiRow: false, columns: [strCol('foo'), numCol('bar')], layerId: 'first', + changeType: 'unchanged', }, { datasourceSuggestionId: 3, isMultiRow: true, columns: [unknownCol(), numCol('bar')], layerId: 'first', + changeType: 'unchanged', }, ], }) @@ -104,6 +111,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('bytes'), dateCol('date')], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -137,6 +145,7 @@ describe('xy_suggestions', () => { strCol('city'), ], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -152,6 +161,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -172,6 +182,150 @@ describe('xy_suggestions', () => { `); }); + test('uses datasource provided title if available', () => { + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'unchanged', + label: 'Datasource title', + }, + ], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.title).toEqual('Datasource title'); + }); + + test('hides reduced suggestions if there is a current state', () => { + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'reduced', + }, + ], + state: { + isHorizontal: false, + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price', 'quantity'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'product', + xAccessor: 'date', + title: '', + }, + ], + }, + }); + + expect(rest).toHaveLength(0); + expect(suggestion.hide).toBeTruthy(); + }); + + test('does not hide reduced suggestions if xy visualization is not active', () => { + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'reduced', + }, + ], + }); + + expect(rest).toHaveLength(0); + expect(suggestion.hide).toBeFalsy(); + }); + + test('suggests an area chart for unchanged table and existing bar chart on non-ordinal x axis', () => { + const currentState: XYState = { + isHorizontal: false, + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price', 'quantity'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'product', + xAccessor: 'date', + title: '', + }, + ], + }; + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), dateCol('date'), strCol('product')], + layerId: 'first', + changeType: 'unchanged', + }, + ], + state: currentState, + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state).toEqual({ + ...currentState, + preferredSeriesType: 'area', + layers: [{ ...currentState.layers[0], seriesType: 'area' }], + }); + expect(suggestion.previewIcon).toEqual('visArea'); + expect(suggestion.title).toEqual('Area chart'); + }); + + test('suggests a flipped chart for unchanged table and existing bar chart on ordinal x axis', () => { + (generateId as jest.Mock).mockReturnValueOnce('dummyCol'); + const currentState: XYState = { + isHorizontal: false, + legend: { isVisible: true, position: 'bottom' }, + preferredSeriesType: 'bar', + layers: [ + { + accessors: ['price', 'quantity'], + layerId: 'first', + seriesType: 'bar', + splitAccessor: 'dummyCol', + xAccessor: 'product', + title: '', + }, + ], + }; + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + datasourceSuggestionId: 1, + isMultiRow: true, + columns: [numCol('price'), numCol('quantity'), strCol('product')], + layerId: 'first', + changeType: 'unchanged', + }, + ], + state: currentState, + }); + + expect(rest).toHaveLength(0); + expect(suggestion.state).toEqual({ + ...currentState, + isHorizontal: true, + }); + expect(suggestion.title).toEqual('Flip'); + }); + test('supports multiple suggestions', () => { (generateId as jest.Mock).mockReturnValueOnce('bbb').mockReturnValueOnce('ccc'); const [s1, s2, ...rest] = getSuggestions({ @@ -181,12 +335,14 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('price'), dateCol('date')], layerId: 'first', + changeType: 'unchanged', }, { datasourceSuggestionId: 1, isMultiRow: true, columns: [numCol('count'), strCol('country')], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -227,6 +383,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('quantity'), numCol('price')], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -264,6 +421,7 @@ describe('xy_suggestions', () => { }, ], layerId: 'first', + changeType: 'unchanged', }, ], }); @@ -290,6 +448,7 @@ describe('xy_suggestions', () => { isMultiRow: true, columns: [numCol('bytes'), dateCol('date')], layerId: 'first', + changeType: 'unchanged', }, ], }); 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 b7b84f9b914e2..99e6fd2bfd941 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 @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { partition, isEqual } from 'lodash'; +import { partition } from 'lodash'; import { Position } from '@elastic/charts'; import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; import { @@ -16,7 +16,7 @@ import { OperationMetadata, TableChangeType, } from '../types'; -import { State, SeriesType, LayerConfig } from './types'; +import { State, SeriesType, XYState } from './types'; import { generateId } from '../id_generator'; import { buildExpression } from './to_expression'; @@ -120,6 +120,72 @@ function getSuggestion( currentState?: State, tableLabel?: string ): VisualizationSuggestion { + const title = getSuggestionTitle(yValues, xValue, tableLabel); + const seriesType: SeriesType = getSeriesType(currentState, layerId, splitBy, xValue); + const isHorizontal = currentState ? currentState.isHorizontal : false; + + const options = { + isHorizontal, + currentState, + seriesType, + layerId, + title, + yValues, + splitBy, + changeType, + datasourceSuggestionId, + xValue, + }; + + // if current state is using the same data, suggest same chart with different presentational configuration + if (currentState && changeType === 'unchanged') { + if (xValue.operation.scale && xValue.operation.scale !== 'ordinal') { + // change chart type for interval or ratio scales on x axis + const newSeriesType = seriesType === 'area' ? 'bar' : 'area'; + return buildSuggestion({ + ...options, + seriesType: newSeriesType, + title: + newSeriesType === 'area' + ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { + defaultMessage: 'Area chart', + }) + : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), + }); + } else { + // flip between horizontal/vertical for ordinal scales + return buildSuggestion({ + ...options, + title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }), + isHorizontal: !options.isHorizontal, + }); + } + } else { + return buildSuggestion(options); + } +} + +function getSeriesType( + currentState: XYState | undefined, + layerId: string, + splitBy: TableSuggestionColumn | undefined, + xValue: TableSuggestionColumn +): SeriesType { + const oldLayer = getExistingLayer(currentState, layerId); + return ( + (oldLayer && oldLayer.seriesType) || + (currentState && currentState.preferredSeriesType) || + (splitBy && xValue.operation.dataType === 'date' ? 'line' : 'bar') + ); +} + +function getSuggestionTitle( + yValues: TableSuggestionColumn[], + xValue: TableSuggestionColumn, + tableLabel: string | undefined +) { const yTitle = yValues .map(col => col.operation.label) .join( @@ -130,11 +196,9 @@ function getSuggestion( }) ); const xTitle = xValue.operation.label; - const isDate = xValue.operation.dataType === 'date'; - - let title = + const title = tableLabel || - (isDate + (xValue.operation.dataType === 'date' ? i18n.translate('xpack.lens.xySuggestions.dateSuggestion', { defaultMessage: '{yTitle} over {xTitle}', description: @@ -147,36 +211,43 @@ function getSuggestion( 'Chart description for a value of some groups, like "Top URLs of top 5 countries"', values: { xTitle, yTitle }, })); + return title; +} - const oldLayer = currentState && currentState.layers.find(layer => layer.layerId === layerId); - const seriesType: SeriesType = - (oldLayer && oldLayer.seriesType) || - (currentState && currentState.preferredSeriesType) || - (splitBy && isDate ? 'line' : 'bar'); - let isHorizontal = currentState ? currentState.isHorizontal : false; - let newLayer = { - ...(oldLayer || {}), +function buildSuggestion({ + isHorizontal, + currentState, + seriesType, + layerId, + title, + yValues, + splitBy, + changeType, + datasourceSuggestionId, + xValue, +}: { + currentState: XYState | undefined; + isHorizontal: boolean; + seriesType: SeriesType; + title: string; + yValues: TableSuggestionColumn[]; + xValue: TableSuggestionColumn; + splitBy: TableSuggestionColumn | undefined; + layerId: string; + changeType: string; + datasourceSuggestionId: number; +}) { + const newLayer = { + ...(getExistingLayer(currentState, layerId) || {}), layerId, seriesType, xAccessor: xValue.columnId, splitAccessor: splitBy ? splitBy.columnId : generateId(), accessors: yValues.map(col => col.columnId), - title: yTitle, + // TODO check whether we need this + title: '', }; - // if current state is using the same data, suggest same chart with different series type - if (oldLayer && changeType === 'unchanged') { - if (xValue.operation.scale && xValue.operation.scale !== 'ordinal') { - const currentSeriesType = oldLayer.seriesType; - const newSeriesType = currentSeriesType === 'area' ? 'bar' : 'area'; - // TODO i18n - title = `${newSeriesType} chart`; - newLayer = { ...newLayer, seriesType: newSeriesType }; - } else { - // todo i18n - title = 'Flip'; - isHorizontal = !isHorizontal; - } - } + const state: State = { isHorizontal, legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right }, @@ -186,15 +257,8 @@ function getSuggestion( newLayer, ], }; - const metadata: Record = {}; - - [xValue, ...yValues, splitBy].forEach(col => { - if (col) { - metadata[col.columnId] = col.operation; - } - }); - const suggestion = { + return { title, // chart with multiple y values and split series will have a score of 1, single y value and no split series reduce score score: ((yValues.length > 1 ? 2 : 1) + (splitBy ? 1 : 0)) / 3, @@ -203,21 +267,48 @@ function getSuggestion( datasourceSuggestionId, state, previewIcon: getIconForSeries(seriesType), - previewExpression: buildExpression( - { - ...state, - layers: state.layers - .filter(layer => layer.layerId === layerId) - .map(layer => ({ ...layer, hide: true })), - legend: { - ...state.legend, - isVisible: false, - }, - }, - { xTitle, yTitle }, - { [layerId]: metadata } - ), + previewExpression: buildPreviewExpression(state, layerId, xValue, yValues, splitBy), }; +} + +function buildPreviewExpression( + state: XYState, + layerId: string, + xValue: TableSuggestionColumn, + yValues: TableSuggestionColumn[], + splitBy: TableSuggestionColumn | undefined +) { + return buildExpression( + { + ...state, + // only show changed layer in preview and hide axes + layers: state.layers + .filter(layer => layer.layerId === layerId) + .map(layer => ({ ...layer, hide: true })), + // hide legend for preview + legend: { + ...state.legend, + isVisible: false, + }, + }, + { [layerId]: collectColumnMetaData(xValue, yValues, splitBy) } + ); +} - return suggestion; +function getExistingLayer(currentState: XYState | undefined, layerId: string) { + return currentState && currentState.layers.find(layer => layer.layerId === layerId); +} + +function collectColumnMetaData( + xValue: TableSuggestionColumn, + yValues: TableSuggestionColumn[], + splitBy: TableSuggestionColumn | undefined +) { + const metadata: Record = {}; + [xValue, ...yValues, splitBy].forEach(col => { + if (col) { + metadata[col.columnId] = col.operation; + } + }); + return metadata; } From 30f27b5882ef96cb808abf7351e54cde27336544 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 21 Aug 2019 17:23:08 +0200 Subject: [PATCH 30/46] remove isMetric --- .../visualization.test.tsx | 1 - .../editor_frame/chart_switch.test.tsx | 4 --- .../indexpattern_plugin/datapanel.test.tsx | 4 --- .../dimension_panel/dimension_panel.test.tsx | 10 ------- .../indexpattern_plugin/indexpattern.test.ts | 5 ---- .../indexpattern_plugin/indexpattern.tsx | 3 +- .../indexpattern_plugin/layerpanel.test.tsx | 2 -- .../operation_definitions/count.tsx | 2 -- .../date_histogram.test.tsx | 4 --- .../operation_definitions/date_histogram.tsx | 2 -- .../filter_ratio.test.tsx | 1 - .../operation_definitions/filter_ratio.tsx | 2 -- .../operation_definitions/metrics.tsx | 2 -- .../operation_definitions/terms.test.tsx | 11 ------- .../operation_definitions/terms.tsx | 2 -- .../indexpattern_plugin/operations.test.ts | 4 --- .../indexpattern_plugin/state_helpers.test.ts | 29 ------------------- .../multi_column_editor.test.tsx | 1 - x-pack/legacy/plugins/lens/public/types.ts | 3 ++ .../xy_config_panel.test.tsx | 2 -- .../xy_config_panel.tsx | 2 +- 21 files changed, 5 insertions(+), 91 deletions(-) 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 39fcce7a2a90c..177dfc9577028 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 @@ -97,7 +97,6 @@ describe('Datatable Visualization', () => { const baseOperation: Operation = { dataType: 'string', isBucketed: true, - isMetric: false, label: '', }; expect(filterOperations({ ...baseOperation })).toEqual(true); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx index 353b53f74396f..5624553d92ddf 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.test.tsx @@ -206,7 +206,6 @@ describe('chart_switch', () => { label: '', dataType: 'string', isBucketed: true, - isMetric: false, }, }, { @@ -215,7 +214,6 @@ describe('chart_switch', () => { label: '', dataType: 'number', isBucketed: false, - isMetric: true, }, }, ], @@ -437,7 +435,6 @@ describe('chart_switch', () => { label: '', dataType: 'string', isBucketed: true, - isMetric: false, }, }, { @@ -446,7 +443,6 @@ describe('chart_switch', () => { label: '', dataType: 'number', isBucketed: false, - isMetric: true, }, }, ], 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 7f74fd8c1f0b8..dfd4adde48560 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 @@ -27,7 +27,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, - isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -42,7 +41,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - isMetric: true, operationType: 'avg', sourceField: 'memory', }, @@ -56,7 +54,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, - isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -71,7 +68,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - isMetric: true, operationType: 'avg', sourceField: 'bytes', }, 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 f45b674e0c19b..2ddfce6b7e0a5 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 @@ -89,7 +89,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -203,7 +202,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -245,7 +243,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -287,7 +284,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -372,7 +368,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -546,7 +541,6 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'avg', sourceField: 'bytes', @@ -584,7 +578,6 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'count', }, @@ -779,7 +772,6 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'count', }, @@ -862,7 +854,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -965,7 +956,6 @@ describe('IndexPatternDimensionPanel', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts index 8307b8e0ab828..336deef6147a3 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts @@ -153,7 +153,6 @@ describe('IndexPattern Data Source', () => { label: 'My Op', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -215,7 +214,6 @@ describe('IndexPattern Data Source', () => { label: 'Count of Documents', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -224,7 +222,6 @@ describe('IndexPattern Data Source', () => { label: 'Date', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -394,7 +391,6 @@ describe('IndexPattern Data Source', () => { const sampleColumn: IndexPatternColumn = { dataType: 'number', isBucketed: false, - isMetric: true, label: 'foo', operationType: 'max', sourceField: 'baz', @@ -445,7 +441,6 @@ describe('IndexPattern Data Source', () => { label: 'My Op', dataType: 'string', isBucketed: true, - isMetric: false, } as Operation); }); 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 388d4de25a792..047cc09f683eb 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -143,13 +143,12 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { }; export function columnToOperation(column: IndexPatternColumn): Operation { - const { dataType, label, isBucketed, isMetric, scale } = column; + const { dataType, label, isBucketed, scale } = column; return { label, dataType, isBucketed, scale, - isMetric, }; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx index 0faa6b4725896..46e381d69741b 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/layerpanel.test.tsx @@ -27,7 +27,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, - isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -42,7 +41,6 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, - isMetric: true, operationType: 'avg', sourceField: 'memory', }, 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 df77db93f7111..3e48968e81fdf 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 @@ -20,7 +20,6 @@ export const countOperation: OperationDefinition = { { dataType: 'number', isBucketed: false, - isMetric: true, scale: 'ratio', }, ]; @@ -34,7 +33,6 @@ export const countOperation: OperationDefinition = { operationType: 'count', suggestedPriority, isBucketed: false, - isMetric: true, scale: 'ratio', }; }, 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 299d5ca8250c7..8e94087f4a5fb 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 @@ -58,7 +58,6 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -77,7 +76,6 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -157,7 +155,6 @@ describe('date_histogram', () => { { dataType: 'date', isBucketed: true, - isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'dateField', @@ -200,7 +197,6 @@ describe('date_histogram', () => { { dataType: 'date', isBucketed: true, - isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'dateField', 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 8f0b0ff0393d9..e454c700bb8db 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 @@ -50,7 +50,6 @@ export const dateHistogramOperation: OperationDefinition { label: 'Filter Ratio', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'filter_ratio', diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx index 875827b080207..8af227b84b8de 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/filter_ratio.tsx @@ -28,7 +28,6 @@ export const filterRatioOperation: OperationDefinition({ { dataType: 'number', isBucketed: false, - isMetric: true, scale: 'ratio', }, ]; @@ -68,7 +67,6 @@ function buildMetricOperation({ suggestedPriority, sourceField: field ? field.name : '', isBucketed: false, - isMetric: true, scale: 'ratio', } as T; }, 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 def66db01a7fe..1f639907b79d0 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 @@ -31,7 +31,6 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -46,7 +45,6 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -93,7 +91,6 @@ describe('terms', () => { { dataType: 'string', isBucketed: true, - isMetric: false, scale: 'ordinal', }, ]); @@ -109,7 +106,6 @@ describe('terms', () => { { dataType: 'boolean', isBucketed: true, - isMetric: false, scale: 'ordinal', }, ]); @@ -162,7 +158,6 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -189,7 +184,6 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -205,7 +199,6 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -220,7 +213,6 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -246,7 +238,6 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -262,7 +253,6 @@ describe('terms', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -320,7 +310,6 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'filter_ratio', 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 a11b49f6ab784..ca0455c9372da 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 @@ -53,7 +53,6 @@ export const termsOperation: OperationDefinition = { { dataType: type, isBucketed: true, - isMetric: false, scale: 'ordinal', }, ]; @@ -86,7 +85,6 @@ export const termsOperation: OperationDefinition = { suggestedPriority, sourceField: field.name, isBucketed: true, - isMetric: false, params: { size: DEFAULT_SIZE, orderBy: existingMetricColumn 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 e4b976021341b..50436eb75b8f6 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 @@ -163,7 +163,6 @@ describe('getOperationTypesForField', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -235,7 +234,6 @@ describe('getOperationTypesForField', () => { "operationMetaData": Object { "dataType": "string", "isBucketed": true, - "isMetric": false, "scale": "ordinal", }, "operations": Array [ @@ -250,7 +248,6 @@ describe('getOperationTypesForField', () => { "operationMetaData": Object { "dataType": "date", "isBucketed": true, - "isMetric": false, "scale": "interval", }, "operations": Array [ @@ -265,7 +262,6 @@ describe('getOperationTypesForField', () => { "operationMetaData": Object { "dataType": "number", "isBucketed": false, - "isMetric": true, "scale": "ratio", }, "operations": Array [ 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 897af8bbc28ff..e44dea4340777 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 @@ -31,7 +31,6 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -56,7 +55,6 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -78,7 +76,6 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -103,7 +100,6 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -131,7 +127,6 @@ describe('state_helpers', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -178,7 +173,6 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'avg', @@ -188,7 +182,6 @@ describe('state_helpers', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'max', @@ -207,7 +200,6 @@ describe('state_helpers', () => { label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -240,7 +232,6 @@ describe('state_helpers', () => { label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -262,7 +253,6 @@ describe('state_helpers', () => { label: 'Date histogram of order_date', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -284,7 +274,6 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -300,7 +289,6 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'avg', @@ -320,7 +308,6 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'count', @@ -356,7 +343,6 @@ describe('state_helpers', () => { label: 'Value of timestamp', dataType: 'string', isBucketed: false, - isMetric: false, // Private operationType: 'date_histogram', @@ -376,7 +362,6 @@ describe('state_helpers', () => { label: 'Top Values of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -393,7 +378,6 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'avg', @@ -403,7 +387,6 @@ describe('state_helpers', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -423,7 +406,6 @@ describe('state_helpers', () => { label: 'Top Values of category', dataType: 'string', isBucketed: true, - isMetric: false, // Private operationType: 'terms', @@ -441,7 +423,6 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, - isMetric: true, // Private operationType: 'avg', @@ -452,7 +433,6 @@ describe('state_helpers', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -532,7 +512,6 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, - isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -545,7 +524,6 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'avg', sourceField: 'xxx', @@ -567,7 +545,6 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, - isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'fieldC', @@ -578,7 +555,6 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'avg', sourceField: 'fieldB', @@ -600,7 +576,6 @@ describe('state_helpers', () => { col1: { dataType: 'date', isBucketed: true, - isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'fieldD', @@ -631,7 +606,6 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, - isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -644,7 +618,6 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'avg', sourceField: 'fieldD', @@ -666,7 +639,6 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, - isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -679,7 +651,6 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, - isMetric: true, label: '', operationType: 'min', sourceField: 'fieldC', 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 index 08a94c2180ab9..012c27d3ce3ff 100644 --- 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 @@ -49,7 +49,6 @@ describe('MultiColumnEditor', () => { dataType: 'number', id, isBucketed: true, - isMetric: false, label: 'BaaaZZZ!', }; }, diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 6e2997d610201..00bb670839f81 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -203,6 +203,9 @@ export interface OperationMetadata { isBucketed: boolean; scale?: 'ordinal' | 'interval' | 'ratio'; // Extra meta-information like cardinality, color + // TODO currently it's not possible to differentiate between a field from a raw + // document and an aggregated metric which might be handy in some cases. Once we + // introduce a raw document datasource, this should be considered here. } export interface LensMultiTable { 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 cc4a9c80853bf..64ceddac2021d 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 @@ -148,7 +148,6 @@ describe('XYConfigPanel', () => { const exampleOperation: Operation = { dataType: 'number', isBucketed: false, - isMetric: true, label: 'bar', }; const bucketedOps: Operation[] = [ @@ -187,7 +186,6 @@ describe('XYConfigPanel', () => { const exampleOperation: Operation = { dataType: 'number', isBucketed: false, - isMetric: true, label: 'bar', }; const ops: Operation[] = [ 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 835b2d9c0bccb..249ea6b5b72ea 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 @@ -28,7 +28,7 @@ import { NativeRenderer } from '../native_renderer'; import { MultiColumnEditor } from '../multi_column_editor'; import { generateId } from '../id_generator'; -const isNumericMetric = (op: OperationMetadata) => op.isMetric && op.dataType === 'number'; +const isNumericMetric = (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; type UnwrapArray = T extends Array ? P : T; From 16c3b034267d5fa86d437dad6c44842f6a2c9223 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 21 Aug 2019 17:49:30 +0200 Subject: [PATCH 31/46] area as default on time dimension --- .../xy_suggestions.test.ts | 136 +++++++++--------- .../xy_visualization_plugin/xy_suggestions.ts | 2 +- 2 files changed, 69 insertions(+), 69 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 123aa87e0fd2b..7182d16e4ea66 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 @@ -118,17 +118,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": "area", + "splitAccessor": "aaa", + "x": "date", + "y": Array [ + "bytes", + ], + }, + ] + `); }); test('does not suggest multiple splits', () => { @@ -168,18 +168,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": "area", + "splitAccessor": "product", + "x": "date", + "y": Array [ + "price", + "quantity", + ], + }, + ] + `); }); test('uses datasource provided title if available', () => { @@ -349,29 +349,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": "area", + "splitAccessor": "bbb", + "x": "date", + "y": Array [ + "price", + ], + }, + ], + Array [ + Object { + "seriesType": "bar", + "splitAccessor": "ccc", + "x": "country", + "y": Array [ + "count", + ], + }, + ], + ] + `); }); test('handles two numeric values', () => { @@ -389,17 +389,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', () => { @@ -427,17 +427,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', () => { 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 99e6fd2bfd941..b88255d7bd1e1 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 @@ -177,7 +177,7 @@ function getSeriesType( return ( (oldLayer && oldLayer.seriesType) || (currentState && currentState.preferredSeriesType) || - (splitBy && xValue.operation.dataType === 'date' ? 'line' : 'bar') + (xValue.operation.dataType === 'date' ? 'area' : 'bar') ); } From d5452fde972304c93bca26cd01481e89c67ffa64 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 21 Aug 2019 18:08:15 +0200 Subject: [PATCH 32/46] fix bug in area chart for time --- .../xy_visualization_plugin/xy_suggestions.ts | 55 +++++++++++++------ 1 file changed, 37 insertions(+), 18 deletions(-) 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 b88255d7bd1e1..d4633dbebb947 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 @@ -121,7 +121,7 @@ function getSuggestion( tableLabel?: string ): VisualizationSuggestion { const title = getSuggestionTitle(yValues, xValue, tableLabel); - const seriesType: SeriesType = getSeriesType(currentState, layerId, splitBy, xValue); + const seriesType: SeriesType = getSeriesType(currentState, layerId, xValue, changeType); const isHorizontal = currentState ? currentState.isHorizontal : false; const options = { @@ -141,18 +141,17 @@ function getSuggestion( if (currentState && changeType === 'unchanged') { if (xValue.operation.scale && xValue.operation.scale !== 'ordinal') { // change chart type for interval or ratio scales on x axis - const newSeriesType = seriesType === 'area' ? 'bar' : 'area'; + const newSeriesType = flipSeriesType(seriesType); return buildSuggestion({ ...options, seriesType: newSeriesType, - title: - newSeriesType === 'area' - ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { - defaultMessage: 'Area chart', - }) - : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { - defaultMessage: 'Bar chart', - }), + title: newSeriesType.startsWith('area') + ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { + defaultMessage: 'Area chart', + }) + : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), }); } else { // flip between horizontal/vertical for ordinal scales @@ -167,18 +166,38 @@ function getSuggestion( } } +function flipSeriesType(oldSeriesType: SeriesType) { + switch (oldSeriesType) { + case 'area': + return 'bar'; + case 'area_stacked': + return 'bar_stacked'; + case 'bar': + return 'area'; + case 'bar_stacked': + return 'area_stacked'; + default: + return 'bar'; + } +} + function getSeriesType( currentState: XYState | undefined, layerId: string, - splitBy: TableSuggestionColumn | undefined, - xValue: TableSuggestionColumn + xValue: TableSuggestionColumn, + changeType: TableChangeType ): SeriesType { - const oldLayer = getExistingLayer(currentState, layerId); - return ( - (oldLayer && oldLayer.seriesType) || - (currentState && currentState.preferredSeriesType) || - (xValue.operation.dataType === 'date' ? 'area' : 'bar') - ); + const defaultType = xValue.operation.dataType === 'date' ? 'area' : 'bar'; + if (changeType === 'initial') { + return defaultType; + } else { + const oldLayer = getExistingLayer(currentState, layerId); + return ( + (oldLayer && oldLayer.seriesType) || + (currentState && currentState.preferredSeriesType) || + defaultType + ); + } } function getSuggestionTitle( From ed1122933f4c07f0055d079f6bdcb8c644d9211a Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 23 Aug 2019 11:41:34 +0200 Subject: [PATCH 33/46] remove title form layer --- x-pack/legacy/plugins/lens/public/app_plugin/app.tsx | 2 ++ .../__snapshots__/xy_visualization.test.ts.snap | 3 --- .../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 | 5 ----- .../lens/public/xy_visualization_plugin/xy_config_panel.tsx | 1 - .../public/xy_visualization_plugin/xy_expression.test.tsx | 2 -- .../public/xy_visualization_plugin/xy_suggestions.test.ts | 3 --- .../lens/public/xy_visualization_plugin/xy_suggestions.ts | 2 -- .../public/xy_visualization_plugin/xy_visualization.test.ts | 2 -- .../lens/public/xy_visualization_plugin/xy_visualization.tsx | 1 - 11 files changed, 5 insertions(+), 22 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 95bbda1102242..21fe885b9456a 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -152,6 +152,8 @@ export function App({ {}} + isDirty={false} onSubmit={({ dateRange, query }) => { setState({ ...state, 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 b034e73ba914d..12902f548e45b 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 @@ -35,9 +35,6 @@ Object { "splitAccessor": Array [ "d", ], - "title": Array [ - "Baz", - ], "xAccessor": Array [ "a", ], 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 ccd8132a4a8b5..bbb27bae778b2 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 @@ -163,7 +163,6 @@ export const buildExpression = ( arguments: { layerId: [layer.layerId], - title: [layer.title], hide: [Boolean(layer.hide)], xAccessor: [layer.xAccessor], 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 ee51f9ce5fa8e..742cc36be4ea6 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 @@ -177,13 +177,14 @@ export const layerConfig: ExpressionFunction< export type SeriesType = 'bar' | 'line' | 'area' | 'bar_stacked' | 'area_stacked'; -export type LayerConfig = AxisConfig & { +export interface LayerConfig { + hide?: boolean; layerId: string; xAccessor: string; accessors: string[]; seriesType: SeriesType; splitAccessor: string; -}; +} export type LayerArgs = LayerConfig & { columnToLabel?: string; // Actually a JSON key-value pair 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 64ceddac2021d..305cda47b3e19 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 @@ -35,7 +35,6 @@ describe('XYConfigPanel', () => { layerId: 'first', splitAccessor: 'baz', xAccessor: 'foo', - title: 'X', accessors: ['bar'], }, ], @@ -310,7 +309,6 @@ describe('XYConfigPanel', () => { layerId: 'first', splitAccessor: 'baz', xAccessor: 'foo', - title: 'X', accessors: ['bar'], }, { @@ -318,7 +316,6 @@ describe('XYConfigPanel', () => { layerId: 'second', splitAccessor: 'baz', xAccessor: 'foo', - title: 'Y', accessors: ['bar'], }, ], @@ -361,7 +358,6 @@ describe('XYConfigPanel', () => { layerId: 'first', splitAccessor: 'baz', xAccessor: 'foo', - title: 'X', accessors: ['bar'], }, { @@ -369,7 +365,6 @@ describe('XYConfigPanel', () => { layerId: 'second', splitAccessor: 'baz', xAccessor: 'foo', - title: 'Y', accessors: ['bar'], }, ], 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 249ea6b5b72ea..07ce118673bd1 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 @@ -49,7 +49,6 @@ function newLayerState(seriesType: SeriesType, layerId: string): LayerConfig { seriesType, xAccessor: generateId(), accessors: [generateId()], - title: '', 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 7eab6ce109e3d..0ac286c7bb83c 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 @@ -46,7 +46,6 @@ function sampleArgs() { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - title: 'A and B', splitAccessor: 'd', columnToLabel: '{"a": "Label A", "b": "Label B", "d": "Label D"}', xScaleType: 'ordinal', @@ -79,7 +78,6 @@ describe('xy_expression', () => { seriesType: 'line', xAccessor: 'c', accessors: ['a', 'b'], - title: 'A and B', splitAccessor: 'd', xScaleType: 'linear', yScaleType: 'linear', 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 7182d16e4ea66..ceab87e9d9516 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 @@ -222,7 +222,6 @@ describe('xy_suggestions', () => { seriesType: 'bar', splitAccessor: 'product', xAccessor: 'date', - title: '', }, ], }, @@ -261,7 +260,6 @@ describe('xy_suggestions', () => { seriesType: 'bar', splitAccessor: 'product', xAccessor: 'date', - title: '', }, ], }; @@ -301,7 +299,6 @@ describe('xy_suggestions', () => { seriesType: 'bar', splitAccessor: 'dummyCol', xAccessor: 'product', - title: '', }, ], }; 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 d4633dbebb947..cacdfc6c56a24 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 @@ -263,8 +263,6 @@ function buildSuggestion({ xAccessor: xValue.columnId, splitAccessor: splitBy ? splitBy.columnId : generateId(), accessors: yValues.map(col => col.columnId), - // TODO check whether we need this - title: '', }; const state: State = { 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 5fb41780838b6..8d9092f63f59b 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 @@ -24,7 +24,6 @@ function exampleState(): State { layerId: 'first', seriesType: 'area', splitAccessor: 'd', - title: 'Baz', xAccessor: 'a', accessors: ['b', 'c'], }, @@ -60,7 +59,6 @@ describe('xy_visualization', () => { "seriesType": "bar", "showGridlines": false, "splitAccessor": "test-id2", - "title": "", "xAccessor": "test-id3", }, ], 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 f0b3c1a79ed2b..15a34abf12651 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 @@ -86,7 +86,6 @@ export const xyVisualization: Visualization = { seriesType: defaultSeriesType, showGridlines: false, splitAccessor: generateId(), - title: '', xAccessor: generateId(), }, ], From 694e845732c2595a89bb9ec6f63c11edb7028d0d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 23 Aug 2019 16:29:14 +0200 Subject: [PATCH 34/46] handle state in app --- .../plugins/lens/public/app_plugin/app.tsx | 28 ++++++++++++++++++- .../editor_frame/index.scss | 6 ++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 95bbda1102242..68734c2296922 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -27,6 +27,25 @@ interface State { query: Query; indexPatternTitles: string[]; persistedDoc?: Document; + localQueryBarState: { + query?: Query; + dateRange?: { + from: string; + to: string; + }; + }; +} + +function isLocalStateDirty( + localState: State['localQueryBarState'], + query: Query, + dateRange: State['dateRange'] +) { + return Boolean( + (localState.query && query && localState.query.query !== query.query) || + (localState.dateRange && dateRange.fromDate !== localState.dateRange.from) || + (localState.dateRange && dateRange.toDate !== localState.dateRange.to) + ); } export function App({ @@ -57,6 +76,7 @@ export function App({ toDate: timeDefaults.to, }, indexPatternTitles: [], + localQueryBarState: {}, }); const lastKnownDocRef = useRef(undefined); @@ -152,7 +172,8 @@ export function App({ { + onSubmit={payload => { + const { dateRange, query } = payload; setState({ ...state, dateRange: { @@ -160,8 +181,13 @@ export function App({ toDate: dateRange.to, }, query: query || state.query, + localQueryBarState: payload, }); }} + onChange={uncommitedQueryBarState => { + setState({ ...state, localQueryBarState: uncommitedQueryBarState }); + }} + isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)} appName={'lens'} indexPatterns={state.indexPatternTitles} store={store} diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss index 72b5f1eb79638..33571793a721c 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/index.scss @@ -115,12 +115,14 @@ $lnsPanelMinWidth: $euiSize * 18; width: 100%; height: 100%; display: flex; - align-items: center; - justify-content: center; overflow-x: hidden; padding: $euiSize; } +.lnsExpressionOutput > * { + flex: 1; +} + .lnsTitleInput { width: 100%; min-width: 100%; From 54ad77cf24b49854df1846bcae00cf746ecce581 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 23 Aug 2019 16:43:16 +0200 Subject: [PATCH 35/46] fix isMetric usages --- .../operation_definitions/date_histogram.test.tsx | 3 +++ .../indexpattern_plugin/operation_definitions/terms.test.tsx | 1 + 2 files changed, 4 insertions(+) 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 123a728f51b6f..f7a758febd8d2 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 @@ -98,6 +98,7 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -197,6 +198,7 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, + isMetric: false, dataType: 'date', params: { interval: 'd', @@ -217,6 +219,7 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, + isMetric: false, dataType: 'date', params: { interval: 'auto', 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 7514e5688f9e2..4ce5d4bd32223 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 @@ -84,6 +84,7 @@ describe('terms', () => { label: 'Top values of source', isBucketed: true, dataType: 'string', + isMetric: false, params: { size: 5, orderBy: { From 82a4fc86a596d8b87f68c834e8473621ff1399cc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 23 Aug 2019 16:53:08 +0200 Subject: [PATCH 36/46] fix integration tests --- .../test/api_integration/apis/xpack_main/features/features.ts | 1 + .../test/saved_object_api_integration/common/suites/export.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/xpack_main/features/features.ts b/x-pack/test/api_integration/apis/xpack_main/features/features.ts index d803dcad90ac1..6fc7c842697ef 100644 --- a/x-pack/test/api_integration/apis/xpack_main/features/features.ts +++ b/x-pack/test/api_integration/apis/xpack_main/features/features.ts @@ -130,6 +130,7 @@ export default function({ getService }: FtrProviderContext) { 'canvas', 'code', 'infrastructure', + 'lens', 'logs', 'maps', 'uptime', diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index d109f47da3f52..d7d1a99e63e02 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -60,7 +60,7 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest Date: Fri, 23 Aug 2019 18:27:47 +0200 Subject: [PATCH 37/46] fix type errors --- .../operation_definitions/date_histogram.test.tsx | 3 --- .../indexpattern_plugin/operation_definitions/terms.test.tsx | 1 - 2 files changed, 4 deletions(-) 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 9c1b86146b7b7..7920039e68220 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 @@ -96,7 +96,6 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -196,7 +195,6 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, - isMetric: false, dataType: 'date', params: { interval: 'd', @@ -217,7 +215,6 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, - isMetric: false, dataType: 'date', params: { interval: 'auto', 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 99229e20a54c3..5226ac9c97d8f 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 @@ -82,7 +82,6 @@ describe('terms', () => { label: 'Top values of source', isBucketed: true, dataType: 'string', - isMetric: false, params: { size: 5, orderBy: { From 3a6a23cad57afac6239db1cf45a87a25051b11af Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 26 Aug 2019 12:22:30 +0200 Subject: [PATCH 38/46] fix date handling on submit --- .../plugins/lens/public/app_plugin/app.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index 68734c2296922..e198d72f31f41 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -76,7 +76,13 @@ export function App({ toDate: timeDefaults.to, }, indexPatternTitles: [], - localQueryBarState: {}, + localQueryBarState: { + query: { query: '', language }, + dateRange: { + from: timeDefaults.from, + to: timeDefaults.to, + }, + }, }); const lastKnownDocRef = useRef(undefined); @@ -184,8 +190,8 @@ export function App({ localQueryBarState: payload, }); }} - onChange={uncommitedQueryBarState => { - setState({ ...state, localQueryBarState: uncommitedQueryBarState }); + onChange={localQueryBarState => { + setState({ ...state, localQueryBarState }); }} isDirty={isLocalStateDirty(state.localQueryBarState, state.query, state.dateRange)} appName={'lens'} @@ -193,9 +199,13 @@ export function App({ store={store} showDatePicker={true} showQueryInput={true} - query={state.query} - dateRangeFrom={state.dateRange && state.dateRange.fromDate} - dateRangeTo={state.dateRange && state.dateRange.toDate} + query={state.localQueryBarState.query} + dateRangeFrom={ + state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.from + } + dateRangeTo={ + state.localQueryBarState.dateRange && state.localQueryBarState.dateRange.to + } uiSettings={uiSettings} />
From 7a31208910bcd43948fd27629a2ae74eb1667ead Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 26 Aug 2019 14:12:49 +0200 Subject: [PATCH 39/46] add new suggestion types --- .../indexpattern_suggestions.test.tsx | 73 +++--------- .../indexpattern_suggestions.ts | 112 ++++++++++++------ 2 files changed, 93 insertions(+), 92 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 130efb3daa3ca..6df8661734851 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -981,14 +981,14 @@ describe('IndexPattern Data Source suggestions', () => { dataType: 'number', isBucketed: false, operationType: 'avg', - sourceField: 'op', + sourceField: 'bytes', scale: 'ratio', }, }, }, }, }); - expect(indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state)).toEqual([ + expect(indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state)[0]).toEqual( expect.objectContaining({ table: { datasourceSuggestionId: 0, @@ -1017,28 +1017,8 @@ describe('IndexPattern Data Source suggestions', () => { ], layerId: 'first', }, - }), - expect.objectContaining({ - table: { - datasourceSuggestionId: 1, - isMultiRow: false, - changeType: 'unchanged', - label: undefined, - columns: [ - { - columnId: 'col1', - operation: { - label: 'My Op', - dataType: 'number', - isBucketed: false, - scale: 'ratio', - }, - }, - ], - layerId: 'first', - }, - }), - ]); + }) + ); }); it('adds date histogram over default time field for tables without time dimension', async () => { @@ -1140,13 +1120,11 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }); - // the single suggestion is the current unchanged state of the data table - expect( - indexPatternDatasource.getDatasourceSuggestionsFromCurrentState({ - ...state, - indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } }, - }).length - ).toEqual(1); + const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState({ + ...state, + indexPatterns: { 1: { ...state.indexPatterns['1'], timeFieldName: undefined } }, + }); + suggestions.forEach(suggestion => expect(suggestion.table.columns.length).toBe(1)); }); it('returns simplified versions of table with more than 2 columns', () => { @@ -1283,7 +1261,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions.length).toBe(8); }); - it('returns uses a different operation on field based columns for only metric suggestions', () => { + it('returns an only metric version of a given table', () => { const state: IndexPatternPrivateState = { currentIndexPatternId: '1', indexPatterns: { @@ -1336,10 +1314,10 @@ describe('IndexPattern Data Source suggestions', () => { }; const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state); - expect(suggestions[1].table.columns[0].operation.label).toBe('Sum of field1'); + expect(suggestions[1].table.columns[0].operation.label).toBe('Average of field1'); }); - it('reuses document based column for only metric suggestions', () => { + it('returns an alternative metric for an only-metric table', () => { const state: IndexPatternPrivateState = { currentIndexPatternId: '1', indexPatterns: { @@ -1353,12 +1331,6 @@ describe('IndexPattern Data Source suggestions', () => { aggregatable: true, searchable: true, }, - { - name: 'field2', - type: 'date', - aggregatable: true, - searchable: true, - }, ], }, }, @@ -1367,31 +1339,22 @@ describe('IndexPattern Data Source suggestions', () => { ...persistedState.layers.first, columns: { col1: { - label: 'Date histogram', - dataType: 'date', - isBucketed: true, - - operationType: 'date_histogram', - sourceField: 'field2', - params: { - interval: 'd', - }, - }, - col2: { - label: 'Count', + label: 'Average of field1', dataType: 'number', isBucketed: false, - operationType: 'count', + operationType: 'avg', + sourceField: 'field1', }, }, - columnOrder: ['col1', 'col2'], + columnOrder: ['col1'], }, }, }; const suggestions = indexPatternDatasource.getDatasourceSuggestionsFromCurrentState(state); - expect(suggestions[1].table.columns[0].operation.label).toBe('Count'); + expect(suggestions[0].table.columns.length).toBe(1); + expect(suggestions[0].table.columns[0].operation.label).toBe('Sum of field1'); }); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 906b2d5204e4f..e832e7709ef29 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -14,6 +14,7 @@ import { IndexPatternLayer, IndexPatternPrivateState, IndexPattern, + IndexPatternColumn, } from './indexpattern'; import { buildColumn, getOperationTypesForField, operationDefinitionMap } from './operations'; import { hasField } from './utils'; @@ -323,15 +324,14 @@ export function getDatasourceSuggestionsFromCurrentState( .filter(([_id, layer]) => layer.columnOrder.length) .map(([layerId, layer], index) => { const indexPattern = state.indexPatterns[layer.indexPatternId]; - const onlyMetric = layer.columnOrder.every(columnId => !layer.columns[columnId].isBucketed); - const onlyBucket = layer.columnOrder.every(columnId => layer.columns[columnId].isBucketed); + const [buckets, metrics] = separateBucketColumns(layer); const timeDimension = layer.columnOrder.find( columnId => layer.columns[columnId].isBucketed && layer.columns[columnId].dataType === 'date' ); const suggestions: Array> = []; - if (onlyBucket) { + if (metrics.length === 0) { // intermediary chart without metric, don't try to suggest reduced versions suggestions.push( buildSuggestion({ @@ -342,12 +342,13 @@ export function getDatasourceSuggestionsFromCurrentState( changeType: 'unchanged', }) ); - } else if (onlyMetric) { + } else if (buckets.length === 0) { if (indexPattern.timeFieldName) { // suggest current metric over time if there is a default time field suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId)); } - // suggest only metric + suggestions.push(...createAlternativeMetricSuggestions(indexPattern, layerId, state)); + // also suggest simple current state suggestions.push( buildSuggestion({ state, @@ -365,6 +366,10 @@ export function getDatasourceSuggestionsFromCurrentState( // and no time dimension yet suggestions.push(createSuggestionWithDefaultDateHistogram(state, layerId)); } + + if (buckets.length === 2) { + suggestions.push(createChangedNestingSuggestion(state, layerId)); + } } return suggestions; @@ -377,6 +382,70 @@ export function getDatasourceSuggestionsFromCurrentState( ); } +function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId: string) { + const layer = state.layers[layerId]; + const [firstBucket, secondBucket, ...rest] = layer.columnOrder; + const updatedLayer = { ...layer, columnOrder: [secondBucket, firstBucket, ...rest] }; + return buildSuggestion({ + state, + layerId, + isMultiRow: true, + updatedLayer, + label: i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', { + defaultMessage: 'Nest within {operation}', + values: { + operation: layer.columns[secondBucket].label, + }, + }), + changeType: 'extended', + }); +} + +function createAlternativeMetricSuggestions( + indexPattern: IndexPattern, + layerId: string, + state: IndexPatternPrivateState +) { + const layer = state.layers[layerId]; + const suggestions: Array> = []; + layer.columnOrder.forEach(columnId => { + const column = layer.columns[columnId]; + if (hasField(column)) { + const field = indexPattern.fields.find( + ({ name }) => hasField(column) && column.sourceField === name + )!; + const alternativeMetricOperations = getOperationTypesForField(field).filter( + operationType => operationType !== column.operationType + ); + if (alternativeMetricOperations.length > 0) { + const newId = generateId(); + const newColumn = buildColumn({ + op: alternativeMetricOperations[0], + columns: layer.columns, + indexPattern, + layerId, + field, + suggestedPriority: undefined, + }); + const updatedLayer = buildLayerByColumnOrder( + { ...layer, columns: { [newId]: newColumn } }, + [newId] + ); + suggestions.push( + buildSuggestion({ + state, + layerId, + isMultiRow: false, + updatedLayer, + changeType: 'initial', + }) + ); + } + } + }); + return suggestions; +} + function createSuggestionWithDefaultDateHistogram( state: IndexPatternPrivateState, layerId: string @@ -411,7 +480,6 @@ function createSuggestionWithDefaultDateHistogram( function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layerId: string) { const layer = state.layers[layerId]; - const indexPattern = state.indexPatterns[layer.indexPatternId]; const [availableBucketedColumns, availableMetricColumns] = separateBucketColumns(layer); @@ -437,37 +505,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer .concat( availableMetricColumns.map(columnId => { // build suggestions with only metrics - let column = layer.columns[columnId]; - // if field based, suggest different metric - if (hasField(column)) { - const field = indexPattern.fields.find( - ({ name }) => hasField(column) && column.sourceField === name - )!; - const alternativeMetricOperations = getOperationTypesForField(field).filter( - operationType => operationType !== column.operationType - ); - if (alternativeMetricOperations.length > 0) { - column = buildColumn({ - op: alternativeMetricOperations[0], - columns: layer.columns, - indexPattern, - layerId, - field, - suggestedPriority: undefined, - }); - } - return buildLayerByColumnOrder( - { - ...layer, - columns: { - [columnId]: column, - }, - }, - [columnId] - ); - } else { - return buildLayerByColumnOrder(layer, [columnId]); - } + return buildLayerByColumnOrder(layer, [columnId]); }) ) .map(updatedLayer => { From 4729ae4af958f2623f26eb1a5919ea77bd3bc699 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 26 Aug 2019 14:19:46 +0200 Subject: [PATCH 40/46] fix test --- x-pack/legacy/plugins/lens/public/app_plugin/app.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx index e198d72f31f41..2b768e621c17d 100644 --- a/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx +++ b/x-pack/legacy/plugins/lens/public/app_plugin/app.tsx @@ -98,6 +98,10 @@ export function App({ isLoading: false, persistedDoc: doc, query: doc.state.query, + localQueryBarState: { + ...state.localQueryBarState, + query: doc.state.query, + }, indexPatternTitles: doc.state.datasourceMetaData.filterableIndexPatterns.map( ({ title }) => title ), From f35c722c9ae72a152205cb0a6d60c9f2a2ee379f Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 26 Aug 2019 14:34:18 +0200 Subject: [PATCH 41/46] do not suggest single tables --- .../public/datatable_visualization_plugin/visualization.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 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 934a91021ef07..5e8e7b951010d 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 @@ -173,8 +173,8 @@ export const datatableVisualization: Visualization< ], }, previewIcon: 'visTable', - // dont show suggestions for reduced versions - hide: table.changeType === 'reduced', + // dont show suggestions for reduced versions or single-line tables + hide: table.changeType === 'reduced' || !table.isMultiRow, }; }) ); From 25cdc68d5842503649d5fb8d6af1db6610fad115 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 26 Aug 2019 15:39:05 +0200 Subject: [PATCH 42/46] remove unused import --- .../lens/public/indexpattern_plugin/indexpattern_suggestions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index e832e7709ef29..bcc18defa8063 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -14,7 +14,6 @@ import { IndexPatternLayer, IndexPatternPrivateState, IndexPattern, - IndexPatternColumn, } from './indexpattern'; import { buildColumn, getOperationTypesForField, operationDefinitionMap } from './operations'; import { hasField } from './utils'; From 5cbca866cb546975ae0c33bccbef18bf986f1d4d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Aug 2019 12:59:08 +0200 Subject: [PATCH 43/46] switch order of appending new string column --- .../indexpattern_suggestions.test.tsx | 4 ++-- .../indexpattern_plugin/indexpattern_suggestions.ts | 11 +++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx index 6df8661734851..d7c61e1f1c73d 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.test.tsx @@ -678,7 +678,7 @@ describe('IndexPattern Data Source suggestions', () => { expect(suggestions).toHaveLength(0); }); - it('prepends a terms column on string field', () => { + it('appends a terms column after the last existing bucket column on string field', () => { const suggestions = indexPatternDatasource.getDatasourceSuggestionsForField(initialState, { field: { name: 'dest', type: 'string', aggregatable: true, searchable: true }, indexPatternId: '1', @@ -690,7 +690,7 @@ describe('IndexPattern Data Source suggestions', () => { layers: { previousLayer: initialState.layers.previousLayer, currentLayer: expect.objectContaining({ - columnOrder: ['newId', 'col1', 'col2'], + columnOrder: ['col1', 'newId', 'col2'], columns: { ...initialState.layers.currentLayer.columns, newId: expect.objectContaining({ diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index bcc18defa8063..95f47ee3d317a 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -179,6 +179,7 @@ function addFieldAsBucketOperation( suggestedPriority: undefined, field, }); + const [buckets, metrics] = separateBucketColumns(layer); const newColumnId = generateId(); const updatedColumns = { ...layer.columns, @@ -186,7 +187,7 @@ function addFieldAsBucketOperation( }; let updatedColumnOrder: string[] = []; if (applicableBucketOperation === 'terms') { - updatedColumnOrder = [newColumnId, ...layer.columnOrder]; + updatedColumnOrder = [...buckets, newColumnId, ...metrics]; } else { const oldDateHistogramColumn = layer.columnOrder.find( columnId => layer.columns[columnId].operationType === 'date_histogram' @@ -197,13 +198,7 @@ function addFieldAsBucketOperation( columnId !== oldDateHistogramColumn ? columnId : newColumnId ); } else { - const bucketedColumns = layer.columnOrder.filter( - columnId => layer.columns[columnId].isBucketed - ); - const metricColumns = layer.columnOrder.filter( - columnId => !layer.columns[columnId].isBucketed - ); - updatedColumnOrder = [...bucketedColumns, newColumnId, ...metricColumns]; + updatedColumnOrder = [...buckets, newColumnId, ...metrics]; } } return { From aad94033701d3c6560bf1e7b75db56f86b4335f8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 27 Aug 2019 14:24:51 +0200 Subject: [PATCH 44/46] resolve merge conflicts --- .../operation_definitions/date_histogram.test.tsx | 3 --- .../indexpattern_plugin/operation_definitions/terms.test.tsx | 1 - 2 files changed, 4 deletions(-) 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 9c1b86146b7b7..7920039e68220 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 @@ -96,7 +96,6 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, - isMetric: false, // Private operationType: 'date_histogram', @@ -196,7 +195,6 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, - isMetric: false, dataType: 'date', params: { interval: 'd', @@ -217,7 +215,6 @@ describe('date_histogram', () => { sourceField: 'timestamp', label: 'Date over timestamp', isBucketed: true, - isMetric: false, dataType: 'date', params: { interval: 'auto', 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 99229e20a54c3..5226ac9c97d8f 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 @@ -82,7 +82,6 @@ describe('terms', () => { label: 'Top values of source', isBucketed: true, dataType: 'string', - isMetric: false, params: { size: 5, orderBy: { From f9bf7b12f82fbcbaa0b49cd8eccea1b6dcc3e7a6 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 28 Aug 2019 11:26:46 +0200 Subject: [PATCH 45/46] fix merge conflicts --- .../indexpattern_plugin/operations/operations.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts index 97493b578adba..0b5a1dd903462 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operations/operations.ts @@ -157,6 +157,13 @@ function getPossibleOperationForField( : undefined; } +function getDefinition(findFunction: (definition: GenericOperationDefinition) => boolean) { + const candidates = operationDefinitions.filter(findFunction); + return candidates.reduce((a, b) => + (a.priority || Number.NEGATIVE_INFINITY) > (b.priority || Number.NEGATIVE_INFINITY) ? a : b + ); +} + /** * Changes the field of the passed in colum. To do so, this method uses the `onFieldChange` function of * the operation definition of the column. Returns a new column object with the field changed. @@ -211,12 +218,12 @@ export function buildColumn({ if (op) { operationDefinition = operationDefinitionMap[op]; } else if (asDocumentOperation) { - operationDefinition = operationDefinitions.find(definition => - getPossibleOperationForDocument(definition, indexPattern) + operationDefinition = getDefinition(definition => + Boolean(getPossibleOperationForDocument(definition, indexPattern)) ); } else if (field) { - operationDefinition = operationDefinitions.find(definition => - getPossibleOperationForField(definition, field) + operationDefinition = getDefinition(definition => + Boolean(getPossibleOperationForField(definition, field)) ); } From 8b7a9e0b7f9b47cf0a91b43ccb5c54605ebb8c75 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 30 Aug 2019 14:50:36 +0200 Subject: [PATCH 46/46] code review --- .../visualization.tsx | 2 +- .../editor_frame/chart_switch.tsx | 31 +++++--- .../editor_frame/expression_helpers.ts | 18 +++-- .../editor_frame/suggestion_panel.tsx | 62 +++++++++------- .../indexpattern_suggestions.ts | 73 +++++++++---------- .../xy_visualization_plugin/xy_suggestions.ts | 53 +++++++------- 6 files changed, 127 insertions(+), 112 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 5e8e7b951010d..8f9e736499069 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 @@ -161,7 +161,7 @@ export const datatableVisualization: Visualization< return { title, - // largest possible table will have a score of 0.2, less columns reduce score + // largest possible table will have a score of 0.2, fewer columns reduce score score: (table.columns.length / maxColumnCount) * 0.2, datasourceSuggestionId: table.datasourceSuggestionId, state: { diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx index d4ae425c66792..770f402bdf5f8 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_plugin/editor_frame/chart_switch.tsx @@ -18,7 +18,7 @@ import { flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; import { Visualization, FramePublicAPI, Datasource } from '../../types'; import { Action } from './state_management'; -import { getSuggestions, switchToSuggestion } from './suggestion_helpers'; +import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers'; interface VisualizationSelection { visualizationId: string; @@ -106,17 +106,7 @@ export function ChartSwitch(props: Props) { ([_layerId, datasource]) => datasource.getTableSpec().length > 0 ); - const topSuggestion = getSuggestions({ - datasourceMap: props.datasourceMap, - datasourceStates: props.datasourceStates, - visualizationMap: { [visualizationId]: newVisualization }, - activeVisualizationId: props.visualizationId, - visualizationState: props.visualizationState, - }).filter(suggestion => { - // don't use extended versions of current data table on switching between visualizations - // to avoid confusing the user. - return suggestion.changeType !== 'extended'; - })[0]; + const topSuggestion = getTopSuggestion(props, visualizationId, newVisualization); let dataLoss: VisualizationSelection['dataLoss']; @@ -246,3 +236,20 @@ export function ChartSwitch(props: Props) {
); } +function getTopSuggestion( + props: Props, + visualizationId: string, + newVisualization: Visualization +): Suggestion | undefined { + return getSuggestions({ + datasourceMap: props.datasourceMap, + datasourceStates: props.datasourceStates, + visualizationMap: { [visualizationId]: newVisualization }, + activeVisualizationId: props.visualizationId, + visualizationState: props.visualizationState, + }).filter(suggestion => { + // don't use extended versions of current data table on switching between visualizations + // to avoid confusing the user. + return suggestion.changeType !== 'extended'; + })[0]; +} 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 a16024e58b254..1b71f28260088 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 @@ -63,7 +63,7 @@ export function prependDatasourceExpression( } export function prependKibanaContext( - expression: Ast | string | null, + expression: Ast | string, { timeRange, query, @@ -73,8 +73,7 @@ export function prependKibanaContext( query?: Query; filters?: Filter[]; } -): Ast | null { - if (!expression) return null; +): Ast { const parsedExpression = typeof expression === 'string' ? fromExpression(expression) : expression; return { @@ -131,8 +130,15 @@ export function buildExpression({ }, }; - return prependKibanaContext( - prependDatasourceExpression(visualizationExpression, datasourceMap, datasourceStates), - expressionContext + const completeExpression = prependDatasourceExpression( + visualizationExpression, + datasourceMap, + datasourceStates ); + + if (completeExpression) { + return prependKibanaContext(completeExpression, expressionContext); + } else { + return null; + } } 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 65c880232d0a0..8c63d9d03806d 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 @@ -7,7 +7,7 @@ import React, { useState, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiIcon, EuiTitle, EuiPanel, EuiIconTip, EuiToolTip } from '@elastic/eui'; -import { toExpression } from '@kbn/interpreter/common'; +import { toExpression, Ast } from '@kbn/interpreter/common'; import { i18n } from '@kbn/i18n'; import { Action } from './state_management'; import { Datasource, Visualization, FramePublicAPI } from '../../types'; @@ -141,45 +141,49 @@ function InnerSuggestionPanel({
- {suggestions.map((suggestion: Suggestion) => { - const previewExpression = preparePreviewExpression( - suggestion, - datasourceMap, - datasourceStates, - frame - ); - return ( - - ); - })} + {suggestions.map((suggestion: Suggestion) => ( + + ))}
); } + function preparePreviewExpression( - suggestion: Suggestion, + expression: string | Ast, datasourceMap: Record>, datasourceStates: Record, - framePublicAPI: FramePublicAPI + framePublicAPI: FramePublicAPI, + suggestionDatasourceId?: string, + suggestionDatasourceState?: unknown ) { - if (!suggestion.previewExpression) return null; - const expressionWithDatasource = prependDatasourceExpression( - suggestion.previewExpression, + expression, datasourceMap, - suggestion.datasourceId + suggestionDatasourceId ? { ...datasourceStates, - [suggestion.datasourceId]: { + [suggestionDatasourceId]: { isLoading: false, - state: suggestion.datasourceState, + state: suggestionDatasourceState, }, } : datasourceStates @@ -193,5 +197,7 @@ function preparePreviewExpression( }, }; - return prependKibanaContext(expressionWithDatasource, expressionContext); + return expressionWithDatasource + ? toExpression(prependKibanaContext(expressionWithDatasource, expressionContext)) + : undefined; } diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts index 95f47ee3d317a..5ed236ee73933 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern_suggestions.ts @@ -22,7 +22,6 @@ function buildSuggestion({ state, updatedLayer, layerId, - isMultiRow, datasourceSuggestionId, label, changeType, @@ -31,13 +30,14 @@ function buildSuggestion({ layerId: string; changeType: TableChangeType; updatedLayer?: IndexPatternLayer; - isMultiRow?: boolean; datasourceSuggestionId?: number; label?: string; }): DatasourceSuggestion { const columnOrder = (updatedLayer || state.layers[layerId]).columnOrder; const columnMap = (updatedLayer || state.layers[layerId]).columns; + const isMultiRow = Object.values(columnMap).some(column => column.isBucketed); + return { state: updatedLayer ? { @@ -54,7 +54,7 @@ function buildSuggestion({ columnId, operation: columnToOperation(columnMap[columnId]), })), - isMultiRow: typeof isMultiRow === 'undefined' || isMultiRow, + isMultiRow, datasourceSuggestionId: datasourceSuggestionId || 0, layerId, changeType, @@ -331,7 +331,6 @@ export function getDatasourceSuggestionsFromCurrentState( buildSuggestion({ state, layerId, - isMultiRow: true, datasourceSuggestionId: index, changeType: 'unchanged', }) @@ -347,7 +346,6 @@ export function getDatasourceSuggestionsFromCurrentState( buildSuggestion({ state, layerId, - isMultiRow: false, datasourceSuggestionId: index, changeType: 'unchanged', }) @@ -383,7 +381,6 @@ function createChangedNestingSuggestion(state: IndexPatternPrivateState, layerId return buildSuggestion({ state, layerId, - isMultiRow: true, updatedLayer, label: i18n.translate('xpack.lens.indexpattern.suggestions.nestingChangeLabel', { defaultMessage: 'Nest within {operation}', @@ -404,38 +401,36 @@ function createAlternativeMetricSuggestions( const suggestions: Array> = []; layer.columnOrder.forEach(columnId => { const column = layer.columns[columnId]; - if (hasField(column)) { - const field = indexPattern.fields.find( - ({ name }) => hasField(column) && column.sourceField === name - )!; - const alternativeMetricOperations = getOperationTypesForField(field).filter( - operationType => operationType !== column.operationType - ); - if (alternativeMetricOperations.length > 0) { - const newId = generateId(); - const newColumn = buildColumn({ - op: alternativeMetricOperations[0], - columns: layer.columns, - indexPattern, - layerId, - field, - suggestedPriority: undefined, - }); - const updatedLayer = buildLayerByColumnOrder( - { ...layer, columns: { [newId]: newColumn } }, - [newId] - ); - suggestions.push( - buildSuggestion({ - state, - layerId, - isMultiRow: false, - updatedLayer, - changeType: 'initial', - }) - ); - } + if (!hasField(column)) { + return; + } + const field = indexPattern.fields.find(({ name }) => column.sourceField === name)!; + const alternativeMetricOperations = getOperationTypesForField(field).filter( + operationType => operationType !== column.operationType + ); + if (alternativeMetricOperations.length === 0) { + return; } + const newId = generateId(); + const newColumn = buildColumn({ + op: alternativeMetricOperations[0], + columns: layer.columns, + indexPattern, + layerId, + field, + suggestedPriority: undefined, + }); + const updatedLayer = buildLayerByColumnOrder({ ...layer, columns: { [newId]: newColumn } }, [ + newId, + ]); + suggestions.push( + buildSuggestion({ + state, + layerId, + updatedLayer, + changeType: 'initial', + }) + ); }); return suggestions; } @@ -463,7 +458,6 @@ function createSuggestionWithDefaultDateHistogram( return buildSuggestion({ state, layerId, - isMultiRow: true, updatedLayer, label: i18n.translate('xpack.lens.indexpattern.suggestions.overTimeLabel', { defaultMessage: 'Over time', @@ -479,7 +473,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer return _.flatten( availableBucketedColumns.map((_col, index) => { - // build suggestions with less buckets + // build suggestions with fewer buckets const bucketedColumns = availableBucketedColumns.slice(0, index + 1); const allMetricsSuggestion = buildLayerByColumnOrder(layer, [ ...bucketedColumns, @@ -506,7 +500,6 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer return buildSuggestion({ state, layerId, - isMultiRow: updatedLayer.columnOrder.length > 1, updatedLayer, changeType: layer.columnOrder.length === updatedLayer.columnOrder.length ? 'unchanged' : 'reduced', 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 cacdfc6c56a24..648a411a13413 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 @@ -137,33 +137,36 @@ function getSuggestion( xValue, }; - // if current state is using the same data, suggest same chart with different presentational configuration - if (currentState && changeType === 'unchanged') { - if (xValue.operation.scale && xValue.operation.scale !== 'ordinal') { - // change chart type for interval or ratio scales on x axis - const newSeriesType = flipSeriesType(seriesType); - return buildSuggestion({ - ...options, - seriesType: newSeriesType, - title: newSeriesType.startsWith('area') - ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { - defaultMessage: 'Area chart', - }) - : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { - defaultMessage: 'Bar chart', - }), - }); - } else { - // flip between horizontal/vertical for ordinal scales - return buildSuggestion({ - ...options, - title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }), - isHorizontal: !options.isHorizontal, - }); - } - } else { + const isSameState = currentState && changeType === 'unchanged'; + + if (!isSameState) { return buildSuggestion(options); } + + // if current state is using the same data, suggest same chart with different presentational configuration + + if (xValue.operation.scale === 'ordinal') { + // flip between horizontal/vertical for ordinal scales + return buildSuggestion({ + ...options, + title: i18n.translate('xpack.lens.xySuggestions.flipTitle', { defaultMessage: 'Flip' }), + isHorizontal: !options.isHorizontal, + }); + } + + // change chart type for interval or ratio scales on x axis + const newSeriesType = flipSeriesType(seriesType); + return buildSuggestion({ + ...options, + seriesType: newSeriesType, + title: newSeriesType.startsWith('area') + ? i18n.translate('xpack.lens.xySuggestions.areaChartTitle', { + defaultMessage: 'Area chart', + }) + : i18n.translate('xpack.lens.xySuggestions.barChartTitle', { + defaultMessage: 'Bar chart', + }), + }); } function flipSeriesType(oldSeriesType: SeriesType) {