diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index ddabce3d5b842..12ee96b21b0c7 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -94,7 +94,7 @@ Some example translations are shown here: **Environment Variable**:: **Kibana Setting** `SERVER_NAME`:: `server.name` `KIBANA_DEFAULTAPPID`:: `kibana.defaultAppId` -`XPACK_MONITORING_ENABLED`:: `xpack.monitoring.enabled` +`MONITORING_ENABLED`:: `monitoring.enabled` In general, any setting listed in <> can be configured with this technique. @@ -125,9 +125,9 @@ images: `server.name`:: `kibana` `server.host`:: `"0"` `elasticsearch.hosts`:: `http://elasticsearch:9200` -`xpack.monitoring.ui.container.elasticsearch.enabled`:: `true` +`monitoring.ui.container.elasticsearch.enabled`:: `true` -NOTE: The setting `xpack.monitoring.ui.container.elasticsearch.enabled` is not +NOTE: The setting `monitoring.ui.container.elasticsearch.enabled` is not defined in the `-oss` image. These settings are defined in the default `kibana.yml`. They can be overridden diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc index cfdc9e2037030..a58ccc7f7d68d 100644 --- a/docs/user/monitoring/cluster-alerts.asciidoc +++ b/docs/user/monitoring/cluster-alerts.asciidoc @@ -49,13 +49,13 @@ To receive email notifications for the Cluster Alerts: . Configure an email account as described in {ref}/actions-email.html#configuring-email[Configuring email accounts]. . Configure the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in `kibana.yml` with your email address. + -- TIP: If you have separate production and monitoring clusters and separate {kib} instances for those clusters, you must put the -`xpack.monitoring.cluster_alerts.email_notifications.email_address` setting in +`monitoring.cluster_alerts.email_notifications.email_address` setting in the {kib} instance that is associated with the production cluster. -- diff --git a/docs/user/monitoring/elasticsearch-details.asciidoc b/docs/user/monitoring/elasticsearch-details.asciidoc index c0e804672d298..93f809cfff650 100644 --- a/docs/user/monitoring/elasticsearch-details.asciidoc +++ b/docs/user/monitoring/elasticsearch-details.asciidoc @@ -164,4 +164,4 @@ image::user/monitoring/images/monitoring-elasticsearch-logs.jpg["Recent {es} log TIP: By default, up to 10 log entries are shown. You can show up to 50 log entries by changing the -<>. +<>. diff --git a/docs/user/monitoring/monitoring-kibana.asciidoc b/docs/user/monitoring/monitoring-kibana.asciidoc index d0f2bd6acd901..9aa10289d299b 100644 --- a/docs/user/monitoring/monitoring-kibana.asciidoc +++ b/docs/user/monitoring/monitoring-kibana.asciidoc @@ -61,8 +61,8 @@ For more information, see {ref}/monitoring-settings.html[Monitoring settings in and {ref}/cluster-update-settings.html[Cluster update settings]. -- -. Verify that `xpack.monitoring.enabled` and -`xpack.monitoring.kibana.collection.enabled` are set to `true` in the +. Verify that `monitoring.enabled` and +`monitoring.kibana.collection.enabled` are set to `true` in the `kibana.yml` file. These are the default values. For more information, see <>. diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index f03a2ce1525a4..61aeaf21d3a4b 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -25,10 +25,10 @@ Add the following setting in the {kib} configuration file (`kibana.yml`): [source,yaml] ---------------------------------- -xpack.monitoring.kibana.collection.enabled: false +monitoring.kibana.collection.enabled: false ---------------------------------- -Leave the `xpack.monitoring.enabled` set to its default value (`true`). +Leave the `monitoring.enabled` set to its default value (`true`). // end::disable-kibana-collection[] For more information, see <>. diff --git a/docs/user/monitoring/monitoring-troubleshooting.asciidoc b/docs/user/monitoring/monitoring-troubleshooting.asciidoc index 7e1d6f94f15fa..bdaa10990c3aa 100644 --- a/docs/user/monitoring/monitoring-troubleshooting.asciidoc +++ b/docs/user/monitoring/monitoring-troubleshooting.asciidoc @@ -52,7 +52,7 @@ The *Stack Monitoring* page in {kib} is empty. . Confirm that {kib} is seeking monitoring data from the appropriate {es} URL. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` setting in the `kibana.yml` file. If you want to retrieve it -from a different monitoring cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different monitoring cluster, set `monitoring.ui.elasticsearch.hosts`. See <>. . Confirm that there is monitoring data available at that URL. It is stored in diff --git a/docs/user/monitoring/viewing-metrics.asciidoc b/docs/user/monitoring/viewing-metrics.asciidoc index 11516e32400fb..0a5535e6e1a91 100644 --- a/docs/user/monitoring/viewing-metrics.asciidoc +++ b/docs/user/monitoring/viewing-metrics.asciidoc @@ -26,14 +26,14 @@ cluster and view them all through the same instance of {kib}. By default, data is retrieved from the cluster specified in the `elasticsearch.hosts` value in the `kibana.yml` file. If you want to retrieve it -from a different cluster, set `xpack.monitoring.elasticsearch.hosts`. +from a different cluster, set `monitoring.ui.elasticsearch.hosts`. To learn more about typical monitoring architectures, see {ref}/how-monitoring-works.html[How monitoring works] and {ref}/monitoring-production.html[Monitoring in a production environment]. -- -. Verify that `xpack.monitoring.ui.enabled` is set to `true`, which is the +. Verify that `monitoring.ui.enabled` is set to `true`, which is the default value, in the `kibana.yml` file. For more information, see <>. @@ -43,8 +43,8 @@ must provide a user ID and password so {kib} can retrieve the data. .. Create a user that has the `monitoring_user` {ref}/built-in-roles.html[built-in role] on the monitoring cluster. -.. Add the `xpack.monitoring.elasticsearch.username` and -`xpack.monitoring.elasticsearch.password` settings in the `kibana.yml` file. +.. Add the `monitoring.ui.elasticsearch.username` and +`monitoring.ui.elasticsearch.password` settings in the `kibana.yml` file. If these settings are omitted, {kib} uses the `elasticsearch.username` and `elasticsearch.password` setting values. For more information, see {kibana-ref}/using-kibana-with-security.html[Configuring security in {kib}]. diff --git a/docs/user/security/securing-communications/index.asciidoc b/docs/user/security/securing-communications/index.asciidoc index 97313c19f44cb..3bdc59b90b3fd 100644 --- a/docs/user/security/securing-communications/index.asciidoc +++ b/docs/user/security/securing-communications/index.asciidoc @@ -188,5 +188,5 @@ verification. For more information about this setting, see < const indexPattern = aggConfig.getIndexPattern(); if (filter && indexPattern && indexPattern.id) { - return buildQueryFilter(filter.query, indexPattern.id, key); + return buildQueryFilter(filter, indexPattern.id, key); } }; diff --git a/src/plugins/data/public/search/aggs/buckets/filters.ts b/src/plugins/data/public/search/aggs/buckets/filters.ts index a42cb70a62b7d..fe013928bba65 100644 --- a/src/plugins/data/public/search/aggs/buckets/filters.ts +++ b/src/plugins/data/public/search/aggs/buckets/filters.ts @@ -107,7 +107,7 @@ export const getFiltersBucketAgg = ({ (typeof filter.input.query === 'string' ? filter.input.query : toAngularJSON(filter.input.query)); - filters[label] = { query }; + filters[label] = query; }, {} ); diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index c9fecb3f7e9ce..091a27a6f418d 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -447,26 +447,6 @@ export class SearchSource { delete searchRequest.highlightAll; } - const translateToQuery = (filter: Filter) => filter && (filter.query || filter); - - // re-write filters within filter aggregations - (function recurse(aggBranch) { - if (!aggBranch) return; - Object.keys(aggBranch).forEach(function(id) { - const agg = aggBranch[id]; - - if (agg.filters) { - // translate filters aggregations - const { filters: aggFilters } = agg.filters; - Object.keys(aggFilters).forEach(filterId => { - aggFilters[filterId] = translateToQuery(aggFilters[filterId]); - }); - } - - recurse(agg.aggs || agg.aggregations); - }); - })(body.aggs || body.aggregations); - return searchRequest; } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts new file mode 100644 index 0000000000000..784b0b4d4f3d7 --- /dev/null +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { shouldReadFieldFromDocValues } from './should_read_field_from_doc_values'; + +describe('shouldReadFieldFromDocValues', () => { + test('should read field from doc values for aggregatable "number" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'number')).toBe(true); + }); + + test('should not read field from doc values for non-aggregatable "number "field', async () => { + expect(shouldReadFieldFromDocValues(false, 'number')).toBe(false); + }); + + test('should not read field from doc values for "text" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'text')).toBe(false); + }); + + test('should not read field from doc values for "geo_shape" field', async () => { + expect(shouldReadFieldFromDocValues(true, 'geo_shape')).toBe(false); + }); + + test('should not read field from doc values for underscore field', async () => { + expect(shouldReadFieldFromDocValues(true, '_source')).toBe(false); + }); +}); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts index 6d58f7a02c134..56a1cf3ccd161 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/should_read_field_from_doc_values.ts @@ -18,5 +18,5 @@ */ export function shouldReadFieldFromDocValues(aggregatable: boolean, esType: string) { - return aggregatable && esType !== 'text' && !esType.startsWith('_'); + return aggregatable && !['text', 'geo_shape'].includes(esType) && !esType.startsWith('_'); } diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts index feba88dbe8b90..6c0cd7eb33dc0 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/index.ts @@ -16,7 +16,7 @@ import { horizontalProgressPill } from './horizontal_progress_pill'; import { image } from './image'; import { lineChart } from './line_chart'; import { markdown } from './markdown'; -import { metric } from './metric'; +import { metricElementInitializer } from './metric'; import { pie } from './pie'; import { plot } from './plot'; import { progressGauge } from './progress_gauge'; @@ -32,7 +32,10 @@ import { verticalBarChart } from './vert_bar_chart'; import { verticalProgressBar } from './vertical_progress_bar'; import { verticalProgressPill } from './vertical_progress_pill'; -export const elementSpecs = applyElementStrings([ +import { SetupInitializer } from '../plugin'; +import { ElementFactory } from '../../types'; + +const elementSpecs = [ areaChart, bubbleChart, debug, @@ -44,7 +47,6 @@ export const elementSpecs = applyElementStrings([ horizontalProgressPill, lineChart, markdown, - metric, pie, plot, progressGauge, @@ -59,4 +61,15 @@ export const elementSpecs = applyElementStrings([ verticalBarChart, verticalProgressBar, verticalProgressPill, -]); +]; + +const initializeElementFactories = [metricElementInitializer]; + +export const initializeElements: SetupInitializer = (core, plugins) => { + const specs = [ + ...elementSpecs, + ...initializeElementFactories.map(factory => factory(core, plugins)), + ]; + + return applyElementStrings(specs); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts index c08c090f11f91..7e9aca9c01782 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/elements/metric/index.ts @@ -6,23 +6,26 @@ import { openSans } from '../../../common/lib/fonts'; import header from './header.png'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; import { ElementFactory } from '../../../types'; -export const metric: ElementFactory = () => ({ - name: 'metric', - displayName: 'Metric', - tags: ['text'], - help: 'A number with a label', - width: 200, - height: 100, - image: header, - expression: `filters -| demodata -| math "unique(country)" -| metric "Countries" - metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} - labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} - metricFormat="${getAdvancedSettings().get('format:number:defaultPattern')}" -| render`, -}); +import { SetupInitializer } from '../../plugin'; + +export const metricElementInitializer: SetupInitializer = (core, setup) => { + return () => ({ + name: 'metric', + displayName: 'Metric', + tags: ['text'], + help: 'A number with a label', + width: 200, + height: 100, + image: header, + expression: `filters + | demodata + | math "unique(country)" + | metric "Countries" + metricFont={font size=48 family="${openSans.value}" color="#000000" align="center" lHeight=48} + labelFont={font size=14 family="${openSans.value}" color="#000000" align="center"} + metricFormat="${core.uiSettings.get('format:number:defaultPattern')}" + | render`, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts index 2f7f5c26ad0b7..61ecd66455e78 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/functions/common/timefilterControl.ts @@ -8,7 +8,7 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { Render } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; -interface Arguments { +export interface Arguments { column: string; compact: boolean; filterGroup: string; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts index a654c6b28b350..4452e5e9e31fe 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/plugin.ts @@ -14,18 +14,16 @@ import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; // @ts-ignore: untyped local import { renderFunctions, renderFunctionFactories } from './renderers'; - -import { elementSpecs } from './elements'; +import { initializeElements } from './elements'; // @ts-ignore Untyped Local import { transformSpecs } from './uis/transforms'; // @ts-ignore Untyped Local import { datasourceSpecs } from './uis/datasources'; // @ts-ignore Untyped Local import { modelSpecs } from './uis/models'; +import { initializeViews } from './uis/views'; // @ts-ignore Untyped Local -import { viewSpecs } from './uis/views'; -// @ts-ignore Untyped Local -import { args as argSpecs } from './uis/arguments'; +import { initializeArgs } from './uis/arguments'; import { tagSpecs } from './uis/tags'; import { templateSpecs } from './templates'; @@ -39,6 +37,9 @@ export interface StartDeps { inspector: InspectorStart; } +export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; +export type StartInitializer = (core: CoreStart, plugins: StartDeps) => T; + /** @internal */ export class CanvasSrcPlugin implements Plugin { public setup(core: CoreSetup, plugins: SetupDeps) { @@ -53,11 +54,11 @@ export class CanvasSrcPlugin implements Plugin ); }); - plugins.canvas.addElements(elementSpecs); + plugins.canvas.addElements(initializeElements(core, plugins)); plugins.canvas.addDatasourceUIs(datasourceSpecs); plugins.canvas.addModelUIs(modelSpecs); - plugins.canvas.addViewUIs(viewSpecs); - plugins.canvas.addArgumentUIs(argSpecs); + plugins.canvas.addViewUIs(initializeViews(core, plugins)); + plugins.canvas.addArgumentUIs(initializeArgs(core, plugins)); plugins.canvas.addTagUIs(tagSpecs); plugins.canvas.addTemplates(templateSpecs); plugins.canvas.addTransformUIs(transformSpecs); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js index 84f92f5149893..263f2d8ec30b5 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/index.js @@ -20,7 +20,7 @@ import { revealImage } from './reveal_image'; import { shape } from './shape'; import { table } from './table'; import { text } from './text'; -import { timeFilter } from './time_filter'; +import { timeFilterFactory } from './time_filter'; export const renderFunctions = [ advancedFilter, @@ -38,7 +38,6 @@ export const renderFunctions = [ shape, table, text, - timeFilter, ]; -export const renderFunctionFactories = [embeddableRendererFactory]; +export const renderFunctionFactories = [embeddableRendererFactory, timeFilterFactory]; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx index 55a453720e2f0..85ea754de670d 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/index.tsx @@ -4,22 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; -import { TimeFilter as Component, Props } from './time_filter'; +import { TimeFilter } from './time_filter'; -export const TimeFilter = (props: Props) => { - const customQuickRanges = (getAdvancedSettings().get('timepicker:quickRanges') || []).map( - ({ from, to, display }: { from: string; to: string; display: string }) => ({ - start: from, - end: to, - label: display, - }) - ); - - const customDateFormat = getAdvancedSettings().get('dateFormat'); - - return ( - - ); -}; +export { TimeFilter }; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js deleted file mode 100644 index cbc514e218d74..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import ReactDOM from 'react-dom'; -import React from 'react'; -import { toExpression } from '@kbn/interpreter/common'; -import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; -import { RendererStrings } from '../../../i18n'; -import { TimeFilter } from './components'; - -const { timeFilter: strings } = RendererStrings; - -export const timeFilter = () => ({ - name: 'time_filter', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, // must be true, otherwise popovers don't work - render(domNode, config, handlers) { - const filterExpression = handlers.getFilter(); - - if (filterExpression !== '') { - // NOTE: setFilter() will cause a data refresh, avoid calling unless required - // compare expression and filter, update filter if needed - const { changed, newAst } = syncFilterExpression(config, filterExpression, [ - 'column', - 'filterGroup', - ]); - - if (changed) { - handlers.setFilter(toExpression(newAst)); - } - } - - ReactDOM.render( - , - domNode, - () => handlers.done() - ); - - handlers.onDestroy(() => { - ReactDOM.unmountComponentAtNode(domNode); - }); - }, -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx new file mode 100644 index 0000000000000..ef5bfb70d4b3d --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/renderers/time_filter/index.tsx @@ -0,0 +1,70 @@ +/* + * 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 ReactDOM from 'react-dom'; +import React from 'react'; +import { toExpression } from '@kbn/interpreter/common'; +import { syncFilterExpression } from '../../../public/lib/sync_filter_expression'; +import { RendererStrings } from '../../../i18n'; +import { TimeFilter } from './components'; +import { StartInitializer } from '../../plugin'; +import { RendererHandlers } from '../../../types'; +import { Arguments } from '../../functions/common/timefilterControl'; +import { RendererFactory } from '../../../types'; + +const { timeFilter: strings } = RendererStrings; + +export const timeFilterFactory: StartInitializer> = (core, plugins) => { + const { uiSettings } = core; + + const customQuickRanges = (uiSettings.get('timepicker:quickRanges') || []).map( + ({ from, to, display }: { from: string; to: string; display: string }) => ({ + start: from, + end: to, + label: display, + }) + ); + + const customDateFormat = uiSettings.get('dateFormat'); + + return () => ({ + name: 'time_filter', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, // must be true, otherwise popovers don't work + render: async (domNode: HTMLElement, config: Arguments, handlers: RendererHandlers) => { + const filterExpression = handlers.getFilter(); + + if (filterExpression !== '') { + // NOTE: setFilter() will cause a data refresh, avoid calling unless required + // compare expression and filter, update filter if needed + const { changed, newAst } = syncFilterExpression(config, filterExpression, [ + 'column', + 'filterGroup', + ]); + + if (changed) { + handlers.setFilter(toExpression(newAst)); + } + } + + ReactDOM.render( + , + domNode, + () => handlers.done() + ); + + handlers.onDestroy(() => { + ReactDOM.unmountComponentAtNode(domNode); + }); + }, + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts index d19bfa64bae76..655a362fe6d33 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/date_format/index.ts @@ -7,38 +7,40 @@ import { compose, withProps } from 'recompose'; import moment from 'moment'; import { DateFormatArgInput as Component, Props as ComponentProps } from './date_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; -const { DateFormat: strings } = ArgumentStrings; +import { SetupInitializer } from '../../../plugin'; -const getFormatMap = () => ({ - DEFAULT: getAdvancedSettings().get('dateFormat'), - NANOS: getAdvancedSettings().get('dateNanosFormat'), - ISO8601: '', - LOCAL_LONG: 'LLLL', - LOCAL_SHORT: 'LLL', - LOCAL_DATE: 'l', - LOCAL_TIME_WITH_SECONDS: 'LTS', -}); +const { DateFormat: strings } = ArgumentStrings; -const now = moment(); +export const dateFormatInitializer: SetupInitializer> = ( + core, + plugins +) => { + const formatMap = { + DEFAULT: core.uiSettings.get('dateFormat'), + NANOS: core.uiSettings.get('dateNanosFormat'), + ISO8601: '', + LOCAL_LONG: 'LLLL', + LOCAL_SHORT: 'LLL', + LOCAL_DATE: 'l', + LOCAL_TIME_WITH_SECONDS: 'LTS', + }; -const dateFormats = Object.values(getFormatMap()).map(format => ({ - value: format, - text: moment.utc(now).format(format), -})); + const dateFormats = Object.values(formatMap).map(format => ({ + value: format, + text: moment.utc(moment()).format(format), + })); -export const DateFormatArgInput = compose(withProps({ dateFormats }))( - Component -); + const DateFormatArgInput = compose(withProps({ dateFormats }))(Component); -export const dateFormat: ArgumentFactory = () => ({ - name: 'dateFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(DateFormatArgInput), -}); + return () => ({ + name: 'dateFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(DateFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts similarity index 55% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts index 829fe3e3bef3d..bd84a2eb97d24 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/index.ts @@ -5,29 +5,41 @@ */ import { axisConfig } from './axis_config'; +// @ts-ignore untyped local import { datacolumn } from './datacolumn'; -import { dateFormat } from './date_format'; +import { dateFormatInitializer } from './date_format'; +// @ts-ignore untyped local import { filterGroup } from './filter_group'; +// @ts-ignore untyped local import { imageUpload } from './image_upload'; +// @ts-ignore untyped local import { number } from './number'; -import { numberFormat } from './number_format'; +import { numberFormatInitializer } from './number_format'; +// @ts-ignore untyped local import { palette } from './palette'; +// @ts-ignore untyped local import { percentage } from './percentage'; +// @ts-ignore untyped local import { range } from './range'; +// @ts-ignore untyped local import { select } from './select'; +// @ts-ignore untyped local import { shape } from './shape'; +// @ts-ignore untyped local import { string } from './string'; +// @ts-ignore untyped local import { textarea } from './textarea'; +// @ts-ignore untyped local import { toggle } from './toggle'; +import { SetupInitializer } from '../../plugin'; + export const args = [ axisConfig, datacolumn, - dateFormat, filterGroup, imageUpload, number, - numberFormat, palette, percentage, range, @@ -37,3 +49,9 @@ export const args = [ textarea, toggle, ]; + +export const initializers = [dateFormatInitializer, numberFormatInitializer]; + +export const initializeArgs: SetupInitializer = (core, plugins) => { + return [...args, ...initializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts index ce6c90c89a5a0..4025d4deaf997 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/arguments/number_format/index.ts @@ -6,40 +6,42 @@ import { compose, withProps } from 'recompose'; import { NumberFormatArgInput as Component, Props as ComponentProps } from './number_format'; -import { getAdvancedSettings } from '../../../../public/lib/kibana_advanced_settings'; // @ts-ignore untyped local lib import { templateFromReactComponent } from '../../../../public/lib/template_from_react_component'; import { ArgumentFactory } from '../../../../types/arguments'; import { ArgumentStrings } from '../../../../i18n'; +import { SetupInitializer } from '../../../plugin'; const { NumberFormat: strings } = ArgumentStrings; -const getFormatMap = () => ({ - NUMBER: getAdvancedSettings().get('format:number:defaultPattern'), - PERCENT: getAdvancedSettings().get('format:percent:defaultPattern'), - CURRENCY: getAdvancedSettings().get('format:currency:defaultPattern'), - DURATION: '00:00:00', - BYTES: getAdvancedSettings().get('format:bytes:defaultPattern'), -}); +export const numberFormatInitializer: SetupInitializer> = ( + core, + plugins +) => { + const formatMap = { + NUMBER: core.uiSettings.get('format:number:defaultPattern'), + PERCENT: core.uiSettings.get('format:percent:defaultPattern'), + CURRENCY: core.uiSettings.get('format:currency:defaultPattern'), + DURATION: '00:00:00', + BYTES: core.uiSettings.get('format:bytes:defaultPattern'), + }; -const getNumberFormats = () => { - const formatMap = getFormatMap(); - return [ + const numberFormats = [ { value: formatMap.NUMBER, text: strings.getFormatNumber() }, { value: formatMap.PERCENT, text: strings.getFormatPercent() }, { value: formatMap.CURRENCY, text: strings.getFormatCurrency() }, { value: formatMap.DURATION, text: strings.getFormatDuration() }, { value: formatMap.BYTES, text: strings.getFormatBytes() }, ]; -}; -export const NumberFormatArgInput = compose( - withProps({ numberFormats: getNumberFormats() }) -)(Component); + const NumberFormatArgInput = compose(withProps({ numberFormats }))( + Component + ); -export const numberFormat: ArgumentFactory = () => ({ - name: 'numberFormat', - displayName: strings.getDisplayName(), - help: strings.getHelp(), - simpleTemplate: templateFromReactComponent(NumberFormatArgInput), -}); + return () => ({ + name: 'numberFormat', + displayName: strings.getDisplayName(), + help: strings.getHelp(), + simpleTemplate: templateFromReactComponent(NumberFormatArgInput), + }); +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts similarity index 56% rename from x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js rename to x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts index 6e2685dcb9893..fdcaf21982050 100644 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.js +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/index.ts @@ -4,27 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ +// @ts-ignore untyped local import { dropdownControl } from './dropdownControl'; +// @ts-ignore untyped local import { getCell } from './getCell'; +// @ts-ignore untyped local import { image } from './image'; +// @ts-ignore untyped local import { markdown } from './markdown'; -import { metric } from './metric'; +// @ts-ignore untyped local +import { metricInitializer } from './metric'; +// @ts-ignore untyped local import { pie } from './pie'; +// @ts-ignore untyped local import { plot } from './plot'; +// @ts-ignore untyped local import { progress } from './progress'; +// @ts-ignore untyped local import { repeatImage } from './repeatImage'; +// @ts-ignore untyped local import { revealImage } from './revealImage'; +// @ts-ignore untyped local import { render } from './render'; +// @ts-ignore untyped local import { shape } from './shape'; +// @ts-ignore untyped local import { table } from './table'; +// @ts-ignore untyped local import { timefilterControl } from './timefilterControl'; +import { SetupInitializer } from '../../plugin'; + export const viewSpecs = [ dropdownControl, getCell, image, markdown, - metric, pie, plot, progress, @@ -35,3 +50,9 @@ export const viewSpecs = [ table, timefilterControl, ]; + +export const viewInitializers = [metricInitializer]; + +export const initializeViews: SetupInitializer = (core, plugins) => { + return [...viewSpecs, ...viewInitializers.map(initializer => initializer(core, plugins))]; +}; diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js deleted file mode 100644 index e69f8f1de5952..0000000000000 --- a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { openSans } from '../../../common/lib/fonts'; -import { getAdvancedSettings } from '../../../public/lib/kibana_advanced_settings'; -import { ViewStrings } from '../../../i18n'; - -const { Metric: strings } = ViewStrings; - -export const metric = () => ({ - name: 'metric', - displayName: strings.getDisplayName(), - modelArgs: [['_', { label: strings.getNumberDisplayName() }]], - requiresContext: false, - args: [ - { - name: 'metricFormat', - displayName: strings.getMetricFormatDisplayName(), - help: strings.getMetricFormatHelp(), - argType: 'numberFormat', - default: `"${getAdvancedSettings().get('format:number:defaultPattern')}"`, - }, - { - name: '_', - displayName: strings.getLabelDisplayName(), - help: strings.getLabelHelp(), - argType: 'string', - default: '""', - }, - { - name: 'metricFont', - displayName: strings.getMetricFontDisplayName(), - help: strings.getMetricFontHelp(), - argType: 'font', - default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, - }, - { - name: 'labelFont', - displayName: strings.getLabelFontDisplayName(), - help: strings.getLabelFontHelp(), - argType: 'font', - default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, - }, - ], -}); diff --git a/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts new file mode 100644 index 0000000000000..93912b7b0517f --- /dev/null +++ b/x-pack/legacy/plugins/canvas/canvas_plugin_src/uis/views/metric.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { openSans } from '../../../common/lib/fonts'; +import { ViewStrings } from '../../../i18n'; +import { SetupInitializer } from '../../plugin'; + +const { Metric: strings } = ViewStrings; + +export const metricInitializer: SetupInitializer = (core, plugin) => { + return () => ({ + name: 'metric', + displayName: strings.getDisplayName(), + modelArgs: [['_', { label: strings.getNumberDisplayName() }]], + requiresContext: false, + args: [ + { + name: 'metricFormat', + displayName: strings.getMetricFormatDisplayName(), + help: strings.getMetricFormatHelp(), + argType: 'numberFormat', + default: `"${core.uiSettings.get('format:number:defaultPattern')}"`, + }, + { + name: '_', + displayName: strings.getLabelDisplayName(), + help: strings.getLabelHelp(), + argType: 'string', + default: '""', + }, + { + name: 'metricFont', + displayName: strings.getMetricFontDisplayName(), + help: strings.getMetricFontHelp(), + argType: 'font', + default: `{font size=48 family="${openSans.value}" color="#000000" align=center lHeight=48}`, + }, + { + name: 'labelFont', + displayName: strings.getLabelFontDisplayName(), + help: strings.getLabelFontHelp(), + argType: 'font', + default: `{font size=18 family="${openSans.value}" color="#000000" align=center}`, + }, + ], + }); +}; diff --git a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts index 3d835bdf31bf8..ce908ac6c5566 100644 --- a/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts +++ b/x-pack/legacy/plugins/canvas/i18n/elements/element_strings.test.ts @@ -3,9 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -jest.mock('ui/new_platform'); import { getElementStrings } from './element_strings'; -import { elementSpecs } from '../../canvas_plugin_src/elements'; +import { initializeElements } from '../../canvas_plugin_src/elements'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +const elementSpecs = initializeElements(coreMock.createSetup() as any, {} as any); import { TagStrings } from '../tags'; diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js index 429d27afb3f0d..226ad420535bd 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/index.js @@ -6,7 +6,8 @@ import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { compose, withState, getContext, withHandlers } from 'recompose'; +import { compose, withState, getContext, withHandlers, withProps } from 'recompose'; +import moment from 'moment'; import * as workpadService from '../../lib/workpad_service'; import { notify } from '../../lib/notify'; import { canUserWrite } from '../../state/selectors/app'; @@ -14,6 +15,7 @@ import { getWorkpad } from '../../state/selectors/workpad'; import { getId } from '../../lib/get_id'; import { downloadWorkpad } from '../../lib/download_workpad'; import { ComponentStrings, ErrorStrings } from '../../../i18n'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { WorkpadLoader as Component } from './workpad_loader'; const { WorkpadLoader: strings } = ComponentStrings; @@ -127,5 +129,12 @@ export const WorkpadLoader = compose( return errors.map(({ id }) => id); }); }, - }) + }), + withKibana, + withProps(props => ({ + formatDate: date => { + const dateFormat = props.kibana.services.uiSettings.get('dateFormat'); + return date && moment(date).format(dateFormat); + }, + })) )(Component); diff --git a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js index 30d4ded8571c5..04378e5603c4b 100644 --- a/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js +++ b/x-pack/legacy/plugins/canvas/public/components/workpad_loader/workpad_loader.js @@ -20,12 +20,10 @@ import { EuiLink, } from '@elastic/eui'; import { sortByOrder } from 'lodash'; -import moment from 'moment'; import { ConfirmModal } from '../confirm_modal'; import { Link } from '../link'; import { Paginate } from '../paginate'; import { ComponentStrings } from '../../../i18n'; -import { getAdvancedSettings } from '../../lib/kibana_advanced_settings'; import { WorkpadDropzone } from './workpad_dropzone'; import { WorkpadCreate } from './workpad_create'; import { WorkpadSearch } from './workpad_search'; @@ -33,8 +31,6 @@ import { uploadWorkpad } from './upload_workpad'; const { WorkpadLoader: strings } = ComponentStrings; -const formatDate = date => date && moment(date).format(getAdvancedSettings().get('dateFormat')); - const getDisplayName = (name, workpad, loadedWorkpad) => { const workpadName = name.length ? name : {workpad.id}; return workpad.id === loadedWorkpad ? {workpadName} : workpadName; @@ -51,6 +47,7 @@ export class WorkpadLoader extends React.PureComponent { removeWorkpads: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, workpads: PropTypes.object, + formatDate: PropTypes.func.isRequired, }; state = { @@ -205,7 +202,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { field: '@timestamp', @@ -213,7 +210,7 @@ export class WorkpadLoader extends React.PureComponent { sortable: true, dataType: 'date', width: '20%', - render: date => formatDate(date), + render: date => this.props.formatDate(date), }, { name: '', actions, width: '5%' }, ]; diff --git a/x-pack/legacy/plugins/monitoring/README.md b/x-pack/legacy/plugins/monitoring/README.md index 3659f4d2fa0f7..e9ececa8c6350 100644 --- a/x-pack/legacy/plugins/monitoring/README.md +++ b/x-pack/legacy/plugins/monitoring/README.md @@ -72,8 +72,8 @@ cluster. 1. Set the Kibana config: ``` % cat config/kibana.dev.yml - xpack.monitoring.elasticsearch: - url: "http://localhost:9210" + monitoring.ui.elasticsearch: + hosts: "http://localhost:9210" username: "kibana" password: "changeme" ``` diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts new file mode 100644 index 0000000000000..943a9c22c6aae --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.test.ts @@ -0,0 +1,14 @@ +/* + * 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 { EndpointAppContextService } from './endpoint_app_context_services'; + +describe('test endpoint app context services', () => { + it('should throw error if start is not called', async () => { + const endpointAppContextService = new EndpointAppContextService(); + expect(() => endpointAppContextService.getIndexPatternRetriever()).toThrow(Error); + expect(() => endpointAppContextService.getAgentService()).toThrow(Error); + }); +}); diff --git a/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts new file mode 100644 index 0000000000000..4d77c5c68c69f --- /dev/null +++ b/x-pack/plugins/endpoint/server/endpoint_app_context_services.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { IndexPatternRetriever } from './index_pattern'; +import { AgentService } from '../../ingest_manager/common/types'; + +/** + * A singleton that holds shared services that are initialized during the start up phase + * of the plugin lifecycle. And stop during the stop phase, if needed. + */ +export class EndpointAppContextService { + private indexPatternRetriever: IndexPatternRetriever | undefined; + private agentService: AgentService | undefined; + + public start(dependencies: { + indexPatternRetriever: IndexPatternRetriever; + agentService: AgentService; + }) { + this.indexPatternRetriever = dependencies.indexPatternRetriever; + this.agentService = dependencies.agentService; + } + + public stop() {} + + public getAgentService(): AgentService { + if (!this.agentService) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.agentService; + } + + public getIndexPatternRetriever(): IndexPatternRetriever { + if (!this.indexPatternRetriever) { + throw new Error(`must call start on ${EndpointAppContextService.name} to call getter`); + } + return this.indexPatternRetriever; + } +} diff --git a/x-pack/plugins/endpoint/server/index_pattern.ts b/x-pack/plugins/endpoint/server/index_pattern.ts index ea612bfd75441..05cfd91f2777a 100644 --- a/x-pack/plugins/endpoint/server/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/index_pattern.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { Logger, LoggerFactory, RequestHandlerContext } from 'kibana/server'; -import { ESIndexPatternService } from '../../ingest_manager/server'; import { EndpointAppConstants } from '../common/types'; +import { ESIndexPatternService } from '../../ingest_manager/common/types'; export interface IndexPatternRetriever { getIndexPattern(ctx: RequestHandlerContext, datasetPath: string): Promise; diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts index 3881840efe9df..8d677cf37f066 100644 --- a/x-pack/plugins/endpoint/server/mocks.ts +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IngestManagerSetupContract } from '../../ingest_manager/server'; -import { AgentService } from '../../ingest_manager/common/types'; +import { AgentService, IngestManagerStartupContract } from '../../ingest_manager/common/types'; /** * Creates a mock IndexPatternRetriever for use in tests. @@ -47,9 +46,9 @@ export const createMockAgentService = (): jest.Mocked => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIngestManagerSetupContract = ( +export const createMockIngestManagerStartupContract = ( indexPattern: string -): IngestManagerSetupContract => { +): IngestManagerStartupContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index c380bc5c3e3d0..44b9c3eabbd6d 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -4,15 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; +import { + EndpointPlugin, + EndpointPluginSetupDependencies, + EndpointPluginStartDependencies, +} from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; -import { createMockIngestManagerSetupContract } from './mocks'; +import { createMockIngestManagerStartupContract } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; let mockCoreSetup: ReturnType; + let mockCoreStart: ReturnType; let mockedEndpointPluginSetupDependencies: jest.Mocked; + let mockedEndpointPluginStartDependencies: jest.Mocked; let mockedPluginSetupContract: jest.Mocked; beforeEach(() => { plugin = new EndpointPlugin( @@ -23,21 +29,32 @@ describe('test endpoint plugin', () => { ); mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); mockedPluginSetupContract = { registerFeature: jest.fn(), getFeatures: jest.fn(), getFeaturesUICapabilities: jest.fn(), registerLegacyAPI: jest.fn(), }; - mockedEndpointPluginSetupDependencies = { - features: mockedPluginSetupContract, - ingestManager: createMockIngestManagerSetupContract(''), - }; }); it('test properly setup plugin', async () => { + mockedEndpointPluginSetupDependencies = { + features: mockedPluginSetupContract, + }; await plugin.setup(mockCoreSetup, mockedEndpointPluginSetupDependencies); expect(mockedPluginSetupContract.registerFeature).toBeCalledTimes(1); expect(mockCoreSetup.http.createRouter).toBeCalledTimes(1); + expect(() => plugin.getEndpointAppContextService().getIndexPatternRetriever()).toThrow(Error); + expect(() => plugin.getEndpointAppContextService().getAgentService()).toThrow(Error); + }); + + it('test properly start plugin', async () => { + mockedEndpointPluginStartDependencies = { + ingestManager: createMockIngestManagerStartupContract(''), + }; + await plugin.start(mockCoreStart, mockedEndpointPluginStartDependencies); + expect(plugin.getEndpointAppContextService().getAgentService()).toBeTruthy(); + expect(plugin.getEndpointAppContextService().getIndexPatternRetriever()).toBeTruthy(); }); }); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index ce6be5aeaf6db..f7ff32bdbfcbf 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -3,10 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Plugin, CoreSetup, PluginInitializerContext, Logger } from 'kibana/server'; +import { Plugin, CoreSetup, PluginInitializerContext, Logger, CoreStart } from 'kibana/server'; import { first } from 'rxjs/operators'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; -import { IngestManagerSetupContract } from '../../ingest_manager/server'; import { createConfig$, EndpointConfigType } from './config'; import { EndpointAppContext } from './types'; @@ -15,14 +14,17 @@ import { registerResolverRoutes } from './routes/resolver'; import { registerIndexPatternRoute } from './routes/index_pattern'; import { registerEndpointRoutes } from './routes/metadata'; import { IngestIndexPatternRetriever } from './index_pattern'; +import { IngestManagerStartupContract } from '../../ingest_manager/common/types'; +import { EndpointAppContextService } from './endpoint_app_context_services'; export type EndpointPluginStart = void; export type EndpointPluginSetup = void; -export interface EndpointPluginStartDependencies {} // eslint-disable-line @typescript-eslint/no-empty-interface +export interface EndpointPluginStartDependencies { + ingestManager: IngestManagerStartupContract; +} export interface EndpointPluginSetupDependencies { features: FeaturesPluginSetupContract; - ingestManager: IngestManagerSetupContract; } export class EndpointPlugin @@ -34,9 +36,15 @@ export class EndpointPlugin EndpointPluginStartDependencies > { private readonly logger: Logger; + private readonly endpointAppContextService: EndpointAppContextService = new EndpointAppContextService(); constructor(private readonly initializerContext: PluginInitializerContext) { this.logger = this.initializerContext.logger.get('endpoint'); } + + public getEndpointAppContextService(): EndpointAppContextService { + return this.endpointAppContextService; + } + public setup(core: CoreSetup, plugins: EndpointPluginSetupDependencies) { plugins.features.registerFeature({ id: 'endpoint', @@ -66,12 +74,8 @@ export class EndpointPlugin }, }); const endpointContext = { - indexPatternRetriever: new IngestIndexPatternRetriever( - plugins.ingestManager.esIndexPatternService, - this.initializerContext.logger - ), - agentService: plugins.ingestManager.agentService, logFactory: this.initializerContext.logger, + service: this.endpointAppContextService, config: (): Promise => { return createConfig$(this.initializerContext) .pipe(first()) @@ -85,10 +89,18 @@ export class EndpointPlugin registerIndexPatternRoute(router, endpointContext); } - public start() { + public start(core: CoreStart, plugins: EndpointPluginStartDependencies) { this.logger.debug('Starting plugin'); + this.endpointAppContextService.start({ + indexPatternRetriever: new IngestIndexPatternRetriever( + plugins.ingestManager.esIndexPatternService, + this.initializerContext.logger + ), + agentService: plugins.ingestManager.agentService, + }); } public stop() { this.logger.debug('Stopping plugin'); + this.endpointAppContextService.stop(); } } diff --git a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts index 39fc2ba4c74bb..1124c977ff924 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -13,25 +13,35 @@ import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test alerts route', () => { let routerMock: jest.Mocked; let mockClusterClient: jest.Mocked; let mockScopedClient: jest.Mocked; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient(); mockScopedClient = elasticsearchServiceMock.createScopedClusterClient(); mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); - registerAlertRoutes(routerMock, { + + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), agentService: createMockAgentService(), + }); + + registerAlertRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + it('should fail to validate when `page_size` is not a number', async () => { const validate = () => { alertingIndexGetQuerySchema.validate({ diff --git a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts index 9055ee4110fbb..e438ab853f3b5 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -26,7 +26,9 @@ export const alertDetailsHandlerWrapper = function( id: alertId, })) as GetResponse; - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const config = await endpointAppContext.config(); const pagination: AlertDetailsPagination = new AlertDetailsPagination( diff --git a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts index f23dffd13db4f..44a0cf8744a9e 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/list/handlers.ts @@ -18,7 +18,9 @@ export const alertListHandlerWrapper = function( res ) => { try { - const indexPattern = await endpointAppContext.indexPatternRetriever.getEventIndexPattern(ctx); + const indexPattern = await endpointAppContext.service + .getIndexPatternRetriever() + .getEventIndexPattern(ctx); const reqData = await getRequestData(req, endpointAppContext); const response = await searchESForAlerts( ctx.core.elasticsearch.dataClient, diff --git a/x-pack/plugins/endpoint/server/routes/index_pattern.ts b/x-pack/plugins/endpoint/server/routes/index_pattern.ts index 3b71f6a6957ba..79083f5f05e14 100644 --- a/x-pack/plugins/endpoint/server/routes/index_pattern.ts +++ b/x-pack/plugins/endpoint/server/routes/index_pattern.ts @@ -8,14 +8,14 @@ import { IRouter, Logger, RequestHandler } from 'kibana/server'; import { EndpointAppContext } from '../types'; import { IndexPatternGetParamsResult, EndpointAppConstants } from '../../common/types'; import { indexPatternGetParamsSchema } from '../../common/schema/index_pattern'; -import { IndexPatternRetriever } from '../index_pattern'; function handleIndexPattern( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); return res.ok({ body: { indexPattern: await indexRetriever.getIndexPattern(context, req.params.datasetPath), @@ -37,6 +37,6 @@ export function registerIndexPatternRoute(router: IRouter, endpointAppContext: E validate: { params: indexPatternGetParamsSchema }, options: { authRequired: true }, }, - handleIndexPattern(log, endpointAppContext.indexPatternRetriever) + handleIndexPattern(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index bc79b828576e0..99dc4ac9f9e33 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -61,9 +61,9 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context - ); + const index = await endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(context); const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, @@ -117,9 +117,9 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { - const index = await metadataRequestContext.endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - metadataRequestContext.requestHandlerContext - ); + const index = await metadataRequestContext.endpointAppContext.service + .getIndexPatternRetriever() + .getMetadataIndexPattern(metadataRequestContext.requestHandlerContext); const query = getESQueryHostMetadataByID(id, index); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser( 'search', @@ -179,10 +179,12 @@ async function enrichHostMetadata( log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); } - const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatusById( - metadataRequestContext.requestHandlerContext.core.savedObjects.client, - elasticAgentId - ); + const status = await metadataRequestContext.endpointAppContext.service + .getAgentService() + .getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR; } catch (e) { if (e.isBoom && e.output.statusCode === 404) { diff --git a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts index a1186aabc7a66..84930118df238 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -28,6 +28,7 @@ import * as data from '../../test_data/all_metadata_data.json'; import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks'; import { AgentService } from '../../../../ingest_manager/common/types'; import Boom from 'boom'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -38,6 +39,7 @@ describe('test endpoint route', () => { let routeHandler: RequestHandler; let routeConfig: RouteConfig; let mockAgentService: jest.Mocked; + let endpointAppContextService: EndpointAppContextService; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -49,14 +51,21 @@ describe('test endpoint route', () => { routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); mockAgentService = createMockAgentService(); - registerEndpointRoutes(routerMock, { + endpointAppContextService = new EndpointAppContextService(); + endpointAppContextService.start({ indexPatternRetriever: createMockMetadataIndexPatternRetriever(), agentService: mockAgentService, + }); + + registerEndpointRoutes(routerMock, { logFactory: loggingServiceMock.create(), + service: endpointAppContextService, config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); }); + afterEach(() => endpointAppContextService.stop()); + function createRouteHandlerContext( dataClient: jest.Mocked, savedObjectsClient: jest.Mocked diff --git a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts index 7e6e3f875cd4c..28bac2fa10e0c 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/query_builders.test.ts @@ -6,11 +6,8 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { - createMockAgentService, - createMockMetadataIndexPatternRetriever, - MetadataIndexPattern, -} from '../../mocks'; +import { MetadataIndexPattern } from '../../mocks'; +import { EndpointAppContextService } from '../../endpoint_app_context_services'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -21,9 +18,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), - agentService: createMockAgentService(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern @@ -73,9 +69,8 @@ describe('query builder', () => { const query = await kibanaRequestToMetadataListESQuery( mockRequest, { - indexPatternRetriever: createMockMetadataIndexPatternRetriever(), - agentService: createMockAgentService(), logFactory: loggingServiceMock.create(), + service: new EndpointAppContextService(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, MetadataIndexPattern diff --git a/x-pack/plugins/endpoint/server/routes/resolver.ts b/x-pack/plugins/endpoint/server/routes/resolver.ts index 77fcbc87baeb1..a96d431225b15 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver.ts @@ -12,7 +12,6 @@ import { handleLifecycle, validateLifecycle } from './resolver/lifecycle'; export function registerResolverRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { const log = endpointAppContext.logFactory.get('resolver'); - const indexPatternService = endpointAppContext.indexPatternRetriever; router.get( { @@ -20,7 +19,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateRelatedEvents, options: { authRequired: true }, }, - handleRelatedEvents(log, indexPatternService) + handleRelatedEvents(log, endpointAppContext) ); router.get( @@ -29,7 +28,7 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateChildren, options: { authRequired: true }, }, - handleChildren(log, indexPatternService) + handleChildren(log, endpointAppContext) ); router.get( @@ -38,6 +37,6 @@ export function registerResolverRoutes(router: IRouter, endpointAppContext: Endp validate: validateLifecycle, options: { authRequired: true }, }, - handleLifecycle(log, indexPatternService) + handleLifecycle(log, endpointAppContext) ); } diff --git a/x-pack/plugins/endpoint/server/routes/resolver/children.ts b/x-pack/plugins/endpoint/server/routes/resolver/children.ts index 05b8f0b5f8608..c3b19b6c912b6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/children.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/children.ts @@ -11,7 +11,7 @@ import { extractEntityID } from './utils/normalize'; import { getPaginationParams } from './utils/pagination'; import { LifecycleQuery } from './queries/lifecycle'; import { ChildrenQuery } from './queries/children'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface ChildrenQueryParams { after?: string; @@ -47,7 +47,7 @@ export const validateChildren = { export function handleChildren( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -55,6 +55,7 @@ export function handleChildren( query: { limit, after, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const pagination = getPaginationParams(limit, after); const indexPattern = await indexRetriever.getEventIndexPattern(context); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts index 6d155b79651a7..91a4c5d49bc54 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/lifecycle.ts @@ -4,13 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { extractParentEntityID } from './utils/normalize'; import { LifecycleQuery } from './queries/lifecycle'; import { ResolverEvent } from '../../../common/types'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface LifecycleQueryParams { ancestors: number; @@ -48,7 +47,7 @@ function getParentEntityID(results: ResolverEvent[]) { export function handleLifecycle( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -56,6 +55,7 @@ export function handleLifecycle( query: { ancestors, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const ancestorLifecycles = []; const client = context.core.elasticsearch.dataClient; const indexPattern = await indexRetriever.getEventIndexPattern(context); diff --git a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts index 46e205464f53c..83e111a1e62e6 100644 --- a/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts +++ b/x-pack/plugins/endpoint/server/routes/resolver/related_events.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RequestHandler, Logger } from 'kibana/server'; import { getPaginationParams } from './utils/pagination'; import { RelatedEventsQuery } from './queries/related_events'; -import { IndexPatternRetriever } from '../../index_pattern'; +import { EndpointAppContext } from '../../types'; interface RelatedEventsQueryParams { after?: string; @@ -44,7 +44,7 @@ export const validateRelatedEvents = { export function handleRelatedEvents( log: Logger, - indexRetriever: IndexPatternRetriever + endpointAppContext: EndpointAppContext ): RequestHandler { return async (context, req, res) => { const { @@ -52,6 +52,7 @@ export function handleRelatedEvents( query: { limit, after, legacyEndpointID }, } = req; try { + const indexRetriever = endpointAppContext.service.getIndexPatternRetriever(); const pagination = getPaginationParams(limit, after); const client = context.core.elasticsearch.dataClient; diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index d43ec58aec428..dfa5950adba5c 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -5,15 +5,17 @@ */ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; -import { IndexPatternRetriever } from './index_pattern'; -import { AgentService } from '../../ingest_manager/common/types'; +import { EndpointAppContextService } from './endpoint_app_context_services'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { - indexPatternRetriever: IndexPatternRetriever; - agentService: AgentService; logFactory: LoggerFactory; config(): Promise; + + /** + * Object readiness is tied to plugin start method + */ + service: EndpointAppContextService; } diff --git a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx index 665da25dcbfff..cdefffeb35c15 100644 --- a/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx +++ b/x-pack/plugins/infra/public/components/alerting/metrics/expression.tsx @@ -20,6 +20,7 @@ import { i18n } from '@kbn/i18n'; import { MetricExpressionParams, Comparator, + Aggregators, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../server/lib/alerting/metric_threshold/types'; import { euiStyled } from '../../../../../observability/public'; @@ -31,6 +32,8 @@ import { // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { builtInComparators } from '../../../../../triggers_actions_ui/public/common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AlertsContextValue } from '../../../../../triggers_actions_ui/public/application/context/alerts_context'; @@ -64,24 +67,25 @@ type MetricExpression = Omit & { metric?: string; }; -enum AGGREGATION_TYPES { - COUNT = 'count', - AVERAGE = 'avg', - SUM = 'sum', - MIN = 'min', - MAX = 'max', - RATE = 'rate', - CARDINALITY = 'cardinality', -} - const defaultExpression = { - aggType: AGGREGATION_TYPES.AVERAGE, + aggType: Aggregators.AVERAGE, comparator: Comparator.GT, threshold: [], timeSize: 1, timeUnit: 'm', } as MetricExpression; +const customComparators = { + ...builtInComparators, + [Comparator.OUTSIDE_RANGE]: { + text: i18n.translate('xpack.infra.metrics.alertFlyout.outsideRangeLabel', { + defaultMessage: 'Is not between', + }), + value: Comparator.OUTSIDE_RANGE, + requiredValues: 2, + }, +}; + export const Expressions: React.FC = props => { const { setAlertParams, alertParams, errors, alertsContext } = props; const { source, createDerivedIndexPattern } = useSourceViaHttp({ @@ -339,7 +343,7 @@ const StyledExpression = euiStyled.div` export const ExpressionRow: React.FC = props => { const { setAlertParams, expression, errors, expressionId, remove, fields, canDelete } = props; const { - aggType = AGGREGATION_TYPES.MAX, + aggType = Aggregators.MAX, metric, comparator = Comparator.GT, threshold = [], @@ -410,6 +414,7 @@ export const ExpressionRow: React.FC = props => { { expect(mostRecentAction(instanceID)).toBe(undefined); expect(getState(instanceID).alertState).toBe(AlertStates.OK); }); + test('alerts as expected with the outside range comparator', async () => { + await execute(Comparator.OUTSIDE_RANGE, [0, 0.75]); + expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + await execute(Comparator.OUTSIDE_RANGE, [0, 1.5]); + expect(mostRecentAction(instanceID)).toBe(undefined); + expect(getState(instanceID).alertState).toBe(AlertStates.OK); + }); test('reports expected values to the action context', async () => { await execute(Comparator.GT, [0.75]); const { action } = mostRecentAction(instanceID); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index c5ea65f7a4d1a..946f1c14bf593 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -11,7 +11,7 @@ import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_ty import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler'; import { getAllCompositeData } from '../../../utils/get_all_composite_data'; import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; -import { MetricExpressionParams, Comparator, AlertStates } from './types'; +import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types'; import { AlertServices, AlertExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; import { getDateHistogramOffset } from '../../snapshot/query_helpers'; @@ -39,7 +39,7 @@ const getCurrentValueFromAggregations = ( const { buckets } = aggregations.aggregatedIntervals; if (!buckets.length) return null; // No Data state const mostRecentBucket = buckets[buckets.length - 1]; - if (aggType === 'count') { + if (aggType === Aggregators.COUNT) { return mostRecentBucket.doc_count; } const { value } = mostRecentBucket.aggregatedValue; @@ -70,10 +70,10 @@ export const getElasticsearchMetricQuery = ( groupBy?: string, filterQuery?: string ) => { - if (aggType === 'count' && metric) { + if (aggType === Aggregators.COUNT && metric) { throw new Error('Cannot aggregate document count with a metric'); } - if (aggType !== 'count' && !metric) { + if (aggType !== Aggregators.COUNT && !metric) { throw new Error('Can only aggregate without a metric if using the document count aggregator'); } const interval = `${timeSize}${timeUnit}`; @@ -85,9 +85,9 @@ export const getElasticsearchMetricQuery = ( const offset = getDateHistogramOffset(from, interval); const aggregations = - aggType === 'count' + aggType === Aggregators.COUNT ? {} - : aggType === 'rate' + : aggType === Aggregators.RATE ? networkTraffic('aggregatedValue', metric) : { aggregatedValue: { @@ -242,7 +242,8 @@ const getMetric: ( const comparatorMap = { [Comparator.BETWEEN]: (value: number, [a, b]: number[]) => value >= Math.min(a, b) && value <= Math.max(a, b), - // `threshold` is always an array of numbers in case the BETWEEN comparator is + [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b, + // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is // used; all other compartors will just destructure the first value in the array [Comparator.GT]: (a: number, [b]: number[]) => a > b, [Comparator.LT]: (a: number, [b]: number[]) => a < b, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 8808219cabaa7..b697af4fa4c3b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -7,8 +7,15 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid'; import { schema } from '@kbn/config-schema'; import { PluginSetupContract } from '../../../../../alerting/server'; +import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; import { createMetricThresholdExecutor, FIRED_ACTIONS } from './metric_threshold_executor'; -import { METRIC_THRESHOLD_ALERT_TYPE_ID } from './types'; +import { METRIC_THRESHOLD_ALERT_TYPE_ID, Comparator } from './types'; + +const oneOfLiterals = (arrayOfLiterals: Readonly) => + schema.string({ + validate: value => + arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`, + }); export async function registerMetricThresholdAlertType(alertingPlugin: PluginSetupContract) { if (!alertingPlugin) { @@ -20,13 +27,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const baseCriterion = { threshold: schema.arrayOf(schema.number()), - comparator: schema.oneOf([ - schema.literal('>'), - schema.literal('<'), - schema.literal('>='), - schema.literal('<='), - schema.literal('between'), - ]), + comparator: oneOfLiterals(Object.values(Comparator)), timeUnit: schema.string(), timeSize: schema.number(), }; @@ -34,13 +35,7 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet const nonCountCriterion = schema.object({ ...baseCriterion, metric: schema.string(), - aggType: schema.oneOf([ - schema.literal('avg'), - schema.literal('min'), - schema.literal('max'), - schema.literal('rate'), - schema.literal('cardinality'), - ]), + aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS), }); const countCriterion = schema.object({ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index abed691f109c0..18f5503fe2c9e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MetricsExplorerAggregation } from '../../../../common/http_api/metrics_explorer'; - export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold'; export enum Comparator { @@ -14,6 +12,17 @@ export enum Comparator { GT_OR_EQ = '>=', LT_OR_EQ = '<=', BETWEEN = 'between', + OUTSIDE_RANGE = 'outside', +} + +export enum Aggregators { + COUNT = 'count', + AVERAGE = 'avg', + SUM = 'sum', + MIN = 'min', + MAX = 'max', + RATE = 'rate', + CARDINALITY = 'cardinality', } export enum AlertStates { @@ -34,7 +43,7 @@ interface BaseMetricExpressionParams { } interface NonCountMetricExpressionParams extends BaseMetricExpressionParams { - aggType: Exclude; + aggType: Exclude; metric: string; } diff --git a/x-pack/plugins/ingest_manager/common/constants/routes.ts b/x-pack/plugins/ingest_manager/common/constants/routes.ts index 0bb0f45cdcfb0..91939cca067c5 100644 --- a/x-pack/plugins/ingest_manager/common/constants/routes.ts +++ b/x-pack/plugins/ingest_manager/common/constants/routes.ts @@ -66,7 +66,8 @@ export const AGENT_API_ROUTES = { ACKS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/acks`, ACTIONS_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/actions`, ENROLL_PATTERN: `${FLEET_API_ROOT}/agents/enroll`, - UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/unenroll`, + UNENROLL_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/unenroll`, + REASSIGN_PATTERN: `${FLEET_API_ROOT}/agents/{agentId}/reassign`, STATUS_PATTERN: `${FLEET_API_ROOT}/agent-status`, }; diff --git a/x-pack/plugins/ingest_manager/common/services/routes.ts b/x-pack/plugins/ingest_manager/common/services/routes.ts index fba5f50853a95..fbaa2c90df458 100644 --- a/x-pack/plugins/ingest_manager/common/services/routes.ts +++ b/x-pack/plugins/ingest_manager/common/services/routes.ts @@ -99,7 +99,10 @@ export const agentRouteService = { getInfoPath: (agentId: string) => AGENT_API_ROUTES.INFO_PATTERN.replace('{agentId}', agentId), getUpdatePath: (agentId: string) => AGENT_API_ROUTES.UPDATE_PATTERN.replace('{agentId}', agentId), getEventsPath: (agentId: string) => AGENT_API_ROUTES.EVENTS_PATTERN.replace('{agentId}', agentId), - getUnenrollPath: () => AGENT_API_ROUTES.UNENROLL_PATTERN, + getUnenrollPath: (agentId: string) => + AGENT_API_ROUTES.UNENROLL_PATTERN.replace('{agentId}', agentId), + getReassignPath: (agentId: string) => + AGENT_API_ROUTES.REASSIGN_PATTERN.replace('{agentId}', agentId), getListPath: () => AGENT_API_ROUTES.LIST_PATTERN, getStatusPath: () => AGENT_API_ROUTES.STATUS_PATTERN, }; diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 150a4c9d60280..438db9a25b8ee 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -9,6 +9,25 @@ import { AgentStatus } from './models'; export * from './models'; export * from './rest_spec'; +/** + * Service to return the index pattern of EPM packages + */ +export interface ESIndexPatternService { + getESIndexPattern( + savedObjectsClient: SavedObjectsClientContract, + pkgName: string, + datasetPath: string + ): Promise; +} + +/** + * Describes public IngestManager plugin contract returned at the `startup` stage. + */ +export interface IngestManagerStartupContract { + esIndexPatternService: ESIndexPatternService; + agentService: AgentService; +} + /** * A service that provides exported functions that return information about an Agent */ diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 4d03a30f9a590..14b2b2e47d17f 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -65,7 +65,7 @@ interface AgentBase { access_api_key_id?: string; default_api_key?: string; config_id?: string; - config_revision?: number; + config_revision?: number | null; config_newest_revision?: number; last_checkin?: string; } diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index 21ab41740ce3e..64ed95db74f4c 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -96,16 +96,23 @@ export interface PostNewAgentActionResponse { } export interface PostAgentUnenrollRequest { - body: { kuery: string } | { ids: string[] }; + params: { + agentId: string; + }; } export interface PostAgentUnenrollResponse { - results: Array<{ - success: boolean; - error?: any; - id: string; - action: string; - }>; + success: boolean; +} + +export interface PutAgentReassignRequest { + params: { + agentId: string; + }; + body: { config_id: string }; +} + +export interface PutAgentReassignResponse { success: boolean; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts index f08b950e71ea8..453bcf2bd81e7 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/agents.ts @@ -10,6 +10,8 @@ import { GetOneAgentResponse, GetOneAgentEventsResponse, GetOneAgentEventsRequest, + PutAgentReassignRequest, + PutAgentReassignResponse, GetAgentsRequest, GetAgentsResponse, GetAgentStatusRequest, @@ -59,3 +61,16 @@ export function sendGetAgentStatus( ...options, }); } + +export function sendPutAgentReassign( + agentId: string, + body: PutAgentReassignRequest['body'], + options?: RequestOptions +) { + return sendRequest({ + method: 'put', + path: agentRouteService.getReassignPath(agentId), + body, + ...options, + }); +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx index 0844368dc214b..653e2eb9a3a3b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/components/details_section.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiTitle, @@ -13,10 +13,13 @@ import { EuiFlexItem, EuiDescriptionList, EuiButton, + EuiPopover, EuiDescriptionListTitle, EuiDescriptionListDescription, EuiButtonEmpty, EuiIconTip, + EuiContextMenuPanel, + EuiContextMenuItem, EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -26,7 +29,7 @@ import { Agent } from '../../../../types'; import { AgentHealth } from '../../components/agent_health'; import { useCapabilities, useGetOneAgentConfig } from '../../../../hooks'; import { Loading } from '../../../../components'; -import { ConnectedLink } from '../../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../../components'; import { AgentUnenrollProvider } from '../../components/agent_unenroll_provider'; const Item: React.FunctionComponent<{ label: string }> = ({ label, children }) => { @@ -56,6 +59,15 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => const hasWriteCapabilites = useCapabilities().write; const metadataFlyout = useFlyout(); const refreshAgent = useAgentRefresh(); + // Actions menu + const [isActionsPopoverOpen, setIsActionsPopoverOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsActionsPopoverOpen(false), [ + setIsActionsPopoverOpen, + ]); + const handleToggleMenu = useCallback(() => setIsActionsPopoverOpen(!isActionsPopoverOpen), [ + isActionsPopoverOpen, + ]); + const [isReassignFlyoutOpen, setIsReassignFlyoutOpen] = useState(false); // Fetch AgentConfig information const { isLoading: isAgentConfigLoading, data: agentConfigData } = useGetOneAgentConfig( @@ -111,6 +123,9 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => return ( <> + {isReassignFlyoutOpen && ( + setIsReassignFlyoutOpen(false)} /> + )} @@ -123,21 +138,55 @@ export const AgentDetailSection: React.FunctionComponent = ({ agent }) => - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt([agent.id], 1, refreshAgent); - }} - > + - )} - + } + isOpen={isActionsPopoverOpen} + closePopover={handleCloseMenu} + > + { + handleCloseMenu(); + setIsReassignFlyoutOpen(true); + }} + key="reassignConfig" + > + + , + + + {unenrollAgentsPrompt => ( + { + unenrollAgentsPrompt([agent.id], 1, refreshAgent); + }} + > + + + )} + , + ]} + /> + diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index d363c472f2305..c79255104a030 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -35,7 +35,7 @@ import { useUrlParams, useLink, } from '../../../hooks'; -import { ConnectedLink } from '../components'; +import { ConnectedLink, AgentReassignConfigFlyout } from '../components'; import { SearchBar } from '../../../components/search_bar'; import { AgentHealth } from '../components/agent_health'; import { AgentUnenrollProvider } from '../components/agent_unenroll_provider'; @@ -71,61 +71,76 @@ const statusFilters = [ }, ] as Array<{ label: string; status: string }>; -const RowActions = React.memo<{ agent: Agent; refresh: () => void }>(({ agent, refresh }) => { - const hasWriteCapabilites = useCapabilities().write; - const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); - const [isOpen, setIsOpen] = useState(false); - const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); - const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); +const RowActions = React.memo<{ agent: Agent; onReassignClick: () => void; refresh: () => void }>( + ({ agent, refresh, onReassignClick }) => { + const hasWriteCapabilites = useCapabilities().write; + const DETAILS_URI = useLink(FLEET_AGENT_DETAIL_PATH); + const [isOpen, setIsOpen] = useState(false); + const handleCloseMenu = useCallback(() => setIsOpen(false), [setIsOpen]); + const handleToggleMenu = useCallback(() => setIsOpen(!isOpen), [isOpen]); - return ( - - } - isOpen={isOpen} - closePopover={handleCloseMenu} - > - - - , + return ( + + } + isOpen={isOpen} + closePopover={handleCloseMenu} + > + + + , + { + handleCloseMenu(); + onReassignClick(); + }} + key="reassignConfig" + > + + , - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt([agent.id], 1, () => { - refresh(); - }); - }} - > - - - )} - , - ]} - /> - - ); -}); + + {unenrollAgentsPrompt => ( + { + unenrollAgentsPrompt([agent.id], 1, () => { + refresh(); + }); + }} + > + + + )} + , + ]} + /> + + ); + } +); export const AgentListPage: React.FunctionComponent<{}> = () => { const defaultKuery: string = (useUrlParams().urlParams.kuery as string) || ''; @@ -136,8 +151,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Table and search states const [search, setSearch] = useState(defaultKuery); const { pagination, pageSizeOptions, setPagination } = usePagination(); - const [selectedAgents, setSelectedAgents] = useState([]); - const [areAllAgentsSelected, setAreAllAgentsSelected] = useState(false); // Configs state (for filtering) const [isConfigsFilterOpen, setIsConfigsFilterOpen] = useState(false); @@ -159,6 +172,9 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { // Agent enrollment flyout state const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false); + // Agent reassignment flyout state + const [agentToReassignId, setAgentToReassignId] = useState(undefined); + let kuery = search.trim(); if (selectedConfigs.length) { if (kuery) { @@ -227,47 +243,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { {host} ), - footer: () => { - if (selectedAgents.length === agents.length && totalAgents > selectedAgents.length) { - return areAllAgentsSelected ? ( - setAreAllAgentsSelected(false)}> - - - ), - }} - /> - ) : ( - setAreAllAgentsSelected(true)}> - - - ), - }} - /> - ); - } - return null; - }, }, { field: 'active', @@ -350,7 +325,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { actions: [ { render: (agent: Agent) => { - return agentsRequest.sendRequest()} />; + return ( + agentsRequest.sendRequest()} + onReassignClick={() => setAgentToReassignId(agent.id)} + /> + ); }, }, ], @@ -381,6 +362,8 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { /> ); + const agentToReassign = agentToReassignId && agents.find(a => a.id === agentToReassignId); + return ( <> {isEnrollmentFlyoutOpen ? ( @@ -389,48 +372,16 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { onClose={() => setIsEnrollmentFlyoutOpen(false)} /> ) : null} + {agentToReassign && ( + { + setAgentToReassignId(undefined); + agentsRequest.sendRequest(); + }} + /> + )} - {selectedAgents.length ? ( - - - {unenrollAgentsPrompt => ( - { - unenrollAgentsPrompt( - areAllAgentsSelected ? search : selectedAgents.map(agent => agent.id), - areAllAgentsSelected ? totalAgents : selectedAgents.length, - () => { - // Reload agents if on first page and no search query, otherwise - // reset to first page and reset search, which will trigger a reload - if (pagination.currentPage === 1 && !search) { - agentsRequest.sendRequest(); - } else { - setPagination({ - ...pagination, - currentPage: 1, - }); - setSearch(''); - } - - setAreAllAgentsSelected(false); - setSelectedAgents([]); - } - ); - }} - > - - - )} - - - ) : null} @@ -575,14 +526,6 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { items={totalAgents ? agents : []} itemId="id" columns={columns} - isSelectable={true} - selection={{ - selectable: (agent: Agent) => agent.active, - onSelectionChange: (newSelectedAgents: Agent[]) => { - setSelectedAgents(newSelectedAgents); - setAreAllAgentsSelected(false); - }, - }} pagination={{ pageIndex: pagination.currentPage - 1, pageSize: pagination.pageSize, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx new file mode 100644 index 0000000000000..11a049047b787 --- /dev/null +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_reassign_config_flyout/index.tsx @@ -0,0 +1,186 @@ +/* + * 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, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiFlyoutFooter, + EuiSelect, + EuiFormRow, + EuiText, + EuiBadge, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Datasource, Agent } from '../../../../types'; +import { + useGetOneAgentConfig, + sendPutAgentReassign, + useCore, + useGetAgentConfigs, +} from '../../../../hooks'; +import { PackageIcon } from '../../../../components/package_icon'; + +interface Props { + onClose: () => void; + agent: Agent; +} + +export const AgentReassignConfigFlyout: React.FunctionComponent = ({ onClose, agent }) => { + const { notifications } = useCore(); + const [selectedAgentConfigId, setSelectedAgentConfigId] = useState( + agent.config_id + ); + + const agentConfigsRequest = useGetAgentConfigs(); + const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : []; + + const agentConfigRequest = useGetOneAgentConfig(selectedAgentConfigId as string); + const agentConfig = agentConfigRequest.data ? agentConfigRequest.data.item : null; + + const [isSubmitting, setIsSubmitting] = useState(false); + + async function onSubmit() { + try { + setIsSubmitting(true); + if (!selectedAgentConfigId) { + throw new Error('No selected config id'); + } + const res = await sendPutAgentReassign(agent.id, { + config_id: selectedAgentConfigId, + }); + if (res.error) { + throw res.error; + } + setIsSubmitting(false); + const successMessage = i18n.translate( + 'xpack.ingestManager.agentReassignConfig.successSingleNotificationTitle', + { + defaultMessage: 'Agent configuration reassigned', + } + ); + notifications.toasts.addSuccess(successMessage); + onClose(); + } catch (error) { + setIsSubmitting(false); + notifications.toasts.addError(error, { + title: 'Unable to reassign agent configuration', + }); + } + } + + return ( + + + +

