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; + } } 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/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.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 888f662621af8d..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 }); }; @@ -191,6 +288,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 +303,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; 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(); } }); diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 75a9d0443c7368..55845b48240443 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..9d555370b2e104 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": "颜色比例", @@ -9290,7 +9289,6 @@ "xpack.siem.notes.addNoteButtonLabel": "添加备注", "xpack.siem.notes.cancelButtonLabel": "取消", "xpack.siem.notes.copyToClipboardButtonLabel": "复制到剪贴板", - "xpack.siem.notes.noteTitle": "{totalCount, plural, =1 {Note} other {Notes}}", "xpack.siem.notes.previewMarkdownTitle": "预览 (Markdown)", "xpack.siem.notes.search.FilterByUserOrNotePlaceholder": "按用户或备注筛选", "xpack.siem.open.timeline.cancelButton": "取消",