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 02e14bde5be94..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,12 +7,13 @@ 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'; import { SavedObjectIndexStore } from '../persistence'; import { xyVisualizationSetup, xyVisualizationStop } from '../xy_visualization_plugin'; +import { metricVisualizationSetup, metricVisualizationStop } from '../metric_visualization_plugin'; import { datatableVisualizationSetup, datatableVisualizationStop, @@ -22,7 +23,6 @@ import { EditorFrameInstance } from '../types'; export class AppPlugin { private instance: EditorFrameInstance | null = null; - private chrome: Chrome | null = null; constructor() {} @@ -32,14 +32,14 @@ export class AppPlugin { const indexPattern = indexPatternDatasourceSetup(); const datatableVisualization = datatableVisualizationSetup(); const xyVisualization = xyVisualizationSetup(); + const metricVisualization = metricVisualizationSetup(); const editorFrame = editorFrameSetup(); + const store = new SavedObjectIndexStore(chrome!.getSavedObjectsClient()); editorFrame.registerDatasource('indexpattern', indexPattern); editorFrame.registerVisualization(xyVisualization); editorFrame.registerVisualization(datatableVisualization); - - this.chrome = chrome; - const store = new SavedObjectIndexStore(this.chrome!.getSavedObjectsClient()); + editorFrame.registerVisualization(metricVisualization); this.instance = editorFrame.createInstance({}); @@ -87,6 +87,7 @@ export class AppPlugin { // TODO this will be handled by the plugin platform itself indexPatternDatasourceStop(); xyVisualizationStop(); + metricVisualizationStop(); datatableVisualizationStop(); editorFrameStop(); } 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 177dfc9577028..39fcce7a2a90c 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,6 +97,7 @@ 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 a3d9f02c9def3..4413d3b0c474b 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 @@ -205,6 +205,7 @@ describe('chart_switch', () => { label: '', dataType: 'string', isBucketed: true, + isMetric: false, }, }, { @@ -213,6 +214,7 @@ describe('chart_switch', () => { label: '', dataType: 'number', isBucketed: false, + isMetric: true, }, }, ], @@ -433,6 +435,7 @@ describe('chart_switch', () => { label: '', dataType: 'string', isBucketed: true, + isMetric: false, }, }, { @@ -441,6 +444,7 @@ 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 dfd4adde48560..7f74fd8c1f0b8 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,6 +27,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -41,6 +42,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'number', isBucketed: false, + isMetric: true, operationType: 'avg', sourceField: 'memory', }, @@ -54,6 +56,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -68,6 +71,7 @@ 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 2ddfce6b7e0a5..f45b674e0c19b 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,6 +89,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -202,6 +203,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -243,6 +245,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -284,6 +287,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -368,6 +372,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -541,6 +546,7 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'avg', sourceField: 'bytes', @@ -578,6 +584,7 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'count', }, @@ -772,6 +779,7 @@ describe('IndexPatternDimensionPanel', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'count', }, @@ -854,6 +862,7 @@ describe('IndexPatternDimensionPanel', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -956,6 +965,7 @@ 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.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.test.ts similarity index 98% 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 336deef6147a3..8307b8e0ab828 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 @@ -153,6 +153,7 @@ describe('IndexPattern Data Source', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -214,6 +215,7 @@ describe('IndexPattern Data Source', () => { label: 'Count of Documents', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -222,6 +224,7 @@ describe('IndexPattern Data Source', () => { label: 'Date', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -391,6 +394,7 @@ describe('IndexPattern Data Source', () => { const sampleColumn: IndexPatternColumn = { dataType: 'number', isBucketed: false, + isMetric: true, label: 'foo', operationType: 'max', sourceField: 'baz', @@ -441,6 +445,7 @@ 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 16202ec50f28d..cdea9fdf9b932 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/indexpattern.tsx @@ -143,12 +143,13 @@ export type IndexPatternPrivateState = IndexPatternPersistedState & { }; export function columnToOperation(column: IndexPatternColumn): Operation { - const { dataType, label, isBucketed, scale } = column; + const { dataType, label, isBucketed, isMetric, scale } = column; return { label, dataType, isBucketed, scale, + isMetric, }; } 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..37fee8f279d7a 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 @@ -153,6 +153,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -529,6 +530,7 @@ describe('IndexPattern Data Source suggestions', () => { col1: { dataType: 'string', isBucketed: true, + isMetric: false, sourceField: 'source', label: 'values of source', operationType: 'terms', @@ -541,6 +543,7 @@ describe('IndexPattern Data Source suggestions', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, sourceField: 'bytes', label: 'Min of bytes', operationType: 'min', @@ -564,6 +567,7 @@ describe('IndexPattern Data Source suggestions', () => { col1: { dataType: 'date', isBucketed: true, + isMetric: false, sourceField: 'timestamp', label: 'date histogram of timestamp', operationType: 'date_histogram', @@ -574,6 +578,7 @@ describe('IndexPattern Data Source suggestions', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, sourceField: 'bytes', label: 'Min of bytes', operationType: 'min', @@ -884,6 +889,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op 2', dataType: 'number', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -910,6 +916,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, }, }, ], @@ -927,6 +934,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op 2', dataType: 'number', isBucketed: true, + isMetric: false, }, }, ], @@ -947,6 +955,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'field1', @@ -960,6 +969,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'field2', @@ -973,6 +983,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'field3', @@ -986,6 +997,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'number', isBucketed: false, + isMetric: true, operationType: 'avg', sourceField: 'field4', @@ -994,6 +1006,7 @@ describe('IndexPattern Data Source suggestions', () => { label: 'My Op', dataType: 'number', isBucketed: false, + isMetric: true, operationType: 'min', sourceField: 'field5', 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 97% 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 1b5007cbe5558..a3a04802b0a31 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 @@ -31,7 +31,12 @@ function buildSuggestion({ datasourceSuggestionId?: number; }) { const columnOrder = (updatedLayer || state.layers[layerId]).columnOrder; - const columns = (updatedLayer || state.layers[layerId]).columns; + const columnMap = (updatedLayer || state.layers[layerId]).columns; + const columns = columnOrder.map(columnId => ({ + columnId, + operation: columnToOperation(columnMap[columnId]), + })); + return { state: updatedLayer ? { @@ -44,11 +49,8 @@ function buildSuggestion({ : state, table: { - columns: columnOrder.map(columnId => ({ - columnId, - operation: columnToOperation(columns[columnId]), - })), - isMultiRow: isMultiRow || true, + columns, + isMultiRow: isMultiRow || columns.some(col => !columnMap[col.columnId].isMetric), datasourceSuggestionId: datasourceSuggestionId || 0, layerId, }, @@ -100,6 +102,7 @@ function getExistingLayerSuggestionsForField( } else if (!usableAsBucketOperation && operations.length > 0) { updatedLayer = addFieldAsMetricOperation(layer, layerId, indexPattern, field); } + return updatedLayer ? [ buildSuggestion({ 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 46e381d69741b..0faa6b4725896 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,6 +27,7 @@ const initialState: IndexPatternPrivateState = { label: 'My Op', dataType: 'string', isBucketed: true, + isMetric: false, operationType: 'terms', sourceField: 'source', params: { @@ -41,6 +42,7 @@ 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 0cb4838faa12c..304999ca8f5bc 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_plugin/operation_definitions/count.tsx @@ -19,6 +19,7 @@ export const countOperation: OperationDefinition = { { dataType: 'number', isBucketed: false, + isMetric: true, scale: 'ratio', }, ]; @@ -32,6 +33,7 @@ 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 8e94087f4a5fb..299d5ca8250c7 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,6 +58,7 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -76,6 +77,7 @@ describe('date_histogram', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -155,6 +157,7 @@ describe('date_histogram', () => { { dataType: 'date', isBucketed: true, + isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'dateField', @@ -197,6 +200,7 @@ 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 e454c700bb8db..8f0b0ff0393d9 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,6 +50,7 @@ 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 3f4e7c3dc407d..7caada3c9f7a5 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 @@ -27,6 +27,7 @@ export const filterRatioOperation: OperationDefinition( { dataType: 'number', isBucketed: false, + isMetric: true, scale: 'ratio', }, ]; @@ -60,6 +61,7 @@ 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 1f639907b79d0..def66db01a7fe 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,6 +31,7 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -45,6 +46,7 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -91,6 +93,7 @@ describe('terms', () => { { dataType: 'string', isBucketed: true, + isMetric: false, scale: 'ordinal', }, ]); @@ -106,6 +109,7 @@ describe('terms', () => { { dataType: 'boolean', isBucketed: true, + isMetric: false, scale: 'ordinal', }, ]); @@ -158,6 +162,7 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -184,6 +189,7 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -199,6 +205,7 @@ describe('terms', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -213,6 +220,7 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -238,6 +246,7 @@ describe('terms', () => { label: 'Top value of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -253,6 +262,7 @@ describe('terms', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -310,6 +320,7 @@ 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 ca0455c9372da..a11b49f6ab784 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,6 +53,7 @@ export const termsOperation: OperationDefinition = { { dataType: type, isBucketed: true, + isMetric: false, scale: 'ordinal', }, ]; @@ -85,6 +86,7 @@ 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 d4353f52c4a67..49719c35a09c2 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,6 +163,7 @@ describe('getOperationTypesForField', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -234,6 +235,7 @@ describe('getOperationTypesForField', () => { "operationMetaData": Object { "dataType": "string", "isBucketed": true, + "isMetric": false, "scale": "ordinal", }, "operations": Array [ @@ -248,6 +250,7 @@ describe('getOperationTypesForField', () => { "operationMetaData": Object { "dataType": "date", "isBucketed": true, + "isMetric": false, "scale": "interval", }, "operations": Array [ @@ -262,6 +265,7 @@ 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 e44dea4340777..897af8bbc28ff 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,6 +31,7 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -55,6 +56,7 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -76,6 +78,7 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -100,6 +103,7 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -127,6 +131,7 @@ describe('state_helpers', () => { label: 'Value of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -173,6 +178,7 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'avg', @@ -182,6 +188,7 @@ describe('state_helpers', () => { label: 'Max of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'max', @@ -200,6 +207,7 @@ describe('state_helpers', () => { label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -232,6 +240,7 @@ describe('state_helpers', () => { label: 'Date histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -253,6 +262,7 @@ describe('state_helpers', () => { label: 'Date histogram of order_date', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -274,6 +284,7 @@ describe('state_helpers', () => { label: 'Top values of source', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -289,6 +300,7 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'avg', @@ -308,6 +320,7 @@ describe('state_helpers', () => { label: 'Count', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'count', @@ -343,6 +356,7 @@ describe('state_helpers', () => { label: 'Value of timestamp', dataType: 'string', isBucketed: false, + isMetric: false, // Private operationType: 'date_histogram', @@ -362,6 +376,7 @@ describe('state_helpers', () => { label: 'Top Values of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -378,6 +393,7 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'avg', @@ -387,6 +403,7 @@ describe('state_helpers', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -406,6 +423,7 @@ describe('state_helpers', () => { label: 'Top Values of category', dataType: 'string', isBucketed: true, + isMetric: false, // Private operationType: 'terms', @@ -423,6 +441,7 @@ describe('state_helpers', () => { label: 'Average of bytes', dataType: 'number', isBucketed: false, + isMetric: true, // Private operationType: 'avg', @@ -433,6 +452,7 @@ describe('state_helpers', () => { label: 'Date Histogram of timestamp', dataType: 'date', isBucketed: true, + isMetric: false, // Private operationType: 'date_histogram', @@ -512,6 +532,7 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, + isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -524,6 +545,7 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'avg', sourceField: 'xxx', @@ -545,6 +567,7 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, + isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'fieldC', @@ -555,6 +578,7 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'avg', sourceField: 'fieldB', @@ -576,6 +600,7 @@ describe('state_helpers', () => { col1: { dataType: 'date', isBucketed: true, + isMetric: false, label: '', operationType: 'date_histogram', sourceField: 'fieldD', @@ -606,6 +631,7 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, + isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -618,6 +644,7 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'avg', sourceField: 'fieldD', @@ -639,6 +666,7 @@ describe('state_helpers', () => { col1: { dataType: 'string', isBucketed: true, + isMetric: false, label: '', operationType: 'terms', sourceField: 'fieldA', @@ -651,6 +679,7 @@ describe('state_helpers', () => { col2: { dataType: 'number', isBucketed: false, + isMetric: true, label: '', operationType: 'min', sourceField: 'fieldC', 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 new file mode 100644 index 0000000000000..60008d1237d82 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 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(2000, 2000), mockElement(3000, 5000))).toBe(0.4); + 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/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx new file mode 100644 index 0000000000000..9ca58c1944803 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/auto_scale.tsx @@ -0,0 +1,119 @@ +/* + * 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 _ from 'lodash'; +import { EuiResizeObserver } from '@elastic/eui'; + +interface Props extends React.HTMLAttributes { + 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; + 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. + this.state = { scale: 0 }; + } + + setParent = (el: Element | null) => { + if (el && this.parent !== el) { + this.parent = el; + setTimeout(() => this.scale()); + } + }; + + setChild = (el: Element | null) => { + if (el && this.child !== el) { + this.child = el; + setTimeout(() => this.scale()); + } + }; + + render() { + const { children } = this.props; + const { scale } = this.state; + const style = this.props.style || {}; + + return ( + + {resizeRef => ( +
{ + this.setParent(el); + resizeRef(el); + }} + style={{ + ...style, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + maxWidth: '100%', + maxHeight: '100%', + overflow: 'hidden', + }} + > +
+ {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 +) { + const MAX_SCALE = 1; + const MIN_SCALE = 0.3; + + if (!parent || !child) { + return 1; + } + + const scaleX = parent.clientWidth / child.clientWidth; + const scaleY = parent.clientHeight / child.clientHeight; + + 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/index.ts b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/index.ts new file mode 100644 index 0000000000000..f75dce9b7507f --- /dev/null +++ b/x-pack/legacy/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/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 new file mode 100644 index 0000000000000..e2c184a7a4803 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.test.tsx @@ -0,0 +1,74 @@ +/* + * 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 { DatasourceDimensionPanelProps, Operation, DatasourcePublicAPI } from '../types'; +import { State } from './types'; +import { NativeRendererProps } from '../native_renderer'; +import { createMockFramePublicAPI, createMockDatasource } from '../editor_frame_plugin/mocks'; + +describe('MetricConfigPanel', () => { + const dragDropContext = { dragging: undefined, setDragging: jest.fn() }; + + function mockDatasource(): DatasourcePublicAPI { + return createMockDatasource().publicAPIMock; + } + + function testState(): State { + return { + accessor: 'foo', + layerId: 'bar', + }; + } + + function testSubj(component: ReactWrapper, subj: string) { + return component + .find(`[data-test-subj="${subj}"]`) + .first() + .props(); + } + + test('the value dimension panel only accepts singular numeric operations', () => { + 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', + isBucketed: false, + isMetric: false, + label: 'bar', + }; + const ops: Operation[] = [ + { ...exampleOperation, isMetric: true, dataType: 'number' }, + { ...exampleOperation, isMetric: false, dataType: 'number' }, + { ...exampleOperation, dataType: 'string' }, + { ...exampleOperation, isMetric: true, dataType: 'boolean' }, + { ...exampleOperation, isMetric: false, dataType: 'boolean' }, + { ...exampleOperation, dataType: 'date' }, + ]; + expect(columnId).toEqual('shazm'); + expect(ops.filter(filterOperations)).toEqual([ + { ...exampleOperation, isMetric: true, dataType: 'number' }, + { ...exampleOperation, isMetric: true, dataType: 'boolean' }, + ]); + }); +}); 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 new file mode 100644 index 0000000000000..e5d1d7dc731fc --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_config_panel.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiFormRow, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { State } from './types'; +import { VisualizationProps, OperationMetadata } from '../types'; +import { NativeRenderer } from '../native_renderer'; + +const isMetric = (op: OperationMetadata) => op.isMetric; + +export function MetricConfigPanel(props: VisualizationProps) { + const { state, frame } = props; + const [datasource] = Object.values(frame.datasourceLayers); + const [layerId] = Object.keys(frame.datasourceLayers); + + return ( + + + + + + + + + + ); +} 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 new file mode 100644 index 0000000000000..69d91d4c97fe1 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.test.tsx @@ -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 { metricChart, MetricChart } from './metric_expression'; +import { LensMultiTable } from '../types'; +import React from 'react'; +import { shallow } from 'enzyme'; +import { MetricConfig } from './types'; + +function sampleArgs() { + 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 = { + accessor: 'a', + layerId: 'l1', + title: 'My fanci metric chart', + }; + + 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( x} />)) + .toMatchInlineSnapshot(` +
+ +
+ 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 new file mode 100644 index 0000000000000..daff873feb18c --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_expression.tsx @@ -0,0 +1,118 @@ +/* + * 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 { FormatFactory } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { MetricConfig } from './types'; +import { LensMultiTable } from '../types'; +import { RenderFunction } from './plugin'; +import { AutoScale } from './auto_scale'; + +export interface MetricChartProps { + data: LensMultiTable; + args: MetricConfig; +} + +export interface MetricRender { + type: 'render'; + as: 'lens_metric_chart_renderer'; + value: MetricChartProps; +} + +export const metricChart: ExpressionFunction< + 'lens_metric_chart', + LensMultiTable, + 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: ['lens_multitable'], + }, + fn(data: LensMultiTable, 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', + LensMultiTable, + MetricConfig, + MetricRender +>; + +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); + }, +}); + +export function MetricChart({ + data, + args, + formatFactory, +}: MetricChartProps & { formatFactory: FormatFactory }) { + const { title, accessor } = args; + 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 ( +
+ +
{value}
+
{title}
+
+
+ ); +} 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 new file mode 100644 index 0000000000000..bf9d5ad4340f2 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.test.ts @@ -0,0 +1,125 @@ +/* + * 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 { TableSuggestionColumn } from '..'; + +describe('metric_suggestions', () => { + function numCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'number', + label: `Avg ${columnId}`, + isBucketed: false, + isMetric: true, + }, + }; + } + + function strCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'string', + label: `Top 5 ${columnId}`, + isBucketed: true, + isMetric: false, + }, + }; + } + + function dateCol(columnId: string): TableSuggestionColumn { + return { + columnId, + operation: { + dataType: 'date', + isBucketed: true, + isMetric: false, + 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: [ + { 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([]); + }); + + test('suggests a basic metric chart', () => { + const [suggestion, ...rest] = getSuggestions({ + tables: [ + { + columns: [numCol('bytes')], + datasourceSuggestionId: 0, + isMultiRow: false, + layerId: 'l1', + }, + ], + }); + + expect(rest).toHaveLength(0); + 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 { + "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 new file mode 100644 index 0000000000000..85981be00c3a0 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_suggestions.ts @@ -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 { 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, + previewIcon: 'visMetric', + previewExpression: { + type: 'expression', + chain: [ + { + type: 'function', + function: 'lens_metric_chart', + arguments: { + title: [''], + accessor: [col.columnId], + }, + }, + ], + }, + 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 new file mode 100644 index 0000000000000..fa68aa2c7122a --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.test.ts @@ -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 { metricVisualization } from './metric_visualization'; +import { State } from './types'; +import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_plugin/mocks'; +import { generateId } from '../id_generator'; +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', () => { + (generateId as jest.Mock).mockReturnValueOnce('test-id1'); + 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(mockFrame(), 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', () => { + const datasource: DatasourcePublicAPI = { + ...createMockDatasource().publicAPIMock, + getOperationForColumnId(_: string) { + return { + id: 'a', + dataType: 'number', + isBucketed: false, + isMetric: true, + label: 'shazm', + }; + }, + }; + + const frame = { + ...mockFrame(), + datasourceLayers: { l1: datasource }, + }; + + expect(metricVisualization.toExpression(exampleState(), frame)).toMatchInlineSnapshot(` + Object { + "chain": Array [ + Object { + "arguments": Object { + "accessor": Array [ + "a", + ], + "title": Array [ + "shazm", + ], + }, + "function": "lens_metric_chart", + "type": "function", + }, + ], + "type": "expression", + } + `); + }); + }); +}); 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 new file mode 100644 index 0000000000000..f178b2bd4fe5e --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/metric_visualization.tsx @@ -0,0 +1,78 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { getSuggestions } from './metric_suggestions'; +import { MetricConfigPanel } from './metric_config_panel'; +import { Visualization } from '../types'; +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(frame, state) { + return ( + state || { + layerId: frame.addNewLayer(), + accessor: generateId(), + } + ); + }, + + getPersistableState: state => state, + + renderConfigPanel: (domElement, props) => + render( + + + , + domElement + ), + + toExpression(state, frame) { + const [datasource] = Object.values(frame.datasourceLayers); + const operation = datasource && 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/plugin.tsx b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx new file mode 100644 index 0000000000000..f8bfd15b49892 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/plugin.tsx @@ -0,0 +1,84 @@ +/* + * 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 { FormatFactory, getFormat } from 'ui/visualize/loader/pipeline_helpers/utilities'; +import { metricVisualization } from './metric_visualization'; +import { + renderersRegistry, + functionsRegistry, +} from '../../../../../../src/legacy/core_plugins/interpreter/public/registries'; +import { ExpressionFunction } from '../../../../../../src/legacy/core_plugins/interpreter/public'; +import { metricChart, getMetricChartRenderer } 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; + // TODO this is a simulated NP plugin. + // Once field formatters are actually migrated, the actual shim can be used + fieldFormat: { + formatFactory: FormatFactory; + }; +} + +class MetricVisualizationPlugin { + constructor() {} + + setup( + _core: CoreSetup | null, + { interpreter, fieldFormat }: MetricVisualizationPluginSetupPlugins + ) { + interpreter.functionsRegistry.register(() => metricChart); + + interpreter.renderersRegistry.register( + () => getMetricChartRenderer(fieldFormat.formatFactory) as RenderFunction + ); + + return metricVisualization; + } + + stop() {} +} + +const plugin = new MetricVisualizationPlugin(); + +export const metricVisualizationSetup = () => + plugin.setup(null, { + interpreter: { + renderersRegistry, + functionsRegistry, + }, + fieldFormat: { + formatFactory: getFormat, + }, + }); + +export const metricVisualizationStop = () => plugin.stop(); 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 new file mode 100644 index 0000000000000..89d41552639c4 --- /dev/null +++ b/x-pack/legacy/plugins/lens/public/metric_visualization_plugin/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface State { + layerId: string; + accessor: string; +} + +export interface MetricConfig extends State { + title: string; +} + +export type PersistableState = State; 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 012c27d3ce3ff..08a94c2180ab9 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,6 +49,7 @@ describe('MultiColumnEditor', () => { dataType: 'number', id, isBucketed: true, + isMetric: false, label: 'BaaaZZZ!', }; }, 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(); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index 3f448f00a8d75..e74310ad36bf7 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -158,6 +158,7 @@ export interface OperationMetadata { // A bucketed operation is grouped by duplicate values, otherwise each row is // treated as unique isBucketed: boolean; + isMetric: boolean; scale?: 'ordinal' | 'interval' | 'ratio'; // Extra meta-information like cardinality, color } 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 c7c600172a2b9..ee51f9ce5fa8e 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 @@ -35,12 +35,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) { @@ -59,7 +63,9 @@ 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', + }), }, hide: { types: ['boolean'], 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..cc4a9c80853bf 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,6 +148,7 @@ describe('XYConfigPanel', () => { const exampleOperation: Operation = { dataType: 'number', isBucketed: false, + isMetric: true, label: 'bar', }; const bucketedOps: Operation[] = [ @@ -186,6 +187,7 @@ 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 249ea6b5b72ea..835b2d9c0bccb 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.isBucketed && op.dataType === 'number'; +const isNumericMetric = (op: OperationMetadata) => op.isMetric && op.dataType === 'number'; const isBucketed = (op: OperationMetadata) => op.isBucketed; type UnwrapArray = T extends Array ? P : T; 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 0ab051bc15971..3d7af67e9483a 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 @@ -21,6 +21,7 @@ import { I18nProvider } from '@kbn/i18n/react'; import { ExpressionFunction } from 'src/legacy/core_plugins/interpreter/types'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, IconType } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { FormatFactory } from '../../../../../../src/legacy/ui/public/visualize/loader/pipeline_helpers/utilities'; import { LensMultiTable } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; @@ -40,7 +41,9 @@ export interface XYRender { export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, 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: { xTitle: { types: ['string'], @@ -52,7 +55,9 @@ export const xyChart: ExpressionFunction<'lens_xy_chart', LensMultiTable, XYArgs }, legend: { types: ['lens_xy_legendConfig'], - help: 'Configure the chart legend.', + help: i18n.translate('xpack.lens.xyChart.legend.help', { + defaultMessage: 'Configure the chart legend.', + }), }, layers: { types: ['lens_xy_layer'], @@ -91,7 +96,9 @@ export const getXyChartRenderer = (dependencies: { }): 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) => { 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..d3422138fec65 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 @@ -20,6 +20,7 @@ describe('xy_suggestions', () => { dataType: 'number', label: `Avg ${columnId}`, isBucketed: false, + isMetric: true, }, }; } @@ -31,6 +32,7 @@ describe('xy_suggestions', () => { dataType: 'string', label: `Top 5 ${columnId}`, isBucketed: true, + isMetric: false, }, }; } @@ -41,6 +43,7 @@ describe('xy_suggestions', () => { operation: { dataType: 'date', isBucketed: true, + isMetric: false, label: `${columnId} histogram`, }, }; @@ -258,6 +261,7 @@ describe('xy_suggestions', () => { columnId: 'mybool', operation: { dataType: 'boolean', + isMetric: false, isBucketed: false, label: 'Yes / No', },