+ +

+
+ + + + +
+ + + + + ({ + value: config.id, + text: config.name, + }))} + value={selectedAgentConfigId} + onChange={e => setSelectedAgentConfigId(e.target.value)} + /> + + + + + + {agentConfig && ( + + {agentConfig.datasources.length}, + }} + /> + + )} + + {agentConfig && + (agentConfig.datasources as Datasource[]).map((datasource, idx) => { + if (!datasource.package) { + return null; + } + return ( + + + + + + {datasource.package.title} + + + ); + })} + + + + + + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx index 25499495a7897..fec2253c0dd56 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_unenroll_provider.tsx @@ -39,7 +39,8 @@ export const AgentUnenrollProvider: React.FunctionComponent = ({ children ) => { if ( agentsToUnenroll === undefined || - (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length === 0) + // !Only supports unenrolling one agent + (Array.isArray(agentsToUnenroll) && agentsToUnenroll.length !== 1) ) { throw new Error('No agents specified for unenrollment'); } @@ -60,55 +61,27 @@ export const AgentUnenrollProvider: React.FunctionComponent = ({ children setIsLoading(true); try { - const unenrollByKuery = typeof agents === 'string'; - const { data, error } = await sendRequest({ - path: agentRouteService.getUnenrollPath(), + const agentId = agents[0]; + const { error } = await sendRequest({ + path: agentRouteService.getUnenrollPath(agentId), method: 'post', - body: JSON.stringify({ - kuery: unenrollByKuery ? agents : undefined, - ids: !unenrollByKuery ? agents : undefined, - }), }); if (error) { throw new Error(error.message); } - const results = data ? data.results : []; - - const successfulResults = results.filter(result => result.success); - const failedResults = results.filter(result => !result.success); - - if (successfulResults.length) { - const hasMultipleSuccesses = successfulResults.length > 1; - const successMessage = hasMultipleSuccesses - ? i18n.translate('xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle', { - defaultMessage: 'Unenrolled {count} agents', - values: { count: successfulResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', { - defaultMessage: "Unenrolled agent '{id}'", - values: { id: successfulResults[0].id }, - }); - core.notifications.toasts.addSuccess(successMessage); - } - - if (failedResults.length) { - const hasMultipleFailures = failedResults.length > 1; - const failureMessage = hasMultipleFailures - ? i18n.translate('xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle', { - defaultMessage: 'Error unenrolling {count} agents', - values: { count: failedResults.length }, - }) - : i18n.translate('xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle', { - defaultMessage: "Error unenrolling agent '{id}'", - values: { id: failedResults[0].id }, - }); - core.notifications.toasts.addDanger(failureMessage); - } + const successMessage = i18n.translate( + 'xpack.ingestManager.unenrollAgents.successSingleNotificationTitle', + { + defaultMessage: "Unenrolled agent '{id}'", + values: { id: agentId }, + } + ); + core.notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { - onSuccessCallback.current(successfulResults.map(result => result.id)); + onSuccessCallback.current([agentId]); } } catch (e) { core.notifications.toasts.addDanger( diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx index 19378fe2fb952..a0092f4073e5a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/index.tsx @@ -5,5 +5,6 @@ */ export * from './loading'; +export * from './agent_reassign_config_flyout'; export * from './navigation/child_routes'; export * from './navigation/connected_link'; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts index 4b5141a08bb91..aed9bb996a57c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/types/index.ts @@ -40,6 +40,8 @@ export { GetOneAgentEventsResponse, GetAgentStatusRequest, GetAgentStatusResponse, + PutAgentReassignRequest, + PutAgentReassignResponse, // API schemas - Enrollment API Keys GetEnrollmentAPIKeysResponse, GetEnrollmentAPIKeysRequest, diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index d99eb2a9bb4bb..0f49de0d5fdd1 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -7,9 +7,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { PluginInitializerContext } from 'src/core/server'; import { IngestManagerPlugin } from './plugin'; -export { ESIndexPatternService } from './services'; -export { IngestManagerSetupContract } from './plugin'; - export const config = { exposeToBrowser: { epm: true, diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 116c2319f3aa6..97bbeef17dc2e 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -41,22 +41,10 @@ import { registerSettingsRoutes, } from './routes'; -import { AgentService, IngestManagerConfigType } from '../common'; -import { - appContextService, - ESIndexPatternService, - ESIndexPatternSavedObjectService, -} from './services'; +import { IngestManagerConfigType, IngestManagerStartupContract } from '../common'; +import { appContextService, ESIndexPatternSavedObjectService } from './services'; import { getAgentStatusById } from './services/agents'; -/** - * Describes public IngestManager plugin contract returned at the `setup` stage. - */ -export interface IngestManagerSetupContract { - esIndexPatternService: ESIndexPatternService; - agentService: AgentService; -} - export interface IngestManagerSetupDeps { licensing: LicensingPluginSetup; security?: SecurityPluginSetup; @@ -80,7 +68,7 @@ const allSavedObjectTypes = [ ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE, ]; -export class IngestManagerPlugin implements Plugin { +export class IngestManagerPlugin implements Plugin { private config$: Observable; private security: SecurityPluginSetup | undefined; @@ -88,10 +76,7 @@ export class IngestManagerPlugin implements Plugin { this.config$ = this.initializerContext.config.create(); } - public async setup( - core: CoreSetup, - deps: IngestManagerSetupDeps - ): Promise> { + public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { if (deps.security) { this.security = deps.security; } @@ -152,12 +137,6 @@ export class IngestManagerPlugin implements Plugin { basePath: core.http.basePath, }); } - return deepFreeze({ - esIndexPatternService: new ESIndexPatternSavedObjectService(), - agentService: { - getAgentStatusById, - }, - }); } public async start( @@ -165,13 +144,19 @@ export class IngestManagerPlugin implements Plugin { plugins: { encryptedSavedObjects: EncryptedSavedObjectsPluginStart; } - ) { + ): Promise> { appContextService.start({ encryptedSavedObjects: plugins.encryptedSavedObjects, security: this.security, config$: this.config$, savedObjects: core.savedObjects, }); + return deepFreeze({ + esIndexPatternService: new ESIndexPatternSavedObjectService(), + agentService: { + getAgentStatusById, + }, + }); } public async stop() { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 89c827abe30ec..5820303e2a1a7 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -14,6 +14,7 @@ import { PostAgentEnrollResponse, PostAgentUnenrollResponse, GetAgentStatusResponse, + PutAgentReassignResponse, } from '../../../common/types'; import { GetAgentsRequestSchema, @@ -25,6 +26,7 @@ import { PostAgentEnrollRequestSchema, PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; @@ -293,60 +295,36 @@ export const getAgentsHandler: RequestHandler< } }; -export const postAgentsUnenrollHandler: RequestHandler< - undefined, +export const postAgentsUnenrollHandler: RequestHandler> = async (context, request, response) => { + const soClient = context.core.savedObjects.client; + try { + await AgentService.unenrollAgent(soClient, request.params.agentId); + + const body: PostAgentUnenrollResponse = { + success: true, + }; + return response.ok({ body }); + } catch (e) { + return response.customError({ + statusCode: 500, + body: { message: e.message }, + }); + } +}; + +export const putAgentsReassignHandler: RequestHandler< + TypeOf, undefined, - TypeOf + TypeOf > = async (context, request, response) => { const soClient = context.core.savedObjects.client; try { - const kuery = (request.body as { kuery: string }).kuery; - let toUnenrollIds: string[] = (request.body as { ids: string[] }).ids || []; - - if (kuery) { - let hasMore = true; - let page = 1; - while (hasMore) { - const { agents } = await AgentService.listAgents(soClient, { - page: page++, - perPage: 100, - kuery, - showInactive: true, - }); - if (agents.length === 0) { - hasMore = false; - } - const agentIds = agents.filter(a => a.active).map(a => a.id); - toUnenrollIds = toUnenrollIds.concat(agentIds); - } - } - const results = (await AgentService.unenrollAgents(soClient, toUnenrollIds)).map( - ({ - success, - id, - error, - }): { - success: boolean; - id: string; - action: 'unenrolled'; - error?: { - message: string; - }; - } => { - return { - success, - id, - action: 'unenrolled', - error: error && { - message: error.message, - }, - }; - } - ); + await AgentService.reassignAgent(soClient, request.params.agentId, request.body.config_id); - const body: PostAgentUnenrollResponse = { - results, - success: results.every(result => result.success), + const body: PutAgentReassignResponse = { + success: true, }; return response.ok({ body }); } catch (e) { diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts index ac27e47db155e..78bb178dce402 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/index.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/index.ts @@ -23,6 +23,7 @@ import { PostAgentUnenrollRequestSchema, GetAgentStatusRequestSchema, PostNewAgentActionRequestSchema, + PutAgentReassignRequestSchema, } from '../../types'; import { getAgentsHandler, @@ -35,6 +36,7 @@ import { postAgentsUnenrollHandler, getAgentStatusForConfigHandler, getInternalUserSOClient, + putAgentsReassignHandler, } from './handlers'; import { postAgentAcksHandlerBuilder } from './acks_handlers'; import * as AgentService from '../../services/agents'; @@ -135,6 +137,15 @@ export const registerRoutes = (router: IRouter) => { postAgentsUnenrollHandler ); + router.put( + { + path: AGENT_API_ROUTES.REASSIGN_PATTERN, + validate: PutAgentReassignRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + putAgentsReassignHandler + ); + // Get agent events router.get( { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts index d98052ea87e86..ec10ca6e77e05 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts @@ -118,5 +118,19 @@ describe('Agent checkin service', () => { expect(res).toBeTruthy(); }); + + it('should return true if this agent has no revision currently set', () => { + const res = shouldCreateConfigAction( + getAgent({ + config_id: 'config1', + last_checkin: '2018-01-02T00:00:00', + config_revision: null, + config_newest_revision: 2, + }), + [] + ); + + expect(res).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts index 9a2b3f22b9431..2873aad7f691a 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin.ts @@ -156,9 +156,13 @@ export function shouldCreateConfigAction(agent: Agent, actions: AgentAction[]): } const isAgentConfigOutdated = - agent.config_revision && - agent.config_newest_revision && - agent.config_revision < agent.config_newest_revision; + // Config reassignment + (!agent.config_revision && agent.config_newest_revision) || + // new revision of a config + (agent.config_revision && + agent.config_newest_revision && + agent.config_revision < agent.config_newest_revision); + if (!isAgentConfigOutdated) { return false; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/index.ts index c95c9ecc2a1d8..257091af0ebd0 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/index.ts @@ -13,3 +13,4 @@ export * from './status'; export * from './crud'; export * from './update'; export * from './actions'; +export * from './reassign'; diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts new file mode 100644 index 0000000000000..f8142af376eb3 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts @@ -0,0 +1,28 @@ +/* + * 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 { SavedObjectsClientContract } from 'kibana/server'; +import Boom from 'boom'; +import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; +import { AgentSOAttributes } from '../../types'; +import { agentConfigService } from '../agent_config'; + +export async function reassignAgent( + soClient: SavedObjectsClientContract, + agentId: string, + newConfigId: string +) { + const config = await agentConfigService.get(soClient, newConfigId); + if (!config) { + throw Boom.notFound(`Agent Configuration not found: ${newConfigId}`); + } + + await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, { + config_id: newConfigId, + config_revision: null, + config_newest_revision: config.revision, + }); +} diff --git a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts index 18af9fd4de73f..7f729b0b531ca 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/unenroll.ts @@ -10,31 +10,7 @@ import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; import { getAgent } from './crud'; import * as APIKeyService from '../api_keys'; -export async function unenrollAgents( - soClient: SavedObjectsClientContract, - toUnenrollIds: string[] -) { - const response = []; - for (const id of toUnenrollIds) { - try { - await unenrollAgent(soClient, id); - response.push({ - id, - success: true, - }); - } catch (error) { - response.push({ - id, - error, - success: false, - }); - } - } - - return response; -} - -async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { +export async function unenrollAgent(soClient: SavedObjectsClientContract, agentId: string) { const agent = await getAgent(soClient, agentId); await Promise.all([ diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts index 59d0ad31d1a64..948e518dff5b4 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts @@ -7,7 +7,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { listAgents } from './crud'; import { AGENT_SAVED_OBJECT_TYPE } from '../../constants'; -import { unenrollAgents } from './unenroll'; +import { unenrollAgent } from './unenroll'; import { agentConfigService } from '../agent_config'; export async function updateAgentsForConfigId( @@ -55,9 +55,8 @@ export async function unenrollForConfigId(soClient: SavedObjectsClientContract, if (agents.length === 0) { hasMore = false; } - await unenrollAgents( - soClient, - agents.map(a => a.id) - ); + for (const agent of agents) { + await unenrollAgent(soClient, agent.id); + } } } diff --git a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts index 167e22873979c..e2c27cdacda2f 100644 --- a/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts +++ b/x-pack/plugins/ingest_manager/server/services/es_index_pattern.ts @@ -4,15 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import { SavedObjectsClientContract } from 'kibana/server'; -import { getInstallation } from './epm/packages/get'; - -export interface ESIndexPatternService { - getESIndexPattern( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - datasetPath: string - ): Promise; -} +import { getInstallation } from './epm/packages'; +import { ESIndexPatternService } from '../../common/types'; export class ESIndexPatternSavedObjectService implements ESIndexPatternService { public async getESIndexPattern( diff --git a/x-pack/plugins/ingest_manager/server/services/index.ts b/x-pack/plugins/ingest_manager/server/services/index.ts index 33b29d3d082af..7d3d4c0b8f5b5 100644 --- a/x-pack/plugins/ingest_manager/server/services/index.ts +++ b/x-pack/plugins/ingest_manager/server/services/index.ts @@ -6,7 +6,7 @@ import * as settingsService from './settings'; export { appContextService } from './app_context'; -export { ESIndexPatternService, ESIndexPatternSavedObjectService } from './es_index_pattern'; +export { ESIndexPatternSavedObjectService } from './es_index_pattern'; // Saved object services export { datasourceService } from './datasource'; diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts index f94c02ccee40b..ac1679101312e 100644 --- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts @@ -62,14 +62,18 @@ export const PostNewAgentActionRequestSchema = { }; export const PostAgentUnenrollRequestSchema = { - body: schema.oneOf([ - schema.object({ - kuery: schema.string(), - }), - schema.object({ - ids: schema.arrayOf(schema.string()), - }), - ]), + params: schema.object({ + agentId: schema.string(), + }), +}; + +export const PutAgentReassignRequestSchema = { + params: schema.object({ + agentId: schema.string(), + }), + body: schema.object({ + config_id: schema.string(), + }), }; export const GetOneAgentEventsRequestSchema = { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/_chart_switch.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_chart_switch.scss diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss new file mode 100644 index 0000000000000..1965b51f97034 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_config_panel.scss @@ -0,0 +1,7 @@ +.lnsConfigPanel__addLayerBtn { + color: transparentize($euiColorMediumShade, .3); + // Remove EuiButton's default shadow to make button more subtle + // sass-lint:disable-block no-important + box-shadow: none !important; + border: 1px dashed currentColor; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss new file mode 100644 index 0000000000000..254807d06d386 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss @@ -0,0 +1,9 @@ +.lnsDimensionPopover { + line-height: 0; + flex-grow: 1; +} + +.lnsDimensionPopover__trigger { + max-width: 100%; + display: block; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss new file mode 100644 index 0000000000000..8f09a358dd5e4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss @@ -0,0 +1,4 @@ +@import 'chart_switch'; +@import 'config_panel'; +@import 'dimension_popover'; +@import 'layer_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss similarity index 52% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 62a7f6b023f31..3fbc42f9a25a0 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/_config_panel_wrapper.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -1,8 +1,8 @@ -.lnsConfigPanel__panel { +.lnsLayerPanel { margin-bottom: $euiSizeS; } -.lnsConfigPanel__row { +.lnsLayerPanel__row { background: $euiColorLightestShade; padding: $euiSizeS; border-radius: $euiBorderRadius; @@ -13,15 +13,7 @@ } } -.lnsConfigPanel__addLayerBtn { - color: transparentize($euiColorMediumShade, .3); - // Remove EuiButton's default shadow to make button more subtle - // sass-lint:disable-block no-important - box-shadow: none !important; - border: 1px dashed currentColor; -} - -.lnsConfigPanel__dimension { +.lnsLayerPanel__dimension { @include euiFontSizeS; background: lightOrDarkTheme($euiColorEmptyShade, $euiColorLightestShade); border-radius: $euiBorderRadius; @@ -31,12 +23,7 @@ overflow: hidden; } -.lnsConfigPanel__trigger { - max-width: 100%; - display: block; -} - -.lnsConfigPanel__triggerLink { +.lnsLayerPanel__triggerLink { padding: $euiSizeS; width: 100%; display: flex; @@ -44,7 +31,3 @@ min-height: $euiSizeXXL; } -.lnsConfigPanel__popover { - line-height: 0; - flex-grow: 1; -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx similarity index 98% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx index 87c359eb1345b..3c61d270b1bcf 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.test.tsx @@ -5,13 +5,17 @@ */ import React from 'react'; -import { createMockVisualization, createMockFramePublicAPI, createMockDatasource } from '../mocks'; -import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; -import { ChartSwitch } from './chart_switch'; -import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../types'; +import { + createMockVisualization, + createMockFramePublicAPI, + createMockDatasource, +} from '../../mocks'; import { EuiKeyPadMenuItem } from '@elastic/eui'; -import { Action } from './state_management'; +import { mountWithIntl as mount } from 'test_utils/enzyme_helpers'; +import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types'; +import { Action } from '../state_management'; +import { ChartSwitch } from './chart_switch'; describe('chart_switch', () => { function generateVisualization(id: string): jest.Mocked { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx similarity index 98% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx index 6bf7983d32023..1461449f3c1c8 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/chart_switch.tsx @@ -15,10 +15,10 @@ import { } from '@elastic/eui'; import { flatten } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Visualization, FramePublicAPI, Datasource } from '../../types'; -import { Action } from './state_management'; -import { getSuggestions, switchToSuggestion, Suggestion } from './suggestion_helpers'; -import { trackUiEvent } from '../../lens_ui_telemetry'; +import { Visualization, FramePublicAPI, Datasource } from '../../../types'; +import { Action } from '../state_management'; +import { getSuggestions, switchToSuggestion, Suggestion } from '../suggestion_helpers'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; interface VisualizationSelection { visualizationId: string; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx new file mode 100644 index 0000000000000..e5d3e93258c0a --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.tsx @@ -0,0 +1,177 @@ +/* + * 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, { useMemo, memo } from 'react'; +import { EuiFlexItem, EuiToolTip, EuiButton, EuiForm } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { Visualization } from '../../../types'; +import { ChartSwitch } from './chart_switch'; +import { LayerPanel } from './layer_panel'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { removeLayer, appendLayer } from './layer_actions'; +import { ConfigPanelWrapperProps } from './types'; + +export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { + const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; + const { visualizationState } = props; + + return ( + <> + + {activeVisualization && visualizationState && ( + + )} + + ); +}); + +function LayerPanels( + props: ConfigPanelWrapperProps & { + activeDatasourceId: string; + activeVisualization: Visualization; + } +) { + const { + framePublicAPI, + activeVisualization, + visualizationState, + dispatch, + activeDatasourceId, + datasourceMap, + } = props; + const setVisualizationState = useMemo( + () => (newState: unknown) => { + props.dispatch({ + type: 'UPDATE_VISUALIZATION_STATE', + visualizationId: activeVisualization.id, + newState, + clearStagedPreview: false, + }); + }, + [props.dispatch, activeVisualization] + ); + const updateDatasource = useMemo( + () => (datasourceId: string, newState: unknown) => { + props.dispatch({ + type: 'UPDATE_DATASOURCE_STATE', + updater: () => newState, + datasourceId, + clearStagedPreview: false, + }); + }, + [props.dispatch] + ); + const updateAll = useMemo( + () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { + props.dispatch({ + type: 'UPDATE_STATE', + subType: 'UPDATE_ALL_STATES', + updater: prevState => { + return { + ...prevState, + datasourceStates: { + ...prevState.datasourceStates, + [datasourceId]: { + state: newDatasourceState, + isLoading: false, + }, + }, + visualization: { + ...prevState.visualization, + state: newVisualizationState, + }, + stagedPreview: undefined, + }; + }, + }); + }, + [props.dispatch] + ); + const layerIds = activeVisualization.getLayerIds(visualizationState); + + return ( + + {layerIds.map(layerId => ( + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'REMOVE_OR_CLEAR_LAYER', + updater: state => + removeLayer({ + activeVisualization, + layerId, + trackUiEvent, + datasourceMap, + state, + }), + }); + }} + /> + ))} + {activeVisualization.appendLayer && visualizationState && ( + + + { + dispatch({ + type: 'UPDATE_STATE', + subType: 'ADD_LAYER', + updater: state => + appendLayer({ + activeVisualization, + generateId, + trackUiEvent, + activeDatasource: datasourceMap[activeDatasourceId], + state, + }), + }); + }} + iconType="plusInCircleFilled" + /> + + + )} + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx new file mode 100644 index 0000000000000..36db13b74ac4f --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiPopover } from '@elastic/eui'; +import { VisualizationDimensionGroupConfig } from '../../../types'; +import { DimensionPopoverState } from './types'; + +export function DimensionPopover({ + popoverState, + setPopoverState, + groups, + accessor, + groupId, + trigger, + panel, +}: { + popoverState: DimensionPopoverState; + setPopoverState: (newState: DimensionPopoverState) => void; + groups: VisualizationDimensionGroupConfig[]; + accessor: string; + groupId: string; + trigger: React.ReactElement; + panel: React.ReactElement; +}) { + const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(accessor)) : false; + return ( + { + setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); + }} + button={trigger} + anchorPosition="leftUp" + withTitle + panelPaddingSize="s" + > + {panel} + + ); +} diff --git a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts similarity index 68% rename from x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts index f57f3188a8184..754b3fb5c6fde 100644 --- a/x-pack/legacy/plugins/canvas/public/lib/kibana_advanced_settings.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getCoreStart } from '../legacy'; - -export const getAdvancedSettings = () => getCoreStart().uiSettings; +export { ConfigPanelWrapper } from './config_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts similarity index 100% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.test.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.test.ts diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts similarity index 95% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index cc2cbb172d23e..3d1d590664238 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -5,8 +5,8 @@ */ import _ from 'lodash'; -import { EditorFrameState } from './state_management'; -import { Datasource, Visualization } from '../../types'; +import { EditorFrameState } from '../state_management'; +import { Datasource, Visualization } from '../../../types'; interface RemoveLayerOptions { trackUiEvent: (name: string) => void; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx new file mode 100644 index 0000000000000..f7be82dd34ba3 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_panel.tsx @@ -0,0 +1,405 @@ +/* + * 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, { useContext, useState } from 'react'; +import { + EuiPanel, + EuiSpacer, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFormRow, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, FramePublicAPI, StateSetter } from '../../../types'; +import { DragContext, DragDrop, ChildDragDropProvider } from '../../../drag_drop'; +import { LayerSettings } from './layer_settings'; +import { trackUiEvent } from '../../../lens_ui_telemetry'; +import { generateId } from '../../../id_generator'; +import { ConfigPanelWrapperProps, DimensionPopoverState } from './types'; +import { DimensionPopover } from './dimension_popover'; + +export function LayerPanel( + props: Exclude & { + frame: FramePublicAPI; + layerId: string; + isOnlyLayer: boolean; + activeVisualization: Visualization; + visualizationState: unknown; + updateVisualization: StateSetter; + updateDatasource: (datasourceId: string, newState: unknown) => void; + updateAll: ( + datasourceId: string, + newDatasourcestate: unknown, + newVisualizationState: unknown + ) => void; + onRemoveLayer: () => void; + } +) { + const dragDropContext = useContext(DragContext); + const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; + const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; + if (!datasourcePublicAPI) { + return null; + } + const layerVisualizationConfigProps = { + layerId, + dragDropContext, + state: props.visualizationState, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + const datasourceId = datasourcePublicAPI.datasourceId; + const layerDatasourceState = props.datasourceStates[datasourceId].state; + const layerDatasource = props.datasourceMap[datasourceId]; + + const layerDatasourceDropProps = { + layerId, + dragDropContext, + state: layerDatasourceState, + setState: (newState: unknown) => { + props.updateDatasource(datasourceId, newState); + }, + }; + + const layerDatasourceConfigProps = { + ...layerDatasourceDropProps, + frame: props.framePublicAPI, + dateRange: props.framePublicAPI.dateRange, + }; + + const [popoverState, setPopoverState] = useState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + + const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); + const isEmptyLayer = !groups.some(d => d.accessors.length > 0); + + return ( + + + + + + + + {layerDatasource && ( + + { + const newState = + typeof updater === 'function' ? updater(layerDatasourceState) : updater; + // Look for removed columns + const nextPublicAPI = layerDatasource.getPublicAPI({ + state: newState, + layerId, + dateRange: props.framePublicAPI.dateRange, + }); + const nextTable = new Set( + nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) + ); + const removed = datasourcePublicAPI + .getTableSpec() + .map(({ columnId }) => columnId) + .filter(columnId => !nextTable.has(columnId)); + let nextVisState = props.visualizationState; + removed.forEach(columnId => { + nextVisState = activeVisualization.removeDimension({ + layerId, + columnId, + prevState: nextVisState, + }); + }); + + props.updateAll(datasourceId, newState, nextVisState); + }, + }} + /> + + )} + + + + + {groups.map((group, index) => { + const newId = generateId(); + const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; + return ( + + <> + {group.accessors.map(accessor => ( + { + layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: accessor, + filterOperations: group.filterOperations, + }); + }} + > + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: accessor, + addingToGroupId: null, // not set for existing dimension + }); + } + }, + }} + /> + } + panel={ + + } + /> + + { + trackUiEvent('indexpattern_dimension_removed'); + props.updateAll( + datasourceId, + layerDatasource.removeColumn({ + layerId, + columnId: accessor, + prevState: layerDatasourceState, + }), + props.activeVisualization.removeDimension({ + layerId, + columnId: accessor, + prevState: props.visualizationState, + }) + ); + }} + /> + + ))} + {group.supportsMoreColumns ? ( + { + const dropSuccess = layerDatasource.onDrop({ + ...layerDatasourceDropProps, + droppedItem, + columnId: newId, + filterOperations: group.filterOperations, + }); + if (dropSuccess) { + props.updateVisualization( + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + } + }} + > + + { + if (popoverState.isOpen) { + setPopoverState({ + isOpen: false, + openId: null, + addingToGroupId: null, + }); + } else { + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: group.groupId, + }); + } + }} + size="xs" + > + + + + } + panel={ + { + props.updateAll( + datasourceId, + newState, + activeVisualization.setDimension({ + layerId, + groupId: group.groupId, + columnId: newId, + prevState: props.visualizationState, + }) + ); + setPopoverState({ + isOpen: true, + openId: newId, + addingToGroupId: null, // clear now that dimension exists + }); + }, + }} + /> + } + /> + + ) : null} + + + ); + })} + + + + + + { + // If we don't blur the remove / clear button, it remains focused + // which is a strange UX in this case. e.target.blur doesn't work + // due to who knows what, but probably event re-writing. Additionally, + // activeElement does not have blur so, we need to do some casting + safeguards. + const el = (document.activeElement as unknown) as { blur: () => void }; + + if (el?.blur) { + el.blur(); + } + + onRemoveLayer(); + }} + > + {isOnlyLayer + ? i18n.translate('xpack.lens.resetLayer', { + defaultMessage: 'Reset layer', + }) + : i18n.translate('xpack.lens.deleteLayer', { + defaultMessage: 'Delete layer', + })} + + + + + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx new file mode 100644 index 0000000000000..57588e31590b4 --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_settings.tsx @@ -0,0 +1,53 @@ +/* + * 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, { useState } from 'react'; +import { EuiPopover, EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { NativeRenderer } from '../../../native_renderer'; +import { Visualization, VisualizationLayerWidgetProps } from '../../../types'; + +export function LayerSettings({ + layerId, + activeVisualization, + layerConfigProps, +}: { + layerId: string; + activeVisualization: Visualization; + layerConfigProps: VisualizationLayerWidgetProps; +}) { + const [isOpen, setIsOpen] = useState(false); + + if (!activeVisualization.renderLayerContextMenu) { + return null; + } + + return ( + setIsOpen(!isOpen)} + data-test-subj="lns_layer_settings" + /> + } + isOpen={isOpen} + closePopover={() => setIsOpen(false)} + anchorPosition="leftUp" + > + + + ); +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts new file mode 100644 index 0000000000000..df510d3648f8c --- /dev/null +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { Action } from '../state_management'; +import { + Visualization, + FramePublicAPI, + Datasource, + DatasourceDimensionEditorProps, +} from '../../../types'; + +export interface ConfigPanelWrapperProps { + activeDatasourceId: string; + visualizationState: unknown; + visualizationMap: Record; + activeVisualizationId: string | null; + dispatch: (action: Action) => void; + framePublicAPI: FramePublicAPI; + datasourceMap: Record; + datasourceStates: Record< + string, + { + isLoading: boolean; + state: unknown; + } + >; + core: DatasourceDimensionEditorProps['core']; +} + +export interface DimensionPopoverState { + isOpen: boolean; + openId: string | null; + addingToGroupId: string | null; +} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx deleted file mode 100644 index da812e948b23f..0000000000000 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel_wrapper.tsx +++ /dev/null @@ -1,655 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo, useContext, memo, useState } from 'react'; -import { - EuiPanel, - EuiSpacer, - EuiPopover, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiToolTip, - EuiButton, - EuiForm, - EuiFormRow, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { NativeRenderer } from '../../native_renderer'; -import { Action } from './state_management'; -import { - Visualization, - FramePublicAPI, - Datasource, - VisualizationLayerWidgetProps, - DatasourceDimensionEditorProps, - StateSetter, -} from '../../types'; -import { DragContext, DragDrop, ChildDragDropProvider } from '../../drag_drop'; -import { ChartSwitch } from './chart_switch'; -import { trackUiEvent } from '../../lens_ui_telemetry'; -import { generateId } from '../../id_generator'; -import { removeLayer, appendLayer } from './layer_actions'; - -interface ConfigPanelWrapperProps { - activeDatasourceId: string; - visualizationState: unknown; - visualizationMap: Record; - activeVisualizationId: string | null; - dispatch: (action: Action) => void; - framePublicAPI: FramePublicAPI; - datasourceMap: Record; - datasourceStates: Record< - string, - { - isLoading: boolean; - state: unknown; - } - >; - core: DatasourceDimensionEditorProps['core']; -} - -export const ConfigPanelWrapper = memo(function ConfigPanelWrapper(props: ConfigPanelWrapperProps) { - const activeVisualization = props.visualizationMap[props.activeVisualizationId || '']; - const { visualizationState } = props; - - return ( - <> - - {activeVisualization && visualizationState && ( - - )} - - ); -}); - -function LayerPanels( - props: ConfigPanelWrapperProps & { - activeDatasourceId: string; - activeVisualization: Visualization; - } -) { - const { - framePublicAPI, - activeVisualization, - visualizationState, - dispatch, - activeDatasourceId, - datasourceMap, - } = props; - const setVisualizationState = useMemo( - () => (newState: unknown) => { - props.dispatch({ - type: 'UPDATE_VISUALIZATION_STATE', - visualizationId: activeVisualization.id, - newState, - clearStagedPreview: false, - }); - }, - [props.dispatch, activeVisualization] - ); - const updateDatasource = useMemo( - () => (datasourceId: string, newState: unknown) => { - props.dispatch({ - type: 'UPDATE_DATASOURCE_STATE', - updater: () => newState, - datasourceId, - clearStagedPreview: false, - }); - }, - [props.dispatch] - ); - const updateAll = useMemo( - () => (datasourceId: string, newDatasourceState: unknown, newVisualizationState: unknown) => { - props.dispatch({ - type: 'UPDATE_STATE', - subType: 'UPDATE_ALL_STATES', - updater: prevState => { - return { - ...prevState, - datasourceStates: { - ...prevState.datasourceStates, - [datasourceId]: { - state: newDatasourceState, - isLoading: false, - }, - }, - visualization: { - ...prevState.visualization, - state: newVisualizationState, - }, - stagedPreview: undefined, - }; - }, - }); - }, - [props.dispatch] - ); - const layerIds = activeVisualization.getLayerIds(visualizationState); - - return ( - - {layerIds.map(layerId => ( - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'REMOVE_OR_CLEAR_LAYER', - updater: state => - removeLayer({ - activeVisualization, - layerId, - trackUiEvent, - datasourceMap, - state, - }), - }); - }} - /> - ))} - {activeVisualization.appendLayer && ( - - - { - dispatch({ - type: 'UPDATE_STATE', - subType: 'ADD_LAYER', - updater: state => - appendLayer({ - activeVisualization, - generateId, - trackUiEvent, - activeDatasource: datasourceMap[activeDatasourceId], - state, - }), - }); - }} - iconType="plusInCircleFilled" - /> - - - )} - - ); -} - -function LayerPanel( - props: Exclude & { - frame: FramePublicAPI; - layerId: string; - isOnlyLayer: boolean; - activeVisualization: Visualization; - visualizationState: unknown; - updateVisualization: StateSetter; - updateDatasource: (datasourceId: string, newState: unknown) => void; - updateAll: ( - datasourceId: string, - newDatasourcestate: unknown, - newVisualizationState: unknown - ) => void; - onRemoveLayer: () => void; - } -) { - const dragDropContext = useContext(DragContext); - const { framePublicAPI, layerId, activeVisualization, isOnlyLayer, onRemoveLayer } = props; - const datasourcePublicAPI = framePublicAPI.datasourceLayers[layerId]; - if (!datasourcePublicAPI) { - return null; - } - const layerVisualizationConfigProps = { - layerId, - dragDropContext, - state: props.visualizationState, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - const datasourceId = datasourcePublicAPI.datasourceId; - const layerDatasourceState = props.datasourceStates[datasourceId].state; - const layerDatasource = props.datasourceMap[datasourceId]; - - const layerDatasourceDropProps = { - layerId, - dragDropContext, - state: layerDatasourceState, - setState: (newState: unknown) => { - props.updateDatasource(datasourceId, newState); - }, - }; - - const layerDatasourceConfigProps = { - ...layerDatasourceDropProps, - frame: props.framePublicAPI, - dateRange: props.framePublicAPI.dateRange, - }; - - const [popoverState, setPopoverState] = useState<{ - isOpen: boolean; - openId: string | null; - addingToGroupId: string | null; - }>({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - - const { groups } = activeVisualization.getConfiguration(layerVisualizationConfigProps); - const isEmptyLayer = !groups.some(d => d.accessors.length > 0); - - function wrapInPopover( - id: string, - groupId: string, - trigger: React.ReactElement, - panel: React.ReactElement - ) { - const noMatch = popoverState.isOpen ? !groups.some(d => d.accessors.includes(id)) : false; - return ( - { - setPopoverState({ isOpen: false, openId: null, addingToGroupId: null }); - }} - button={trigger} - anchorPosition="leftUp" - withTitle - panelPaddingSize="s" - > - {panel} - - ); - } - - return ( - - - - - - - - {layerDatasource && ( - - { - const newState = - typeof updater === 'function' ? updater(layerDatasourceState) : updater; - // Look for removed columns - const nextPublicAPI = layerDatasource.getPublicAPI({ - state: newState, - layerId, - dateRange: props.framePublicAPI.dateRange, - }); - const nextTable = new Set( - nextPublicAPI.getTableSpec().map(({ columnId }) => columnId) - ); - const removed = datasourcePublicAPI - .getTableSpec() - .map(({ columnId }) => columnId) - .filter(columnId => !nextTable.has(columnId)); - let nextVisState = props.visualizationState; - removed.forEach(columnId => { - nextVisState = activeVisualization.removeDimension({ - layerId, - columnId, - prevState: nextVisState, - }); - }); - - props.updateAll(datasourceId, newState, nextVisState); - }, - }} - /> - - )} - - - - - {groups.map((group, index) => { - const newId = generateId(); - const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0; - return ( - - <> - {group.accessors.map(accessor => ( - { - layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: accessor, - filterOperations: group.filterOperations, - }); - }} - > - {wrapInPopover( - accessor, - group.groupId, - { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: accessor, - addingToGroupId: null, // not set for existing dimension - }); - } - }, - }} - />, - - )} - - { - trackUiEvent('indexpattern_dimension_removed'); - props.updateAll( - datasourceId, - layerDatasource.removeColumn({ - layerId, - columnId: accessor, - prevState: layerDatasourceState, - }), - props.activeVisualization.removeDimension({ - layerId, - columnId: accessor, - prevState: props.visualizationState, - }) - ); - }} - /> - - ))} - {group.supportsMoreColumns ? ( - { - const dropSuccess = layerDatasource.onDrop({ - ...layerDatasourceDropProps, - droppedItem, - columnId: newId, - filterOperations: group.filterOperations, - }); - if (dropSuccess) { - props.updateVisualization( - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - } - }} - > - {wrapInPopover( - newId, - group.groupId, -
- { - if (popoverState.isOpen) { - setPopoverState({ - isOpen: false, - openId: null, - addingToGroupId: null, - }); - } else { - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: group.groupId, - }); - } - }} - size="xs" - > - - -
, - { - props.updateAll( - datasourceId, - newState, - activeVisualization.setDimension({ - layerId, - groupId: group.groupId, - columnId: newId, - prevState: props.visualizationState, - }) - ); - setPopoverState({ - isOpen: true, - openId: newId, - addingToGroupId: null, // clear now that dimension exists - }); - }, - }} - /> - )} -
- ) : null} - -
- ); - })} - - - - - - { - // If we don't blur the remove / clear button, it remains focused - // which is a strange UX in this case. e.target.blur doesn't work - // due to who knows what, but probably event re-writing. Additionally, - // activeElement does not have blur so, we need to do some casting + safeguards. - const el = (document.activeElement as unknown) as { blur: () => void }; - - if (el?.blur) { - el.blur(); - } - - onRemoveLayer(); - }} - > - {isOnlyLayer - ? i18n.translate('xpack.lens.resetLayer', { - defaultMessage: 'Reset layer', - }) - : i18n.translate('xpack.lens.deleteLayer', { - defaultMessage: 'Delete layer', - })} - - - -
-
- ); -} - -function LayerSettings({ - layerId, - activeVisualization, - layerConfigProps, -}: { - layerId: string; - activeVisualization: Visualization; - layerConfigProps: VisualizationLayerWidgetProps; -}) { - const [isOpen, setIsOpen] = useState(false); - - if (!activeVisualization.renderLayerContextMenu) { - return null; - } - - return ( - setIsOpen(!isOpen)} - data-test-subj="lns_layer_settings" - /> - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - anchorPosition="leftUp" - > - - - ); -} diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index b72d9081bbc91..5cd803e7cebbc 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -16,7 +16,7 @@ import { } from '../../types'; import { reducer, getInitialState } from './state_management'; import { DataPanelWrapper } from './data_panel_wrapper'; -import { ConfigPanelWrapper } from './config_panel_wrapper'; +import { ConfigPanelWrapper } from './config_panel'; import { FrameLayout } from './frame_layout'; import { SuggestionPanel } from './suggestion_panel'; import { WorkspacePanel } from './workspace_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss index d4b27c6c98b3c..5e3726c953f11 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/index.scss @@ -1,8 +1,6 @@ -@import 'chart_switch'; -@import 'config_panel_wrapper'; +@import 'config_panel/index'; @import 'data_panel_wrapper'; @import 'expression_renderer'; @import 'frame_layout'; @import 'suggestion_panel'; @import 'workspace_panel_wrapper'; - diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx index b3bd08d3bbfbe..583832aafcbe8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.tsx @@ -192,7 +192,7 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens return ( { props.togglePopover(); }} diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 0b432c0c70727..181f192520d0d 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -273,7 +273,7 @@ export type VisualizationLayerWidgetProps = VisualizationConfigProp setState: (newState: T) => void; }; -type VisualizationDimensionGroupConfig = SharedDimensionProps & { +export type VisualizationDimensionGroupConfig = SharedDimensionProps & { groupLabel: string; /** ID is passed back to visualization. For example, `x` */ @@ -368,55 +368,86 @@ export interface VisualizationType { } export interface Visualization { + /** Plugin ID, such as "lnsXY" */ id: string; + /** + * Initialize is allowed to modify the state stored in memory. The initialize function + * is called with a previous state in two cases: + * - Loadingn from a saved visualization + * - When using suggestions, the suggested state is passed in + */ + initialize: (frame: FramePublicAPI, state?: P) => T; + /** + * Can remove any state that should not be persisted to saved object, such as UI state + */ + getPersistableState: (state: T) => P; + + /** + * Visualizations must provide at least one type for the chart switcher, + * but can register multiple subtypes + */ visualizationTypes: VisualizationType[]; + /** + * If the visualization has subtypes, update the subtype in state. + */ + switchVisualizationType?: (visualizationTypeId: string, state: T) => T; + /** Description is displayed as the clickable text in the chart switcher */ + getDescription: (state: T) => { icon?: IconType; label: string }; + /** Frame needs to know which layers the visualization is currently using */ getLayerIds: (state: T) => string[]; + /** Reset button on each layer triggers this */ clearLayer: (state: T, layerId: string) => T; + /** Optional, if the visualization supports multiple layers */ removeLayer?: (state: T, layerId: string) => T; + /** Track added layers in internal state */ appendLayer?: (state: T, layerId: string) => T; - // Layer context menu is used by visualizations for styling the entire layer - // For example, the XY visualization uses this to have multiple chart types - getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; - + /** + * For consistency across different visualizations, the dimension configuration UI is standardized + */ getConfiguration: ( props: VisualizationConfigProps ) => { groups: VisualizationDimensionGroupConfig[] }; - getDescription: ( - state: T - ) => { - icon?: IconType; - label: string; - }; - - switchVisualizationType?: (visualizationTypeId: string, state: T) => T; - - // For initializing from saved object - initialize: (frame: FramePublicAPI, state?: P) => T; - - getPersistableState: (state: T) => P; + /** + * Popover contents that open when the user clicks the contextMenuIcon. This can be used + * for extra configurability, such as for styling the legend or axis + */ + renderLayerContextMenu?: (domElement: Element, props: VisualizationLayerWidgetProps) => void; + /** + * Visualizations can provide a custom icon which will open a layer-specific popover + * If no icon is provided, gear icon is default + */ + getLayerContextMenuIcon?: (opts: { state: T; layerId: string }) => IconType | undefined; - // Actions triggered by the frame which tell the datasource that a dimension is being changed - setDimension: ( - props: VisualizationDimensionChangeProps & { - groupId: string; - } - ) => T; + /** + * The frame is telling the visualization to update or set a dimension based on user interaction + * groupId is coming from the groupId provided in getConfiguration + */ + setDimension: (props: VisualizationDimensionChangeProps & { groupId: string }) => T; + /** + * The frame is telling the visualization to remove a dimension. The visualization needs to + * look at its internal state to determine which dimension is being affected. + */ removeDimension: (props: VisualizationDimensionChangeProps) => T; - toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; + /** + * The frame will call this function on all visualizations at different times. The + * main use cases where visualization suggestions are requested are: + * - When dragging a field + * - When opening the chart switcher + * If the state is provided when requesting suggestions, the visualization is active. + * Most visualizations will apply stricter filtering to suggestions when they are active, + * because suggestions have the potential to remove the users's work in progress. + */ + getSuggestions: (context: SuggestionRequest) => Array>; + toExpression: (state: T, frame: FramePublicAPI) => Ast | string | null; /** - * Epression to render a preview version of the chart in very constraint space. + * Expression to render a preview version of the chart in very constrained space. * If there is no expression provided, the preview icon is used. */ toPreviewExpression?: (state: T, frame: FramePublicAPI) => Ast | string | null; - - // The frame will call this function on all visualizations when the table changes, or when - // rendering additional ways of using the data - getSuggestions: (context: SuggestionRequest) => Array>; } diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 6f9c0985f5f4a..fd972219563a8 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -215,3 +215,5 @@ export enum SCALING_TYPES { } export const RGBA_0000 = 'rgba(0,0,0,0)'; + +export const SPATIAL_FILTERS_LAYER_ID = 'SPATIAL_FILTERS_LAYER_ID'; diff --git a/x-pack/plugins/maps/public/components/alpha_slider.tsx b/x-pack/plugins/maps/public/components/alpha_slider.tsx new file mode 100644 index 0000000000000..921c386292050 --- /dev/null +++ b/x-pack/plugins/maps/public/components/alpha_slider.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +// @ts-ignore +import { ValidatedRange } from './validated_range'; + +interface Props { + alpha: number; + onChange: (alpha: number) => void; +} + +export function AlphaSlider({ alpha, onChange }: Props) { + const onAlphaChange = (newAlpha: number) => { + onChange(newAlpha / 100); + }; + + return ( + + + + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js index 168c735ab7a6c..d84d05260f982 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/layer_settings/layer_settings.js @@ -8,7 +8,7 @@ import React, { Fragment } from 'react'; import { EuiTitle, EuiPanel, EuiFormRow, EuiFieldText, EuiSpacer } from '@elastic/eui'; -import { ValidatedRange } from '../../../components/validated_range'; +import { AlphaSlider } from '../../../components/alpha_slider'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ValidatedDualRange } from '../../../../../../../src/plugins/kibana_react/public'; @@ -24,8 +24,7 @@ export function LayerSettings(props) { }; const onAlphaChange = alpha => { - const alphaDecimal = alpha / 100; - props.updateAlpha(props.layerId, alphaDecimal); + props.updateAlpha(props.layerId, alpha); }; const renderZoomSliders = () => { @@ -64,34 +63,6 @@ export function LayerSettings(props) { ); }; - const renderAlphaSlider = () => { - const alphaPercent = Math.round(props.alpha * 100); - - return ( - - - - ); - }; - return ( @@ -107,7 +78,7 @@ export function LayerSettings(props) { {renderLabel()} {renderZoomSliders()} - {renderAlphaSlider()} + diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js index d20faa39d6492..a69e06458a6a0 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/draw_control/draw_control.js @@ -64,13 +64,28 @@ export class DrawControl extends React.Component { if (this.props.drawState.drawType === DRAW_TYPE.DISTANCE) { const circle = e.features[0]; - roundCoordinates(circle.properties.center); + const distanceKm = _.round( + circle.properties.radiusKm, + circle.properties.radiusKm > 10 ? 0 : 2 + ); + // Only include as much precision as needed for distance + let precision = 2; + if (distanceKm <= 1) { + precision = 5; + } else if (distanceKm <= 10) { + precision = 4; + } else if (distanceKm <= 100) { + precision = 3; + } const filter = createDistanceFilterWithMeta({ alias: this.props.drawState.filterLabel, - distanceKm: _.round(circle.properties.radiusKm, circle.properties.radiusKm > 10 ? 0 : 2), + distanceKm, geoFieldName: this.props.drawState.geoFieldName, indexPatternId: this.props.drawState.indexPatternId, - point: circle.properties.center, + point: [ + _.round(circle.properties.center[0], precision), + _.round(circle.properties.center[1], precision), + ], }); this.props.addFilters([filter]); this.props.disableDrawState(); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/index.js b/x-pack/plugins/maps/public/connected_components/map/mb/index.js index 459b38d422694..f8daf0804265b 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/index.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/index.js @@ -23,6 +23,7 @@ import { isInteractiveDisabled, isTooltipControlDisabled, isViewControlHidden, + getSpatialFiltersLayer, getMapSettings, } from '../../../selectors/map_selectors'; @@ -33,6 +34,7 @@ function mapStateToProps(state = {}) { isMapReady: getMapReady(state), settings: getMapSettings(state), layerList: getLayerList(state), + spatialFiltersLayer: getSpatialFiltersLayer(state), goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), scrollZoom: getScrollZoom(state), diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js index a8c4f61a00da3..4774cdc556c24 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/mb.utils.test.js @@ -5,6 +5,7 @@ */ import { removeOrphanedSourcesAndLayers, syncLayerOrderForSingleLayer } from './utils'; +import { SPATIAL_FILTERS_LAYER_ID } from '../../../../common/constants'; import _ from 'lodash'; class MockMbMap { @@ -121,7 +122,8 @@ function makeMultiSourceMockLayer(layerId) { ); } -describe('mb/utils', () => { +describe('removeOrphanedSourcesAndLayers', () => { + const spatialFilterLayer = makeMultiSourceMockLayer(SPATIAL_FILTERS_LAYER_ID); test('should remove foo and bar layer', async () => { const bazLayer = makeSingleSourceMockLayer('baz'); const fooLayer = makeSingleSourceMockLayer('foo'); @@ -133,7 +135,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -151,7 +153,7 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); @@ -169,13 +171,23 @@ describe('mb/utils', () => { const currentStyle = getMockStyle(currentLayerList); const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList); + removeOrphanedSourcesAndLayers(mockMbMap, nextLayerList, spatialFilterLayer); const removedStyle = mockMbMap.getStyle(); const nextStyle = getMockStyle(nextLayerList); expect(removedStyle).toEqual(nextStyle); }); + test('should not remove spatial filter layer and sources when spatialFilterLayer is provided', async () => { + const styleWithSpatialFilters = getMockStyle([spatialFilterLayer]); + const mockMbMap = new MockMbMap(styleWithSpatialFilters); + + removeOrphanedSourcesAndLayers(mockMbMap, [], spatialFilterLayer); + expect(mockMbMap.getStyle()).toEqual(styleWithSpatialFilters); + }); +}); + +describe('syncLayerOrderForSingleLayer', () => { test('should move bar layer in front of foo layer', async () => { const fooLayer = makeSingleSourceMockLayer('foo'); const barLayer = makeSingleSourceMockLayer('bar'); @@ -250,40 +262,4 @@ describe('mb/utils', () => { const nextStyle = getMockStyle(nextLayerListOrder); expect(orderedStyle).toEqual(nextStyle); }); - - test('should reorder foo and bar and remove baz', async () => { - const bazLayer = makeSingleSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeSingleSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); - - test('should reorder foo and bar and remove baz, when having multi-source multi-layer data', async () => { - const bazLayer = makeMultiSourceMockLayer('baz'); - const fooLayer = makeSingleSourceMockLayer('foo'); - const barLayer = makeMultiSourceMockLayer('bar'); - - const currentLayerOrder = [bazLayer, fooLayer, barLayer]; - const nextLayerListOrder = [barLayer, fooLayer]; - - const currentStyle = getMockStyle(currentLayerOrder); - const mockMbMap = new MockMbMap(currentStyle); - removeOrphanedSourcesAndLayers(mockMbMap, nextLayerListOrder); - syncLayerOrderForSingleLayer(mockMbMap, nextLayerListOrder); - const orderedStyle = mockMbMap.getStyle(); - - const nextStyle = getMockStyle(nextLayerListOrder); - expect(orderedStyle).toEqual(nextStyle); - }); }); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js index 7be2cd9e67084..adf109a087d27 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/utils.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/utils.js @@ -7,11 +7,16 @@ import _ from 'lodash'; import { RGBAImage } from './image_utils'; -export function removeOrphanedSourcesAndLayers(mbMap, layerList) { +export function removeOrphanedSourcesAndLayers(mbMap, layerList, spatialFilterLayer) { const mbStyle = mbMap.getStyle(); const mbLayerIdsToRemove = []; mbStyle.layers.forEach(mbLayer => { + // ignore mapbox layers from spatial filter layer + if (spatialFilterLayer.ownsMbLayerId(mbLayer.id)) { + return; + } + const layer = layerList.find(layer => { return layer.ownsMbLayerId(mbLayer.id); }); @@ -24,6 +29,11 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList) { const mbSourcesToRemove = []; for (const mbSourceId in mbStyle.sources) { if (mbStyle.sources.hasOwnProperty(mbSourceId)) { + // ignore mapbox sources from spatial filter layer + if (spatialFilterLayer.ownsMbSourceId(mbSourceId)) { + return; + } + const layer = layerList.find(layer => { return layer.ownsMbSourceId(mbSourceId); }); @@ -35,6 +45,21 @@ export function removeOrphanedSourcesAndLayers(mbMap, layerList) { mbSourcesToRemove.forEach(mbSourceId => mbMap.removeSource(mbSourceId)); } +export function moveLayerToTop(mbMap, layer) { + const mbStyle = mbMap.getStyle(); + + if (!mbStyle.layers || mbStyle.layers.length === 0) { + return; + } + + layer.getMbLayerIds().forEach(mbLayerId => { + const mbLayer = mbMap.getLayer(mbLayerId); + if (mbLayer) { + mbMap.moveLayer(mbLayerId); + } + }); +} + /** * This is function assumes only a single layer moved in the layerList, compared to mbMap * It is optimized to minimize the amount of mbMap.moveLayer calls. @@ -47,9 +72,12 @@ export function syncLayerOrderForSingleLayer(mbMap, layerList) { } const mbLayers = mbMap.getStyle().layers.slice(); - const layerIds = mbLayers.map(mbLayer => { + const layerIds = []; + mbLayers.forEach(mbLayer => { const layer = layerList.find(layer => layer.ownsMbLayerId(mbLayer.id)); - return layer.getId(); + if (layer) { + layerIds.push(layer.getId()); + } }); const currentLayerOrderLayerIds = _.uniq(layerIds); diff --git a/x-pack/plugins/maps/public/connected_components/map/mb/view.js b/x-pack/plugins/maps/public/connected_components/map/mb/view.js index 71c1af44e493b..6bb5a4fed6e52 100644 --- a/x-pack/plugins/maps/public/connected_components/map/mb/view.js +++ b/x-pack/plugins/maps/public/connected_components/map/mb/view.js @@ -11,6 +11,7 @@ import { syncLayerOrderForSingleLayer, removeOrphanedSourcesAndLayers, addSpritesheetToMap, + moveLayerToTop, } from './utils'; import { getGlyphUrl, isRetina } from '../../../meta'; import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; @@ -74,7 +75,7 @@ export class MBMapContainer extends React.Component { } _debouncedSync = _.debounce(() => { - if (this._isMounted || !this.props.isMapReady) { + if (this._isMounted && this.props.isMapReady) { if (!this.state.hasSyncedLayerList) { this.setState( { @@ -86,6 +87,7 @@ export class MBMapContainer extends React.Component { } ); } + this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); this._syncSettings(); } }, 256); @@ -260,9 +262,14 @@ export class MBMapContainer extends React.Component { }; _syncMbMapWithLayerList = () => { - removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList); + removeOrphanedSourcesAndLayers( + this.state.mbMap, + this.props.layerList, + this.props.spatialFiltersLayer + ); this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); + moveLayerToTop(this.state.mbMap, this.props.spatialFiltersLayer); }; _syncMbMapWithInspector = () => { diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx index 36ed29e92cf69..a89f4461fff06 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; import { NavigationPanel } from './navigation_panel'; +import { SpatialFiltersPanel } from './spatial_filters_panel'; interface Props { cancelChanges: () => void; @@ -60,6 +61,8 @@ export function MapSettingsPanel({
+ +
diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx new file mode 100644 index 0000000000000..cae703e982966 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx @@ -0,0 +1,98 @@ +/* + * 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 { EuiFormRow, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { AlphaSlider } from '../../components/alpha_slider'; +import { MbValidatedColorPicker } from '../../layers/styles/vector/components/color/mb_validated_color_picker'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function SpatialFiltersPanel({ settings, updateMapSetting }: Props) { + const onAlphaChange = (alpha: number) => { + updateMapSetting('spatialFiltersAlpa', alpha); + }; + + const onFillColorChange = (color: string) => { + updateMapSetting('spatialFiltersFillColor', color); + }; + + const onLineColorChange = (color: string) => { + updateMapSetting('spatialFiltersLineColor', color); + }; + + const onShowSpatialFiltersChange = (event: EuiSwitchEvent) => { + updateMapSetting('showSpatialFilters', event.target.checked); + }; + + const renderStyleInputs = () => { + if (!settings.showSpatialFilters) { + return null; + } + + return ( + <> + + + + + + + + + + + ); + }; + + return ( + + +
+ +
+
+ + + + + + {renderStyleInputs()} +
+ ); +} diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index 417c5d84f8916..888fce7e7afe0 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -18,6 +18,7 @@ import { } from '../common/constants'; import { getEsSpatialRelationLabel } from '../common/i18n_getters'; import { SPATIAL_FILTER_TYPE } from './kibana_services'; +import turfCircle from '@turf/circle'; function ensureGeoField(type) { const expectedTypes = [ES_GEO_FIELD_TYPE.GEO_POINT, ES_GEO_FIELD_TYPE.GEO_SHAPE]; @@ -330,7 +331,7 @@ export function createDistanceFilterWithMeta({ values: { distanceKm, geoFieldName, - pointLabel: point.join(','), + pointLabel: point.join(', '), }, }), }; @@ -451,3 +452,40 @@ export function clamp(val, min, max) { return val; } } + +export function extractFeaturesFromFilters(filters) { + const features = []; + filters + .filter(filter => { + return filter.meta.key && filter.meta.type === SPATIAL_FILTER_TYPE; + }) + .forEach(filter => { + let geometry; + if (filter.geo_distance && filter.geo_distance[filter.meta.key]) { + const distanceSplit = filter.geo_distance.distance.split('km'); + const distance = parseFloat(distanceSplit[0]); + const circleFeature = turfCircle(filter.geo_distance[filter.meta.key], distance); + geometry = circleFeature.geometry; + } else if ( + filter.geo_shape && + filter.geo_shape[filter.meta.key] && + filter.geo_shape[filter.meta.key].shape + ) { + geometry = filter.geo_shape[filter.meta.key].shape; + } else { + // do not know how to convert spatial filter to geometry + // this includes pre-indexed shapes + return; + } + + features.push({ + type: 'Feature', + geometry, + properties: { + filter: filter.meta.alias, + }, + }); + }); + + return features; +} diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index fc02e19173843..d13291a8e2ba5 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -19,6 +19,7 @@ import { createExtentFilter, convertMapExtentToPolygon, roundCoordinates, + extractFeaturesFromFilters, } from './elasticsearch_geo_utils'; import { indexPatterns } from '../../../../src/plugins/data/public'; @@ -503,3 +504,131 @@ describe('roundCoordinates', () => { ]); }); }); + +describe('extractFeaturesFromFilters', () => { + it('should ignore non-spatial filers', () => { + const phraseFilter = { + meta: { + alias: null, + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'machine.os', + negate: false, + params: { + query: 'ios', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'machine.os': 'ios', + }, + }, + }; + expect(extractFeaturesFromFilters([phraseFilter])).toEqual([]); + }); + + it('should convert geo_distance filter to feature', () => { + const spatialFilter = { + geo_distance: { + distance: '1096km', + 'geo.coordinates': [-89.87125, 53.49454], + }, + meta: { + alias: 'geo.coordinates within 1096km of -89.87125,53.49454', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + const features = extractFeaturesFromFilters([spatialFilter]); + expect(features[0].geometry.coordinates[0][0]).toEqual([-89.87125, 63.35109118642093]); + expect(features[0].properties).toEqual({ + filter: 'geo.coordinates within 1096km of -89.87125,53.49454', + }); + }); + + it('should convert geo_shape filter to feature', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + type: 'Polygon', + }, + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in bounds', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-101.21639, 48.1413], + [-101.21639, 41.84905], + [-90.95149, 41.84905], + [-90.95149, 48.1413], + [-101.21639, 48.1413], + ], + ], + }, + properties: { + filter: 'geo.coordinates in bounds', + }, + }, + ]); + }); + + it('should ignore geo_shape filter with pre-index shape', () => { + const spatialFilter = { + geo_shape: { + 'geo.coordinates': { + indexed_shape: { + id: 's5gldXEBkTB2HMwpC8y0', + index: 'world_countries_v1', + path: 'coordinates', + }, + relation: 'INTERSECTS', + }, + ignore_unmapped: true, + }, + meta: { + alias: 'geo.coordinates in multipolygon', + disabled: false, + index: '90943e30-9a47-11e8-b64d-95841ca0b247', + key: 'geo.coordinates', + negate: false, + type: 'spatial_filter', + value: '', + }, + }; + + expect(extractFeaturesFromFilters([spatialFilter])).toEqual([]); + }); +}); diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index 7d3aa86a0fc56..bfbcca1eb3f61 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -51,7 +51,7 @@ function getDocValueAndSourceFields(indexPattern, fieldNames) { lang: field.lang, }, }; - } else if (field.type !== ES_GEO_FIELD_TYPE.GEO_SHAPE && field.readFromDocValues) { + } else if (field.readFromDocValues) { const docValueField = field.type === 'date' ? { diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts index 81622ea9581b0..fe21b37434edd 100644 --- a/x-pack/plugins/maps/public/reducers/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -11,5 +11,9 @@ export function getDefaultMapSettings(): MapSettings { return { maxZoom: MAX_ZOOM, minZoom: MIN_ZOOM, + showSpatialFilters: true, + spatialFiltersAlpa: 0.3, + spatialFiltersFillColor: '#DA8B45', + spatialFiltersLineColor: '#DA8B45', }; } diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index af2d96eb75562..be0700d4bdd6d 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -42,6 +42,10 @@ export type MapContext = { export type MapSettings = { maxZoom: number; minZoom: number; + showSpatialFilters: boolean; + spatialFiltersAlpa: number; + spatialFiltersFillColor: string; + spatialFiltersLineColor: string; }; export type MapState = { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index fed344b7744fe..4d0f652af982a 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -8,6 +8,7 @@ import { AnyAction } from 'redux'; import { MapCenter } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; import { MapSettings } from '../reducers/map'; +import { IVectorLayer } from '../layers/vector_layer'; export function getHiddenLayerIds(state: MapStoreState): string[]; @@ -20,3 +21,5 @@ export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[ export function getMapSettings(state: MapStoreState): MapSettings; export function hasMapSettingsChanges(state: MapStoreState): boolean; + +export function getSpatialFiltersLayer(state: MapStoreState): IVectorLayer; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index 20d1f7e08c910..f43c92d4c9945 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -6,27 +6,26 @@ import { createSelector } from 'reselect'; import _ from 'lodash'; - import { TileLayer } from '../layers/tile_layer'; - import { VectorTileLayer } from '../layers/vector_tile_layer'; - import { VectorLayer } from '../layers/vector_layer'; - import { HeatmapLayer } from '../layers/heatmap_layer'; - import { BlendedVectorLayer } from '../layers/blended_vector_layer'; - import { getTimeFilter } from '../kibana_services'; - import { getInspectorAdapters } from '../reducers/non_serializable_instances'; import { TiledVectorLayer } from '../layers/tiled_vector_layer'; - import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; - import { InnerJoin } from '../layers/joins/inner_join'; - import { getSourceByType } from '../layers/sources/source_registry'; +import { GeojsonFileSource } from '../layers/sources/client_file_source'; +import { + LAYER_TYPE, + SOURCE_DATA_ID_ORIGIN, + STYLE_TYPE, + VECTOR_STYLES, + SPATIAL_FILTERS_LAYER_ID, +} from '../../common/constants'; +import { extractFeaturesFromFilters } from '../elasticsearch_geo_utils'; function createLayerInstance(layerDescriptor, inspectorAdapters) { const source = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); @@ -195,6 +194,53 @@ export const getDataFilters = createSelector( } ); +export const getSpatialFiltersLayer = createSelector( + getFilters, + getMapSettings, + (filters, settings) => { + const featureCollection = { + type: 'FeatureCollection', + features: extractFeaturesFromFilters(filters), + }; + const geoJsonSourceDescriptor = GeojsonFileSource.createDescriptor( + featureCollection, + 'spatialFilters' + ); + + return new VectorLayer({ + layerDescriptor: { + id: SPATIAL_FILTERS_LAYER_ID, + visible: settings.showSpatialFilters, + alpha: settings.spatialFiltersAlpa, + type: LAYER_TYPE.VECTOR, + __dataRequests: [ + { + dataId: SOURCE_DATA_ID_ORIGIN, + data: featureCollection, + }, + ], + style: { + properties: { + [VECTOR_STYLES.FILL_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersFillColor, + }, + }, + [VECTOR_STYLES.LINE_COLOR]: { + type: STYLE_TYPE.STATIC, + options: { + color: settings.spatialFiltersLineColor, + }, + }, + }, + }, + }, + source: new GeojsonFileSource(geoJsonSourceDescriptor), + }); + } +); + export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 68463b2f44259..ba79ab6b9aa83 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -8301,7 +8301,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "ステータス", "xpack.ingestManager.agentDetails.typeLabel": "タイプ", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "この構成は利用できなくなりました", - "xpack.ingestManager.agentDetails.unenrollButtonText": "登録解除", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "エージェントを読み込む間にエラーが発生しました", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "ユーザー提供メタデータ", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "ご希望のエージェント構成とプラットフォームをすばやく選択できます。次いで、以下の手順に従ってエージェントをセットアップして登録します。", @@ -8333,8 +8332,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "アクション", "xpack.ingestManager.agentList.actionsMenuText": "開く", "xpack.ingestManager.agentList.addButton": "新しいエージェントをインストール", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "このページで {count, plural, one {# エージェント} other {# エージェント}}が選択されます。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "{count} エージェントすべてが選択されます。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "フィルターを消去", "xpack.ingestManager.agentList.configColumnTitle": "構成", "xpack.ingestManager.agentList.configFilterText": "構成", @@ -8346,15 +8343,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "エージェントが見つかりません。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "最新ではありません", "xpack.ingestManager.agentList.revisionNumber": "rev. {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "{count} エージェントすべてを選択", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "このページのみを選択", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "非アクティブエージェントを表示", "xpack.ingestManager.agentList.statusColumnTitle": "ステータス", "xpack.ingestManager.agentList.statusErrorFilterText": "エラー", "xpack.ingestManager.agentList.statusFilterText": "ステータス", "xpack.ingestManager.agentList.statusOfflineFilterText": "オフライン", "xpack.ingestManager.agentList.statusOnlineFilterText": "オンライン", - "xpack.ingestManager.agentList.unenrollButton": "{count, plural, one {# エージェント} other {# エージェント}} の登録を解除", "xpack.ingestManager.agentList.unenrollOneButton": "登録解除", "xpack.ingestManager.agentList.versionTitle": "バージョン", "xpack.ingestManager.agentList.viewActionText": "エージェントの表示", @@ -8502,10 +8496,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "{count, plural, one {# エージェント} other {# エージェント}}の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "エージェント「{id}」の登録を解除しますか?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "読み込み中...", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "{count} 件のエージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "エージェント「{id}」の登録解除エラー", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "エージェントの登録解除エラー", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "{count} 件のエージェントの登録を解除しました", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "エージェント「{id}」の登録を解除しました", "xpack.ingestManager.yamlConfig.instructionDescription": "この構成でエージェントを登録するには、ホストで次のコマンドをコピーして実行します。", "xpack.ingestManager.yamlConfig.instructionTittle": "フリートに登録", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 0964ab53b7fee..a8d785f47144c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -8304,7 +8304,6 @@ "xpack.ingestManager.agentDetails.statusLabel": "状态", "xpack.ingestManager.agentDetails.typeLabel": "类型", "xpack.ingestManager.agentDetails.unavailableConfigTooltipText": "此配置不再可用", - "xpack.ingestManager.agentDetails.unenrollButtonText": "取消注册", "xpack.ingestManager.agentDetails.unexceptedErrorTitle": "加载代理时发生错误", "xpack.ingestManager.agentDetails.userProvidedMetadataSectionSubtitle": "用户提供的元数据", "xpack.ingestManager.agentEnrollment.apiKeySelectionDescription": "快速选择所需的代理配置和平台。然后,根据下面的说明设置和注册代理。", @@ -8336,8 +8335,6 @@ "xpack.ingestManager.agentList.actionsColumnTitle": "操作", "xpack.ingestManager.agentList.actionsMenuText": "打开", "xpack.ingestManager.agentList.addButton": "安装新代理", - "xpack.ingestManager.agentList.agentsOnPageSelectedMessage": "已选择此页面上的 {count, plural, one {# 个代理} other {# 个代理}}。{selectAllLink}", - "xpack.ingestManager.agentList.allAgentsSelectedMessage": "已选择所有 {count} 个代理。{clearSelectionLink}", "xpack.ingestManager.agentList.clearFiltersLinkText": "清除筛选", "xpack.ingestManager.agentList.configColumnTitle": "配置", "xpack.ingestManager.agentList.configFilterText": "配置", @@ -8349,15 +8346,12 @@ "xpack.ingestManager.agentList.noFilteredAgentsPrompt": "未找到任何代理。{clearFiltersLink}", "xpack.ingestManager.agentList.outOfDateLabel": "过时", "xpack.ingestManager.agentList.revisionNumber": "修订 {revNumber}", - "xpack.ingestManager.agentList.selectAllAgentsLinkText": "选择所有 {count} 个代理", - "xpack.ingestManager.agentList.selectPageAgentsLinkText": "仅选择此页面", "xpack.ingestManager.agentList.showInactiveSwitchLabel": "显示非活动代理", "xpack.ingestManager.agentList.statusColumnTitle": "状态", "xpack.ingestManager.agentList.statusErrorFilterText": "错误", "xpack.ingestManager.agentList.statusFilterText": "状态", "xpack.ingestManager.agentList.statusOfflineFilterText": "脱机", "xpack.ingestManager.agentList.statusOnlineFilterText": "联机", - "xpack.ingestManager.agentList.unenrollButton": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}", "xpack.ingestManager.agentList.unenrollOneButton": "取消注册", "xpack.ingestManager.agentList.versionTitle": "版本", "xpack.ingestManager.agentList.viewActionText": "查看代理", @@ -8505,10 +8499,7 @@ "xpack.ingestManager.unenrollAgents.confirmModal.deleteMultipleTitle": "取消注册 {count, plural, one {# 个代理} other {# 个代理}}?", "xpack.ingestManager.unenrollAgents.confirmModal.deleteSingleTitle": "取消注册“{id}”?", "xpack.ingestManager.unenrollAgents.confirmModal.loadingButtonLabel": "正在加载……", - "xpack.ingestManager.unenrollAgents.failureMultipleNotificationTitle": "取消注册 {count} 个代理时出错", - "xpack.ingestManager.unenrollAgents.failureSingleNotificationTitle": "取消注册代理“{id}”时出错", "xpack.ingestManager.unenrollAgents.fatalErrorNotificationTitle": "取消注册代理时出错", - "xpack.ingestManager.unenrollAgents.successMultipleNotificationTitle": "已取消注册 {count} 个代理", "xpack.ingestManager.unenrollAgents.successSingleNotificationTitle": "已取消注册代理“{id}”", "xpack.ingestManager.yamlConfig.instructionDescription": "要将代理注册到此配置,请在您的主机上复制并运行以下命令。", "xpack.ingestManager.yamlConfig.instructionTittle": "注册到 fleet", diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index b484f1f5a8ed2..2acfca63995f1 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -61,50 +61,29 @@ export default function(providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); - it('should not allow both ids and kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - ids: ['agent:1'], - kuery: ['agents.id:1'], - }) - .expect(400); - }); - - it('should not allow no ids or kuery in the payload', async () => { - await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({}) - .expect(400); - }); - it('allow to unenroll using a list of ids', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); - expect(body.results).to.have.length(1); - expect(body.results[0].success).to.be(true); }); it('should invalidate related API keys', async () => { const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) + .post(`/api/ingest_manager/fleet/agents/agent1/unenroll`) .set('kbn-xsrf', 'xxx') .send({ ids: ['agent1'], }) .expect(200); - expect(body).to.have.keys('results', 'success'); + expect(body).to.have.keys('success'); expect(body.success).to.be(true); const { @@ -119,25 +98,5 @@ export default function(providerContext: FtrProviderContext) { expect(outputAPIKeys).length(1); expect(outputAPIKeys[0].invalidated).eql(true); }); - - it('allow to unenroll using a kibana query', async () => { - const { body } = await supertest - .post(`/api/ingest_manager/fleet/agents/unenroll`) - .set('kbn-xsrf', 'xxx') - .send({ - kuery: 'agents.shared_id:agent2_filebeat OR agents.shared_id:agent3_metricbeat', - }) - .expect(200); - - expect(body).to.have.keys('results', 'success'); - expect(body.success).to.be(true); - expect(body.results).to.have.length(2); - expect(body.results[0].success).to.be(true); - - const agentsUnenrolledIds = body.results.map((r: { id: string }) => r.id); - - expect(agentsUnenrolledIds).to.contain('agent2'); - expect(agentsUnenrolledIds).to.contain('agent3'); - }); }); } diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index dc8c488460100..76ca613af4b55 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -28,8 +28,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60535 - describe.skip('security', () => { + describe('security', () => { before(async () => { await esArchiver.load('discover/feature_controls/security'); await esArchiver.loadIfNeeded('logstash_functional'); diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index f33b8b4899d16..4bedc757f0b57 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -24,8 +24,7 @@ export default function({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); } - // FLAKY: https://github.com/elastic/kibana/issues/60559 - describe.skip('spaces', () => { + describe('spaces', () => { before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); });