From 874b8cf3bb9345a280666469adf302b83fbf31f0 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 28 May 2019 18:12:57 +0200 Subject: [PATCH 1/5] [ML] Data Frames: Fix to avoid queries for indices with more than 1024 fields. (#37183) (#37226) - 7.0 introduced indices.query.bool.max_clause_count which defaults to 1024. This can break certain queries (e.g. simple_query) for indices which have more than 1024 fields (e.g. certain beats indices). The optional data frames query uses simple_query and could therefor break the source index preview as well the pivot preview and pivot job itself given these conditions. - Originally the default query (* used for simple_query) was always applied for source index previews and pivot previews. A new check isDefaultQuery() will now allow a) the source index preview to use a more efficient match_all query and b) avoid adding the query to the pivot config. This avoids triggering the max_clause_count when no optional query is set. - If an index has more than 1024 fields, the input form for an optional query will be hidden. A helper text explains the reasoning. This avoids triggering max_clause_count related errors from within the UI. A user can still copy a UI created config to the clipboard and add an optional query in Kibana dev console. - Additionally, this PR adds a fix to format date fields in the source index preview table using moment-timezone and formatHumanReadableDateTimeSeconds to display dates with the correct timezone. --- .../public/data_frame/common/request.test.ts | 25 +++++++- .../ml/public/data_frame/common/request.ts | 13 ++++- .../components/define_pivot/common.test.ts | 8 +-- .../define_pivot/define_pivot_form.tsx | 57 ++++++++++++------- .../source_index_preview.tsx | 15 ++++- .../use_source_index_data.ts | 5 +- 6 files changed, 91 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/ml/public/data_frame/common/request.test.ts b/x-pack/plugins/ml/public/data_frame/common/request.test.ts index 2892e96bcef00c..2f16f2494c90a3 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.test.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.test.ts @@ -11,9 +11,32 @@ import { DefinePivotExposedState } from '../components/define_pivot/define_pivot import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs'; -import { getDataFramePreviewRequest, getDataFrameRequest, getPivotQuery } from './request'; +import { + getDataFramePreviewRequest, + getDataFrameRequest, + getPivotQuery, + isDefaultQuery, + isSimpleQuery, + PivotQuery, +} from './request'; + +const defaultQuery: PivotQuery = { query_string: { query: '*' } }; +const matchAllQuery: PivotQuery = { match_all: {} }; +const simpleQuery: PivotQuery = { query_string: { query: 'airline:AAL' } }; describe('Data Frame: Common', () => { + test('isSimpleQuery()', () => { + expect(isSimpleQuery(defaultQuery)).toBe(true); + expect(isSimpleQuery(matchAllQuery)).toBe(false); + expect(isSimpleQuery(simpleQuery)).toBe(true); + }); + + test('isDefaultQuery()', () => { + expect(isDefaultQuery(defaultQuery)).toBe(true); + expect(isDefaultQuery(matchAllQuery)).toBe(false); + expect(isDefaultQuery(simpleQuery)).toBe(false); + }); + test('getPivotQuery()', () => { const query = getPivotQuery('the-query'); diff --git a/x-pack/plugins/ml/public/data_frame/common/request.ts b/x-pack/plugins/ml/public/data_frame/common/request.ts index eb611cd90018fe..14aa1cded8ce97 100644 --- a/x-pack/plugins/ml/public/data_frame/common/request.ts +++ b/x-pack/plugins/ml/public/data_frame/common/request.ts @@ -68,6 +68,14 @@ export function getPivotQuery(search: string | SavedSearchQuery): PivotQuery { return search; } +export function isSimpleQuery(arg: any): arg is SimpleQuery { + return arg.query_string !== undefined; +} + +export function isDefaultQuery(query: PivotQuery): boolean { + return isSimpleQuery(query) && query.query_string.query === '*'; +} + export function getDataFramePreviewRequest( indexPatternTitle: IndexPattern['title'], query: PivotQuery, @@ -77,7 +85,6 @@ export function getDataFramePreviewRequest( const request: DataFramePreviewRequest = { source: { index: indexPatternTitle, - query, }, pivot: { group_by: {}, @@ -85,6 +92,10 @@ export function getDataFramePreviewRequest( }, }; + if (!isDefaultQuery(query)) { + request.source.query = query; + } + groupBy.forEach(g => { if (g.agg === PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS) { const termsAgg: TermsAgg = { diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts index 9c591a3d4e7051..6431af5bfb5a20 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/common.test.ts @@ -128,13 +128,7 @@ describe('Data Frame: Define Pivot Common', () => { expect(pivotPreviewDevConsoleStatement).toBe(`POST _data_frame/transforms/_preview { "source": { - "index": "the-index-pattern-title", - "query": { - "query_string": { - "query": "*", - "default_operator": "AND" - } - } + "index": "the-index-pattern-title" }, "pivot": { "group_by": { diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx index 888f662621af8d..bf82cc28a4d88e 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx @@ -191,6 +191,11 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang ] ); + // TODO This should use the actual value of `indices.query.bool.max_clause_count` + const maxIndexFields = 1024; + const numIndexFields = indexPattern.fields.length; + const disabledQuery = numIndexFields > maxIndexFields; + return ( @@ -201,28 +206,42 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang label={i18n.translate('xpack.ml.dataframe.definePivotForm.indexPatternLabel', { defaultMessage: 'Index pattern', })} + helpText={ + disabledQuery + ? i18n.translate('xpack.ml.dataframe.definePivotForm.indexPatternHelpText', { + defaultMessage: + 'An optional query for this index pattern is not supported. The number of supported index fields is {maxIndexFields} whereas this index has {numIndexFields} fields.', + values: { + maxIndexFields, + numIndexFields, + }, + }) + : '' + } > {kibanaContext.currentIndexPattern.title} - - - + {!disabledQuery && ( + + + + )} )} diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx index 542bf4b45ac4c4..80477e9f1b6847 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/source_index_preview.tsx @@ -5,6 +5,7 @@ */ import React, { FunctionComponent, useContext, useState } from 'react'; +import moment from 'moment-timezone'; import { i18n } from '@kbn/i18n'; @@ -36,7 +37,9 @@ interface ExpandableTableProps extends EuiInMemoryTableProps { const ExpandableTable = (EuiInMemoryTable as any) as FunctionComponent; +import { KBN_FIELD_TYPES } from '../../../../common/constants/field_types'; import { Dictionary } from '../../../../common/types/common'; +import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils'; import { isKibanaContext, KibanaContext, PivotQuery } from '../../common'; @@ -208,15 +211,23 @@ export const SourceIndexPreview: React.SFC = React.memo(({ cellClick, que const column = { field: `_source.${k}`, name: k, - render: undefined, sortable: true, truncateText: true, } as Dictionary; + const field = indexPattern.fields.find(f => f.name === k); + const render = (d: string) => { + return field !== undefined && field.type === KBN_FIELD_TYPES.DATE + ? formatHumanReadableDateTimeSeconds(moment(d).unix() * 1000) + : d; + }; + + column.render = render; + if (CELL_CLICK_ENABLED && cellClick) { column.render = (d: string) => ( cellClick(`${k}:(${d})`)}> - {d} + {render(d)} ); } diff --git a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts index 433312182dbbf8..9183092407596a 100644 --- a/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts +++ b/x-pack/plugins/ml/public/data_frame/components/source_index_preview/use_source_index_data.ts @@ -12,7 +12,7 @@ import { IndexPattern } from 'ui/index_patterns'; import { ml } from '../../../services/ml_api_service'; -import { PivotQuery } from '../../common'; +import { isDefaultQuery, PivotQuery } from '../../common'; import { EsDoc, EsFieldName, getDefaultSelectableFields } from './common'; const SEARCH_SIZE = 1000; @@ -48,7 +48,8 @@ export const useSourceIndexData = ( const resp: SearchResponse = await ml.esSearch({ index: indexPattern.title, size: SEARCH_SIZE, - body: { query }, + // Instead of using the default query (`*`), fall back to a more efficient `match_all` query. + body: { query: isDefaultQuery(query) ? { match_all: {} } : query }, }); const docs = resp.hits.hits; From 4abb77eec443fe3f7f9df24e4395f242f7c34dde Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 28 May 2019 18:14:47 +0200 Subject: [PATCH 2/5] Add automatic alignment to gauge visualisation (#34845) (#37235) --- .../public/controls/gauge_options.html | 17 ++-- .../kbn_vislib_vis_types/public/gauge.js | 15 +++- src/legacy/core_plugins/kibana/migrations.js | 30 ++++++- .../core_plugins/kibana/migrations.test.js | 48 +++++++++++ .../data_sets/ecommerce/saved_objects.js | 4 +- .../data_sets/flights/saved_objects.js | 4 +- .../data_sets/logs/saved_objects.js | 2 +- .../__tests__/visualizations/gauge_chart.js | 13 ++- .../vislib/visualizations/gauge_chart.js | 79 ++++++++++++++----- .../vislib/visualizations/gauges/meter.js | 6 ++ .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 12 files changed, 182 insertions(+), 38 deletions(-) diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html index 9d29f390ac358f..37be3cc365e334 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/controls/gauge_options.html @@ -31,16 +31,17 @@
-   - +
diff --git a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js index c780b5ab2adbe4..d421936d6e1055 100644 --- a/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js +++ b/src/legacy/core_plugins/kbn_vislib_vis_types/public/gauge.js @@ -40,7 +40,7 @@ export default function GaugeVisType(Private) { addLegend: true, isDisplayWarning: false, gauge: { - verticalSplit: false, + alignment: 'automatic', extendRange: true, percentageMode: false, gaugeType: 'Arc', @@ -86,6 +86,19 @@ export default function GaugeVisType(Private) { collections: { gaugeTypes: ['Arc', 'Circle'], gaugeColorMode: ['None', 'Labels', 'Background'], + alignments: [ + { + id: 'automatic', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentAutomaticTitle', { defaultMessage: 'Automatic' }) + }, + { + id: 'horizontal', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentHorizontalTitle', { defaultMessage: 'Horizontal' }) + }, + { + id: 'vertical', + label: i18n.translate('kbnVislibVisTypes.gauge.alignmentVerticalTitle', { defaultMessage: 'Vertical' }) }, + ], scales: ['linear', 'log', 'square root'], colorSchemas: Object.values(vislibColorMaps).map(value => ({ id: value.id, label: value.label })), }, diff --git a/src/legacy/core_plugins/kibana/migrations.js b/src/legacy/core_plugins/kibana/migrations.js index 376010fa7477ff..19a43a8873b85d 100644 --- a/src/legacy/core_plugins/kibana/migrations.js +++ b/src/legacy/core_plugins/kibana/migrations.js @@ -163,6 +163,33 @@ function removeDateHistogramTimeZones(doc) { return doc; } +// migrate gauge verticalSplit to alignment +// https://github.com/elastic/kibana/issues/34636 +function migrateGaugeVerticalSplitToAlignment(doc) { + const visStateJSON = get(doc, 'attributes.visState'); + + if (visStateJSON) { + try { + const visState = JSON.parse(visStateJSON); + if (visState && visState.type === 'gauge') { + + visState.params.gauge.alignment = visState.params.gauge.verticalSplit ? 'vertical' : 'horizontal'; + delete visState.params.gauge.verticalSplit; + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(visState), + }, + }; + } + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + } + return doc; +} + export const migrations = { 'index-pattern': { '6.5.0': (doc) => { @@ -264,7 +291,8 @@ export const migrations = { } }, '7.0.1': removeDateHistogramTimeZones, - '7.2.0': doc => executeMigrations720(doc) + '7.2.0': doc => executeMigrations720(doc), + '7.3.0': migrateGaugeVerticalSplitToAlignment }, dashboard: { '7.0.0': (doc) => { diff --git a/src/legacy/core_plugins/kibana/migrations.test.js b/src/legacy/core_plugins/kibana/migrations.test.js index c5494c761cbe23..6b42a1fe361871 100644 --- a/src/legacy/core_plugins/kibana/migrations.test.js +++ b/src/legacy/core_plugins/kibana/migrations.test.js @@ -843,6 +843,54 @@ Object { expect(aggs[3]).not.toHaveProperty('params.customInterval'); }); }); + describe('7.3.0', () => { + const migrate = doc => migrations.visualization['7.3.0'](doc); + + it('migrates type = gauge verticalSplit: false to alignment: vertical', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: false } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"horizontal\\"}}}", + }, +} +`); + }); + + it('migrates type = gauge verticalSplit: false to alignment: horizontal', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge', params: { gauge: { verticalSplit: true } } }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\",\\"params\\":{\\"gauge\\":{\\"alignment\\":\\"vertical\\"}}}", + }, +} +`); + }); + + it('doesnt migrate type = gauge containing invalid visState object', () => { + const migratedDoc = migrate({ + attributes: { + visState: JSON.stringify({ type: 'gauge' }), + }, + }); + expect(migratedDoc).toMatchInlineSnapshot(` +Object { + "attributes": Object { + "visState": "{\\"type\\":\\"gauge\\"}", + }, +} +`); + }); + }); }); describe('dashboard', () => { diff --git a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js index e2be84b14e7a5f..cc2672a6063c65 100644 --- a/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/ecommerce/saved_objects.js @@ -166,7 +166,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.averageSalesPriceTitle', { defaultMessage: '[eCommerce] Average Sales Price', }), - "visState": "{\"title\":\"[eCommerce] Average Sales Price\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"taxful_total_price\",\"customLabel\":\"average spend\"}}]}", + "visState": "{\"title\":\"[eCommerce] Average Sales Price\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":50},{\"from\":50,\"to\":75},{\"from\":75,\"to\":100}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"taxful_total_price\",\"customLabel\":\"average spend\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 50\":\"rgb(165,0,38)\",\"50 - 75\":\"rgb(255,255,190)\",\"75 - 100\":\"rgb(0,104,55)\"}}}", "description": "", "version": 1, @@ -185,7 +185,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.ecommerceSpec.averageSoldQuantityTitle', { defaultMessage: '[eCommerce] Average Sold Quantity', }), - "visState": "{\"title\":\"[eCommerce] Average Sold Quantity\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":2},{\"from\":2,\"to\":3},{\"from\":3,\"to\":4}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"total_quantity\",\"customLabel\":\"average items\"}}]}", + "visState": "{\"title\":\"[eCommerce] Average Sold Quantity\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Circle\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":2},{\"from\":2,\"to\":3},{\"from\":3,\"to\":4}],\"invertColors\":true,\"labels\":{\"show\":true,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"per order\",\"fontSize\":60,\"labelColor\":true},\"minAngle\":0,\"maxAngle\":6.283185307179586}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"avg\",\"schema\":\"metric\",\"params\":{\"field\":\"total_quantity\",\"customLabel\":\"average items\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 2\":\"rgb(165,0,38)\",\"2 - 3\":\"rgb(255,255,190)\",\"3 - 4\":\"rgb(0,104,55)\"}}}", "description": "", "version": 1, diff --git a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js index 1e8a355d331df7..72fe45d5f6f801 100644 --- a/src/legacy/server/sample_data/data_sets/flights/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/flights/saved_objects.js @@ -294,7 +294,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.flightsSpec.totalFlightDelaysTitle', { defaultMessage: '[Flights] Total Flight Delays', }), - "visState": "{\"title\":\"[Flights] Total Flight Delays\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Delays\"}}]}", + "visState": "{\"title\":\"[Flights] Total Flight Delays\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Delays\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 75\":\"rgb(8,48,107)\",\"75 - 150\":\"rgb(55,135,192)\",\"150 - 225\":\"rgb(171,208,230)\",\"225 - 300\":\"rgb(247,251,255)\"}}}", "description": "", "version": 1, @@ -313,7 +313,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.flightsSpec.totalFlightCancellationsTitle', { defaultMessage: '[Flights] Total Flight Cancellations', }), - "visState": "{\"title\":\"[Flights] Total Flight Cancellations\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Cancellations\"}}]}", + "visState": "{\"title\":\"[Flights] Total Flight Cancellations\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":true,\"isDisplayWarning\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Blues\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":75},{\"from\":75,\"to\":150},{\"from\":150,\"to\":225},{\"from\":225,\"to\":300}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":false,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"\",\"fontSize\":60,\"labelColor\":true}}},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"schema\":\"metric\",\"params\":{\"customLabel\":\"Total Cancellations\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 75\":\"rgb(8,48,107)\",\"75 - 150\":\"rgb(55,135,192)\",\"150 - 225\":\"rgb(171,208,230)\",\"225 - 300\":\"rgb(247,251,255)\"}}}", "description": "", "version": 1, diff --git a/src/legacy/server/sample_data/data_sets/logs/saved_objects.js b/src/legacy/server/sample_data/data_sets/logs/saved_objects.js index 974392d1e08bbd..a40ed7c1caf8f3 100644 --- a/src/legacy/server/sample_data/data_sets/logs/saved_objects.js +++ b/src/legacy/server/sample_data/data_sets/logs/saved_objects.js @@ -109,7 +109,7 @@ export const getSavedObjects = () => [ "title": i18n.translate('server.sampleData.logsSpec.goalsTitle', { defaultMessage: '[Logs] Goals', }), - "visState": "{\"title\":\"[Logs] Goals\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":false,\"gauge\":{\"verticalSplit\":false,\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":1000},{\"from\":1000,\"to\":1500}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"visitors\",\"fontSize\":60,\"labelColor\":true}},\"isDisplayWarning\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"clientip\",\"customLabel\":\"Unique Visitors\"}}]}", + "visState": "{\"title\":\"[Logs] Goals\",\"type\":\"gauge\",\"params\":{\"type\":\"gauge\",\"addTooltip\":true,\"addLegend\":false,\"gauge\":{\"alignment\":\"automatic\",\"extendRange\":true,\"percentageMode\":false,\"gaugeType\":\"Arc\",\"gaugeStyle\":\"Full\",\"backStyle\":\"Full\",\"orientation\":\"vertical\",\"colorSchema\":\"Green to Red\",\"gaugeColorMode\":\"Labels\",\"colorsRange\":[{\"from\":0,\"to\":500},{\"from\":500,\"to\":1000},{\"from\":1000,\"to\":1500}],\"invertColors\":true,\"labels\":{\"show\":false,\"color\":\"black\"},\"scale\":{\"show\":true,\"labels\":false,\"color\":\"#333\"},\"type\":\"meter\",\"style\":{\"bgWidth\":0.9,\"width\":0.9,\"mask\":false,\"bgMask\":false,\"maskBars\":50,\"bgFill\":\"#eee\",\"bgColor\":false,\"subText\":\"visitors\",\"fontSize\":60,\"labelColor\":true}},\"isDisplayWarning\":false},\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"cardinality\",\"schema\":\"metric\",\"params\":{\"field\":\"clientip\",\"customLabel\":\"Unique Visitors\"}}]}", "uiStateJSON": "{\"vis\":{\"defaultColors\":{\"0 - 500\":\"rgb(165,0,38)\",\"500 - 1000\":\"rgb(255,255,190)\",\"1000 - 1500\":\"rgb(0,104,55)\"},\"colors\":{\"75 - 100\":\"#629E51\",\"50 - 75\":\"#EAB839\",\"0 - 50\":\"#E24D42\",\"0 - 100\":\"#E24D42\",\"200 - 300\":\"#7EB26D\",\"500 - 1000\":\"#E5AC0E\",\"0 - 500\":\"#E24D42\",\"1000 - 1500\":\"#7EB26D\"},\"legendOpen\":true}}", "description": "", "version": 1, diff --git a/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js b/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js index 04bcad2535538b..bb4aa5679b3740 100644 --- a/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js +++ b/src/legacy/ui/public/vislib/__tests__/visualizations/gauge_chart.js @@ -36,7 +36,7 @@ describe('Vislib Gauge Chart Test Suite', function () { addTooltip: true, addLegend: false, gauge: { - verticalSplit: false, + alignment: 'horizontal', autoExtend: false, percentageMode: false, gaugeStyle: 'Full', @@ -122,10 +122,19 @@ describe('Vislib Gauge Chart Test Suite', function () { expect($(chartEl).find('svg > g > g > text').text()).to.equal('94%77%61%24%45%'); }); + it('creates gauge with automatic mode', function () { + generateVis({ + gauge: { + alignment: 'automatic' + } + }); + expect($(chartEl).find('svg').width()).to.equal(197); + }); + it('creates gauge with vertical mode', function () { generateVis({ gauge: { - verticalSplit: true + alignment: 'vertical' } }); expect($(chartEl).find('svg').width()).to.equal($(chartEl).width()); diff --git a/src/legacy/ui/public/vislib/visualizations/gauge_chart.js b/src/legacy/ui/public/vislib/visualizations/gauge_chart.js index c8a02b6a6fa204..09d827429427d8 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauge_chart.js +++ b/src/legacy/ui/public/vislib/visualizations/gauge_chart.js @@ -18,12 +18,10 @@ */ import d3 from 'd3'; -import $ from 'jquery'; import { VislibVisualizationsChartProvider } from './_chart'; import { GaugeTypesProvider } from './gauges/gauge_types'; export function GaugeChartProvider(Private) { - const Chart = Private(VislibVisualizationsChartProvider); const gaugeTypes = Private(GaugeTypesProvider); @@ -41,53 +39,96 @@ export function GaugeChartProvider(Private) { .call(events.addHoverEvent()) .call(events.addMouseoutEvent()) .call(events.addClickEvent()); + + } + + /** + * returns the displayed width and height of a single gauge depending on selected alignment + * @param alignment - automatic | horizontal | vertical + * @param containerDom + * @param nrOfItems + * @returns {{width: number, height: number}} + */ + calcGaugeDim(alignment, containerDom, nrOfItems) { + const containerWidth = containerDom.clientWidth; + const containerHeight = containerDom.clientHeight; + const containerMargin = 25; + + //there are a few pixel of margin between multiple gauges + //subtracting this margin prevents displaying scrollbars + //this is because of the "chart-title" element, + //that's inserted after the gauges + const gaugeBottomMargin = Math.ceil(25 / nrOfItems); + const availableWidth = containerWidth - containerMargin; + const availableHeight = containerHeight - containerMargin; + + const adaptedWidth = Math.floor(availableWidth / nrOfItems); + const adaptedHeight = Math.floor(availableHeight / nrOfItems) - gaugeBottomMargin; + + switch (alignment) { + case 'vertical': + return { + width: containerWidth, //for compatiblity with tests + height: adaptedHeight, + alignment, + }; + + case 'horizontal': + return { + width: adaptedWidth, + height: availableHeight, + alignment, + }; + + default: + return { + width: availableWidth < availableHeight ? containerWidth : adaptedWidth, + height: availableWidth < availableHeight ? adaptedHeight : availableHeight, + alignment: availableWidth < availableHeight ? 'vertical' : 'horizontal', + }; + } } draw() { const self = this; - const verticalSplit = this.gaugeConfig.verticalSplit; + const { gaugeConfig } = this; return function (selection) { selection.each(function (data) { const div = d3.select(this); - const containerMargin = 20; - const containerWidth = $(this).width() - containerMargin; - const containerHeight = $(this).height() - containerMargin; - const width = Math.floor(verticalSplit ? $(this).width() : containerWidth / data.series.length); - const height = Math.floor((verticalSplit ? containerHeight / data.series.length : $(this).height()) - 25); + const { width, height } = self.calcGaugeDim( + gaugeConfig.alignment, + this, + data.series.length + ); if (height < 0 || width < 0) return; - div - .style('text-align', 'center') - .style('overflow-y', 'auto'); + div.style('text-align', 'center').style('overflow-y', 'auto'); data.series.forEach(series => { - const svg = div.append('svg') + const svg = div + .append('svg') .style('display', 'inline-block') .style('overflow', 'hidden') .attr('focusable', 'false') .attr('width', width); const g = svg.append('g'); - const gauges = self.gauge.drawGauge(g, series, width, height); - svg.attr('height', height); - const transformX = width / 2; - const transformY = self.gaugeConfig.gaugeType === 'Arc' ? height / (2 * 0.75) : height / 2; - g.attr('transform', `translate(${transformX}, ${transformY})`); self.addEvents(gauges); }); - div.append('div') + div + .append('div') .attr('class', 'chart-title') .style('text-align', 'center') .text(data.label || data.yAxisLabel); self.events.emit('rendered', { - chart: data + chart: data, }); return div; diff --git a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js index 5642c6e76c4d70..f407dbc96367ee 100644 --- a/src/legacy/ui/public/vislib/visualizations/gauges/meter.js +++ b/src/legacy/ui/public/vislib/visualizations/gauges/meter.js @@ -332,6 +332,12 @@ export function MeterGaugeProvider() { this.gaugeChart.handler.alerts.show('Some labels were hidden due to size constraints'); } + //center the visualization + const transformX = width / 2; + const transformY = height / 2 > maxRadius ? height / 2 : maxRadius; + + svg.attr('transform', `translate(${transformX}, ${transformY})`); + return series; } } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1762df7426b58e..a15ccfeaf36c63 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2433,7 +2433,6 @@ "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "スタイル", "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "サブテキスト", "kbnVislibVisTypes.controls.gaugeOptions.toTitle": "終了:", - "kbnVislibVisTypes.controls.gaugeOptions.verticalSplitLabel": "縦分割", "kbnVislibVisTypes.controls.heatmapOptions.addRangeButtonLabel": "範囲を追加", "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "色", "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "カラースケール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 26b27e0bafc6d6..cfb881b71a2068 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2447,7 +2447,6 @@ "kbnVislibVisTypes.controls.gaugeOptions.styleTitle": "样式", "kbnVislibVisTypes.controls.gaugeOptions.subTextLabel": "子文本", "kbnVislibVisTypes.controls.gaugeOptions.toTitle": "到", - "kbnVislibVisTypes.controls.gaugeOptions.verticalSplitLabel": "垂直拆分", "kbnVislibVisTypes.controls.heatmapOptions.addRangeButtonLabel": "添加范围", "kbnVislibVisTypes.controls.heatmapOptions.colorLabel": "颜色", "kbnVislibVisTypes.controls.heatmapOptions.colorScaleLabel": "颜色比例", From 4acd5fff1974675ec1043017f327b162bfc1b590 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 28 May 2019 18:28:42 +0200 Subject: [PATCH 3/5] [ML] Fix access denied for data frames. (#37178) (#37237) With insufficient privileges, a user would be redirected to the ML plugin's access-denied page which mentions the required user roles to access the pages. Since data frames introduces new user roles these messages were not correct. This PR fixes it by redirecting to a specific access-denied page for data frames. To avoid to much refactoring as a fix, the page is a copy and port to React of the original one. In a follow up for 7.3, we should merge the two pages and it should have options to display required user roles given a certain context like anomaly detection or data frames. --- x-pack/plugins/ml/public/data_frame/index.ts | 2 + .../pages/access_denied/directive.tsx | 47 ++++++++++ .../pages/access_denied/page.test.tsx | 38 ++++++++ .../data_frame/pages/access_denied/page.tsx | 93 +++++++++++++++++++ .../data_frame/pages/access_denied/route.ts | 17 ++++ .../ml/public/privilege/check_privilege.ts | 2 +- 6 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx create mode 100644 x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx create mode 100644 x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx create mode 100644 x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts diff --git a/x-pack/plugins/ml/public/data_frame/index.ts b/x-pack/plugins/ml/public/data_frame/index.ts index fb8c8fadd3eccb..b493d1459655e6 100644 --- a/x-pack/plugins/ml/public/data_frame/index.ts +++ b/x-pack/plugins/ml/public/data_frame/index.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import './pages/access_denied/directive'; +import './pages/access_denied/route'; import './pages/job_management/directive'; import './pages/job_management/route'; import './pages/data_frame_new_pivot/directive'; diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx new file mode 100644 index 00000000000000..fd8b3bc480ec58 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/directive.tsx @@ -0,0 +1,47 @@ +/* + * 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'; + +// @ts-ignore +import { uiModules } from 'ui/modules'; +import uiChrome from 'ui/chrome'; + +const module = uiModules.get('apps/ml', ['react']); + +import { I18nContext } from 'ui/i18n'; +import { InjectorService } from '../../../../common/types/angular'; + +import { Page } from './page'; + +module.directive('mlDataFrameAccessDenied', ($injector: InjectorService) => { + return { + scope: {}, + restrict: 'E', + link: (scope: ng.IScope, element: ng.IAugmentedJQuery) => { + const kbnBaseUrl = $injector.get('kbnBaseUrl'); + const kbnUrl = $injector.get('kbnUrl'); + + const goToKibana = () => { + window.location.href = uiChrome.getBasePath() + kbnBaseUrl; + }; + + const retry = () => { + kbnUrl.redirect('/data_frames'); + }; + + const props = { goToKibana, retry }; + + ReactDOM.render({React.createElement(Page, props)}, element[0]); + + element.on('$destroy', () => { + ReactDOM.unmountComponentAtNode(element[0]); + scope.$destroy(); + }); + }, + }; +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx new file mode 100644 index 00000000000000..d38cf18b4a78d1 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.test.tsx @@ -0,0 +1,38 @@ +/* + * 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, fireEvent, cleanup } from 'react-testing-library'; + +import { I18nProvider } from '@kbn/i18n/react'; + +import { Page } from './page'; + +afterEach(cleanup); + +describe('Data Frame: Access denied ', () => { + test('Minimal initialization', () => { + const props = { + goToKibana: jest.fn(), + retry: jest.fn(), + }; + + const tree = ( + + + + ); + + const { getByText } = render(tree); + + fireEvent.click(getByText(/Back to Kibana home/i)); + fireEvent.click(getByText(/Retry/i)); + + expect(props.goToKibana).toHaveBeenCalledTimes(1); + expect(props.retry).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx new file mode 100644 index 00000000000000..fa41b5490b7cd7 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/page.tsx @@ -0,0 +1,93 @@ +/* + * 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, { SFC } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiPage, + EuiPageBody, + EuiPageContentBody, + EuiPageContentHeader, + EuiPageContentHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; + +interface PageProps { + goToKibana: () => void; + retry: () => void; +} +export const Page: SFC = ({ goToKibana, retry }) => ( + + + + + +

+ +

+
+
+
+ + + + +

+ kibana_user, + dataFrameUserParam: ( + data_frame_transforms_user + ), + br:
, + }} + /> +

+
+
+ + + + + + + + + + + + + +
+
+
+); diff --git a/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts b/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts new file mode 100644 index 00000000000000..63689b4ec551e1 --- /dev/null +++ b/x-pack/plugins/ml/public/data_frame/pages/access_denied/route.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import uiRoutes from 'ui/routes'; + +// @ts-ignore +import { getDataFrameBreadcrumbs } from '../../breadcrumbs'; + +const template = ``; + +uiRoutes.when('/data_frames/access-denied', { + template, + k7Breadcrumbs: getDataFrameBreadcrumbs, +}); diff --git a/x-pack/plugins/ml/public/privilege/check_privilege.ts b/x-pack/plugins/ml/public/privilege/check_privilege.ts index be46aebbeb90e2..00a0d8dbeff16a 100644 --- a/x-pack/plugins/ml/public/privilege/check_privilege.ts +++ b/x-pack/plugins/ml/public/privilege/check_privilege.ts @@ -71,7 +71,7 @@ export function checkGetDataFrameJobsPrivilege(kbnUrl: any): Promise if (privileges.canGetDataFrameJobs) { return resolve(privileges); } else { - kbnUrl.redirect('/access-denied'); + kbnUrl.redirect('/data_frames/access-denied'); return reject(); } }); From c0faaf7e057cc34811e234995087bbbb91c7ec5a Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 28 May 2019 18:48:34 +0200 Subject: [PATCH 4/5] [ML] Fix to not add configs with names which would result in nesting conflicts (#37212) (#37240) Two group-by or aggregation names could result in conflicting nested fields which would return an error for pivot previews. For example, two names like responsetime and responsetime.avg are not allowed. This PR fixes the issue by extending the tests whether a configuration is allowed to be added to the list of group-by and aggregation configurations. If a conflict is detected, a toast notification gets triggered and the configuration won't be added. --- .../define_pivot/define_pivot_form.test.tsx | 79 +++++++++- .../define_pivot/define_pivot_form.tsx | 145 +++++++++++++++--- 2 files changed, 198 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx index 701b6dbc6cb9e1..390e703cdfe60a 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.test.tsx @@ -7,8 +7,14 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { KibanaContext } from '../../common'; -import { DefinePivotForm } from './define_pivot_form'; +import { + KibanaContext, + PivotAggsConfigDict, + PivotGroupByConfigDict, + PIVOT_SUPPORTED_AGGS, + PIVOT_SUPPORTED_GROUP_BY_AGGS, +} from '../../common'; +import { DefinePivotForm, isAggNameConflict } from './define_pivot_form'; // workaround to make React.memo() work with enzyme jest.mock('react', () => { @@ -46,3 +52,72 @@ describe('Data Frame: ', () => { expect(wrapper).toMatchSnapshot(); }); }); + +describe('Data Frame: isAggNameConflict()', () => { + test('detect aggregation name conflicts', () => { + const aggList: PivotAggsConfigDict = { + 'the-agg-name': { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-field-name', + aggName: 'the-agg-name', + dropDownName: 'the-dropdown-name', + }, + 'the-namespaced-agg-name.namespace': { + agg: PIVOT_SUPPORTED_AGGS.AVG, + field: 'the-field-name', + aggName: 'the-namespaced-agg-name.namespace', + dropDownName: 'the-dropdown-name', + }, + }; + + const groupByList: PivotGroupByConfigDict = { + 'the-group-by-agg-name': { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-field-name', + aggName: 'the-group-by-agg-name', + dropDownName: 'the-dropdown-name', + }, + 'the-namespaced-group-by-agg-name.namespace': { + agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, + field: 'the-field-name', + aggName: 'the-namespaced-group-by-agg-name.namespace', + dropDownName: 'the-dropdown-name', + }, + }; + + // no conflict, completely different name, no namespacing involved + expect(isAggNameConflict('the-other-agg-name', aggList, groupByList)).toBe(false); + // no conflict, completely different name and no conflicting namespace + expect(isAggNameConflict('the-other-agg-name.namespace', aggList, groupByList)).toBe(false); + + // exact match conflict on aggregation name + expect(isAggNameConflict('the-agg-name', aggList, groupByList)).toBe(true); + // namespace conflict with `the-agg-name` aggregation + expect(isAggNameConflict('the-agg-name.namespace', aggList, groupByList)).toBe(true); + + // exact match conflict on group-by name + expect(isAggNameConflict('the-group-by-agg-name', aggList, groupByList)).toBe(true); + // namespace conflict with `the-group-by-agg-name` group-by + expect(isAggNameConflict('the-group-by-agg-name.namespace', aggList, groupByList)).toBe(true); + + // exact match conflict on namespaced agg name + expect(isAggNameConflict('the-namespaced-agg-name.namespace', aggList, groupByList)).toBe(true); + // no conflict, same base agg name but different namespace + expect(isAggNameConflict('the-namespaced-agg-name.namespace2', aggList, groupByList)).toBe( + false + ); + // namespace conflict because the new agg name is base name of existing nested field + expect(isAggNameConflict('the-namespaced-agg-name', aggList, groupByList)).toBe(true); + + // exact match conflict on namespaced group-by name + expect( + isAggNameConflict('the-namespaced-group-by-agg-name.namespace', aggList, groupByList) + ).toBe(true); + // no conflict, same base group-by name but different namespace + expect( + isAggNameConflict('the-namespaced-group-by-agg-name.namespace2', aggList, groupByList) + ).toBe(false); + // namespace conflict because the new group-by name is base name of existing nested field + expect(isAggNameConflict('the-namespaced-group-by-agg-name', aggList, groupByList)).toBe(true); + }); +}); diff --git a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx index bf82cc28a4d88e..4d734684fc6394 100644 --- a/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx +++ b/x-pack/plugins/ml/public/data_frame/components/define_pivot/define_pivot_form.tsx @@ -66,6 +66,101 @@ export function getDefaultPivotState(kibanaContext: KibanaContextValue): DefineP valid: false, }; } +export function isAggNameConflict( + aggName: AggName, + aggList: PivotAggsConfigDict, + groupByList: PivotGroupByConfigDict +) { + if (aggList[aggName] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.aggExistsErrorMessage', { + defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }) + ); + return true; + } + + if (groupByList[aggName] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.groupByExistsErrorMessage', { + defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, + values: { aggName }, + }) + ); + return true; + } + + let conflict = false; + + // check the new aggName against existing aggs and groupbys + const aggNameSplit = aggName.split('.'); + let aggNameCheck: string; + aggNameSplit.forEach(aggNamePart => { + aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; + if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggNameCheck}'.`, + values: { aggName, aggNameCheck }, + }) + ); + conflict = true; + } + }); + + if (conflict) { + return true; + } + + // check all aggs against new aggName + conflict = Object.keys(aggList).some(aggListName => { + const aggListNameSplit = aggListName.split('.'); + let aggListNameCheck: string; + return aggListNameSplit.some(aggListNamePart => { + aggListNameCheck = + aggListNameCheck === undefined ? aggListNamePart : `${aggListNameCheck}.${aggListNamePart}`; + if (aggListNameCheck === aggName) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedAggListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{aggListName}'.`, + values: { aggName, aggListName }, + }) + ); + return true; + } + return false; + }); + }); + + if (conflict) { + return true; + } + + // check all group-bys against new aggName + conflict = Object.keys(groupByList).some(groupByListName => { + const groupByListNameSplit = groupByListName.split('.'); + let groupByListNameCheck: string; + return groupByListNameSplit.some(groupByListNamePart => { + groupByListNameCheck = + groupByListNameCheck === undefined + ? groupByListNamePart + : `${groupByListNameCheck}.${groupByListNamePart}`; + if (groupByListNameCheck === aggName) { + toastNotifications.addDanger( + i18n.translate('xpack.ml.dataframe.definePivot.nestedGroupByListConflictErrorMessage', { + defaultMessage: `Couldn't add configuration '{aggName}' because of a nesting conflict with '{groupByListName}'.`, + values: { aggName, groupByListName }, + }) + ); + return true; + } + return false; + }); + }); + + return conflict; +} interface Props { overrides?: DefinePivotExposedState; @@ -111,23 +206,24 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang const config: PivotGroupByConfig = groupByOptionsData[label]; const aggName: AggName = config.aggName; - if (groupByList[aggName] === undefined) { - groupByList[aggName] = config; - setGroupByList({ ...groupByList }); - } else { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.definePivot.groupByExistsErrorMessage', { - defaultMessage: `A group by configuration with the name '{aggName}' already exists.`, - values: { aggName }, - }) - ); + if (isAggNameConflict(aggName, aggList, groupByList)) { + return; } + + groupByList[aggName] = config; + setGroupByList({ ...groupByList }); }; const updateGroupBy = (previousAggName: AggName, item: PivotGroupByConfig) => { - delete groupByList[previousAggName]; - groupByList[item.aggName] = item; - setGroupByList({ ...groupByList }); + const groupByListWithoutPrevious = { ...groupByList }; + delete groupByListWithoutPrevious[previousAggName]; + + if (isAggNameConflict(item.aggName, aggList, groupByListWithoutPrevious)) { + return; + } + + groupByListWithoutPrevious[item.aggName] = item; + setGroupByList({ ...groupByListWithoutPrevious }); }; const deleteGroupBy = (aggName: AggName) => { @@ -143,21 +239,22 @@ export const DefinePivotForm: SFC = React.memo(({ overrides = {}, onChang const config: PivotAggsConfig = aggOptionsData[label]; const aggName: AggName = config.aggName; - if (aggList[aggName] === undefined) { - aggList[aggName] = config; - setAggList({ ...aggList }); - } else { - toastNotifications.addDanger( - i18n.translate('xpack.ml.dataframe.definePivot.aggExistsErrorMessage', { - defaultMessage: `An aggregation configuration with the name '{aggName}' already exists.`, - values: { aggName }, - }) - ); + if (isAggNameConflict(aggName, aggList, groupByList)) { + return; } + + aggList[aggName] = config; + setAggList({ ...aggList }); }; const updateAggregation = (previousAggName: AggName, item: PivotAggsConfig) => { - delete aggList[previousAggName]; + const aggListWithoutPrevious = { ...aggList }; + delete aggListWithoutPrevious[previousAggName]; + + if (isAggNameConflict(item.aggName, aggListWithoutPrevious, groupByList)) { + return; + } + aggList[item.aggName] = item; setAggList({ ...aggList }); }; From 00ee70086d265f92b171436dc7141073156a292c Mon Sep 17 00:00:00 2001 From: Todd Kennedy Date: Tue, 28 May 2019 09:52:04 -0700 Subject: [PATCH 5/5] [feat] add reach-like functionality to object (#37118) (#37139) * [feat] add reach-like functionality to object In #36804 we need to validate a `Partial` object which may or may not have all the required keys attached to it. This is the type of use-case for `Joi.reach`, so we should expose a reach-like method on the object validation class so that you can return the validator for a specific key on an object. * [fix] change to validateKey * [fix] use throw error matcher * [fix] use same interface for validate * [fix] change test name] --- .../src/types/object_type.test.ts | 12 ++++++++++++ .../kbn-config-schema/src/types/object_type.ts | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/kbn-config-schema/src/types/object_type.test.ts b/packages/kbn-config-schema/src/types/object_type.test.ts index e0eaabadb8ef59..e47932a3a3ed2f 100644 --- a/packages/kbn-config-schema/src/types/object_type.test.ts +++ b/packages/kbn-config-schema/src/types/object_type.test.ts @@ -199,3 +199,15 @@ test('includes namespace in failure when wrong value type', () => { expect(() => type.validate(value, {}, 'foo-namespace')).toThrowErrorMatchingSnapshot(); }); + +test('individual keys can validated', () => { + const type = schema.object({ + foo: schema.boolean(), + }); + + const value = false; + expect(() => type.validateKey('foo', value)).not.toThrowError(); + expect(() => type.validateKey('bar', '')).toThrowErrorMatchingInlineSnapshot( + `"bar is not a valid part of this schema"` + ); +}); diff --git a/packages/kbn-config-schema/src/types/object_type.ts b/packages/kbn-config-schema/src/types/object_type.ts index e61fcd90ef016f..7ee4014b08d0ef 100644 --- a/packages/kbn-config-schema/src/types/object_type.ts +++ b/packages/kbn-config-schema/src/types/object_type.ts @@ -20,6 +20,7 @@ import typeDetect from 'type-detect'; import { AnySchema, internals } from '../internals'; import { Type, TypeOptions } from './type'; +import { ValidationError } from '../errors'; export type Props = Record>; @@ -31,6 +32,8 @@ export type TypeOf> = RT['type']; export type ObjectResultType

= Readonly<{ [K in keyof P]: TypeOf }>; export class ObjectType

extends Type> { + private props: Record; + constructor(props: P, options: TypeOptions<{ [K in keyof P]: TypeOf }> = {}) { const schemaKeys = {} as Record; for (const [key, value] of Object.entries(props)) { @@ -44,6 +47,7 @@ export class ObjectType

extends Type> .default(); super(schema, options); + this.props = schemaKeys; } protected handleError(type: string, { reason, value }: Record) { @@ -57,4 +61,15 @@ export class ObjectType

extends Type> return reason[0]; } } + + validateKey(key: string, value: any) { + if (!this.props[key]) { + throw new Error(`${key} is not a valid part of this schema`); + } + const { value: validatedValue, error } = this.props[key].validate(value); + if (error) { + throw new ValidationError(error as any, key); + } + return validatedValue; + } }