diff --git a/.eslintrc.js b/.eslintrc.js index a2b8ae7622d0b..b3a1274d1cbeb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -96,7 +96,7 @@ module.exports = { }, }, { - files: ['x-pack/legacy/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], + files: ['x-pack/plugins/cross_cluster_replication/**/*.{js,ts,tsx}'], rules: { 'jsx-a11y/click-events-have-key-events': 'off', }, diff --git a/.sass-lint.yml b/.sass-lint.yml index 5c2c88a1dad5d..89735342a2d6f 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -9,6 +9,7 @@ files: - 'x-pack/legacy/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' - 'x-pack/plugins/lens/**/*.s+(a|c)ss' + - 'x-pack/plugins/cross_cluster_replication/**/*.s+(a|c)ss' - 'x-pack/legacy/plugins/maps/**/*.s+(a|c)ss' - 'x-pack/plugins/maps/**/*.s+(a|c)ss' ignore: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md index 3e966caa30799..25ce6eaa688f8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md @@ -8,6 +8,7 @@ ```typescript actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index a623e91388fd6..4f43f10ce089e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -14,7 +14,7 @@ export interface DataPublicPluginStart | Property | Type | Description | | --- | --- | --- | -| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromEvent: typeof createFiltersFromEvent;
} | | +| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction;
createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction;
} | | | [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | | | [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | | | [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md index 244633c3c4c9e..d39871b99f744 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormat: typeof FieldFormat; FieldFormatsRegistry: typeof FieldFormatsRegistry; - serialize: (agg: import("./search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md index 2b986aee508e2..11f18a195d271 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.fieldformats.md @@ -10,7 +10,7 @@ fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; diff --git a/docs/images/clone_panel.gif b/docs/images/clone_panel.gif new file mode 100644 index 0000000000000..e521e438d051a Binary files /dev/null and b/docs/images/clone_panel.gif differ diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index f62a4d28dfc0d..7081590931a99 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -217,6 +217,8 @@ might increase the search time. This setting is off by default. Users must opt-i [horizontal] `siem:defaultAnomalyScore`:: The threshold above which Machine Learning job anomalies are displayed in the SIEM app. `siem:defaultIndex`:: A comma-delimited list of Elasticsearch indices from which the SIEM app collects events. +`siem:ipReputationLinks`:: A JSON array containing links for verifying the reputation of an IP address. The links are displayed on +{siem-guide}/siem-ui-overview.html#network-ui[IP detail] pages. `siem:enableNewsFeed`:: Enables the security news feed on the SIEM *Overview* page. `siem:newsFeedUrl`:: The URL from which the security news feed content is diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 6528568e86890..b71e1c672756a 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -8,7 +8,6 @@ You do not need to configure any settings to use {kib} {ml-features}. They are enabled by default. -[float] [[general-ml-settings-kb]] ==== General {ml} settings @@ -19,3 +18,11 @@ If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, you can still use the {ml} APIs. To disable {ml} entirely, see the {ref}/ml-settings.html[{es} {ml} settings]. + +[[data-visualizer-settings]] +==== {data-viz} settings + +`xpack.ml.file_data_visualizer.max_file_size`:: +Sets the file size limit when importing data in the {data-viz}. The default +value is `100MB`. The highest supported value for this setting is `1GB`. + diff --git a/docs/siem/images/cases-ui.png b/docs/siem/images/cases-ui.png new file mode 100644 index 0000000000000..b513efb664740 Binary files /dev/null and b/docs/siem/images/cases-ui.png differ diff --git a/docs/siem/siem-ui.asciidoc b/docs/siem/siem-ui.asciidoc index 85253daaf2933..985138756622d 100644 --- a/docs/siem/siem-ui.asciidoc +++ b/docs/siem/siem-ui.asciidoc @@ -35,7 +35,7 @@ image::siem/images/network-ui.png[] [float] [[detections-ui]] -=== Detections (Beta) +=== Detections (beta) The Detections feature automatically searches for threats and creates signals when they are detected. Signal detection rules define the conditions @@ -50,6 +50,22 @@ or the Detections API. [role="screenshot"] image::siem/images/detections-ui.png[] +[float] +[[cases-ui]] +=== Cases (beta) + +Cases are used to open and track security issues directly in SIEM. +Cases list the original reporter and all users who contribute to a case +(`participants`). Case comments support Markdown syntax, and allow linking to +saved Timelines. Additionally, you can send cases to external systems from +within SIEM (currently ServiceNow). + +For information about opening, updating, and closing cases, see +{siem-guide}/cases-overview.html[Cases] in the SIEM Guide. + +[role="screenshot"] +image::siem/images/cases-ui.png[] + [float] [[timelines-ui]] === Timeline diff --git a/docs/user/dashboard.asciidoc b/docs/user/dashboard.asciidoc index ab529a533d5e3..de714ae40086b 100644 --- a/docs/user/dashboard.asciidoc +++ b/docs/user/dashboard.asciidoc @@ -98,6 +98,24 @@ to the new dimensions. * To delete a panel, open the panel menu and select *Delete from dashboard.* Deleting a panel from a dashboard does *not* delete the saved visualization or search. +[float] +[[cloning-a-panel]] +=== Clone dashboard elements + +In *Edit* mode, you can clone any panel on a dashboard. + +To clone an existing panel, open the panel menu of the element you wish to clone, then select *Clone panel*. + +* Cloned panels appear beside the original, and will move other panels down to make room if necessary. + +* Clones support all of the original panel's functionality, including renaming, editing, and cloning. + +* All cloned visualizations will appear in the visualization list. + +[role="screenshot"] +image:images/clone_panel.gif[clone panel] + + [float] [[viewing-detailed-information]] === Inspect and edit elements diff --git a/docs/user/ml/images/ml-data-visualizer-sample.jpg b/docs/user/ml/images/ml-data-visualizer-sample.jpg index 6c2e018932717..ce2bb660d7da1 100644 Binary files a/docs/user/ml/images/ml-data-visualizer-sample.jpg and b/docs/user/ml/images/ml-data-visualizer-sample.jpg differ diff --git a/docs/user/ml/index.asciidoc b/docs/user/ml/index.asciidoc index cca0dc5e4530f..e9ef4a55b2b3a 100644 --- a/docs/user/ml/index.asciidoc +++ b/docs/user/ml/index.asciidoc @@ -4,31 +4,31 @@ [partintro] -- -As datasets increase in size and complexity, the human effort required to +As data sets increase in size and complexity, the human effort required to inspect dashboards or maintain rules for spotting infrastructure problems, cyber attacks, or business issues becomes impractical. Elastic {ml-features} such as {anomaly-detect} and {oldetection} make it easier to notice suspicious activities with minimal human interference. -If you have a basic license, you can use the *Data Visualizer* to learn more -about your data. In particular, if your data is stored in {es} and contains a -time field, you can use the *Data Visualizer* to identify possible fields for -{anomaly-detect}: +{kib} includes a free *{data-viz}* to learn more about your data. In particular, +if your data is stored in {es} and contains a time field, you can use the +*{data-viz}* to identify possible fields for {anomaly-detect}: [role="screenshot"] -image::user/ml/images/ml-data-visualizer-sample.jpg[Data Visualizer for sample flight data] +image::user/ml/images/ml-data-visualizer-sample.jpg[{data-viz} for sample flight data] -experimental[] You can also upload a CSV, NDJSON, or log file (up to 100 MB in -size). The *Data Visualizer* identifies the file format and field mappings. You -can then optionally import that data into an {es} index. +experimental[] You can also upload a CSV, NDJSON, or log file. The *{data-viz}* +identifies the file format and field mappings. You can then optionally import +that data into an {es} index. To change the default file size limit, see +<>. -You need the following permissions to use the Data Visualizer with file upload: +You need the following permissions to use the {data-viz} with file upload: * cluster privileges: `monitor`, `manage_ingest_pipelines` * index privileges: `read`, `manage`, `index` For more information, see {ref}/security-privileges.html[Security privileges] -and {ref}/built-in-roles.html[Built-in roles]. +and {ml-docs}/setup.html[Set up {ml-features}]. -- diff --git a/docs/visualize/for-dashboard.asciidoc b/docs/visualize/for-dashboard.asciidoc index d6e39d35b7b23..400179e9ceae7 100644 --- a/docs/visualize/for-dashboard.asciidoc +++ b/docs/visualize/for-dashboard.asciidoc @@ -13,13 +13,29 @@ on a dashboard. You can add two types of interactive inputs: -* *Options list* - Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. +* *Options list* — Filters content based on one or more specified options. The dropdown menu is dynamically populated with the results of a terms aggregation. For example, use the options list on the sample flight dashboard when you want to filter the data by origin city and destination city. -* *Range slider* - Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. +* *Range slider* — Filters data within a specified range of numbers. The minimum and maximum values are dynamically populated with the results of a min and max aggregation. For example, use the range slider when you want to filter the sample flight dashboard by a specific average ticket price. [role="screenshot"] image::images/dashboard-controls.png[] +[float] +[[controls-options]] +==== Controls options + +Configure the settings that apply to the interactive inputs on a dashboard. + +. Click *Options*, then configure the following: + +* *Update {kib} filters on each change* — When selected, all interactive inputs create filters that refresh the dashboard. When unselected, {kib} filters are created only when you click *Apply changes*. + +* *Use time filter* — When selected, the aggregations that generate the options list and time range are connected to the <>. + +* *Pin filters to global state* — When selected, all filters created by interacting with the inputs are automatically pinned. + +. Click *Update*. + [float] [[markdown-widget]] === Markdown diff --git a/packages/kbn-storybook/storybook_config/webpack.config.js b/packages/kbn-storybook/storybook_config/webpack.config.js index 1531c1d22b01b..779d8a4153644 100644 --- a/packages/kbn-storybook/storybook_config/webpack.config.js +++ b/packages/kbn-storybook/storybook_config/webpack.config.js @@ -49,6 +49,13 @@ module.exports = async ({ config }) => { }, }); + config.module.rules.push({ + test: /\.(html|md|txt|tmpl)$/, + use: { + loader: 'raw-loader', + }, + }); + // Handle Typescript files config.module.rules.push({ test: /\.tsx?$/, diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index f0b21adf62ff7..9e098c06ba155 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -128,56 +128,6 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ renameFromRoot('optimize.lazyHost', 'optimize.watchHost'), renameFromRoot('optimize.lazyPrebuild', 'optimize.watchPrebuild'), renameFromRoot('optimize.lazyProxyTimeout', 'optimize.watchProxyTimeout'), - // Monitoring renames - // TODO: Remove these from here once the monitoring plugin is migrated to NP - renameFromRoot('xpack.monitoring.enabled', 'monitoring.enabled'), - renameFromRoot('xpack.monitoring.ui.enabled', 'monitoring.ui.enabled'), - renameFromRoot( - 'xpack.monitoring.kibana.collection.enabled', - 'monitoring.kibana.collection.enabled' - ), - renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), - renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), - renameFromRoot( - 'xpack.monitoring.show_license_expiration', - 'monitoring.ui.show_license_expiration' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.elasticsearch.enabled', - 'monitoring.ui.container.elasticsearch.enabled' - ), - renameFromRoot( - 'xpack.monitoring.ui.container.logstash.enabled', - 'monitoring.ui.container.logstash.enabled' - ), - renameFromRoot( - 'xpack.monitoring.tests.cloud_detector.enabled', - 'monitoring.tests.cloud_detector.enabled' - ), - renameFromRoot( - 'xpack.monitoring.kibana.collection.interval', - 'monitoring.kibana.collection.interval' - ), - renameFromRoot('xpack.monitoring.elasticsearch.hosts', 'monitoring.ui.elasticsearch.hosts'), - renameFromRoot('xpack.monitoring.elasticsearch.username', 'monitoring.ui.elasticsearch.username'), - renameFromRoot('xpack.monitoring.elasticsearch.password', 'monitoring.ui.elasticsearch.password'), - renameFromRoot( - 'xpack.monitoring.xpack_api_polling_frequency_millis', - 'monitoring.xpack_api_polling_frequency_millis' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.enabled', - 'monitoring.cluster_alerts.email_notifications.enabled' - ), - renameFromRoot( - 'xpack.monitoring.cluster_alerts.email_notifications.email_address', - 'monitoring.cluster_alerts.email_notifications.email_address' - ), - renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), - renameFromRoot( - 'xpack.monitoring.elasticsearch.logFetchCount', - 'monitoring.ui.elasticsearch.logFetchCount' - ), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index ef57fae159d7e..86192245bd2d1 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -230,6 +230,7 @@ export { SavedObjectsMigrationLogger, SavedObjectsRawDoc, SavedObjectSanitizedDoc, + SavedObjectUnsanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, SavedObjectsSchema, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7ca5c75f19e8f..6369720ada2c3 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1679,8 +1679,6 @@ export interface SavedObjectMigrationContext { log: SavedObjectsMigrationLogger; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectUnsanitizedDoc" needs to be exported by the entry point index.d.ts -// // @public export type SavedObjectMigrationFn = (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => SavedObjectUnsanitizedDoc; @@ -2314,6 +2312,9 @@ export class SavedObjectTypeRegistry { registerType(type: SavedObjectsType): void; } +// @public +export type SavedObjectUnsanitizedDoc = SavedObjectDoc & Partial; + // @public export type ScopeableRequest = KibanaRequest | LegacyRequest | FakeRequest; diff --git a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js index 52928d6e47fc4..8e8d69a4dfefa 100644 --- a/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js +++ b/src/dev/build/tasks/nodejs_modules/clean_client_modules_on_dll_task.js @@ -48,12 +48,12 @@ export const CleanClientModulesOnDLLTask = { ]; const discoveredLegacyCorePluginEntries = await globby([ `${baseDir}/src/legacy/core_plugins/*/index.js`, - // Small exception to load dynamically discovered functions for timelion plugin - `${baseDir}/src/legacy/core_plugins/timelion/server/*_functions/**/*.js`, `!${baseDir}/src/legacy/core_plugins/**/public`, ]); const discoveredPluginEntries = await globby([ `${baseDir}/src/plugins/*/server/index.js`, + // Small exception to load dynamically discovered functions for timelion plugin + `${baseDir}/src/plugins/vis_type_timelion/server/*_functions/**/*.js`, `!${baseDir}/src/plugins/**/public`, ]); const discoveredNewPlatformXpackPlugins = await globby([ diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 1b5110a61cbc4..a75a6997a8cb2 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -116,11 +116,6 @@ export const TEMPORARILY_IGNORED_PATHS = [ 'src/legacy/core_plugins/tile_map/public/__tests__/scaledCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedCircleMarkers.png', 'src/legacy/core_plugins/tile_map/public/__tests__/shadedGeohashGrid.png', - 'src/legacy/core_plugins/timelion/server/lib/asSorted.js', - 'src/legacy/core_plugins/timelion/server/lib/unzipPairs.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/bucketList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/seriesList.js', - 'src/legacy/core_plugins/timelion/server/series_functions/__tests__/fixtures/tlConfig.js', 'src/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data/terms/_seriesMultiple.js', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index 989583742acd0..6fc7c2f0a01ab 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -21,7 +21,6 @@ import Fs from 'fs'; import { resolve } from 'path'; import { promisify } from 'util'; -import { migrations } from './migrations'; import { importApi } from './server/routes/api/import'; import { exportApi } from './server/routes/api/export'; import mappings from './mappings.json'; @@ -120,23 +119,6 @@ export default function(kibana) { ], savedObjectsManagement: { - dashboard: { - icon: 'dashboardApp', - defaultSearchField: 'title', - isImportableAndExportable: true, - getTitle(obj) { - return obj.attributes.title; - }, - getEditUrl(obj) { - return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, - uiCapabilitiesPath: 'dashboard.show', - }; - }, - }, url: { defaultSearchField: 'url', isImportableAndExportable: true, @@ -177,8 +159,6 @@ export default function(kibana) { mappings, uiSettingDefaults: getUiSettingDefaults(), - - migrations, }, uiCapabilities: async function() { diff --git a/src/legacy/core_plugins/kibana/mappings.json b/src/legacy/core_plugins/kibana/mappings.json index af3f79588552b..d61810f755d63 100644 --- a/src/legacy/core_plugins/kibana/mappings.json +++ b/src/legacy/core_plugins/kibana/mappings.json @@ -1,58 +1,4 @@ { - "dashboard": { - "properties": { - "description": { - "type": "text" - }, - "hits": { - "type": "integer" - }, - "kibanaSavedObjectMeta": { - "properties": { - "searchSourceJSON": { - "type": "text" - } - } - }, - "optionsJSON": { - "type": "text" - }, - "panelsJSON": { - "type": "text" - }, - "refreshInterval": { - "properties": { - "display": { - "type": "keyword" - }, - "pause": { - "type": "boolean" - }, - "section": { - "type": "integer" - }, - "value": { - "type": "integer" - } - } - }, - "timeFrom": { - "type": "keyword" - }, - "timeRestore": { - "type": "boolean" - }, - "timeTo": { - "type": "keyword" - }, - "title": { - "type": "text" - }, - "version": { - "type": "integer" - } - } - }, "url": { "properties": { "accessCount": { diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.js b/src/legacy/core_plugins/kibana/migrations/migrations.js deleted file mode 100644 index 029dbde555a4b..0000000000000 --- a/src/legacy/core_plugins/kibana/migrations/migrations.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 { get } from 'lodash'; -import { - migrateMatchAllQuery, - migrations730 as dashboardMigrations730, -} from '../public/dashboard/migrations'; - -function migrateIndexPattern(doc) { - const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); - if (typeof searchSourceJSON !== 'string') { - return; - } - let searchSource; - try { - searchSource = JSON.parse(searchSourceJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return; - } - if (searchSource.index) { - searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; - doc.references.push({ - name: searchSource.indexRefName, - type: 'index-pattern', - id: searchSource.index, - }); - delete searchSource.index; - } - if (searchSource.filter) { - searchSource.filter.forEach((filterRow, i) => { - if (!filterRow.meta || !filterRow.meta.index) { - return; - } - filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; - doc.references.push({ - name: filterRow.meta.indexRefName, - type: 'index-pattern', - id: filterRow.meta.index, - }); - delete filterRow.meta.index; - }); - } - doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); -} - -export const migrations = { - dashboard: { - '6.7.2': migrateMatchAllQuery, - '7.0.0': doc => { - // Set new "references" attribute - doc.references = doc.references || []; - - // Migrate index pattern - migrateIndexPattern(doc); - // Migrate panels - const panelsJSON = get(doc, 'attributes.panelsJSON'); - if (typeof panelsJSON !== 'string') { - return doc; - } - let panels; - try { - panels = JSON.parse(panelsJSON); - } catch (e) { - // Let it go, the data is invalid and we'll leave it as is - return doc; - } - if (!Array.isArray(panels)) { - return doc; - } - panels.forEach((panel, i) => { - if (!panel.type || !panel.id) { - return; - } - panel.panelRefName = `panel_${i}`; - doc.references.push({ - name: `panel_${i}`, - type: panel.type, - id: panel.id, - }); - delete panel.type; - delete panel.id; - }); - doc.attributes.panelsJSON = JSON.stringify(panels); - return doc; - }, - '7.3.0': dashboardMigrations730, - }, -}; diff --git a/src/legacy/core_plugins/kibana/public/discover/plugin.ts b/src/legacy/core_plugins/kibana/public/discover/plugin.ts index 42883abe98171..702331529b879 100644 --- a/src/legacy/core_plugins/kibana/public/discover/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/discover/plugin.ts @@ -147,6 +147,9 @@ export class DiscoverPlugin implements Plugin { await this.initializeServices(); await this.initializeInnerAngular(); + // make sure the index pattern list is up to date + const [, { data: dataStart }] = await core.getStartServices(); + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./np_ready/application'); const unmount = await renderApp(innerAngularName, params.element); return () => { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts index ef3f664252856..26800f8a1620e 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts +++ b/src/legacy/core_plugins/vis_type_vislib/public/plugin.ts @@ -108,6 +108,6 @@ export class VisTypeVislibPlugin implements Plugin { public start(core: CoreStart, { data }: VisTypeVislibPluginStartDependencies) { setFormatService(data.fieldFormats); - setDataActions({ createFiltersFromEvent: data.actions.createFiltersFromEvent }); + setDataActions(data.actions); } } diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx index c378ae7b05b37..6bf66c2bdd788 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.test.tsx @@ -36,7 +36,9 @@ jest.mock('../../../legacy_imports', () => ({ })); jest.mock('../../../services', () => ({ - getDataActions: () => ({ createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']) }), + getDataActions: () => ({ + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + }), })); const vis = { diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx index 2fe16bbfeb625..7eb25e3930718 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx @@ -101,7 +101,7 @@ export class VisLegend extends PureComponent { return false; } - const filters = await getDataActions().createFiltersFromEvent(item.values); + const filters = await getDataActions().createFiltersFromValueClickAction({ data: item.values }); return Boolean(filters.length); }; diff --git a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js index ecf67ee3e017c..f33ce0395af1f 100644 --- a/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js +++ b/src/legacy/core_plugins/vis_type_vislib/public/vislib/lib/handler.js @@ -83,10 +83,21 @@ export class Handler { // memoize so that the same function is returned every time, // allowing us to remove/re-add the same function - this.getProxyHandler = _.memoize(function(event) { + this.getProxyHandler = _.memoize(function(eventType) { const self = this; - return function(e) { - self.vis.emit(event, e); + return function(eventPayload) { + switch (eventType) { + case 'brush': + const xRaw = _.get(eventPayload.data, 'series[0].values[0].xRaw'); + if (!xRaw) return; // not sure if this is possible? + return self.vis.emit(eventType, { + table: xRaw.table, + range: eventPayload.range, + column: xRaw.column, + }); + case 'click': + return self.vis.emit(eventType, eventPayload); + } }; }); diff --git a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js index f14f26613ef01..271586bb8c582 100644 --- a/src/legacy/ui/public/new_platform/new_platform.karma_mock.js +++ b/src/legacy/ui/public/new_platform/new_platform.karma_mock.js @@ -377,7 +377,8 @@ export const npStart = { }, data: { actions: { - createFiltersFromEvent: Promise.resolve(['yes']), + createFiltersFromValueClickAction: Promise.resolve(['yes']), + createFiltersFromRangeSelectAction: sinon.fake(), }, autocomplete: { getProvider: sinon.fake(), diff --git a/src/plugins/dashboard/public/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts similarity index 93% rename from src/plugins/dashboard/public/bwc/types.ts rename to src/plugins/dashboard/common/bwc/types.ts index d5655e525e9bd..2427799345463 100644 --- a/src/plugins/dashboard/public/bwc/types.ts +++ b/src/plugins/dashboard/common/bwc/types.ts @@ -18,33 +18,28 @@ */ import { SavedObjectReference } from 'kibana/public'; -import { GridData } from '../application'; -export interface SavedObjectAttributes { +import { GridData } from '../'; + +interface SavedObjectAttributes { kibanaSavedObjectMeta: { searchSourceJSON: string; }; } -export interface Doc { +interface Doc { references: SavedObjectReference[]; attributes: Attributes; id: string; type: string; } -export interface DocPre700 { +interface DocPre700 { attributes: Attributes; id: string; type: string; } -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} - interface DashboardAttributes extends SavedObjectAttributes { panelsJSON: string; description: string; @@ -55,8 +50,6 @@ interface DashboardAttributes extends SavedObjectAttributes { optionsJSON?: string; } -export type DashboardAttributes730ToLatest = DashboardAttributes; - interface DashboardAttributesTo720 extends SavedObjectAttributes { panelsJSON: string; description: string; diff --git a/src/legacy/core_plugins/kibana/migrations/index.ts b/src/plugins/dashboard/common/embeddable/types.ts similarity index 89% rename from src/legacy/core_plugins/kibana/migrations/index.ts rename to src/plugins/dashboard/common/embeddable/types.ts index 68c843d2343c8..eb76d73af7a58 100644 --- a/src/legacy/core_plugins/kibana/migrations/index.ts +++ b/src/plugins/dashboard/common/embeddable/types.ts @@ -17,5 +17,10 @@ * under the License. */ -// @ts-ignore -export { migrations } from './migrations'; +export interface GridData { + w: number; + h: number; + x: number; + y: number; + i: string; +} diff --git a/src/plugins/dashboard/common/index.ts b/src/plugins/dashboard/common/index.ts new file mode 100644 index 0000000000000..e3f3f629ae5d0 --- /dev/null +++ b/src/plugins/dashboard/common/index.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +export { GridData } from './embeddable/types'; +export { + RawSavedDashboardPanel730ToLatest, + DashboardDoc730ToLatest, + DashboardDoc700To720, + DashboardDocPre700, +} from './bwc/types'; +export { + SavedDashboardPanelTo60, + SavedDashboardPanel610, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel640To720, + SavedDashboardPanel730ToLatest, +} from './types'; + +export { migratePanelsTo730 } from './migrate_to_730_panels'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.test.ts index 4dd71fd8ee5f4..0867909225ddb 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.test.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.test.ts @@ -19,15 +19,12 @@ import { migratePanelsTo730 } from './migrate_to_730_panels'; import { RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, - DEFAULT_PANEL_WIDTH, - DEFAULT_PANEL_HEIGHT, - SavedDashboardPanelTo60, - SavedDashboardPanel730ToLatest, -} from '../../../../../../plugins/dashboard/public'; + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, +} from './bwc/types'; +import { SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest } from './types'; test('6.0 migrates uiState, sort, scales, and gridData', async () => { const uiState = { @@ -96,8 +93,8 @@ test('6.0 migration gives default width and height when missing', () => { }, ]; const newPanels = migratePanelsTo730(panels, '8.0.0', true); - expect(newPanels[0].gridData.w).toBe(DEFAULT_PANEL_WIDTH); - expect(newPanels[0].gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(newPanels[0].gridData.w).toBe(24); + expect(newPanels[0].gridData.h).toBe(15); expect(newPanels[0].version).toBe('8.0.0'); }); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts b/src/plugins/dashboard/common/migrate_to_730_panels.ts similarity index 97% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts rename to src/plugins/dashboard/common/migrate_to_730_panels.ts index a19c861f092d5..b89345f0a872c 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.ts @@ -21,17 +21,19 @@ import semver from 'semver'; import uuid from 'uuid'; import { GridData, + SavedDashboardPanelTo60, + SavedDashboardPanel620, + SavedDashboardPanel630, + SavedDashboardPanel610, +} from './'; +import { RawSavedDashboardPanelTo60, RawSavedDashboardPanel630, RawSavedDashboardPanel640To720, RawSavedDashboardPanel730ToLatest, RawSavedDashboardPanel610, RawSavedDashboardPanel620, - SavedDashboardPanelTo60, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel610, -} from '../../../../../../plugins/dashboard/public'; +} from './bwc/types'; const PANEL_HEIGHT_SCALE_FACTOR = 5; const PANEL_HEIGHT_SCALE_FACTOR_WITH_MARGINS = 4; @@ -92,7 +94,7 @@ function migratePre61PanelToLatest( ): RawSavedDashboardPanel730ToLatest { if (panel.col === undefined || panel.row === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.1.0" backwards compatibility, panel does not contain expected col and/or row fields', }) @@ -151,7 +153,7 @@ function migrate610PanelToLatest( (['w', 'x', 'h', 'y'] as Array).forEach(key => { if (panel.gridData[key] === undefined) { throw new Error( - i18n.translate('kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { + i18n.translate('dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage', { defaultMessage: 'Unable to migrate panel data for "6.3.0" backwards compatibility, panel does not contain expected field: {key}', values: { key }, diff --git a/src/plugins/dashboard/common/types.ts b/src/plugins/dashboard/common/types.ts new file mode 100644 index 0000000000000..7cc82a9173976 --- /dev/null +++ b/src/plugins/dashboard/common/types.ts @@ -0,0 +1,76 @@ +/* + * 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 { + RawSavedDashboardPanelTo60, + RawSavedDashboardPanel610, + RawSavedDashboardPanel620, + RawSavedDashboardPanel630, + RawSavedDashboardPanel640To720, + RawSavedDashboardPanel730ToLatest, +} from './bwc/types'; + +export type SavedDashboardPanel640To720 = Pick< + RawSavedDashboardPanel640To720, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel630 = Pick< + RawSavedDashboardPanel630, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel620 = Pick< + RawSavedDashboardPanel620, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanel610 = Pick< + RawSavedDashboardPanel610, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +export type SavedDashboardPanelTo60 = Pick< + RawSavedDashboardPanelTo60, + Exclude +> & { + readonly id: string; + readonly type: string; +}; + +// id becomes optional starting in 7.3.0 +export type SavedDashboardPanel730ToLatest = Pick< + RawSavedDashboardPanel730ToLatest, + Exclude +> & { + readonly id?: string; + readonly type: string; +}; diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 9bcd999c2dcc0..4cd8f3c7d981f 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -11,6 +11,6 @@ "savedObjects" ], "optionalPlugins": ["home", "share", "usageCollection"], - "server": false, + "server": true, "ui": true } diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 3134a5bfe2c67..a1696298117b0 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -38,12 +38,7 @@ import { EmbeddableStart } from '../../../embeddable/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../navigation/public'; import { DataPublicPluginStart } from '../../../data/public'; import { SharePluginStart } from '../../../share/public'; -import { - KibanaLegacyStart, - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, -} from '../../../kibana_legacy/public'; +import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public'; import { SavedObjectLoader } from '../../../saved_objects/public'; export interface RenderDeps { @@ -114,13 +109,11 @@ function mountDashboardApp(appBasePath: string, element: HTMLElement) { function createLocalAngularModule(core: AppMountContext['core'], navigation: NavigationStart) { createLocalI18nModule(); - createLocalTopNavModule(navigation); createLocalIconModule(); const dashboardAngularModule = angular.module(moduleName, [ ...thirdPartyAngularDependencies, 'app/dashboard/I18n', - 'app/dashboard/TopNav', 'app/dashboard/icon', ]); return dashboardAngularModule; @@ -132,13 +125,6 @@ function createLocalIconModule() { .directive('icon', reactDirective => reactDirective(EuiIcon)); } -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('app/dashboard/TopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - function createLocalI18nModule() { angular .module('app/dashboard/I18n', []) diff --git a/src/plugins/dashboard/public/application/dashboard_app.html b/src/plugins/dashboard/public/application/dashboard_app.html index 3cf8932958b6d..87a5728ac2059 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.html +++ b/src/plugins/dashboard/public/application/dashboard_app.html @@ -2,52 +2,7 @@ class="app-container dshAppContainer" ng-class="{'dshAppContainer--withMargins': model.useMargins}" > - - - - - - - - +

{{screenTitle}}

diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index 150cd8f8fcbb5..f101935b9288d 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -33,7 +33,6 @@ import { SavedObjectDashboard } from '../saved_dashboards'; export interface DashboardAppScope extends ng.IScope { dash: SavedObjectDashboard; appState: DashboardAppState; - screenTitle: string; model: { query: Query; filters: Filter[]; @@ -54,21 +53,7 @@ export interface DashboardAppScope extends ng.IScope { getShouldShowEditHelp: () => boolean; getShouldShowViewHelp: () => boolean; updateQueryAndFetch: ({ query, dateRange }: { query: Query; dateRange?: TimeRange }) => void; - onRefreshChange: ({ - isPaused, - refreshInterval, - }: { - isPaused: boolean; - refreshInterval: any; - }) => void; - onFiltersUpdated: (filters: Filter[]) => void; - onCancelApplyFilters: () => void; - onApplyFilters: (filters: Filter[]) => void; - onQuerySaved: (savedQuery: SavedQuery) => void; - onSavedQueryUpdated: (savedQuery: SavedQuery) => void; - onClearSavedQuery: () => void; topNavMenu: any; - showFilterBar: () => boolean; showAddPanel: any; showSaveQuery: boolean; kbnTopNav: any; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 283fe9f0a83a4..fa2f06bfcdcdd 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -21,12 +21,15 @@ import _, { uniq } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EUI_MODAL_CANCEL_BUTTON } from '@elastic/eui'; import React from 'react'; +import ReactDOM from 'react-dom'; import angular from 'angular'; import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { History } from 'history'; import { SavedObjectSaveOpts } from 'src/plugins/saved_objects/public'; +import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navigation/public'; +import { TimeRange } from 'src/plugins/data/public'; import { DashboardEmptyScreen, DashboardEmptyScreenProps } from './dashboard_empty_screen'; import { @@ -87,6 +90,7 @@ export interface DashboardAppControllerDependencies extends RenderDeps { dashboardConfig: KibanaLegacyStart['dashboardConfig']; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; + navigation: NavigationStart; } export class DashboardAppController { @@ -123,10 +127,13 @@ export class DashboardAppController { history, kbnUrlStateStorage, usageCollection, + navigation, }: DashboardAppControllerDependencies) { const filterManager = queryService.filterManager; const queryFilter = filterManager; const timefilter = queryService.timefilter.timefilter; + let showSearchBar = true; + let showQueryBar = true; let lastReloadRequestTime = 0; const dash = ($scope.dash = $route.current.locals.dash); @@ -243,6 +250,9 @@ export class DashboardAppController { } }; + const showFilterBar = () => + $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); + const getEmptyScreenProps = ( shouldShowEditHelp: boolean, isEmptyInReadOnlyMode: boolean @@ -310,7 +320,6 @@ export class DashboardAppController { refreshInterval: timefilter.getRefreshInterval(), }; $scope.panels = dashboardStateManager.getPanels(); - $scope.screenTitle = dashboardStateManager.getTitle(); }; updateState(); @@ -515,49 +524,8 @@ export class DashboardAppController { } }; - $scope.onRefreshChange = function({ isPaused, refreshInterval }) { - timefilter.setRefreshInterval({ - pause: isPaused, - value: refreshInterval ? refreshInterval : $scope.model.refreshInterval.value, - }); - }; - - $scope.onFiltersUpdated = filters => { - // The filters will automatically be set when the queryFilter emits an update event (see below) - queryFilter.setFilters(filters); - }; - - $scope.onQuerySaved = savedQuery => { - $scope.savedQuery = savedQuery; - }; - - $scope.onSavedQueryUpdated = savedQuery => { - $scope.savedQuery = { ...savedQuery }; - }; - - $scope.onClearSavedQuery = () => { - delete $scope.savedQuery; - dashboardStateManager.setSavedQueryId(undefined); - dashboardStateManager.applyFilters( - { - query: '', - language: - localStorage.get('kibana.userQueryLanguage') || uiSettings.get('search:queryLanguage'), - }, - queryFilter.getGlobalFilters() - ); - // Making this method sync broke the updates. - // Temporary fix, until we fix the complex state in this file. - setTimeout(() => { - queryFilter.setFilters(queryFilter.getGlobalFilters()); - }, 0); - }; - const updateStateFromSavedQuery = (savedQuery: SavedQuery) => { - const savedQueryFilters = savedQuery.attributes.filters || []; - const globalFilters = queryFilter.getGlobalFilters(); - const allFilters = [...globalFilters, ...savedQueryFilters]; - + const allFilters = filterManager.getFilters(); dashboardStateManager.applyFilters(savedQuery.attributes.query, allFilters); if (savedQuery.attributes.timefilter) { timefilter.setTime({ @@ -616,6 +584,48 @@ export class DashboardAppController { } ); + const onSavedQueryIdChange = (savedQueryId?: string) => { + dashboardStateManager.setSavedQueryId(savedQueryId); + }; + + const getNavBarProps = () => { + const isFullScreenMode = dashboardStateManager.getFullScreenMode(); + const screenTitle = dashboardStateManager.getTitle(); + return { + appName: 'dashboard', + config: $scope.isVisible ? $scope.topNavMenu : undefined, + className: isFullScreenMode ? 'kbnTopNavMenu-isFullScreen' : undefined, + screenTitle, + showSearchBar, + showQueryBar, + showFilterBar: showFilterBar(), + indexPatterns: $scope.indexPatterns, + showSaveQuery: $scope.showSaveQuery, + query: $scope.model.query, + savedQuery: $scope.savedQuery, + onSavedQueryIdChange, + savedQueryId: dashboardStateManager.getSavedQueryId(), + useDefaultBehaviors: true, + onQuerySubmit: (payload: { dateRange: TimeRange; query?: Query }): void => { + if (!payload.query) { + $scope.updateQueryAndFetch({ query: $scope.model.query, dateRange: payload.dateRange }); + } else { + $scope.updateQueryAndFetch({ query: payload.query, dateRange: payload.dateRange }); + } + }, + }; + }; + const dashboardNavBar = document.getElementById('dashboardChrome'); + const updateNavBar = () => { + ReactDOM.render(, dashboardNavBar); + }; + + const unmountNavBar = () => { + if (dashboardNavBar) { + ReactDOM.unmountComponentAtNode(dashboardNavBar); + } + }; + $scope.timefilterSubscriptions$ = new Subscription(); $scope.timefilterSubscriptions$.add( @@ -707,6 +717,8 @@ export class DashboardAppController { revertChangesAndExitEditMode(); } }); + + updateNavBar(); }; /** @@ -761,9 +773,6 @@ export class DashboardAppController { }); } - $scope.showFilterBar = () => - $scope.model.filters.length > 0 || !dashboardStateManager.getFullScreenMode(); - $scope.showAddPanel = () => { dashboardStateManager.setFullScreenMode(false); /* @@ -785,7 +794,11 @@ export class DashboardAppController { const navActions: { [key: string]: NavAction; } = {}; - navActions[TopNavIds.FULL_SCREEN] = () => dashboardStateManager.setFullScreenMode(true); + navActions[TopNavIds.FULL_SCREEN] = () => { + dashboardStateManager.setFullScreenMode(true); + showQueryBar = false; + updateNavBar(); + }; navActions[TopNavIds.EXIT_EDIT_MODE] = () => onChangeViewMode(ViewMode.VIEW); navActions[TopNavIds.ENTER_EDIT_MODE] = () => onChangeViewMode(ViewMode.EDIT); navActions[TopNavIds.SAVE] = () => { @@ -858,6 +871,7 @@ export class DashboardAppController { if ((response as { error: Error }).error) { dashboardStateManager.setTitle(currentTitle); } + updateNavBar(); return response; }); }; @@ -939,6 +953,9 @@ export class DashboardAppController { const visibleSubscription = chrome.getIsVisible$().subscribe(isVisible => { $scope.$evalAsync(() => { $scope.isVisible = isVisible; + showSearchBar = isVisible || showFilterBar(); + showQueryBar = !dashboardStateManager.getFullScreenMode() && isVisible; + updateNavBar(); }); }); @@ -949,9 +966,17 @@ export class DashboardAppController { navActions, dashboardConfig.getHideWriteControls() ); + updateNavBar(); + }); + + $scope.$watch('indexPatterns', () => { + updateNavBar(); }); $scope.$on('$destroy', () => { + // we have to unmount nav bar manually to make sure all internal subscriptions are unsubscribed + unmountNavBar(); + updateSubscription.unsubscribe(); stopSyncingQueryServiceStateWithUrl(); stopSyncingAppFilters(); diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx index b15a813aff903..fb33649093c8d 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.tsx @@ -29,9 +29,10 @@ import _ from 'lodash'; import React from 'react'; import { Subscription } from 'rxjs'; import ReactGridLayout, { Layout } from 'react-grid-layout'; +import { GridData } from '../../../../common'; import { ViewMode, EmbeddableChildPanel } from '../../../embeddable_plugin'; import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../dashboard_constants'; -import { DashboardPanelState, GridData } from '../types'; +import { DashboardPanelState } from '../types'; import { withKibana } from '../../../../../kibana_react/public'; import { DashboardContainerInput } from '../dashboard_container'; import { DashboardContainer, DashboardReactContextValue } from '../dashboard_container'; diff --git a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts index 70a6c83418587..b95b7f394a27d 100644 --- a/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/application/embeddable/panel/dashboard_panel_placement.ts @@ -18,7 +18,8 @@ */ import { PanelNotFoundError } from '../../../embeddable_plugin'; -import { DashboardPanelState, GridData, DASHBOARD_GRID_COLUMN_COUNT } from '..'; +import { GridData } from '../../../../common'; +import { DashboardPanelState, DASHBOARD_GRID_COLUMN_COUNT } from '..'; export type PanelPlacementMethod = ( args: PlacementArgs diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts index 30a93989649a7..b3ce2f1e57d5f 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts @@ -33,6 +33,10 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition return false; } + public canCreateNew() { + return false; + } + public async create(initialInput: EmbeddableInput, parent?: IContainer) { return new PlaceholderEmbeddable(initialInput, parent); } diff --git a/src/plugins/dashboard/public/application/embeddable/types.ts b/src/plugins/dashboard/public/application/embeddable/types.ts index 6d0221cb10e8b..66cdd22ed6bd4 100644 --- a/src/plugins/dashboard/public/application/embeddable/types.ts +++ b/src/plugins/dashboard/public/application/embeddable/types.ts @@ -17,18 +17,11 @@ * under the License. */ import { SavedObjectEmbeddableInput } from 'src/plugins/embeddable/public'; +import { GridData } from '../../../common'; import { PanelState, EmbeddableInput } from '../../embeddable_plugin'; export type PanelId = string; export type SavedObjectId = string; -export interface GridData { - w: number; - h: number; - x: number; - y: number; - i: string; -} - export interface DashboardPanelState< TEmbeddableInput extends EmbeddableInput | SavedObjectEmbeddableInput = SavedObjectEmbeddableInput > extends PanelState { diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts index 8f8de3663518a..f4d97578adebf 100644 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts +++ b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts @@ -22,18 +22,16 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { DashboardAppState, SavedDashboardPanel } from '../../types'; import { - DashboardAppState, + migratePanelsTo730, SavedDashboardPanelTo60, SavedDashboardPanel730ToLatest, SavedDashboardPanel610, SavedDashboardPanel630, SavedDashboardPanel640To720, SavedDashboardPanel620, - SavedDashboardPanel, -} from '../../types'; -// should be moved in src/plugins/dashboard/common right after https://github.com/elastic/kibana/pull/61895 is merged -import { migratePanelsTo730 } from '../../../../../legacy/core_plugins/kibana/public/dashboard/migrations/migrate_to_730_panels'; +} from '../../../common'; /** * Attempts to migrate the state stored in the URL into the latest version of it. diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index ca0ea0293b07c..44733499cdcba 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -20,29 +20,6 @@ import { PluginInitializerContext } from '../../../core/public'; import { DashboardPlugin } from './plugin'; -/** - * These types can probably be internal once all of dashboard app is migrated into this plugin. Right - * now, migrations are still in legacy land. - */ -export { - DashboardDoc730ToLatest, - DashboardDoc700To720, - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from './bwc'; -export { - SavedDashboardPanelTo60, - SavedDashboardPanel610, - SavedDashboardPanel620, - SavedDashboardPanel630, - SavedDashboardPanel730ToLatest, -} from './types'; - export { DashboardContainer, DashboardContainerInput, @@ -51,7 +28,6 @@ export { // Types below here can likely be made private when dashboard app moved into this NP plugin. DEFAULT_PANEL_WIDTH, DEFAULT_PANEL_HEIGHT, - GridData, } from './application'; export { DashboardConstants, createDashboardEditUrl } from './dashboard_constants'; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 203c784d9df4e..5f6b67ee6ad20 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -251,6 +251,8 @@ export class DashboardPlugin localStorage: new Storage(localStorage), usageCollection, }; + // make sure the index pattern list is up to date + await dataStart.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/src/plugins/dashboard/public/types.ts b/src/plugins/dashboard/public/types.ts index d96d2cdf75626..21c6bbc1bfc51 100644 --- a/src/plugins/dashboard/public/types.ts +++ b/src/plugins/dashboard/public/types.ts @@ -19,14 +19,7 @@ import { Query, Filter } from 'src/plugins/data/public'; import { SavedObject as SavedObjectType, SavedObjectAttributes } from 'src/core/public'; -import { - RawSavedDashboardPanelTo60, - RawSavedDashboardPanel610, - RawSavedDashboardPanel620, - RawSavedDashboardPanel630, - RawSavedDashboardPanel640To720, - RawSavedDashboardPanel730ToLatest, -} from './bwc'; +import { SavedDashboardPanel730ToLatest } from '../common'; import { ViewMode } from './embeddable_plugin'; export interface DashboardCapabilities { @@ -83,55 +76,6 @@ export type NavAction = (anchorElement?: any) => void; */ export type SavedDashboardPanel = SavedDashboardPanel730ToLatest; -// id becomes optional starting in 7.3.0 -export type SavedDashboardPanel730ToLatest = Pick< - RawSavedDashboardPanel730ToLatest, - Exclude -> & { - readonly id?: string; - readonly type: string; -}; - -export type SavedDashboardPanel640To720 = Pick< - RawSavedDashboardPanel640To720, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel630 = Pick< - RawSavedDashboardPanel630, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel620 = Pick< - RawSavedDashboardPanel620, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanel610 = Pick< - RawSavedDashboardPanel610, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - -export type SavedDashboardPanelTo60 = Pick< - RawSavedDashboardPanelTo60, - Exclude -> & { - readonly id: string; - readonly type: string; -}; - export interface DashboardAppState { panels: SavedDashboardPanel[]; fullScreenMode: boolean; diff --git a/src/legacy/core_plugins/kibana/migrations/is_doc.ts b/src/plugins/dashboard/server/index.ts similarity index 66% rename from src/legacy/core_plugins/kibana/migrations/is_doc.ts rename to src/plugins/dashboard/server/index.ts index cc50dfa3b2d26..9719586001c59 100644 --- a/src/legacy/core_plugins/kibana/migrations/is_doc.ts +++ b/src/plugins/dashboard/server/index.ts @@ -17,15 +17,14 @@ * under the License. */ -import { Doc } from './types'; +import { PluginInitializerContext } from '../../../core/server'; +import { DashboardPlugin } from './plugin'; -export function isDoc(doc: { [key: string]: unknown } | Doc): doc is Doc { - return ( - typeof doc.id === 'string' && - typeof doc.type === 'string' && - doc.attributes !== null && - typeof doc.attributes === 'object' && - doc.references !== null && - typeof doc.references === 'object' - ); +// This exports static code and TypeScript types, +// as well as, Kibana Platform `plugin()` initializer. + +export function plugin(initializerContext: PluginInitializerContext) { + return new DashboardPlugin(initializerContext); } + +export { DashboardPluginSetup, DashboardPluginStart } from './types'; diff --git a/src/legacy/core_plugins/kibana/migrations/types.ts b/src/plugins/dashboard/server/plugin.ts similarity index 51% rename from src/legacy/core_plugins/kibana/migrations/types.ts rename to src/plugins/dashboard/server/plugin.ts index 839f753670b20..5d1b66002e749 100644 --- a/src/legacy/core_plugins/kibana/migrations/types.ts +++ b/src/plugins/dashboard/server/plugin.ts @@ -17,24 +17,37 @@ * under the License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectReference } from '../../../../core/server'; +import { + PluginInitializerContext, + CoreSetup, + CoreStart, + Plugin, + Logger, +} from '../../../core/server'; -export interface SavedObjectAttributes { - kibanaSavedObjectMeta: { - searchSourceJSON: string; - }; -} +import { dashboardSavedObjectType } from './saved_objects'; -export interface Doc { - references: SavedObjectReference[]; - attributes: Attributes; - id: string; - type: string; -} +import { DashboardPluginSetup, DashboardPluginStart } from './types'; + +export class DashboardPlugin implements Plugin { + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup(core: CoreSetup) { + this.logger.debug('dashboard: Setup'); + + core.savedObjects.registerType(dashboardSavedObjectType); + + return {}; + } + + public start(core: CoreStart) { + this.logger.debug('dashboard: Started'); + return {}; + } -export interface DocPre700 { - attributes: Attributes; - id: string; - type: string; + public stop() {} } diff --git a/src/plugins/dashboard/server/saved_objects/dashboard.ts b/src/plugins/dashboard/server/saved_objects/dashboard.ts new file mode 100644 index 0000000000000..65d5a4021f962 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard.ts @@ -0,0 +1,67 @@ +/* + * 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 { SavedObjectsType } from 'kibana/server'; +import { dashboardSavedObjectTypeMigrations } from './dashboard_migrations'; + +export const dashboardSavedObjectType: SavedObjectsType = { + name: 'dashboard', + hidden: false, + namespaceType: 'single', + management: { + icon: 'dashboardApp', + defaultSearchField: 'title', + importableAndExportable: true, + getTitle(obj) { + return obj.attributes.title; + }, + getEditUrl(obj) { + return `/management/kibana/objects/savedDashboards/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: `/app/kibana#/dashboard/${encodeURIComponent(obj.id)}`, + uiCapabilitiesPath: 'dashboard.show', + }; + }, + }, + mappings: { + properties: { + description: { type: 'text' }, + hits: { type: 'integer' }, + kibanaSavedObjectMeta: { properties: { searchSourceJSON: { type: 'text' } } }, + optionsJSON: { type: 'text' }, + panelsJSON: { type: 'text' }, + refreshInterval: { + properties: { + display: { type: 'keyword' }, + pause: { type: 'boolean' }, + section: { type: 'integer' }, + value: { type: 'integer' }, + }, + }, + timeFrom: { type: 'keyword' }, + timeRestore: { type: 'boolean' }, + timeTo: { type: 'keyword' }, + title: { type: 'text' }, + version: { type: 'integer' }, + }, + }, + migrations: dashboardSavedObjectTypeMigrations, +}; diff --git a/src/legacy/core_plugins/kibana/migrations/migrations.test.js b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts similarity index 95% rename from src/legacy/core_plugins/kibana/migrations/migrations.test.js rename to src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts index b02081128c858..9829498118cc0 100644 --- a/src/legacy/core_plugins/kibana/migrations/migrations.test.js +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.test.ts @@ -17,14 +17,15 @@ * under the License. */ -import { migrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; describe('dashboard', () => { describe('7.0.0', () => { - const migration = migrations.dashboard['7.0.0']; + const migration = migrations['7.0.0']; test('skips error on empty object', () => { - expect(migration({})).toMatchInlineSnapshot(` + expect(migration({} as SavedObjectUnsanitizedDoc)).toMatchInlineSnapshot(` Object { "references": Array [], } @@ -329,7 +330,7 @@ Object { attributes: { panelsJSON: 123, }, - }; + } as SavedObjectUnsanitizedDoc; expect(migration(doc)).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -347,7 +348,7 @@ Object { attributes: { panelsJSON: '{123abc}', }, - }; + } as SavedObjectUnsanitizedDoc; expect(migration(doc)).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -365,7 +366,7 @@ Object { attributes: { panelsJSON: '{}', }, - }; + } as SavedObjectUnsanitizedDoc; expect(migration(doc)).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -383,7 +384,7 @@ Object { attributes: { panelsJSON: '[{"id":"123"}]', }, - }; + } as SavedObjectUnsanitizedDoc; expect(migration(doc)).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -401,7 +402,7 @@ Object { attributes: { panelsJSON: '[{"type":"visualization"}]', }, - }; + } as SavedObjectUnsanitizedDoc; expect(migration(doc)).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -420,7 +421,7 @@ Object { panelsJSON: '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, - }; + } as SavedObjectUnsanitizedDoc; const migratedDoc = migration(doc); expect(migratedDoc).toMatchInlineSnapshot(` Object { diff --git a/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts new file mode 100644 index 0000000000000..7c1d0568cd3d7 --- /dev/null +++ b/src/plugins/dashboard/server/saved_objects/dashboard_migrations.ts @@ -0,0 +1,117 @@ +/* + * 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 { get, flow } from 'lodash'; + +import { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrations730 } from './migrations_730'; +import { migrateMatchAllQuery } from './migrate_match_all_query'; +import { DashboardDoc700To720 } from '../../common'; + +function migrateIndexPattern(doc: DashboardDoc700To720) { + const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); + if (typeof searchSourceJSON !== 'string') { + return; + } + let searchSource; + try { + searchSource = JSON.parse(searchSourceJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return; + } + if (searchSource.index) { + searchSource.indexRefName = 'kibanaSavedObjectMeta.searchSourceJSON.index'; + doc.references.push({ + name: searchSource.indexRefName, + type: 'index-pattern', + id: searchSource.index, + }); + delete searchSource.index; + } + if (searchSource.filter) { + searchSource.filter.forEach((filterRow: any, i: number) => { + if (!filterRow.meta || !filterRow.meta.index) { + return; + } + filterRow.meta.indexRefName = `kibanaSavedObjectMeta.searchSourceJSON.filter[${i}].meta.index`; + doc.references.push({ + name: filterRow.meta.indexRefName, + type: 'index-pattern', + id: filterRow.meta.index, + }); + delete filterRow.meta.index; + }); + } + doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource); +} + +const migrations700: SavedObjectMigrationFn = (doc): DashboardDoc700To720 => { + // Set new "references" attribute + doc.references = doc.references || []; + + // Migrate index pattern + migrateIndexPattern(doc as DashboardDoc700To720); + // Migrate panels + const panelsJSON = get(doc, 'attributes.panelsJSON'); + if (typeof panelsJSON !== 'string') { + return doc as DashboardDoc700To720; + } + let panels; + try { + panels = JSON.parse(panelsJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + return doc as DashboardDoc700To720; + } + if (!Array.isArray(panels)) { + return doc as DashboardDoc700To720; + } + panels.forEach((panel, i) => { + if (!panel.type || !panel.id) { + return; + } + panel.panelRefName = `panel_${i}`; + doc.references!.push({ + name: `panel_${i}`, + type: panel.type, + id: panel.id, + }); + delete panel.type; + delete panel.id; + }); + doc.attributes.panelsJSON = JSON.stringify(panels); + return doc as DashboardDoc700To720; +}; + +export const dashboardSavedObjectTypeMigrations = { + /** + * We need to have this migration twice, once with a version prior to 7.0.0 once with a version + * after it. The reason for that is, that this migration has been introduced once 7.0.0 was already + * released. Thus a user who already had 7.0.0 installed already got the 7.0.0 migrations below running, + * so we need a version higher than that. But this fix was backported to the 6.7 release, meaning if we + * would only have the 7.0.1 migration in here a user on the 6.7 release will migrate their saved objects + * to the 7.0.1 state, and thus when updating their Kibana to 7.0, will never run the 7.0.0 migrations introduced + * in that version. So we apply this twice, once with 6.7.2 and once with 7.0.1 while the backport to 6.7 + * only contained the 6.7.2 migration and not the 7.0.1 migration. + */ + '6.7.2': flow(migrateMatchAllQuery), + '7.0.0': flow<(doc: SavedObjectUnsanitizedDoc) => DashboardDoc700To720>(migrations700), + '7.3.0': flow(migrations730), +}; diff --git a/src/plugins/dashboard/public/bwc/index.ts b/src/plugins/dashboard/server/saved_objects/index.ts similarity index 93% rename from src/plugins/dashboard/public/bwc/index.ts rename to src/plugins/dashboard/server/saved_objects/index.ts index d8f7b5091eb8f..ca97b9d2a6b70 100644 --- a/src/plugins/dashboard/public/bwc/index.ts +++ b/src/plugins/dashboard/server/saved_objects/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export * from './types'; +export { dashboardSavedObjectType } from './dashboard'; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts similarity index 70% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts rename to src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts index d8f8882a218dd..c9b35263a549f 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/is_dashboard_doc.ts +++ b/src/plugins/dashboard/server/saved_objects/is_dashboard_doc.ts @@ -17,8 +17,21 @@ * under the License. */ -import { DashboardDoc730ToLatest } from '../../../../../../plugins/dashboard/public'; -import { isDoc } from '../../../migrations/is_doc'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; + +function isDoc( + doc: { [key: string]: unknown } | SavedObjectUnsanitizedDoc +): doc is SavedObjectUnsanitizedDoc { + return ( + typeof doc.id === 'string' && + typeof doc.type === 'string' && + doc.attributes !== null && + typeof doc.attributes === 'object' && + doc.references !== null && + typeof doc.references === 'object' + ); +} export function isDashboardDoc( doc: { [key: string]: unknown } | DashboardDoc730ToLatest diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.test.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.test.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts similarity index 95% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts rename to src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts index 707aae9e5d4ac..5b8582bf821ef 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrate_match_all_query.ts +++ b/src/plugins/dashboard/server/saved_objects/migrate_match_all_query.ts @@ -19,7 +19,7 @@ import { SavedObjectMigrationFn } from 'kibana/server'; import { get } from 'lodash'; -import { DEFAULT_QUERY_LANGUAGE } from '../../../../../../plugins/data/common'; +import { DEFAULT_QUERY_LANGUAGE } from '../../../data/common'; export const migrateMatchAllQuery: SavedObjectMigrationFn = doc => { const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON'); diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts similarity index 91% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.test.ts index 5a4970897098d..aa744324428a4 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.test.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.test.ts @@ -17,20 +17,18 @@ * under the License. */ -import { migrations } from '../../../migrations'; +import { dashboardSavedObjectTypeMigrations as migrations } from './dashboard_migrations'; import { migrations730 } from './migrations_730'; -import { - DashboardDoc700To720, - DashboardDoc730ToLatest, - RawSavedDashboardPanel730ToLatest, - DashboardDocPre700, -} from '../../../../../../plugins/dashboard/public'; - -const mockLogger = { - warning: () => {}, - warn: () => {}, - debug: () => {}, - info: () => {}, +import { DashboardDoc700To720, DashboardDoc730ToLatest, DashboardDocPre700 } from '../../common'; +import { RawSavedDashboardPanel730ToLatest } from '../../common'; + +const mockContext = { + log: { + warning: () => {}, + warn: () => {}, + debug: () => {}, + info: () => {}, + }, }; test('dashboard migration 7.3.0 migrates filters to query on search source', () => { @@ -53,7 +51,7 @@ test('dashboard migration 7.3.0 migrates filters to query on search source', () '[{"id":"1","type":"visualization","foo":true},{"id":"2","type":"visualization","bar":true}]', }, }; - const newDoc = migrations730(doc, mockLogger); + const newDoc = migrations730(doc, mockContext); expect(newDoc).toMatchInlineSnapshot(` Object { @@ -97,8 +95,8 @@ test('dashboard migration 7.3.0 migrates filters to query on search source when }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -129,8 +127,8 @@ test('dashboard migration works when panelsJSON is missing panelIndex', () => { }, }; - const doc700: DashboardDoc700To720 = migrations.dashboard['7.0.0'](doc, mockLogger); - const newDoc = migrations.dashboard['7.3.0'](doc700, mockLogger); + const doc700: DashboardDoc700To720 = migrations['7.0.0'](doc); + const newDoc = migrations['7.3.0'](doc700, mockContext); const parsedSearchSource = JSON.parse(newDoc.attributes.kibanaSavedObjectMeta.searchSourceJSON); expect(parsedSearchSource.filter.length).toBe(0); @@ -159,7 +157,7 @@ test('dashboard migration 7.3.0 migrates panels', () => { }, }; - const newDoc = migrations730(doc, mockLogger) as DashboardDoc730ToLatest; + const newDoc = migrations730(doc, mockContext) as DashboardDoc730ToLatest; const newPanels = JSON.parse(newDoc.attributes.panelsJSON) as RawSavedDashboardPanel730ToLatest[]; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts b/src/plugins/dashboard/server/saved_objects/migrations_730.ts similarity index 79% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts rename to src/plugins/dashboard/server/saved_objects/migrations_730.ts index 56856f7b21303..e9d483f68a5da 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/migrations_730.ts +++ b/src/plugins/dashboard/server/saved_objects/migrations_730.ts @@ -16,26 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -// This file should be moved to dashboard/server/ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsMigrationLogger } from 'src/core/server'; + import { inspect } from 'util'; -import { - DashboardDoc730ToLatest, - DashboardDoc700To720, -} from '../../../../../../plugins/dashboard/public'; +import { SavedObjectMigrationContext } from 'kibana/server'; +import { DashboardDoc730ToLatest } from '../../common'; import { isDashboardDoc } from './is_dashboard_doc'; import { moveFiltersToQuery } from './move_filters_to_query'; -import { migratePanelsTo730 } from './migrate_to_730_panels'; +import { migratePanelsTo730, DashboardDoc700To720 } from '../../common'; -export function migrations730( - doc: - | { - [key: string]: unknown; - } - | DashboardDoc700To720, - logger: SavedObjectsMigrationLogger -): DashboardDoc730ToLatest | { [key: string]: unknown } { +export const migrations730 = (doc: DashboardDoc700To720, { log }: SavedObjectMigrationContext) => { if (!isDashboardDoc(doc)) { // NOTE: we should probably throw an error here... but for now following suit and in the // case of errors, just returning the same document. @@ -48,7 +37,7 @@ export function migrations730( moveFiltersToQuery(searchSource) ); } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard query filters!\n` + `${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -75,7 +64,7 @@ export function migrations730( delete doc.attributes.uiStateJSON; } catch (e) { - logger.warning( + log.warning( `Exception @ migrations730 while trying to migrate dashboard panels!\n` + `Error: ${e.stack}\n` + `dashboard: ${inspect(doc, false, null)}` @@ -84,4 +73,4 @@ export function migrations730( } return doc as DashboardDoc730ToLatest; -} +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts similarity index 96% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts index 621983b1ca8a5..a06f64e0f0c40 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.test.ts +++ b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.test.ts @@ -17,8 +17,8 @@ * under the License. */ +import { esFilters, Filter } from 'src/plugins/data/public'; import { moveFiltersToQuery, Pre600FilterQuery } from './move_filters_to_query'; -import { esFilters, Filter } from '../../../../../../plugins/data/public'; const filter: Filter = { meta: { disabled: false, negate: false, alias: '' }, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts b/src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts similarity index 100% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/move_filters_to_query.ts rename to src/plugins/dashboard/server/saved_objects/move_filters_to_query.ts diff --git a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts b/src/plugins/dashboard/server/types.ts similarity index 78% rename from src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts rename to src/plugins/dashboard/server/types.ts index f333ce97d120f..1151b06dbdab7 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/migrations/index.ts +++ b/src/plugins/dashboard/server/types.ts @@ -17,5 +17,7 @@ * under the License. */ -export { migrations730 } from './migrations_730'; -export { migrateMatchAllQuery } from './migrate_match_all_query'; +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DashboardPluginStart {} diff --git a/src/plugins/data/public/actions/filters/brush_event.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts similarity index 58% rename from src/plugins/data/public/actions/filters/brush_event.test.ts rename to src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts index 60244354f06e4..5d21b395b994f 100644 --- a/src/plugins/data/public/actions/filters/brush_event.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.test.ts @@ -19,30 +19,34 @@ import moment from 'moment'; -import { onBrushEvent, BrushEvent } from './brush_event'; +import { createFiltersFromRangeSelectAction } from './create_filters_from_range_select'; -import { IndexPatternsContract } from '../../../public'; +import { IndexPatternsContract, RangeFilter } from '../../../public'; import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns } from '../../../public/services'; import { mockDataServices } from '../../../public/search/aggs/test_helpers'; +import { TriggerContextMapping } from '../../../../ui_actions/public'; describe('brushEvent', () => { const DAY_IN_MS = 24 * 60 * 60 * 1000; const JAN_01_2014 = 1388559600000; - let baseEvent: BrushEvent; + let baseEvent: TriggerContextMapping['SELECT_RANGE_TRIGGER']['data']; + + const indexPattern = { + id: 'indexPatternId', + timeFieldName: 'time', + fields: { + getByName: () => undefined, + filter: () => [], + }, + }; const aggConfigs = [ { params: { field: {}, }, - getIndexPattern: () => ({ - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), + getIndexPattern: () => indexPattern, }, ]; @@ -50,56 +54,37 @@ describe('brushEvent', () => { mockDataServices(); setIndexPatterns(({ ...dataPluginMock.createStartContract().indexPatterns, - get: async () => ({ - id: 'indexPatternId', - timeFieldName: 'time', - fields: { - getByName: () => undefined, - filter: () => [], - }, - }), + get: async () => indexPattern, } as unknown) as IndexPatternsContract); baseEvent = { - data: { - ordered: { - date: false, - }, - series: [ + column: 0, + table: { + type: 'kibana_datatable', + columns: [ { - values: [ - { - xRaw: { - column: 0, - table: { - columns: [ - { - id: '1', - meta: { - type: 'histogram', - indexPatternId: 'indexPatternId', - aggConfigParams: aggConfigs[0].params, - }, - }, - ], - }, - }, - }, - ], + id: '1', + name: '1', + meta: { + type: 'histogram', + indexPatternId: 'indexPatternId', + aggConfigParams: aggConfigs[0].params, + }, }, ], + rows: [], }, range: [], }; }); test('should be a function', () => { - expect(typeof onBrushEvent).toBe('function'); + expect(typeof createFiltersFromRangeSelectAction).toBe('function'); }); test('ignores event when data.xAxisField not provided', async () => { - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); }); describe('handles an event when the x-axis field is a date field', () => { @@ -109,29 +94,29 @@ describe('brushEvent', () => { name: 'time', type: 'date', }; - baseEvent.data.ordered = { date: true }; }); afterAll(() => { baseEvent.range = []; - baseEvent.data.ordered = { date: false }; + aggConfigs[0].params.field = {}; }); test('by ignoring the event when range spans zero time', async () => { baseEvent.range = [JAN_01_2014, JAN_01_2014]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); }); test('by updating the timefilter', async () => { baseEvent.range = [JAN_01_2014, JAN_01_2014 + DAY_IN_MS]; - const filter = await onBrushEvent(baseEvent); + const filter = await createFiltersFromRangeSelectAction(baseEvent); expect(filter).toBeDefined(); - if (filter) { - expect(filter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.time.gte).toBe(new Date(JAN_01_2014).toISOString()); // Set to a baseline timezone for comparison. - expect(filter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); + expect(rangeFilter.range.time.lt).toBe(new Date(JAN_01_2014 + DAY_IN_MS).toISOString()); } }); }); @@ -142,26 +127,26 @@ describe('brushEvent', () => { name: 'anotherTimeField', type: 'date', }; - baseEvent.data.ordered = { date: true }; }); afterAll(() => { baseEvent.range = []; - baseEvent.data.ordered = { date: false }; + aggConfigs[0].params.field = {}; }); test('creates a new range filter', async () => { const rangeBegin = JAN_01_2014; const rangeEnd = rangeBegin + DAY_IN_MS; baseEvent.range = [rangeBegin, rangeEnd]; - const filter = await onBrushEvent(baseEvent); + const filter = await createFiltersFromRangeSelectAction(baseEvent); expect(filter).toBeDefined(); - if (filter) { - expect(filter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); - expect(filter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); - expect(filter.range.anotherTimeField).toHaveProperty( + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.anotherTimeField.gte).toBe(moment(rangeBegin).toISOString()); + expect(rangeFilter.range.anotherTimeField.lt).toBe(moment(rangeEnd).toISOString()); + expect(rangeFilter.range.anotherTimeField).toHaveProperty( 'format', 'strict_date_optional_time' ); @@ -184,20 +169,21 @@ describe('brushEvent', () => { test('by ignoring the event when range does not span at least 2 values', async () => { baseEvent.range = [1]; - const filter = await onBrushEvent(baseEvent); - expect(filter).toBeUndefined(); + const filter = await createFiltersFromRangeSelectAction(baseEvent); + expect(filter).toEqual([]); }); test('by creating a new filter', async () => { baseEvent.range = [1, 2, 3, 4]; - const filter = await onBrushEvent(baseEvent); + const filter = await createFiltersFromRangeSelectAction(baseEvent); expect(filter).toBeDefined(); - if (filter) { - expect(filter.range.numberField.gte).toBe(1); - expect(filter.range.numberField.lt).toBe(4); - expect(filter.range.numberField).not.toHaveProperty('format'); + if (filter.length) { + const rangeFilter = filter[0] as RangeFilter; + expect(rangeFilter.range.numberField.gte).toBe(1); + expect(rangeFilter.range.numberField.lt).toBe(4); + expect(rangeFilter.range.numberField).not.toHaveProperty('format'); } }); }); diff --git a/src/plugins/data/public/actions/filters/brush_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts similarity index 74% rename from src/plugins/data/public/actions/filters/brush_event.ts rename to src/plugins/data/public/actions/filters/create_filters_from_range_select.ts index 714f005fbeb6d..409614ca9c380 100644 --- a/src/plugins/data/public/actions/filters/brush_event.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_range_select.ts @@ -17,34 +17,18 @@ * under the License. */ -import { get, last } from 'lodash'; +import { last } from 'lodash'; import moment from 'moment'; import { esFilters, IFieldType, RangeFilterParams } from '../../../public'; import { getIndexPatterns } from '../../../public/services'; import { deserializeAggConfig } from '../../search/expressions/utils'; +import { RangeSelectTriggerContext } from '../../../../embeddable/public'; -export interface BrushEvent { - data: { - ordered: { - date: boolean; - }; - series: Array>; - }; - range: number[]; -} - -export async function onBrushEvent(event: BrushEvent) { - const isDate = get(event.data, 'ordered.date'); - const xRaw: Record = get(event.data, 'series[0].values[0].xRaw'); - - if (!xRaw) { - return; - } - - const column: Record = xRaw.table.columns[xRaw.column]; +export async function createFiltersFromRangeSelectAction(event: RangeSelectTriggerContext['data']) { + const column: Record = event.table.columns[event.column]; if (!column || !column.meta) { - return; + return []; } const indexPattern = await getIndexPatterns().get(column.meta.indexPatternId); @@ -55,16 +39,18 @@ export async function onBrushEvent(event: BrushEvent) { const field: IFieldType = aggConfig.params.field; if (!field || event.range.length <= 1) { - return; + return []; } const min = event.range[0]; const max = last(event.range); if (min === max) { - return; + return []; } + const isDate = field.type === 'date'; + const range: RangeFilterParams = { gte: isDate ? moment(min).toISOString() : min, lt: isDate ? moment(max).toISOString() : max, @@ -74,5 +60,5 @@ export async function onBrushEvent(event: BrushEvent) { range.format = 'strict_date_optional_time'; } - return esFilters.buildRangeFilter(field, range, indexPattern); + return esFilters.mapAndFlattenFilters([esFilters.buildRangeFilter(field, range, indexPattern)]); } diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts similarity index 85% rename from src/plugins/data/public/actions/filters/create_filters_from_event.test.ts rename to src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts index 1ed09002816d1..a0e285c20d776 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.test.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.test.ts @@ -26,7 +26,8 @@ import { import { dataPluginMock } from '../../../public/mocks'; import { setIndexPatterns } from '../../../public/services'; import { mockDataServices } from '../../../public/search/aggs/test_helpers'; -import { createFiltersFromEvent, EventData } from './create_filters_from_event'; +import { createFiltersFromValueClickAction } from './create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../../embeddable/public'; const mockField = { name: 'bytes', @@ -37,8 +38,8 @@ const mockField = { format: new fieldFormats.BytesFormat({}, (() => {}) as FieldFormatsGetConfigFn), }; -describe('createFiltersFromEvent', () => { - let dataPoints: EventData[]; +describe('createFiltersFromValueClick', () => { + let dataPoints: ValueClickTriggerContext['data']['data']; beforeEach(() => { dataPoints = [ @@ -86,7 +87,7 @@ describe('createFiltersFromEvent', () => { test('ignores event when value for rows is not provided', async () => { dataPoints[0].table.rows[0]['1-1'] = null; - const filters = await createFiltersFromEvent(dataPoints); + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); expect(filters.length).toEqual(0); }); @@ -95,14 +96,14 @@ describe('createFiltersFromEvent', () => { if (dataPoints[0].table.columns[0].meta) { dataPoints[0].table.columns[0].meta.type = 'terms'; } - const filters = await createFiltersFromEvent(dataPoints); + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); expect(filters.length).toEqual(1); expect(filters[0].query.match_phrase.bytes).toEqual('2048'); }); test('handles an event when aggregations type is not terms', async () => { - const filters = await createFiltersFromEvent(dataPoints); + const filters = await createFiltersFromValueClickAction({ data: dataPoints }); expect(filters.length).toEqual(1); diff --git a/src/plugins/data/public/actions/filters/create_filters_from_event.ts b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts similarity index 90% rename from src/plugins/data/public/actions/filters/create_filters_from_event.ts rename to src/plugins/data/public/actions/filters/create_filters_from_value_click.ts index e62945a592072..2b426813a98a4 100644 --- a/src/plugins/data/public/actions/filters/create_filters_from_event.ts +++ b/src/plugins/data/public/actions/filters/create_filters_from_value_click.ts @@ -21,13 +21,7 @@ import { KibanaDatatable } from '../../../../../plugins/expressions/public'; import { deserializeAggConfig } from '../../search/expressions'; import { esFilters, Filter } from '../../../public'; import { getIndexPatterns } from '../../../public/services'; - -export interface EventData { - table: Pick; - column: number; - row: number; - value: any; -} +import { ValueClickTriggerContext } from '../../../../embeddable/public'; /** * For terms aggregations on `__other__` buckets, this assembles a list of applicable filter @@ -39,7 +33,7 @@ export interface EventData { * @return {array} - array of terms to filter against */ const getOtherBucketFilterTerms = ( - table: EventData['table'], + table: Pick, columnIndex: number, rowIndex: number ) => { @@ -76,7 +70,11 @@ const getOtherBucketFilterTerms = ( * @param {string} cellValue - value of the current cell * @return {Filter[]|undefined} - list of filters to provide to queryFilter.addFilters() */ -const createFilter = async (table: EventData['table'], columnIndex: number, rowIndex: number) => { +const createFilter = async ( + table: Pick, + columnIndex: number, + rowIndex: number +) => { if (!table || !table.columns || !table.columns[columnIndex]) { return; } @@ -113,11 +111,14 @@ const createFilter = async (table: EventData['table'], columnIndex: number, rowI }; /** @public */ -export const createFiltersFromEvent = async (dataPoints: EventData[], negate?: boolean) => { +export const createFiltersFromValueClickAction = async ({ + data, + negate, +}: ValueClickTriggerContext['data']) => { const filters: Filter[] = []; await Promise.all( - dataPoints + data .filter(point => point) .map(async val => { const { table, column, row } = val; @@ -133,5 +134,5 @@ export const createFiltersFromEvent = async (dataPoints: EventData[], negate?: b }) ); - return filters; + return esFilters.mapAndFlattenFilters(filters); }; diff --git a/src/plugins/data/public/actions/index.ts b/src/plugins/data/public/actions/index.ts index cdb84ff13f25e..ef9014aafe82d 100644 --- a/src/plugins/data/public/actions/index.ts +++ b/src/plugins/data/public/actions/index.ts @@ -18,6 +18,7 @@ */ export { ACTION_GLOBAL_APPLY_FILTER, createFilterAction } from './apply_filter_action'; -export { createFiltersFromEvent } from './filters/create_filters_from_event'; +export { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +export { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; export { selectRangeAction } from './select_range_action'; export { valueClickAction } from './value_click_action'; diff --git a/src/plugins/data/public/actions/select_range_action.ts b/src/plugins/data/public/actions/select_range_action.ts index 6e1f16a09e803..4882e8eafc0d3 100644 --- a/src/plugins/data/public/actions/select_range_action.ts +++ b/src/plugins/data/public/actions/select_range_action.ts @@ -23,19 +23,17 @@ import { IncompatibleActionError, ActionByType, } from '../../../../plugins/ui_actions/public'; -import { onBrushEvent } from './filters/brush_event'; +import { createFiltersFromRangeSelectAction } from './filters/create_filters_from_range_select'; +import { RangeSelectTriggerContext } from '../../../embeddable/public'; import { FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_SELECT_RANGE = 'ACTION_SELECT_RANGE'; -export interface SelectRangeActionContext { - data: any; - timeFieldName: string; -} +export type SelectRangeActionContext = RangeSelectTriggerContext; async function isCompatible(context: SelectRangeActionContext) { try { - return Boolean(await onBrushEvent(context.data)); + return Boolean(await createFiltersFromRangeSelectAction(context.data)); } catch { return false; } @@ -48,6 +46,7 @@ export function selectRangeAction( return createAction({ type: ACTION_SELECT_RANGE, id: ACTION_SELECT_RANGE, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', @@ -59,13 +58,7 @@ export function selectRangeAction( throw new IncompatibleActionError(); } - const filter = await onBrushEvent(data); - - if (!filter) { - return; - } - - const selectedFilters = esFilters.mapAndFlattenFilters([filter]); + const selectedFilters = await createFiltersFromRangeSelectAction(data); if (timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( diff --git a/src/plugins/data/public/actions/value_click_action.ts b/src/plugins/data/public/actions/value_click_action.ts index 01c32e27da07d..210a58b3f75aa 100644 --- a/src/plugins/data/public/actions/value_click_action.ts +++ b/src/plugins/data/public/actions/value_click_action.ts @@ -26,21 +26,17 @@ import { } from '../../../../plugins/ui_actions/public'; import { getOverlays, getIndexPatterns } from '../services'; import { applyFiltersPopover } from '../ui/apply_filters'; -import { createFiltersFromEvent } from './filters/create_filters_from_event'; +import { createFiltersFromValueClickAction } from './filters/create_filters_from_value_click'; +import { ValueClickTriggerContext } from '../../../embeddable/public'; import { Filter, FilterManager, TimefilterContract, esFilters } from '..'; export const ACTION_VALUE_CLICK = 'ACTION_VALUE_CLICK'; -export interface ValueClickActionContext { - data: any; - timeFieldName: string; -} +export type ValueClickActionContext = ValueClickTriggerContext; async function isCompatible(context: ValueClickActionContext) { try { - const filters: Filter[] = - (await createFiltersFromEvent(context.data.data || [context.data], context.data.negate)) || - []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); return filters.length > 0; } catch { return false; @@ -54,23 +50,23 @@ export function valueClickAction( return createAction({ type: ACTION_VALUE_CLICK, id: ACTION_VALUE_CLICK, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', }); }, isCompatible, - execute: async ({ timeFieldName, data }: ValueClickActionContext) => { - if (!(await isCompatible({ timeFieldName, data }))) { + execute: async (context: ValueClickActionContext) => { + if (!(await isCompatible(context))) { throw new IncompatibleActionError(); } - const filters: Filter[] = - (await createFiltersFromEvent(data.data || [data], data.negate)) || []; + const filters: Filter[] = await createFiltersFromValueClickAction(context.data); - let selectedFilters: Filter[] = esFilters.mapAndFlattenFilters(filters); + let selectedFilters = filters; - if (selectedFilters.length > 1) { + if (filters.length > 1) { const indexPatterns = await Promise.all( filters.map(filter => { return getIndexPatterns().get(filter.meta.index!); @@ -102,9 +98,9 @@ export function valueClickAction( selectedFilters = await filterSelectionPromise; } - if (timeFieldName) { + if (context.timeFieldName) { const { timeRangeFilter, restOfFilters } = esFilters.extractTimeFilter( - timeFieldName, + context.timeFieldName, selectedFilters ); filterManager.addFilters(restOfFilters); diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index 2d43cae79ac98..1f604b9eb6baa 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -45,7 +45,8 @@ const createStartContract = (): Start => { const queryStartMock = queryServiceMock.createStartContract(); return { actions: { - createFiltersFromEvent: jest.fn().mockResolvedValue(['yes']), + createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), + createFiltersFromRangeSelectAction: jest.fn(), }, autocomplete: autocompleteMock, search: searchStartMock, diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 1723545b32522..ccf94171235fe 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -58,7 +58,12 @@ import { VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER, } from '../../ui_actions/public'; -import { ACTION_GLOBAL_APPLY_FILTER, createFilterAction, createFiltersFromEvent } from './actions'; +import { + ACTION_GLOBAL_APPLY_FILTER, + createFilterAction, + createFiltersFromValueClickAction, + createFiltersFromRangeSelectAction, +} from './actions'; import { ApplyGlobalFilterActionContext } from './actions/apply_filter_action'; import { selectRangeAction, @@ -162,7 +167,8 @@ export class DataPublicPlugin implements Plugin import("../../expressions/common").SerializedFieldFormat; + serialize: (agg: import("./search").AggConfig) => import("../../expressions").SerializedFieldFormat; DEFAULT_CONVERTER_COLOR: { range: string; regex: string; @@ -1892,8 +1893,9 @@ export type TSearchStrategyProvider = (context: ISearc // src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:33:33 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:37:1 - (ae-forgotten-export) The symbol "QueryStateChange" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromEvent" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:60:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:52:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:53:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/types.ts:61:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index e24e01d241278..5414de16be310 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -24,7 +24,7 @@ import { ExpressionsSetup } from 'src/plugins/expressions/public'; import { UiActionsSetup, UiActionsStart } from 'src/plugins/ui_actions/public'; import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { FieldFormatsSetup, FieldFormatsStart } from './field_formats'; -import { createFiltersFromEvent } from './actions'; +import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; import { IndexPatternSelectProps } from './ui/index_pattern_select'; @@ -49,7 +49,8 @@ export interface DataPublicPluginSetup { export interface DataPublicPluginStart { actions: { - createFiltersFromEvent: typeof createFiltersFromEvent; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; }; autocomplete: AutocompleteStart; indexPatterns: IndexPatternsContract; diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c41023eab6d20..f8a9a7792c492 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -283,7 +283,7 @@ export interface FieldFormatConfig { export const fieldFormats: { FieldFormatsRegistry: typeof FieldFormatsRegistry; FieldFormat: typeof FieldFormat; - serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions/common").SerializedFieldFormat; + serializeFieldFormat: (agg: import("../public/search").AggConfig) => import("../../expressions").SerializedFieldFormat; BoolFormat: typeof BoolFormat; BytesFormat: typeof BytesFormat; ColorFormat: typeof ColorFormat; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index bdb7bfbddc308..5ee66f9d19ac0 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -47,7 +47,8 @@ export { EmbeddableOutput, EmbeddablePanel, EmbeddableRoot, - EmbeddableVisTriggerContext, + ValueClickTriggerContext, + RangeSelectTriggerContext, ErrorEmbeddable, IContainer, IEmbeddable, diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts index 5297cf6cd365c..636ce3e623c5b 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts @@ -31,6 +31,14 @@ test('has expected display name', () => { expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`); }); +describe('getIconType()', () => { + test('returns "filter" icon', async () => { + const action = createFilterAction(); + const result = action.getIconType({} as any); + expect(result).toBe('filter'); + }); +}); + describe('isCompatible()', () => { test('when embeddable filters and filters exist, returns true', async () => { const action = createFilterAction(); diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts index 4680512fb81c8..1cdb5af00e748 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction(): ActionByType { return createAction({ type: ACTION_APPLY_FILTER, id: ACTION_APPLY_FILTER, + getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { defaultMessage: 'Apply filter to current view', diff --git a/src/plugins/embeddable/public/lib/triggers/triggers.ts b/src/plugins/embeddable/public/lib/triggers/triggers.ts index e29302fd6cc13..da7be1eea199a 100644 --- a/src/plugins/embeddable/public/lib/triggers/triggers.ts +++ b/src/plugins/embeddable/public/lib/triggers/triggers.ts @@ -18,18 +18,34 @@ */ import { Trigger } from '../../../../ui_actions/public'; +import { KibanaDatatable } from '../../../../expressions'; import { IEmbeddable } from '..'; export interface EmbeddableContext { embeddable: IEmbeddable; } -export interface EmbeddableVisTriggerContext { +export interface ValueClickTriggerContext { embeddable?: IEmbeddable; timeFieldName?: string; data: { - e?: MouseEvent; - data: unknown; + data: Array<{ + table: Pick; + column: number; + row: number; + value: any; + }>; + negate?: boolean; + }; +} + +export interface RangeSelectTriggerContext { + embeddable?: IEmbeddable; + timeFieldName?: string; + data: { + table: KibanaDatatable; + column: number; + range: number[]; }; } diff --git a/src/plugins/navigation/public/top_nav_menu/_index.scss b/src/plugins/navigation/public/top_nav_menu/_index.scss index 5befe4789dd6c..a6ddf7a8b4264 100644 --- a/src/plugins/navigation/public/top_nav_menu/_index.scss +++ b/src/plugins/navigation/public/top_nav_menu/_index.scss @@ -8,4 +8,8 @@ padding: 0 $euiSizeS; } } + + .kbnTopNavMenu-isFullScreen { + padding: 0; + } } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 8e0e8b3031132..74cfd125c2e3a 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -75,4 +75,17 @@ describe('TopNavMenu', () => { expect(component.find(TOP_NAV_ITEM_SELECTOR).length).toBe(0); expect(component.find(SEARCH_BAR_SELECTOR).length).toBe(1); }); + + it('Should render with a class name', () => { + const component = shallowWithIntl( + + ); + expect(component.find('.kbnTopNavMenu').length).toBe(1); + expect(component.find('.myCoolClass').length).toBeTruthy(); + }); }); diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index 14ad40f13e388..d492c7feb61a7 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -21,6 +21,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import classNames from 'classnames'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; @@ -29,6 +30,7 @@ export type TopNavMenuProps = StatefulSearchBarProps & { config?: TopNavMenuData[]; showSearchBar?: boolean; data?: DataPublicPluginStart; + className?: string; }; /* @@ -65,6 +67,7 @@ export function TopNavMenu(props: TopNavMenuProps) { } function renderLayout() { + const className = classNames('kbnTopNavMenu', props.className); return ( {renderItems()} diff --git a/src/plugins/ui_actions/public/types.ts b/src/plugins/ui_actions/public/types.ts index c7e6d61e15f31..e6247a8bafff7 100644 --- a/src/plugins/ui_actions/public/types.ts +++ b/src/plugins/ui_actions/public/types.ts @@ -19,9 +19,10 @@ import { ActionByType } from './actions/action'; import { TriggerInternal } from './triggers/trigger_internal'; -import { EmbeddableVisTriggerContext, IEmbeddable } from '../../embeddable/public'; import { Filter } from '../../data/public'; import { SELECT_RANGE_TRIGGER, VALUE_CLICK_TRIGGER, APPLY_FILTER_TRIGGER } from './triggers'; +import { IEmbeddable } from '../../embeddable/public'; +import { RangeSelectTriggerContext, ValueClickTriggerContext } from '../../embeddable/public'; export type TriggerRegistry = Map>; export type ActionRegistry = Map>; @@ -36,8 +37,8 @@ export type TriggerContext = BaseContext; export interface TriggerContextMapping { [DEFAULT_TRIGGER]: TriggerContext; - [SELECT_RANGE_TRIGGER]: EmbeddableVisTriggerContext; - [VALUE_CLICK_TRIGGER]: EmbeddableVisTriggerContext; + [SELECT_RANGE_TRIGGER]: RangeSelectTriggerContext; + [VALUE_CLICK_TRIGGER]: ValueClickTriggerContext; [APPLY_FILTER_TRIGGER]: { embeddable: IEmbeddable; filters: Filter[]; diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index ffb028ff131b3..1c545bb36cff0 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -33,7 +33,6 @@ import { EmbeddableInput, EmbeddableOutput, Embeddable, - EmbeddableVisTriggerContext, IContainer, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; @@ -261,7 +260,7 @@ export class VisualizeEmbeddable extends Embeddable { if (!this.eventsSubject) return; - this.eventsSubject.next({ name: 'filterBucket', data }); + this.eventsSubject.next({ + name: 'filterBucket', + data: data.data + ? { + data: data.data, + negate: data.negate, + } + : { data: [data] }, + }); }, brush: (data: any) => { if (!this.eventsSubject) return; diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index ab64e083a553d..df8479bc891b8 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -123,6 +123,8 @@ export class VisualizePlugin }; setServices(deps); + // make sure the index pattern list is up to date + await pluginsStart.data.indexPatterns.clearCache(); const { renderApp } = await import('./application/application'); const unmount = renderApp(params.element, params.appBasePath, deps); return () => { diff --git a/test/functional/apps/dashboard/dashboard_saved_query.js b/test/functional/apps/dashboard/dashboard_saved_query.js new file mode 100644 index 0000000000000..99d0aed082e70 --- /dev/null +++ b/test/functional/apps/dashboard/dashboard_saved_query.js @@ -0,0 +1,128 @@ +/* + * 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 expect from '@kbn/expect'; + +export default function({ getService, getPageObjects }) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const PageObjects = getPageObjects(['common', 'dashboard', 'timePicker']); + const browser = getService('browser'); + const queryBar = getService('queryBar'); + const savedQueryManagementComponent = getService('savedQueryManagementComponent'); + const testSubjects = getService('testSubjects'); + + describe('dashboard saved queries', function describeIndexTests() { + before(async function() { + await esArchiver.load('dashboard/current/kibana'); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await PageObjects.common.navigateToApp('dashboard'); + }); + + describe('saved query management component functionality', function() { + before(async () => { + await PageObjects.dashboard.gotoDashboardLandingPage(); + await PageObjects.dashboard.clickNewDashboard(); + }); + + it('should show the saved query management component when there are no saved queries', async () => { + await savedQueryManagementComponent.openSavedQueryManagementComponent(); + const descriptionText = await testSubjects.getVisibleText('saved-query-management-popover'); + expect(descriptionText).to.eql( + 'SAVED QUERIES\nThere are no saved queries. Save query text and filters that you want to use again.\nSave current query' + ); + }); + + it('should allow a query to be saved via the saved objects management component', async () => { + await queryBar.setQuery('response:200'); + await savedQueryManagementComponent.saveNewQuery( + 'OkResponse', + '200 responses for .jpg over 24 hours', + true, + true + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); + }); + + it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { + await PageObjects.timePicker.setDefaultAbsoluteRange(); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + }); + + it('preserves the currently loaded query when the page is reloaded', async () => { + await browser.refresh(); + const timePickerValues = await PageObjects.timePicker.getTimeConfigAsAbsoluteTimes(); + expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime); + expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime); + expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse'); + }); + + it('allows saving changes to a currently loaded query via the saved query management component', async () => { + await queryBar.setQuery('response:404'); + await savedQueryManagementComponent.updateCurrentlyLoadedQuery( + 'OkResponse', + '404 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows saving the currently loaded query as a new query', async () => { + await savedQueryManagementComponent.saveCurrentlyLoadedAsNewQuery( + 'OkResponseCopy', + '200 responses', + false, + false + ); + await savedQueryManagementComponent.savedQueryExistOrFail('OkResponseCopy'); + }); + + it('allows deleting the currently loaded saved query in the saved query management component and clears the query', async () => { + await savedQueryManagementComponent.deleteSavedQuery('OkResponseCopy'); + await savedQueryManagementComponent.savedQueryMissingOrFail('OkResponseCopy'); + expect(await queryBar.getQueryString()).to.eql(''); + }); + + it('resets any changes to a loaded query on reloading the same saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await queryBar.setQuery('response:503'); + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + expect(await queryBar.getQueryString()).to.eql('response:404'); + }); + + it('allows clearing the currently loaded saved query', async () => { + await savedQueryManagementComponent.loadSavedQuery('OkResponse'); + await savedQueryManagementComponent.clearCurrentlyLoadedQuery(); + expect(await queryBar.getQueryString()).to.eql(''); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/full_screen_mode.js b/test/functional/apps/dashboard/full_screen_mode.js index df00f64530ca0..17eb6d8f08a9c 100644 --- a/test/functional/apps/dashboard/full_screen_mode.js +++ b/test/functional/apps/dashboard/full_screen_mode.js @@ -25,6 +25,7 @@ export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); const dashboardPanelActions = getService('dashboardPanelActions'); const PageObjects = getPageObjects(['dashboard', 'common']); + const filterBar = getService('filterBar'); describe('full screen mode', () => { before(async () => { @@ -81,5 +82,22 @@ export default function({ getService, getPageObjects }) { expect(isChromeVisible).to.be(true); }); }); + + it('shows filter bar in fullscreen mode', async () => { + await filterBar.addFilter('bytes', 'is', '12345678'); + await PageObjects.dashboard.waitForRenderComplete(); + await PageObjects.dashboard.clickFullScreenMode(); + await retry.try(async () => { + const isChromeHidden = await PageObjects.common.isChromeHidden(); + expect(isChromeHidden).to.be(true); + }); + expect(await filterBar.getFilterCount()).to.be(1); + await PageObjects.dashboard.clickExitFullScreenLogoButton(); + await retry.try(async () => { + const isChromeVisible = await PageObjects.common.isChromeVisible(); + expect(isChromeVisible).to.be(true); + }); + await filterBar.removeFilter('bytes'); + }); }); } diff --git a/test/functional/apps/dashboard/index.js b/test/functional/apps/dashboard/index.js index 6666ccc57d584..bd8e6812147e1 100644 --- a/test/functional/apps/dashboard/index.js +++ b/test/functional/apps/dashboard/index.js @@ -74,6 +74,7 @@ export default function({ getService, loadTestFile }) { loadTestFile(require.resolve('./panel_expand_toggle')); loadTestFile(require.resolve('./dashboard_grid')); loadTestFile(require.resolve('./view_edit')); + loadTestFile(require.resolve('./dashboard_saved_query')); // Order of test suites *shouldn't* be important but there's a bug for the view_edit test above // https://github.com/elastic/kibana/issues/46752 // The dashboard_snapshot test below requires the timestamped URL which breaks the view_edit test. diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js index 76f3a3aea365f..9b50eeda20073 100644 --- a/test/functional/apps/discover/_saved_queries.js +++ b/test/functional/apps/discover/_saved_queries.js @@ -74,6 +74,7 @@ export default function({ getService, getPageObjects }) { true ); await savedQueryManagementComponent.savedQueryExistOrFail('OkResponse'); + await savedQueryManagementComponent.savedQueryTextExist('response:200'); }); it('reinstates filters and the time filter when a saved query has filters and a time filter included', async () => { diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index a20d7ae9a5372..b76ce141a4418 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -215,6 +215,8 @@ export function DashboardPageProvider({ getService, getPageObjects }: FtrProvide public async clickNewDashboard() { await listingTable.clickNewButton('createDashboardPromptButton'); + // make sure the dashboard page is shown + await this.waitForRenderComplete(); } public async clickCreateDashboardPrompt() { diff --git a/test/functional/services/saved_query_management_component.ts b/test/functional/services/saved_query_management_component.ts index 244c1cd214de5..66bf15f3da53c 100644 --- a/test/functional/services/saved_query_management_component.ts +++ b/test/functional/services/saved_query_management_component.ts @@ -151,6 +151,12 @@ export function SavedQueryManagementComponentProvider({ getService }: FtrProvide await testSubjects.existOrFail(`~load-saved-query-${title}-button`); } + async savedQueryTextExist(text: string) { + await this.openSavedQueryManagementComponent(); + const queryString = await queryBar.getQueryString(); + expect(queryString).to.eql(text); + } + async savedQueryMissingOrFail(title: string) { await retry.try(async () => { await this.openSavedQueryManagementComponent(); diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index c8715ac3447bd..8e5563e4ff674 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -8,7 +8,7 @@ "xpack.apm": ["legacy/plugins/apm", "plugins/apm"], "xpack.beatsManagement": "legacy/plugins/beats_management", "xpack.canvas": "legacy/plugins/canvas", - "xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication", + "xpack.crossClusterReplication": "plugins/cross_cluster_replication", "xpack.dashboardMode": "legacy/plugins/dashboard_mode", "xpack.data": "plugins/data_enhanced", "xpack.drilldowns": "plugins/drilldowns", diff --git a/x-pack/index.js b/x-pack/index.js index 1a78c24b1221b..43ae5c3e5c5dd 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -21,7 +21,6 @@ import { taskManager } from './legacy/plugins/task_manager'; import { rollup } from './legacy/plugins/rollup'; import { siem } from './legacy/plugins/siem'; import { remoteClusters } from './legacy/plugins/remote_clusters'; -import { crossClusterReplication } from './legacy/plugins/cross_cluster_replication'; import { upgradeAssistant } from './legacy/plugins/upgrade_assistant'; import { uptime } from './legacy/plugins/uptime'; import { encryptedSavedObjects } from './legacy/plugins/encrypted_saved_objects'; @@ -49,7 +48,6 @@ module.exports = function(kibana) { rollup(kibana), siem(kibana), remoteClusters(kibana), - crossClusterReplication(kibana), upgradeAssistant(kibana), uptime(kibana), encryptedSavedObjects(kibana), diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx index 0c04b7cccbd23..e5c20b260e097 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.test.tsx @@ -35,7 +35,7 @@ const data = [ ]; describe('CustomLink', () => { - let callApmApiSpy: Function; + let callApmApiSpy: jasmine.Spy; beforeAll(() => { callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); }); @@ -101,7 +101,7 @@ describe('CustomLink', () => { ]); }); - it('checks if create custom link button is available and working', () => { + it('checks if create custom link button is available and working', async () => { const { queryByText, getByText } = render( @@ -113,6 +113,7 @@ describe('CustomLink', () => { act(() => { fireEvent.click(getByText('Create custom link')); }); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); expect(queryByText('Create link')).toBeInTheDocument(); }); }); @@ -144,8 +145,10 @@ describe('CustomLink', () => { act(() => { fireEvent.click(component.getByText('Create custom link')); }); - await wait(() => component.queryByText('Create link')); - expect(component.queryByText('Create link')).toBeInTheDocument(); + await wait(() => + expect(component.queryByText('Create link')).toBeInTheDocument() + ); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); return component; }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx index ce42bd3e39ad1..8dc2076eab5b5 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/TransactionActionMenu/__test__/TransactionActionMenu.test.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { render, fireEvent, act } from '@testing-library/react'; +import { render, fireEvent, act, wait } from '@testing-library/react'; import { TransactionActionMenu } from '../TransactionActionMenu'; import { Transaction } from '../../../../../../../../plugins/apm/typings/es_schemas/ui/transaction'; import * as Transactions from './mockData'; @@ -143,8 +143,9 @@ describe('TransactionActionMenu component', () => { }); describe('Custom links', () => { + let callApmApiSpy: jasmine.Spy; beforeAll(() => { - spyOn(apmApi, 'callApmApi').and.returnValue({}); + callApmApiSpy = spyOn(apmApi, 'callApmApi').and.returnValue({}); }); afterAll(() => { jest.resetAllMocks(); @@ -257,7 +258,7 @@ describe('TransactionActionMenu component', () => { }); expectTextsInDocument(component, ['Custom Links']); }); - it('opens flyout with filters prefilled', () => { + it('opens flyout with filters prefilled', async () => { const license = new License({ signature: 'test signature', license: { @@ -287,6 +288,7 @@ describe('TransactionActionMenu component', () => { fireEvent.click(component.getByText('Create custom link')); }); expectTextsInDocument(component, ['Create link']); + await wait(() => expect(callApmApiSpy).toHaveBeenCalled()); const getFilterKeyValue = (key: string) => { return { [(component.getAllByText(key)[0] as HTMLOptionElement) diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts deleted file mode 100644 index 0a948793e07db..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/base_path.ts +++ /dev/null @@ -1,11 +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. - */ - -export const BASE_PATH = '/management/elasticsearch/cross_cluster_replication'; -export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; -export const API_BASE_PATH = '/api/cross_cluster_replication'; -export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; -export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts b/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts deleted file mode 100644 index 0993a74c8f1fd..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/settings.ts +++ /dev/null @@ -1,18 +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. - */ - -export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { - maxReadRequestOperationCount: 5120, - maxOutstandingReadRequests: 12, - maxReadRequestSize: '32mb', - maxWriteRequestOperationCount: 5120, - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: 9, - maxWriteBufferCount: 2147483647, - maxWriteBufferSize: '512mb', - maxRetryDelay: '500ms', - readPollTimeout: '1m', -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap b/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap deleted file mode 100644 index d001459e8234d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.js.snap +++ /dev/null @@ -1,128 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` -Object { - "leaderIndex": undefined, - "maxOutstandingReadRequests": undefined, - "maxOutstandingWriteRequests": undefined, - "maxReadRequestOperationCount": undefined, - "maxReadRequestSize": undefined, - "maxRetryDelay": undefined, - "maxWriteBufferCount": undefined, - "maxWriteBufferSize": undefined, - "maxWriteRequestOperationCount": undefined, - "maxWriteRequestSize": undefined, - "name": undefined, - "readPollTimeout": undefined, - "remoteCluster": undefined, - "shards": Array [ - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 1", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - Object { - "bytesReadCount": undefined, - "failedReadRequestsCount": undefined, - "failedWriteRequestsCount": undefined, - "followerGlobalCheckpoint": undefined, - "followerMappingVersion": undefined, - "followerMaxSequenceNum": undefined, - "followerSettingsVersion": undefined, - "id": "shard 2", - "lastRequestedSequenceNum": undefined, - "leaderGlobalCheckpoint": undefined, - "leaderIndex": undefined, - "leaderMaxSequenceNum": undefined, - "operationsReadCount": undefined, - "operationsWrittenCount": undefined, - "outstandingReadRequestsCount": undefined, - "outstandingWriteRequestsCount": undefined, - "readExceptions": undefined, - "remoteCluster": undefined, - "successfulReadRequestCount": undefined, - "successfulWriteRequestsCount": undefined, - "timeSinceLastReadMs": undefined, - "totalReadRemoteExecTimeMs": undefined, - "totalReadTimeMs": undefined, - "totalWriteTimeMs": undefined, - "writeBufferOperationsCount": undefined, - "writeBufferSizeBytes": undefined, - }, - ], - "status": "active", -} -`; - -exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` -Object { - "bytesReadCount": "bytes read", - "failedReadRequestsCount": "failed read requests", - "failedWriteRequestsCount": "failed write requests", - "followerGlobalCheckpoint": "follower global checkpoint", - "followerMappingVersion": "follower mapping version", - "followerMaxSequenceNum": "follower max seq no", - "followerSettingsVersion": "follower settings version", - "id": "shard id", - "lastRequestedSequenceNum": "last requested seq no", - "leaderGlobalCheckpoint": "leader global checkpoint", - "leaderIndex": "leader index", - "leaderMaxSequenceNum": "leader max seq no", - "operationsReadCount": "operations read", - "operationsWrittenCount": "operations written", - "outstandingReadRequestsCount": "outstanding read requests", - "outstandingWriteRequestsCount": "outstanding write requests", - "readExceptions": Array [ - "read exception", - ], - "remoteCluster": "remote cluster", - "successfulReadRequestCount": "successful read requests", - "successfulWriteRequestsCount": "successful write requests", - "timeSinceLastReadMs": "time since last read millis", - "totalReadRemoteExecTimeMs": "total read remote exec time millis", - "totalReadTimeMs": "total read time millis", - "totalWriteTimeMs": "total write time millis", - "writeBufferOperationsCount": "write buffer operation count", - "writeBufferSizeBytes": "write buffer size in bytes", -} -`; - -exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` -Object { - "leader_index": "leader index", - "max_outstanding_read_requests": "foo", - "max_outstanding_write_requests": "foo", - "max_read_request_operation_count": "foo", - "max_read_request_size": "foo", - "max_retry_delay": "foo", - "max_write_buffer_count": "foo", - "max_write_buffer_size": "foo", - "max_write_request_operation_count": "foo", - "max_write_request_size": "foo", - "read_poll_timeout": "foo", - "remote_cluster": "remote cluster", -} -`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js deleted file mode 100644 index ae13c625a7d80..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.js +++ /dev/null @@ -1,41 +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. - */ - -export const deserializeAutoFollowPattern = ( - { - name, - pattern: { - active, - // eslint-disable-next-line camelcase - remote_cluster, - // eslint-disable-next-line camelcase - leader_index_patterns, - // eslint-disable-next-line camelcase - follow_index_pattern, - }, - } = { - pattern: {}, - } -) => ({ - name, - active, - remoteCluster: remote_cluster, - leaderIndexPatterns: leader_index_patterns, - followIndexPattern: follow_index_pattern, -}); - -export const deserializeListAutoFollowPatterns = autoFollowPatterns => - autoFollowPatterns.map(deserializeAutoFollowPattern); - -export const serializeAutoFollowPattern = ({ - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}) => ({ - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js b/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js deleted file mode 100644 index e1df917d899ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.js +++ /dev/null @@ -1,175 +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 { - deserializeShard, - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, -} from './follower_index_serialization'; - -describe('[CCR] follower index serialization', () => { - describe('deserializeShard()', () => { - it('deserializes shard', () => { - const serializedShard = { - remote_cluster: 'remote cluster', - leader_index: 'leader index', - shard_id: 'shard id', - leader_global_checkpoint: 'leader global checkpoint', - leader_max_seq_no: 'leader max seq no', - follower_global_checkpoint: 'follower global checkpoint', - follower_max_seq_no: 'follower max seq no', - last_requested_seq_no: 'last requested seq no', - outstanding_read_requests: 'outstanding read requests', - outstanding_write_requests: 'outstanding write requests', - write_buffer_operation_count: 'write buffer operation count', - write_buffer_size_in_bytes: 'write buffer size in bytes', - follower_mapping_version: 'follower mapping version', - follower_settings_version: 'follower settings version', - total_read_time_millis: 'total read time millis', - total_read_remote_exec_time_millis: 'total read remote exec time millis', - successful_read_requests: 'successful read requests', - failed_read_requests: 'failed read requests', - operations_read: 'operations read', - bytes_read: 'bytes read', - total_write_time_millis: 'total write time millis', - successful_write_requests: 'successful write requests', - failed_write_requests: 'failed write requests', - operations_written: 'operations written', - read_exceptions: ['read exception'], - time_since_last_read_millis: 'time since last read millis', - }; - - expect(deserializeShard(serializedShard)).toMatchSnapshot(); - }); - }); - - describe('deserializeFollowerIndex()', () => { - it('deserializes Elasticsearch follower index object', () => { - const serializedFollowerIndex = { - index: 'follower index name', - status: 'active', - shards: [ - { - shard_id: 'shard 1', - }, - { - shard_id: 'shard 2', - }, - ], - }; - - expect(deserializeFollowerIndex(serializedFollowerIndex)).toMatchSnapshot(); - }); - }); - - describe('deserializeListFollowerIndices()', () => { - it('deserializes list of Elasticsearch follower index objects', () => { - const serializedFollowerIndexList = [ - { - follower_index: 'follower index 1', - remote_cluster: 'cluster 1', - leader_index: 'leader 1', - status: 'active', - parameters: { - max_read_request_operation_count: 1, - max_outstanding_read_requests: 1, - max_read_request_size: 1, - max_write_request_operation_count: 1, - max_write_request_size: 1, - max_outstanding_write_requests: 1, - max_write_buffer_count: 1, - max_write_buffer_size: 1, - max_retry_delay: 1, - read_poll_timeout: 1, - }, - shards: [], - }, - { - follower_index: 'follower index 2', - remote_cluster: 'cluster 2', - leader_index: 'leader 2', - status: 'paused', - parameters: { - max_read_request_operation_count: 2, - max_outstanding_read_requests: 2, - max_read_request_size: 2, - max_write_request_operation_count: 2, - max_write_request_size: 2, - max_outstanding_write_requests: 2, - max_write_buffer_count: 2, - max_write_buffer_size: 2, - max_retry_delay: 2, - read_poll_timeout: 2, - }, - shards: [], - }, - ]; - - const deserializedFollowerIndexList = [ - { - name: 'follower index 1', - remoteCluster: 'cluster 1', - leaderIndex: 'leader 1', - status: 'active', - maxReadRequestOperationCount: 1, - maxOutstandingReadRequests: 1, - maxReadRequestSize: 1, - maxWriteRequestOperationCount: 1, - maxWriteRequestSize: 1, - maxOutstandingWriteRequests: 1, - maxWriteBufferCount: 1, - maxWriteBufferSize: 1, - maxRetryDelay: 1, - readPollTimeout: 1, - shards: [], - }, - { - name: 'follower index 2', - remoteCluster: 'cluster 2', - leaderIndex: 'leader 2', - status: 'paused', - maxReadRequestOperationCount: 2, - maxOutstandingReadRequests: 2, - maxReadRequestSize: 2, - maxWriteRequestOperationCount: 2, - maxWriteRequestSize: 2, - maxOutstandingWriteRequests: 2, - maxWriteBufferCount: 2, - maxWriteBufferSize: 2, - maxRetryDelay: 2, - readPollTimeout: 2, - shards: [], - }, - ]; - - expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( - deserializedFollowerIndexList - ); - }); - }); - - describe('serializeFollowerIndex()', () => { - it('serializes object to Elasticsearch follower index object', () => { - const deserializedFollowerIndex = { - remoteCluster: 'remote cluster', - leaderIndex: 'leader index', - maxReadRequestOperationCount: 'foo', - maxOutstandingReadRequests: 'foo', - maxReadRequestSize: 'foo', - maxWriteRequestOperationCount: 'foo', - maxWriteRequestSize: 'foo', - maxOutstandingWriteRequests: 'foo', - maxWriteBufferCount: 'foo', - maxWriteBufferSize: 'foo', - maxRetryDelay: 'foo', - readPollTimeout: 'foo', - }; - - expect(serializeFollowerIndex(deserializedFollowerIndex)).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js deleted file mode 100644 index 804fe80cd27b4..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/auto_follow_pattern.js +++ /dev/null @@ -1,49 +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 { getRandomString } from '../../../../test_utils'; - -export const getAutoFollowPatternMock = ( - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [getRandomString()], - followIndexPattern = getRandomString() -) => ({ - name, - pattern: { - remote_cluster: remoteCluster, - leader_index_patterns: leaderIndexPatterns, - follow_index_pattern: followIndexPattern, - }, -}); - -export const getAutoFollowPatternListMock = (total = 3) => { - const list = { - patterns: [], - }; - - let i = total; - while (i--) { - list.patterns.push(getAutoFollowPatternMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- -export const getAutoFollowPatternClientMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndexPatterns = [`${getRandomString()}-*`], - followIndexPattern = getRandomString(), -}) => ({ - name, - remoteCluster, - leaderIndexPatterns, - followIndexPattern, -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js deleted file mode 100644 index a042375e82715..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/es_errors.js +++ /dev/null @@ -1,45 +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. - */ - -/** - * Errors mocks to throw during development to help visualizing - * the different flows in the UI - * - * TODO: Consult the ES team and make sure the error shapes are correct - * for each statusCode. - */ - -const error400 = new Error('Something went wrong'); -error400.statusCode = 400; -error400.response = ` - { - "error": { - "root_cause": [ - { - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - } - ], - "type": "x_content_parse_exception", - "reason": "[2:3] [put_auto_follow_pattern_request] unknown field [remote_clusterxxxxx], parser not found" - }, - "status": 400 -}`; - -const error403 = new Error('Unauthorized'); -error403.statusCode = 403; -error403.response = ` - { - "acknowledged": true, - "trial_was_started": false, - "error_message": "Operation failed: Trial was already activated." - } -`; - -export const esErrors = { - 400: error400, - 403: error403, -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js deleted file mode 100644 index 6c535a665978c..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/follower_index.js +++ /dev/null @@ -1,216 +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. - */ - -const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies -const chance = new Chance(); -import { getRandomString } from '../../../../test_utils'; - -const serializeShard = ({ - id, - remoteCluster, - leaderIndex, - leaderGlobalCheckpoint, - leaderMaxSequenceNum, - followerGlobalCheckpoint, - followerMaxSequenceNum, - lastRequestedSequenceNum, - outstandingReadRequestsCount, - outstandingWriteRequestsCount, - writeBufferOperationsCount, - writeBufferSizeBytes, - followerMappingVersion, - followerSettingsVersion, - totalReadTimeMs, - totalReadRemoteExecTimeMs, - successfulReadRequestCount, - failedReadRequestsCount, - operationsReadCount, - bytesReadCount, - totalWriteTimeMs, - successfulWriteRequestsCount, - failedWriteRequestsCount, - operationsWrittenCount, - readExceptions, - timeSinceLastReadMs, -}) => ({ - shard_id: id, - remote_cluster: remoteCluster, - leader_index: leaderIndex, - leader_global_checkpoint: leaderGlobalCheckpoint, - leader_max_seq_no: leaderMaxSequenceNum, - follower_global_checkpoint: followerGlobalCheckpoint, - follower_max_seq_no: followerMaxSequenceNum, - last_requested_seq_no: lastRequestedSequenceNum, - outstanding_read_requests: outstandingReadRequestsCount, - outstanding_write_requests: outstandingWriteRequestsCount, - write_buffer_operation_count: writeBufferOperationsCount, - write_buffer_size_in_bytes: writeBufferSizeBytes, - follower_mapping_version: followerMappingVersion, - follower_settings_version: followerSettingsVersion, - total_read_time_millis: totalReadTimeMs, - total_read_remote_exec_time_millis: totalReadRemoteExecTimeMs, - successful_read_requests: successfulReadRequestCount, - failed_read_requests: failedReadRequestsCount, - operations_read: operationsReadCount, - bytes_read: bytesReadCount, - total_write_time_millis: totalWriteTimeMs, - successful_write_requests: successfulWriteRequestsCount, - failed_write_requests: failedWriteRequestsCount, - operations_written: operationsWrittenCount, - read_exceptions: readExceptions, - time_since_last_read_millis: timeSinceLastReadMs, -}); - -export const getFollowerIndexStatsMock = ( - name = chance.string(), - shards = [ - { - id: chance.string(), - remoteCluster: chance.string(), - leaderIndex: chance.string(), - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [chance.string()], - timeSinceLastReadMs: chance.integer(), - }, - ] -) => ({ - index: name, - shards: shards.map(serializeShard), -}); - -export const getFollowerIndexListStatsMock = (total = 3, names) => { - const list = { - follow_stats: { - indices: [], - }, - }; - - for (let i = 0; i < total; i++) { - list.follow_stats.indices.push(getFollowerIndexStatsMock(names[i])); - } - - return list; -}; - -export const getFollowerIndexInfoMock = ( - name = chance.string(), - status = chance.string(), - parameters = { - maxReadRequestOperationCount: chance.string(), - maxOutstandingReadRequests: chance.string(), - maxReadRequestSize: chance.string(), - maxWriteRequestOperationCount: chance.string(), - maxWriteRequestSize: chance.string(), - maxOutstandingWriteRequests: chance.string(), - maxWriteBufferCount: chance.string(), - maxWriteBufferSize: chance.string(), - maxRetryDelay: chance.string(), - readPollTimeout: chance.string(), - } -) => { - return { - follower_index: name, - status, - max_read_request_operation_count: parameters.maxReadRequestOperationCount, - max_outstanding_read_requests: parameters.maxOutstandingReadRequests, - max_read_request_size: parameters.maxReadRequestSize, - max_write_request_operation_count: parameters.maxWriteRequestOperationCount, - max_write_request_size: parameters.maxWriteRequestSize, - max_outstanding_write_requests: parameters.maxOutstandingWriteRequests, - max_write_buffer_count: parameters.maxWriteBufferCount, - max_write_buffer_size: parameters.maxWriteBufferSize, - max_retry_delay: parameters.maxRetryDelay, - read_poll_timeout: parameters.readPollTimeout, - }; -}; - -export const getFollowerIndexListInfoMock = (total = 3) => { - const list = { - follower_indices: [], - }; - - for (let i = 0; i < total; i++) { - list.follower_indices.push(getFollowerIndexInfoMock()); - } - - return list; -}; - -// ----------------- -// Client test mock -// ----------------- - -export const getFollowerIndexMock = ({ - name = getRandomString(), - remoteCluster = getRandomString(), - leaderIndex = getRandomString(), - status = 'Active', -} = {}) => ({ - name, - remoteCluster, - leaderIndex, - status, - maxReadRequestOperationCount: chance.integer(), - maxOutstandingReadRequests: chance.integer(), - maxReadRequestSize: getRandomString({ length: 5 }), - maxWriteRequestOperationCount: chance.integer(), - maxWriteRequestSize: '9223372036854775807b', - maxOutstandingWriteRequests: chance.integer(), - maxWriteBufferCount: chance.integer(), - maxWriteBufferSize: getRandomString({ length: 5 }), - maxRetryDelay: getRandomString({ length: 5 }), - readPollTimeout: getRandomString({ length: 5 }), - shards: [ - { - id: 0, - remoteCluster: remoteCluster, - leaderIndex: leaderIndex, - leaderGlobalCheckpoint: chance.integer(), - leaderMaxSequenceNum: chance.integer(), - followerGlobalCheckpoint: chance.integer(), - followerMaxSequenceNum: chance.integer(), - lastRequestedSequenceNum: chance.integer(), - outstandingReadRequestsCount: chance.integer(), - outstandingWriteRequestsCount: chance.integer(), - writeBufferOperationsCount: chance.integer(), - writeBufferSizeBytes: chance.integer(), - followerMappingVersion: chance.integer(), - followerSettingsVersion: chance.integer(), - totalReadTimeMs: chance.integer(), - totalReadRemoteExecTimeMs: chance.integer(), - successfulReadRequestCount: chance.integer(), - failedReadRequestsCount: chance.integer(), - operationsReadCount: chance.integer(), - bytesReadCount: chance.integer(), - totalWriteTimeMs: chance.integer(), - successfulWriteRequestsCount: chance.integer(), - failedWriteRequestsCount: chance.integer(), - operationsWrittenCount: chance.integer(), - readExceptions: [], - timeSinceLastReadMs: chance.integer(), - }, - ], -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js b/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js deleted file mode 100644 index ccfdf8b19f3ee..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/fixtures/index.js +++ /dev/null @@ -1,16 +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. - */ - -export { getAutoFollowPatternMock, getAutoFollowPatternListMock } from './auto_follow_pattern'; - -export { esErrors } from './es_errors'; - -export { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from './follower_index'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/index.js b/x-pack/legacy/plugins/cross_cluster_replication/index.js deleted file mode 100644 index aff4cc5b56738..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/index.js +++ /dev/null @@ -1,57 +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 { resolve } from 'path'; -import { PLUGIN } from './common/constants'; -import { plugin } from './server/np_ready'; - -export function crossClusterReplication(kibana) { - return new kibana.Plugin({ - id: PLUGIN.ID, - configPrefix: 'xpack.ccr', - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main', 'remoteClusters', 'index_management'], - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - managementSections: ['plugins/cross_cluster_replication'], - injectDefaultVars(server) { - const config = server.config(); - return { - ccrUiEnabled: - config.get('xpack.ccr.ui.enabled') && config.get('xpack.remote_clusters.ui.enabled'), - }; - }, - }, - - config(Joi) { - return Joi.object({ - // display menu item - ui: Joi.object({ - enabled: Joi.boolean().default(true), - }).default(), - - // enable plugin - enabled: Joi.boolean().default(true), - }).default(); - }, - isEnabled(config) { - return ( - config.get('xpack.ccr.enabled') && - config.get('xpack.index_management.enabled') && - config.get('xpack.remote_clusters.enabled') - ); - }, - init: function initCcrPlugin(server) { - plugin({}).setup(server.newPlatform.setup.core, { - indexManagement: server.newPlatform.setup.plugins.indexManagement, - __LEGACY: { - server, - ccrUIEnabled: server.config().get('xpack.ccr.ui.enabled'), - }, - }); - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss deleted file mode 100644 index 31317e16e3e9f..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -// Cross-Cluster Replication plugin styles - -// Prefix all styles with "ccr" to avoid conflicts. -// Examples -// ccrChart -// ccrChart__legend -// ccrChart__legend--small -// ccrChart__legend-isLoading - -@import 'np_ready/app/app'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html b/x-pack/legacy/plugins/cross_cluster_replication/public/main.html deleted file mode 100644 index 2129f26267827..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/main.html +++ /dev/null @@ -1,3 +0,0 @@ - -
-
diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss deleted file mode 100644 index 5ee862b1d9e44..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/_app.scss +++ /dev/null @@ -1,14 +0,0 @@ -.ccrFollowerIndicesFormRow { - padding-bottom: 0; -} - -.ccrFollowerIndicesHelpText { - transform: translateY(-3px); -} - -/** - * 1. Prevent context menu popover appearing above confirmation modal - */ -.ccrFollowerIndicesDetailPanel { - z-index: $euiZMask - 1; /* 1 */ -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js deleted file mode 100644 index cc81fce4eebe7..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/index.js +++ /dev/null @@ -1,25 +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 from 'react'; -import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { HashRouter } from 'react-router-dom'; - -import { App } from './app'; -import { ccrStore } from './store'; - -export const renderReact = async (elem, I18nContext) => { - render( - - - - - - - , - elem - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts deleted file mode 100644 index f17926d2bee10..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/documentation_links.ts +++ /dev/null @@ -1,22 +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. - */ - -let esBase: string; - -export const setDocLinks = ({ - DOC_LINK_VERSION, - ELASTIC_WEBSITE_URL, -}: { - ELASTIC_WEBSITE_URL: string; - DOC_LINK_VERSION: string; -}) => { - esBase = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}`; -}; - -export const getAutoFollowPatternUrl = () => `${esBase}/ccr-put-auto-follow-pattern.html`; -export const getFollowerIndexUrl = () => `${esBase}/ccr-put-follow.html`; -export const getByteUnitsUrl = () => `${esBase}/common-options.html#byte-units`; -export const getTimeUnitsUrl = () => `${esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts deleted file mode 100644 index 5e1c3e9e99437..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/notifications.ts +++ /dev/null @@ -1,20 +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 { NotificationsSetup, IToasts, FatalErrorsSetup } from 'src/core/public'; - -let _notifications: IToasts; -let _fatalErrors: FatalErrorsSetup; - -export const setNotifications = ( - notifications: NotificationsSetup, - fatalErrorsSetup: FatalErrorsSetup -) => { - _notifications = notifications.toasts; - _fatalErrors = fatalErrorsSetup; -}; - -export const getNotifications = () => _notifications; -export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js deleted file mode 100644 index 36b9c185b487d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/track_ui_metric.js +++ /dev/null @@ -1,27 +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 { - createUiStatsReporter, - METRIC_TYPE, -} from '../../../../../../../../src/legacy/core_plugins/ui_metric/public'; -import { UIM_APP_NAME } from '../constants'; - -export const trackUiMetric = createUiStatsReporter(UIM_APP_NAME); -export { METRIC_TYPE }; -/** - * Transparently return provided request Promise, while allowing us to track - * a successful completion of the request. - */ -export function trackUserRequest(request, actionType) { - // Only track successful actions. - return request.then(response => { - trackUiMetric(METRIC_TYPE.LOADED, actionType); - // We return the response immediately without waiting for the tracking request to resolve, - // to avoid adding additional latency. - return response; - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts deleted file mode 100644 index 4ffe0db4e3c4e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/extend_index_management.ts +++ /dev/null @@ -1,28 +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 { i18n } from '@kbn/i18n'; -import { get } from 'lodash'; -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -const propertyPath = 'isFollowerIndex'; - -const followerBadgeExtension = { - matchIndex: (index: any) => { - return get(index, propertyPath); - }, - label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { - defaultMessage: 'Follower', - }), - color: 'default', - filterExpression: 'isFollowerIndex:true', -}; - -export const extendIndexManagement = (indexManagement?: IndexManagementPluginSetup) => { - if (indexManagement) { - indexManagement.extensionsService.addBadge(followerBadgeExtension); - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts deleted file mode 100644 index 46259c698b282..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/plugin.ts +++ /dev/null @@ -1,44 +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 { - ChromeBreadcrumb, - CoreSetup, - Plugin, - PluginInitializerContext, - DocLinksStart, -} from 'src/core/public'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/public'; - -// @ts-ignore; -import { setHttpClient } from './app/services/api'; -import { setBreadcrumbSetter } from './app/services/breadcrumbs'; -import { setDocLinks } from './app/services/documentation_links'; -import { setNotifications } from './app/services/notifications'; -import { extendIndexManagement } from './extend_index_management'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - docLinks: DocLinksStart; - }; -} - -export class CrossClusterReplicationUIPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http, notifications, fatalErrors }: CoreSetup, deps: PluginDependencies) { - setHttpClient(http); - setBreadcrumbSetter(deps); - setDocLinks(deps.__LEGACY.docLinks); - setNotifications(notifications, fatalErrors); - extendIndexManagement(deps.indexManagement); - } - - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js b/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js deleted file mode 100644 index 838939f46e523..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/register_routes.js +++ /dev/null @@ -1,94 +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 { unmountComponentAtNode } from 'react-dom'; -import chrome from 'ui/chrome'; -import { management, MANAGEMENT_BREADCRUMB } from 'ui/management'; -import { npSetup, npStart } from 'ui/new_platform'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { i18n } from '@kbn/i18n'; - -import template from './main.html'; -import { BASE_PATH } from '../common/constants'; - -import { plugin } from './np_ready'; - -/** - * TODO: When this file is deleted, use the management section for rendering - */ -import { renderReact } from './np_ready/app'; - -const isAvailable = xpackInfo.get('features.crossClusterReplication.isAvailable'); -const isActive = xpackInfo.get('features.crossClusterReplication.isActive'); -const isLicenseOK = isAvailable && isActive; -const isCcrUiEnabled = chrome.getInjected('ccrUiEnabled'); - -if (isLicenseOK && isCcrUiEnabled) { - const esSection = management.getSection('elasticsearch'); - - esSection.register('ccr', { - visible: true, - display: i18n.translate('xpack.crossClusterReplication.appTitle', { - defaultMessage: 'Cross-Cluster Replication', - }), - order: 4, - url: `#${BASE_PATH}`, - }); - - let elem; - - const CCR_REACT_ROOT = 'ccrReactRoot'; - - plugin({}).setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - docLinks: npStart.core.docLinks, - MANAGEMENT_BREADCRUMB, - }, - }); - - const unmountReactApp = () => elem && unmountComponentAtNode(elem); - - routes.when(`${BASE_PATH}/:section?/:subsection?/:view?/:id?`, { - template, - controllerAs: 'ccr', - controller: class CrossClusterReplicationController { - constructor($scope, $route) { - // React-router's does not play well with the angular router. It will cause this controller - // to re-execute without the $destroy handler being called. This means that the app will be mounted twice - // creating a memory leak when leaving (only 1 app will be unmounted). - // To avoid this, we unmount the React app each time we enter the controller. - unmountReactApp(); - - $scope.$$postDigest(() => { - elem = document.getElementById(CCR_REACT_ROOT); - renderReact(elem, npStart.core.i18n.Context); - - // Angular Lifecycle - const appRoute = $route.current; - const stopListeningForLocationChange = $scope.$on('$locationChangeSuccess', () => { - const currentRoute = $route.current; - const isNavigationInApp = currentRoute.$$route.template === appRoute.$$route.template; - - // When we navigate within CCR, prevent Angular from re-matching the route and rebuild the app - if (isNavigationInApp) { - $route.current = appRoute; - } else { - // Any clean up when User leaves the CCR - } - - $scope.$on('$destroy', () => { - stopListeningForLocationChange && stopListeningForLocationChange(); - unmountReactApp(); - }); - }); - }); - } - }, - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts deleted file mode 100644 index ae15073b979e1..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/cross_cluster_replication_data.ts +++ /dev/null @@ -1,36 +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 { APICaller } from 'src/core/server'; -import { Index } from '../../../../../plugins/index_management/server'; - -export const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { - if (!indicesList?.length) { - return indicesList; - } - const params = { - path: '/_all/_ccr/info', - method: 'GET', - }; - try { - const { follower_indices: followerIndices } = await callWithRequest( - 'transport.request', - params - ); - return indicesList.map(index => { - const isFollowerIndex = !!followerIndices.find( - (followerIndex: { follower_index: string }) => { - return followerIndex.follower_index === index.name; - } - ); - return { - ...index, - isFollowerIndex, - }; - }); - } catch (e) { - return indicesList; - } -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js deleted file mode 100644 index 99d72ce1a0e6e..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/call_with_request_factory.js +++ /dev/null @@ -1,20 +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 { once } from 'lodash'; -import { elasticsearchJsPlugin } from '../../client/elasticsearch_ccr'; - -const callWithRequest = once(server => { - const config = { plugins: [elasticsearchJsPlugin] }; - const cluster = server.plugins.elasticsearch.createCluster('ccr', config); - return cluster.callWithRequest; -}); - -export const callWithRequestFactory = (server, request) => { - return (...args) => { - return callWithRequest(server)(request, ...args); - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js deleted file mode 100644 index 787814d87dff9..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/call_with_request_factory/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { callWithRequestFactory } from './call_with_request_factory'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js deleted file mode 100644 index 6cf12896fa472..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/check_license.js +++ /dev/null @@ -1,70 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo) { - const pluginName = 'Cross-Cluster Replication'; - - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - isAvailable: false, - showLinks: true, - enableLinks: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnavailableMessage', - { - defaultMessage: - 'You cannot use {pluginName} because license information is not available at this time.', - values: { pluginName }, - } - ), - }; - } - - const VALID_LICENSE_MODES = ['trial', 'platinum', 'enterprise']; - - const isLicenseModeValid = xpackLicenseInfo.license.isOneOf(VALID_LICENSE_MODES); - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseModeValid) { - return { - isAvailable: false, - isActive: false, - message: i18n.translate( - 'xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage', - { - defaultMessage: - 'Your {licenseType} license does not support {pluginName}. Please upgrade your license.', - values: { licenseType, pluginName }, - } - ), - }; - } - - // License is valid but not active - if (!isLicenseActive) { - return { - isAvailable: true, - isActive: false, - message: i18n.translate('xpack.crossClusterReplication.checkLicense.errorExpiredMessage', { - defaultMessage: - 'You cannot use {pluginName} because your {licenseType} license has expired', - values: { licenseType, pluginName }, - }), - }; - } - - // License is valid and active - return { - isAvailable: true, - isActive: true, - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js deleted file mode 100644 index 11a6fd4e1d816..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/__tests__/wrap_es_error.test.js +++ /dev/null @@ -1,33 +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 expect from '@kbn/expect'; -import { wrapEsError } from '../wrap_es_error'; - -describe('wrap_es_error', () => { - describe('#wrapEsError', () => { - let originalError; - beforeEach(() => { - originalError = new Error('I am an error'); - originalError.statusCode = 404; - originalError.response = '{}'; - }); - - it('should return the correct object', () => { - const wrappedError = wrapEsError(originalError); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be(originalError.message); - }); - - it('should return the correct object with custom message', () => { - const wrappedError = wrapEsError(originalError, { 404: 'No encontrado!' }); - - expect(wrappedError.statusCode).to.be(originalError.statusCode); - expect(wrappedError.message).to.be('No encontrado!'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js deleted file mode 100644 index 5f2141cce9395..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/__tests__/is_es_error_factory.js +++ /dev/null @@ -1,44 +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 expect from '@kbn/expect'; -import { isEsErrorFactory } from '../is_es_error_factory'; -import { set } from 'lodash'; - -class MockAbstractEsError {} - -describe('is_es_error_factory', () => { - let mockServer; - let isEsError; - - beforeEach(() => { - const mockEsErrors = { - _Abstract: MockAbstractEsError, - }; - mockServer = {}; - set(mockServer, 'plugins.elasticsearch.getCluster', () => ({ errors: mockEsErrors })); - - isEsError = isEsErrorFactory(mockServer); - }); - - describe('#isEsErrorFactory', () => { - it('should return a function', () => { - expect(isEsError).to.be.a(Function); - }); - - describe('returned function', () => { - it('should return true if passed-in err is a known esError', () => { - const knownEsError = new MockAbstractEsError(); - expect(isEsError(knownEsError)).to.be(true); - }); - - it('should return false if passed-in err is not a known esError', () => { - const unknownEsError = {}; - expect(isEsError(unknownEsError)).to.be(false); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts deleted file mode 100644 index fc6405b8e7513..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/is_es_error_factory.ts +++ /dev/null @@ -1,18 +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 { memoize } from 'lodash'; - -const esErrorsFactory = memoize((server: any) => { - return server.plugins.elasticsearch.getCluster('admin').errors; -}); - -export function isEsErrorFactory(server: any) { - const esErrors = esErrorsFactory(server); - return function isEsError(err: any) { - return err instanceof esErrors._Abstract; - }; -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts deleted file mode 100644 index d22505f0e315a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/__jest__/license_pre_routing_factory.test.ts +++ /dev/null @@ -1,64 +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 { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; -import { licensePreRoutingFactory } from '../license_pre_routing_factory'; - -describe('license_pre_routing_factory', () => { - describe('#reportingFeaturePreRoutingFactory', () => { - let mockDeps: any; - let mockLicenseCheckResults: any; - - const anyContext: any = {}; - const anyRequest: any = {}; - - beforeEach(() => { - mockDeps = { - __LEGACY: { - server: { - plugins: { - xpack_main: { - info: { - feature: () => ({ - getLicenseCheckResults: () => mockLicenseCheckResults, - }), - }, - }, - }, - }, - }, - requestHandler: jest.fn(), - }; - }); - - describe('isAvailable is false', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: false, - }; - }); - - it('replies with 403', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - const response = await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(response.status).toBe(403); - }); - }); - - describe('isAvailable is true', () => { - beforeEach(() => { - mockLicenseCheckResults = { - isAvailable: true, - }; - }); - - it('it calls the wrapped handler', async () => { - const licensePreRouting = licensePreRoutingFactory(mockDeps); - await licensePreRouting(anyContext, anyRequest, kibanaResponseFactory); - expect(mockDeps.requestHandler).toHaveBeenCalledTimes(1); - }); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts deleted file mode 100644 index c47faa940a650..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/license_pre_routing_factory.ts +++ /dev/null @@ -1,32 +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 { RequestHandler } from 'src/core/server'; -import { PLUGIN } from '../../../../common/constants'; - -export const licensePreRoutingFactory = ({ - __LEGACY, - requestHandler, -}: { - __LEGACY: { server: any }; - requestHandler: RequestHandler; -}) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - - // License checking and enable/disable logic - const licensePreRouting: RequestHandler = (ctx, request, response) => { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN.ID).getLicenseCheckResults(); - if (!licenseCheckResults.isAvailable) { - return response.forbidden({ - body: licenseCheckResults.message, - }); - } else { - return requestHandler(ctx, request, response); - } - }; - - return licensePreRouting; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js deleted file mode 100644 index 7b0f97c38d129..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/index.js +++ /dev/null @@ -1,7 +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. - */ - -export { registerLicenseChecker } from './register_license_checker'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js deleted file mode 100644 index b9bb34a80ce79..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/register_license_checker/register_license_checker.js +++ /dev/null @@ -1,21 +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 { mirrorPluginStatus } from '../../../../../../server/lib/mirror_plugin_status'; -import { PLUGIN } from '../../../../common/constants'; -import { checkLicense } from '../check_license'; - -export function registerLicenseChecker(__LEGACY) { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const ccrPluggin = __LEGACY.server.plugins[PLUGIN.ID]; - - mirrorPluginStatus(xpackMainPlugin, ccrPluggin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN.ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts deleted file mode 100644 index 829de10ad0177..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/plugin.ts +++ /dev/null @@ -1,38 +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 { Plugin, PluginInitializerContext, CoreSetup } from 'src/core/server'; - -import { IndexManagementPluginSetup } from '../../../../../plugins/index_management/server'; - -// @ts-ignore -import { registerLicenseChecker } from './lib/register_license_checker'; -// @ts-ignore -import { registerRoutes } from './routes/register_routes'; -import { ccrDataEnricher } from './cross_cluster_replication_data'; - -interface PluginDependencies { - indexManagement: IndexManagementPluginSetup; - __LEGACY: { - server: any; - ccrUIEnabled: boolean; - }; -} - -export class CrossClusterReplicationServerPlugin implements Plugin { - // @ts-ignore - constructor(private readonly ctx: PluginInitializerContext) {} - setup({ http }: CoreSetup, { indexManagement, __LEGACY }: PluginDependencies) { - registerLicenseChecker(__LEGACY); - - const router = http.createRouter(); - registerRoutes({ router, __LEGACY }); - if (__LEGACY.ccrUIEnabled && indexManagement && indexManagement.indexDataEnricher) { - indexManagement.indexDataEnricher.add(ccrDataEnricher); - } - } - start() {} -} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js deleted file mode 100644 index f3024515c7213..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/auto_follow_pattern.test.js +++ /dev/null @@ -1,330 +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 { deserializeAutoFollowPattern } from '../../../../../common/services/auto_follow_pattern_serialization'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { getAutoFollowPatternMock, getAutoFollowPatternListMock } from '../../../../../fixtures'; -import { registerAutoFollowPatternRoutes } from '../auto_follow_pattern'; - -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys(deserializeAutoFollowPattern(getAutoFollowPatternMock())); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a "server.route()" call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'create', - 2: 'update', - 3: 'get', - 4: 'delete', - 5: 'pause', - 6: 'resume', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - - registerAutoFollowPatternRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Auto Follow Pattern', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('should deserialize the response from Elasticsearch', async () => { - const totalResult = 2; - setHttpRequestResponse(null, getAutoFollowPatternListMock(totalResult)); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const autoFollowPattern = response.patterns[0]; - - expect(response.patterns.length).toEqual(totalResult); - expect(Object.keys(autoFollowPattern)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should throw a 409 conflict error if id already exists', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response.status).toEqual(409); - }); - - it('should return 200 status when the id does not exist', async () => { - const error = new Error('Resource not found.'); - error.statusCode = 404; - setHttpRequestResponse(error); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute( - routeHandler, - {}, - { - body: { - id: 'some-id', - foo: 'bar', - }, - } - ); - - expect(response).toEqual({ acknowledge: true }); - }); - }); - - describe('update()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().update; - }); - - it('should serialize the payload before sending it to Elasticsearch', async () => { - callWithRequestFactory.mockReturnValueOnce((_, payload) => payload); - - const request = { - params: { id: 'foo' }, - body: { - remoteCluster: 'bar1', - leaderIndexPatterns: ['bar2'], - followIndexPattern: 'bar3', - }, - }; - - const response = await callRoute(routeHandler, {}, request); - - expect(response.options.body).toEqual({ - id: 'foo', - body: { - remote_cluster: 'bar1', - leader_index_patterns: ['bar2'], - follow_index_pattern: 'bar3', - }, - }); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const autoFollowPattern = getAutoFollowPatternMock(); - const esResponse = { patterns: [autoFollowPattern] }; - - setHttpRequestResponse(null, esResponse); - - const response = await callRoute(routeHandler, {}, { params: { id: 1 } }); - expect(Object.keys(response.options.body)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('delete()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().delete; - }); - - it('should delete a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to delete', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsDeleted).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsDeleted).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsPaused).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsPaused).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('accept a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of items to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: 'a,b,c' } }); - - expect(response.options.body.itemsResumed).toEqual(['a', 'b', 'c']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: 'a,b' } }); - - expect(response.itemsResumed).toEqual(['a']); - expect(response.errors[0].id).toEqual('b'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js deleted file mode 100644 index f0139e5bd7011..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/follower_index.test.js +++ /dev/null @@ -1,312 +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 { deserializeFollowerIndex } from '../../../../../common/services/follower_index_serialization'; -import { - getFollowerIndexStatsMock, - getFollowerIndexListStatsMock, - getFollowerIndexInfoMock, - getFollowerIndexListInfoMock, -} from '../../../../../fixtures'; -import { callWithRequestFactory } from '../../../lib/call_with_request_factory'; -import { isEsErrorFactory } from '../../../lib/is_es_error_factory'; -import { registerFollowerIndexRoutes } from '../follower_index'; -import { createRouter, callRoute } from './helpers'; - -jest.mock('../../../lib/call_with_request_factory'); -jest.mock('../../../lib/is_es_error_factory'); -jest.mock('../../../lib/license_pre_routing_factory', () => ({ - licensePreRoutingFactory: ({ requestHandler }) => requestHandler, -})); - -const DESERIALIZED_KEYS = Object.keys( - deserializeFollowerIndex({ - ...getFollowerIndexInfoMock(), - ...getFollowerIndexStatsMock(), - }) -); - -let routeRegistry; - -/** - * Helper to extract all the different server route handler so we can easily call them in our tests. - * - * Important: This method registers the handlers in the order that they appear in the file, so - * if a 'server.route()' call is moved or deleted, then the HANDLER_INDEX_TO_ACTION must be updated here. - */ -const registerHandlers = () => { - const HANDLER_INDEX_TO_ACTION = { - 0: 'list', - 1: 'get', - 2: 'create', - 3: 'edit', - 4: 'pause', - 5: 'resume', - 6: 'unfollow', - }; - - routeRegistry = createRouter(HANDLER_INDEX_TO_ACTION); - registerFollowerIndexRoutes({ - __LEGACY: {}, - router: routeRegistry.router, - }); -}; - -/** - * Queue to save request response and errors - * It allows us to fake multiple responses from the - * callWithRequestFactory() when the request handler call it - * multiple times. - */ -let requestResponseQueue = []; - -/** - * Helper to mock the response from the call to Elasticsearch - * - * @param {*} err The mock error to throw - * @param {*} response The response to return - */ -const setHttpRequestResponse = (error, response) => { - requestResponseQueue.push({ error, response }); -}; - -const resetHttpRequestResponses = () => (requestResponseQueue = []); - -const getNextResponseFromQueue = () => { - if (!requestResponseQueue.length) { - return null; - } - - const next = requestResponseQueue.shift(); - if (next.error) { - return Promise.reject(next.error); - } - return Promise.resolve(next.response); -}; - -describe('[CCR API Routes] Follower Index', () => { - let routeHandler; - - beforeAll(() => { - isEsErrorFactory.mockReturnValue(() => false); - callWithRequestFactory.mockReturnValue(getNextResponseFromQueue); - registerHandlers(); - }); - - describe('list()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().list; - }); - - it('deserializes the response from Elasticsearch', async () => { - const totalResult = 2; - const infoResult = getFollowerIndexListInfoMock(totalResult); - const statsResult = getFollowerIndexListStatsMock( - totalResult, - infoResult.follower_indices.map(index => index.follower_index) - ); - setHttpRequestResponse(null, infoResult); - setHttpRequestResponse(null, statsResult); - - const { - options: { body: response }, - } = await callRoute(routeHandler); - const followerIndex = response.indices[0]; - - expect(response.indices.length).toEqual(totalResult); - expect(Object.keys(followerIndex)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('get()', () => { - beforeEach(() => { - routeHandler = routeRegistry.getRoutes().get; - }); - - it('should return a single resource even though ES return an array with 1 item', async () => { - const mockId = 'test1'; - const followerIndexInfo = getFollowerIndexInfoMock(mockId); - const followerIndexStats = getFollowerIndexStatsMock(mockId); - - setHttpRequestResponse(null, { follower_indices: [followerIndexInfo] }); - setHttpRequestResponse(null, { indices: [followerIndexStats] }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: mockId } }); - expect(Object.keys(response)).toEqual(DESERIALIZED_KEYS); - }); - }); - - describe('create()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().create; - }); - - it('should return 200 status when follower index is created', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute( - routeHandler, - {}, - { - body: { - name: 'follower_index', - remoteCluster: 'remote_cluster', - leaderIndex: 'leader_index', - }, - } - ); - - expect(response.options.body).toEqual({ acknowledge: true }); - }); - }); - - describe('pause()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().pause; - }); - - it('should pause a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to pause', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsPaused).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsPaused).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('resume()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().resume; - }); - - it('should resume a single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to resume', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsResumed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsResumed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); - - describe('unfollow()', () => { - beforeEach(() => { - resetHttpRequestResponses(); - routeHandler = routeRegistry.getRoutes().unfollow; - }); - - it('should unfollow await single item', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors).toEqual([]); - }); - - it('should accept a list of ids to unfollow', async () => { - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - - const response = await callRoute(routeHandler, {}, { params: { id: '1,2,3' } }); - - expect(response.options.body.itemsUnfollowed).toEqual(['1', '2', '3']); - }); - - it('should catch error and return them in array', async () => { - const error = new Error('something went wrong'); - error.response = '{ "error": {} }'; - - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(null, { acknowledge: true }); - setHttpRequestResponse(error); - - const { - options: { body: response }, - } = await callRoute(routeHandler, {}, { params: { id: '1,2' } }); - - expect(response.itemsUnfollowed).toEqual(['1']); - expect(response.errors[0].id).toEqual('2'); - }); - }); -}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts deleted file mode 100644 index 555fc0937c0ad..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/__jest__/helpers.ts +++ /dev/null @@ -1,37 +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 { RequestHandler } from 'src/core/server'; -import { kibanaResponseFactory } from '../../../../../../../../../src/core/server'; - -export const callRoute = ( - route: RequestHandler, - ctx = {}, - request = {}, - response = kibanaResponseFactory -) => { - return route(ctx as any, request as any, response); -}; - -export const createRouter = (indexToActionMap: Record) => { - let index = 0; - const routeHandlers: Record> = {}; - const addHandler = (ignoreCtxForNow: any, handler: RequestHandler) => { - // Save handler and increment index - routeHandlers[indexToActionMap[index]] = handler; - index++; - }; - - return { - getRoutes: () => routeHandlers, - router: { - get: addHandler, - post: addHandler, - put: addHandler, - delete: addHandler, - }, - }; -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts deleted file mode 100644 index d458f1ccb354b..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/auto_follow_pattern.ts +++ /dev/null @@ -1,301 +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 { schema } from '@kbn/config-schema'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { isEsError } from '../../lib/is_es_error'; -// @ts-ignore -import { - deserializeAutoFollowPattern, - deserializeListAutoFollowPatterns, - serializeAutoFollowPattern, - // @ts-ignore -} from '../../../../common/services/auto_follow_pattern_serialization'; - -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; -import { API_BASE_PATH } from '../../../../common/constants'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerAutoFollowPatternRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all auto-follow patterns - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const result = await callWithRequest('ccr.autoFollowPatterns'); - return response.ok({ - body: { - patterns: deserializeListAutoFollowPatterns(result.patterns), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create an auto-follow pattern - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns`, - validate: { - body: schema.object( - { - id: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id, ...rest } = request.body; - const body = serializeAutoFollowPattern(rest); - - /** - * First let's make sur that an auto-follow pattern with - * the same id does not exist. - */ - try { - await callWithRequest('ccr.autoFollowPattern', { id }); - // If we get here it means that an auto-follow pattern with the same id exists - return response.conflict({ - body: `An auto-follow pattern with the name "${id}" already exists.`, - }); - } catch (err) { - if (err.statusCode !== 404) { - return mapErrorToKibanaHttpResponse(err); - } - } - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Update an auto-follow pattern - */ - router.put( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - body: schema.object({}, { unknowns: 'allow' }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const body = serializeAutoFollowPattern(request.body); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveAutoFollowPattern', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single auto-follow pattern - */ - router.get( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const result = await callWithRequest('ccr.autoFollowPattern', { id }); - const autoFollowPattern = result.patterns[0]; - - return response.ok({ - body: deserializeAutoFollowPattern(autoFollowPattern), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Delete an auto-follow pattern - */ - router.delete( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsDeleted: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.deleteAutoFollowPattern', { id: _id }) - .then(() => itemsDeleted.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsDeleted, - errors, - }, - }); - }, - }) - ); - - /** - * Pause auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/pause`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseAutoFollowPattern', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resume auto-follow pattern(s) - */ - router.post( - { - path: `${API_BASE_PATH}/auto_follow_patterns/{id}/resume`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeAutoFollowPattern', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - if (isEsError(err)) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } else { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts deleted file mode 100644 index b08b056ad2c8a..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/ccr.ts +++ /dev/null @@ -1,112 +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 { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -// @ts-ignore -import { deserializeAutoFollowStats } from '../../lib/ccr_stats_serialization'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; -import { RouteDependencies } from '../types'; - -export const registerCcrRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns Auto-follow stats - */ - router.get( - { - path: `${API_BASE_PATH}/stats/auto_follow`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { auto_follow_stats: autoFollowStats } = await callWithRequest('ccr.stats'); - - return response.ok({ - body: deserializeAutoFollowStats(autoFollowStats), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns whether the user has CCR permissions - */ - router.get( - { - path: `${API_BASE_PATH}/permissions`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const xpackMainPlugin = __LEGACY.server.plugins.xpack_main; - const xpackInfo = xpackMainPlugin && xpackMainPlugin.info; - - if (!xpackInfo) { - // xpackInfo is updated via poll, so it may not be available until polling has begun. - // In this rare situation, tell the client the service is temporarily unavailable. - return response.customError({ - statusCode: 503, - body: 'Security info unavailable', - }); - } - - const securityInfo = xpackInfo && xpackInfo.isAvailable() && xpackInfo.feature('security'); - if (!securityInfo || !securityInfo.isAvailable() || !securityInfo.isEnabled()) { - // If security isn't enabled or available (in the case where security is enabled but license reverted to Basic) let the user use CCR. - return response.ok({ - body: { - hasPermission: true, - missingClusterPrivileges: [], - }, - }); - } - - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { has_all_requested: hasPermission, cluster } = await callWithRequest( - 'ccr.permissions', - { - body: { - cluster: ['manage', 'manage_ccr'], - }, - } - ); - - const missingClusterPrivileges = Object.keys(cluster).reduce( - (permissions: any, permissionName: any) => { - if (!cluster[permissionName]) { - permissions.push(permissionName); - return permissions; - } - }, - [] as any[] - ); - - return response.ok({ - body: { - hasPermission, - missingClusterPrivileges, - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts deleted file mode 100644 index 1d7dacf4a8688..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/api/follower_index.ts +++ /dev/null @@ -1,357 +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 { schema } from '@kbn/config-schema'; -import { - deserializeFollowerIndex, - deserializeListFollowerIndices, - serializeFollowerIndex, - serializeAdvancedSettings, - // @ts-ignore -} from '../../../../common/services/follower_index_serialization'; -import { API_BASE_PATH } from '../../../../common/constants'; -// @ts-ignore -import { removeEmptyFields } from '../../../../common/services/utils'; -// @ts-ignore -import { callWithRequestFactory } from '../../lib/call_with_request_factory'; -import { licensePreRoutingFactory } from '../../lib/license_pre_routing_factory'; - -import { RouteDependencies } from '../types'; -import { mapErrorToKibanaHttpResponse } from '../map_to_kibana_http_error'; - -export const registerFollowerIndexRoutes = ({ router, __LEGACY }: RouteDependencies) => { - /** - * Returns a list of all follower indices - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: false, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { - id: '_all', - }); - - const { - follow_stats: { indices: followerIndicesStats }, - } = await callWithRequest('ccr.stats'); - - const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { - map[stats.index] = stats; - return map; - }, {}); - - const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { - return { - ...followerIndex, - ...followerIndicesStatsMap[followerIndex.follower_index], - }; - }); - - return response.ok({ - body: { - indices: deserializeListFollowerIndices(collatedFollowerIndices), - }, - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Returns a single follower index pattern - */ - router.get( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ - id: schema.string(), - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - - const followerIndexInfo = followerIndices && followerIndices[0]; - - if (!followerIndexInfo) { - return response.notFound({ - body: `The follower index "${id}" does not exist.`, - }); - } - - // If this follower is paused, skip call to ES stats api since it will return 404 - if (followerIndexInfo.status === 'paused') { - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - }), - }); - } else { - const { - indices: followerIndicesStats, - } = await callWithRequest('ccr.followerIndexStats', { id }); - - return response.ok({ - body: deserializeFollowerIndex({ - ...followerIndexInfo, - ...(followerIndicesStats ? followerIndicesStats[0] : {}), - }), - }); - } - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Create a follower index - */ - router.post( - { - path: `${API_BASE_PATH}/follower_indices`, - validate: { - body: schema.object( - { - name: schema.string(), - }, - { unknowns: 'allow' } - ), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { name, ...rest } = request.body; - const body = removeEmptyFields(serializeFollowerIndex(rest)); - - try { - return response.ok({ - body: await callWithRequest('ccr.saveFollowerIndex', { name, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Edit a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}`, - validate: { - params: schema.object({ id: schema.string() }), - body: schema.object({ - maxReadRequestOperationCount: schema.maybe(schema.number()), - maxOutstandingReadRequests: schema.maybe(schema.number()), - maxReadRequestSize: schema.maybe(schema.string()), // byte value - maxWriteRequestOperationCount: schema.maybe(schema.number()), - maxWriteRequestSize: schema.maybe(schema.string()), // byte value - maxOutstandingWriteRequests: schema.maybe(schema.number()), - maxWriteBufferCount: schema.maybe(schema.number()), - maxWriteBufferSize: schema.maybe(schema.string()), // byte value - maxRetryDelay: schema.maybe(schema.string()), // time value - readPollTimeout: schema.maybe(schema.string()), // time value - }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - - // We need to first pause the follower and then resume it passing the advanced settings - try { - const { follower_indices: followerIndices } = await callWithRequest('ccr.info', { id }); - const followerIndexInfo = followerIndices && followerIndices[0]; - if (!followerIndexInfo) { - return response.notFound({ body: `The follower index "${id}" does not exist.` }); - } - - // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. - const isPaused = followerIndexInfo.status === 'paused'; - // Pause follower if not already paused - if (!isPaused) { - await callWithRequest('ccr.pauseFollowerIndex', { id }); - } - - // Resume follower - const body = removeEmptyFields(serializeAdvancedSettings(request.body)); - return response.ok({ - body: await callWithRequest('ccr.resumeFollowerIndex', { id, body }), - }); - } catch (err) { - return mapErrorToKibanaHttpResponse(err); - } - }, - }) - ); - - /** - * Pauses a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/pause`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsPaused: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.pauseFollowerIndex', { id: _id }) - .then(() => itemsPaused.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsPaused, - errors, - }, - }); - }, - }) - ); - - /** - * Resumes a follower index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/resume`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsResumed: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(_id => - callWithRequest('ccr.resumeFollowerIndex', { id: _id }) - .then(() => itemsResumed.push(_id)) - .catch((err: Error) => { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - }) - ) - ); - - return response.ok({ - body: { - itemsResumed, - errors, - }, - }); - }, - }) - ); - - /** - * Unfollow follower index's leader index - */ - router.put( - { - path: `${API_BASE_PATH}/follower_indices/{id}/unfollow`, - validate: { - params: schema.object({ id: schema.string() }), - }, - }, - licensePreRoutingFactory({ - __LEGACY, - requestHandler: async (ctx, request, response) => { - const callWithRequest = callWithRequestFactory(__LEGACY.server, request); - const { id } = request.params; - const ids = id.split(','); - - const itemsUnfollowed: string[] = []; - const itemsNotOpen: string[] = []; - const errors: Array<{ id: string; error: any }> = []; - - await Promise.all( - ids.map(async _id => { - try { - // Try to pause follower, let it fail silently since it may already be paused - try { - await callWithRequest('ccr.pauseFollowerIndex', { id: _id }); - } catch (e) { - // Swallow errors - } - - // Close index - await callWithRequest('indices.close', { index: _id }); - - // Unfollow leader - await callWithRequest('ccr.unfollowLeaderIndex', { id: _id }); - - // Try to re-open the index, store failures in a separate array to surface warnings in the UI - // This will allow users to query their index normally after unfollowing - try { - await callWithRequest('indices.open', { index: _id }); - } catch (e) { - itemsNotOpen.push(_id); - } - - // Push success - itemsUnfollowed.push(_id); - } catch (err) { - errors.push({ id: _id, error: mapErrorToKibanaHttpResponse(err) }); - } - }) - ); - - return response.ok({ - body: { - itemsUnfollowed, - itemsNotOpen, - errors, - }, - }); - }, - }) - ); -}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts b/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts deleted file mode 100644 index 6a81bd26dc47d..0000000000000 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/map_to_kibana_http_error.ts +++ /dev/null @@ -1,26 +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 { kibanaResponseFactory } from '../../../../../../../src/core/server'; -// @ts-ignore -import { wrapEsError } from '../lib/error_wrappers'; -import { isEsError } from '../lib/is_es_error'; - -export const mapErrorToKibanaHttpResponse = (err: any) => { - if (isEsError(err)) { - const { statusCode, message, body } = wrapEsError(err); - return kibanaResponseFactory.customError({ - statusCode, - body: { - message, - attributes: { - cause: body?.cause, - }, - }, - }); - } - return kibanaResponseFactory.internalError(err); -}; diff --git a/x-pack/legacy/plugins/maps/public/angular/map_controller.js b/x-pack/legacy/plugins/maps/public/angular/map_controller.js index 9522fd12ad37d..1b1fbf111fe04 100644 --- a/x-pack/legacy/plugins/maps/public/angular/map_controller.js +++ b/x-pack/legacy/plugins/maps/public/angular/map_controller.js @@ -39,6 +39,7 @@ import { replaceLayerList, setQuery, clearTransientLayerStateAndCloseFlyout, + setMapSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../plugins/maps/public/actions/map_actions'; import { @@ -52,10 +53,14 @@ import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails, + openMapSettings, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../plugins/maps/public/actions/ui_actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { getIsFullScreen } from '../../../../../plugins/maps/public/selectors/ui_selectors'; +import { + getIsFullScreen, + getFlyoutDisplay, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../plugins/maps/public/selectors/ui_selectors'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { copyPersistentState } from '../../../../../plugins/maps/public/reducers/util'; import { @@ -395,6 +400,9 @@ app.controller( if (mapState.filters) { savedObjectFilters = mapState.filters; } + if (mapState.settings) { + store.dispatch(setMapSettings(mapState.settings)); + } } if (savedMap.uiStateJSON) { @@ -453,6 +461,7 @@ app.controller( $scope.isFullScreen = false; $scope.isSaveDisabled = false; + $scope.isOpenSettingsDisabled = false; function handleStoreChanges(store) { const nextIsFullScreen = getIsFullScreen(store.getState()); if (nextIsFullScreen !== $scope.isFullScreen) { @@ -474,6 +483,14 @@ app.controller( $scope.isSaveDisabled = nextIsSaveDisabled; }); } + + const flyoutDisplay = getFlyoutDisplay(store.getState()); + const nextIsOpenSettingsDisabled = flyoutDisplay !== FLYOUT_STATE.NONE; + if (nextIsOpenSettingsDisabled !== $scope.isOpenSettingsDisabled) { + $scope.$evalAsync(() => { + $scope.isOpenSettingsDisabled = nextIsOpenSettingsDisabled; + }); + } } $scope.$on('$destroy', () => { @@ -591,6 +608,22 @@ app.controller( getInspector().open(inspectorAdapters, {}); }, }, + { + id: 'mapSettings', + label: i18n.translate('xpack.maps.mapController.openSettingsButtonLabel', { + defaultMessage: `Map settings`, + }), + description: i18n.translate('xpack.maps.mapController.openSettingsDescription', { + defaultMessage: `Open map settings`, + }), + testId: 'openSettingsButton', + disableButton() { + return $scope.isOpenSettingsDisabled; + }, + run() { + store.dispatch(openMapSettings()); + }, + }, ...(getMapsCapabilities().save ? [ { diff --git a/x-pack/legacy/plugins/siem/index.ts b/x-pack/legacy/plugins/siem/index.ts index 6e03583dda69f..d572561944a76 100644 --- a/x-pack/legacy/plugins/siem/index.ts +++ b/x-pack/legacy/plugins/siem/index.ts @@ -8,27 +8,7 @@ import { i18n } from '@kbn/i18n'; import { resolve } from 'path'; import { Root } from 'joi'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { savedObjectMappings } from '../../../plugins/siem/server/saved_objects'; - -import { - APP_ID, - APP_NAME, - DEFAULT_INDEX_KEY, - DEFAULT_ANOMALY_SCORE, - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_FROM, - DEFAULT_TO, - ENABLE_NEWS_FEED_SETTING, - NEWS_FEED_URL_SETTING, - NEWS_FEED_URL_SETTING_DEFAULT, - IP_REPUTATION_LINKS_SETTING, - IP_REPUTATION_LINKS_SETTING_DEFAULT, - DEFAULT_INDEX_PATTERN, -} from '../../../plugins/siem/common/constants'; +import { APP_ID, APP_NAME } from '../../../plugins/siem/common/constants'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/utils'; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -63,101 +43,6 @@ export const siem = (kibana: any) => { category: DEFAULT_APP_CATEGORIES.security, }, ], - uiSettingDefaults: { - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { - defaultMessage: 'Time filter refresh interval', - }), - value: `{ - "pause": ${DEFAULT_INTERVAL_PAUSE}, - "value": ${DEFAULT_INTERVAL_VALUE} -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { - defaultMessage: - '

Default refresh interval for the SIEM time filter, in milliseconds.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_SIEM_TIME_RANGE]: { - type: 'json', - name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { - defaultMessage: 'Time filter period', - }), - value: `{ - "from": "${DEFAULT_FROM}", - "to": "${DEFAULT_TO}" -}`, - description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { - defaultMessage: '

Default period of time in the SIEM time filter.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_INDEX_KEY]: { - name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { - defaultMessage: 'Elasticsearch indices', - }), - value: DEFAULT_INDEX_PATTERN, - description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { - defaultMessage: - '

Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [DEFAULT_ANOMALY_SCORE]: { - name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { - defaultMessage: 'Anomaly threshold', - }), - value: 50, - type: 'number', - description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { - defaultMessage: - '

Value above which Machine Learning job anomalies are displayed in the SIEM app.

Valid values: 0 to 100.

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [ENABLE_NEWS_FEED_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { - defaultMessage: 'News feed', - }), - value: true, - description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { - defaultMessage: '

Enables the News feed

', - }), - type: 'boolean', - category: ['siem'], - requiresPageReload: true, - }, - [NEWS_FEED_URL_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { - defaultMessage: 'News feed URL', - }), - value: NEWS_FEED_URL_SETTING_DEFAULT, - description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { - defaultMessage: '

News feed content will be retrieved from this URL

', - }), - category: ['siem'], - requiresPageReload: true, - }, - [IP_REPUTATION_LINKS_SETTING]: { - name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { - defaultMessage: 'IP Reputation Links', - }), - value: IP_REPUTATION_LINKS_SETTING_DEFAULT, - type: 'json', - description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { - defaultMessage: - 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', - }), - category: ['siem'], - requiresPageReload: true, - }, - }, - mappings: savedObjectMappings, }, config(Joi: Root) { return Joi.object() diff --git a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx index f88ffc3f3c6c4..f75afcd113628 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_page/index.tsx @@ -60,6 +60,7 @@ Badge.displayName = 'Badge'; interface BackOptions { href: LinkIconProps['href']; text: LinkIconProps['children']; + dataTestSubj?: string; } export interface HeaderPageProps extends HeaderProps { @@ -91,7 +92,11 @@ const HeaderPageComponent: React.FC = ({ {backOptions && ( - + {backOptions.text} diff --git a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx b/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx index ba5874d42d515..36f57c46c1628 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_icon/index.tsx @@ -50,12 +50,14 @@ export interface LinkIconProps extends LinkProps { children: string; iconSize?: IconSize; iconType: IconType; + dataTestSubj?: string; } export const LinkIcon = React.memo( ({ children, color, + dataTestSubj, disabled, href, iconSide = 'left', @@ -67,6 +69,7 @@ export const LinkIcon = React.memo( = ({ - + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts index 9d2ac29bc47d7..cc01edcfaab11 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/__mock__/form.ts @@ -3,6 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); export const mockFormHook = { isSubmitted: false, isSubmitting: false, @@ -35,3 +39,5 @@ export const getFormMock = (sampleData: any) => ({ }), getFormData: () => sampleData, }); + +export const useFormMock = useForm as jest.Mock; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index e48e5cb0c5959..9c2a7fc07f2d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -126,10 +126,9 @@ export const getCasesColumns = ( render: (createdAt: Case['createdAt']) => { if (createdAt != null) { return ( - + + + ); } return getEmptyTagValue(); @@ -142,10 +141,9 @@ export const getCasesColumns = ( render: (closedAt: Case['closedAt']) => { if (closedAt != null) { return ( - + + + ); } return getEmptyTagValue(); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index 58d0c1b0faaf3..eb5bca6cc57ff 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -125,8 +125,9 @@ describe('AllCases', () => { wrapper .find(`[data-test-subj="case-table-column-createdAt"]`) .first() + .childAt(0) .prop('value') - ).toEqual(useGetCasesMockState.data.cases[0].createdAt); + ).toBe(useGetCasesMockState.data.cases[0].createdAt); expect( wrapper .find(`[data-test-subj="case-table-case-count"]`) diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index a6eca717a82a3..9dd90074a2e7b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -313,6 +313,7 @@ export const AllCases = React.memo(({ userCanCrud }) => { (({ userCanCrud }) => { (({ userCanCrud }) => { fill href={getCreateCaseUrl(urlSearch)} iconType="plusInCircle" + data-test-subj="createNewCaseBtn" > {i18n.CREATE_TITLE} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index 3cf0405f40637..01b9bc42f8e91 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -216,6 +216,7 @@ export const CaseComponent = React.memo( backOptions={{ href: getCaseUrl(search), text: i18n.BACK_TO_ALL, + dataTestSubj: 'backToCases', }} data-test-subj="case-view-title" titleNode={ diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx index d480744fc932a..0897be6310fa2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.test.tsx @@ -14,6 +14,8 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostCase } from '../../../../containers/case/use_post_case'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; + jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../../../containers/case/use_post_case'); import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; @@ -22,6 +24,14 @@ import { SiemPageName } from '../../../home/types'; jest.mock( '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); export const useFormMock = useForm as jest.Mock; @@ -40,9 +50,11 @@ const defaultInsertTimeline = { handleCursorChange, handleOnTimelineChange, }; + +const sampleTags = ['coke', 'pepsi']; const sampleData = { description: 'what a great description', - tags: ['coke', 'pepsi'], + tags: sampleTags, title: 'what a cool title', }; const defaultPostCase = { @@ -52,14 +64,28 @@ const defaultPostCase = { postCase, }; describe('Create case', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const fetchTags = jest.fn(); const formHookMock = getFormMock(sampleData); - beforeEach(() => { jest.resetAllMocks(); useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); usePostCaseMock.mockImplementation(() => defaultPostCase); useFormMock.mockImplementation(() => ({ form: formHookMock })); jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); }); it('should post case on submit click', async () => { @@ -118,4 +144,19 @@ describe('Create case', () => { ); expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx index 53b792bb9b5eb..0f819f961b396 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/create/index.tsx @@ -3,7 +3,7 @@ * 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, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,8 +15,16 @@ import { import styled, { css } from 'styled-components'; import { Redirect } from 'react-router-dom'; +import { isEqual } from 'lodash/fp'; import { CasePostRequest } from '../../../../../../../../plugins/case/common/api'; -import { Field, Form, getUseField, useForm, UseField } from '../../../../shared_imports'; +import { + Field, + Form, + getUseField, + useForm, + UseField, + FormDataProvider, +} from '../../../../shared_imports'; import { usePostCase } from '../../../../containers/case/use_post_case'; import { schema } from './schema'; import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; @@ -24,6 +32,7 @@ import { useInsertTimeline } from '../../../../components/timeline/insert_timeli import * as i18n from '../../translations'; import { SiemPageName } from '../../../home/types'; import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; export const CommonUseField = getUseField({ component: Field }); @@ -59,6 +68,21 @@ export const Create = React.memo(() => { options: { stripEmptyFields: false }, schema, }); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( form, 'description' @@ -108,6 +132,8 @@ export const Create = React.memo(() => { fullWidth: true, placeholder: '', disabled: isLoading, + options, + noSuggestions: false, }, }} /> @@ -131,6 +157,25 @@ export const Create = React.memo(() => { }} /> + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + (({ caseCount, caseStatus, isLoading }) => { - const openClosedStats = useMemo( - () => [ - { - title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, - description: isLoading ? : caseCount ?? 'N/A', - }, - ], - [caseCount, caseStatus, isLoading] - ); - return ; -}); +export const OpenClosedStats = React.memo( + ({ caseCount, caseStatus, isLoading, dataTestSubj }) => { + const openClosedStats = useMemo( + () => [ + { + title: caseStatus === 'open' ? i18n.OPEN_CASES : i18n.CLOSED_CASES, + description: isLoading ? : caseCount ?? 'N/A', + }, + ], + [caseCount, caseStatus, isLoading, dataTestSubj] + ); + return ( + + ); + } +); OpenClosedStats.displayName = 'OpenClosedStats'; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx index 8ad2f8f8cb737..b5627376ac8aa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.test.tsx @@ -6,17 +6,26 @@ import React from 'react'; import { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { TagList } from './'; import { getFormMock } from '../__mock__/form'; import { TestProviders } from '../../../../mock'; import { wait } from '../../../../lib/helpers'; import { useForm } from '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { act } from 'react-dom/test-utils'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; jest.mock( '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); +jest.mock('../../../../containers/case/use_get_tags'); +jest.mock( + '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); const onSubmit = jest.fn(); const defaultProps = { disabled: false, @@ -26,11 +35,27 @@ const defaultProps = { }; describe('TagList ', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ const sampleTags = ['coke', 'pepsi']; + const fetchTags = jest.fn(); const formHookMock = getFormMock({ tags: sampleTags }); beforeEach(() => { jest.resetAllMocks(); (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); }); it('Renders no tags, and then edit', () => { const wrapper = mount( @@ -80,6 +105,23 @@ describe('TagList ', () => { expect(onSubmit).toBeCalledWith(sampleTags); }); }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); it('Cancels on cancel', async () => { const props = { ...defaultProps, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx index 9bac000b93235..c61feab0bab98 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/tag_list/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { EuiText, EuiHorizontalRule, @@ -17,10 +17,12 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { isEqual } from 'lodash/fp'; import * as i18n from './translations'; -import { Form, useForm } from '../../../../shared_imports'; +import { Form, FormDataProvider, useForm } from '../../../../shared_imports'; import { schema } from './schema'; import { CommonUseField } from '../create'; +import { useGetTags } from '../../../../containers/case/use_get_tags'; interface TagListProps { disabled?: boolean; @@ -54,6 +56,22 @@ export const TagList = React.memo( setIsEditTags(false); } }, [form, onSubmit]); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); return ( @@ -75,7 +93,7 @@ export const TagList = React.memo( )} - + {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} {tags.length > 0 && !isEditTags && @@ -98,9 +116,30 @@ export const TagList = React.memo( euiFieldProps: { fullWidth: true, placeholder: '', + options, + noSuggestions: false, }, }} /> + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} +
diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx index 1c71260422d4b..ff402e8ea1c8b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx @@ -8,17 +8,13 @@ import React from 'react'; import { mount } from 'enzyme'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { getFormMock } from '../__mock__/form'; +import { getFormMock, useFormMock } from '../__mock__/form'; import { useUpdateComment } from '../../../../containers/case/use_update_comment'; import { basicCase, getUserAction } from '../../../../containers/case/mock'; import { UserActionTree } from './'; import { TestProviders } from '../../../../mock'; -import { useFormMock } from '../create/index.test'; import { wait } from '../../../../lib/helpers'; import { act } from 'react-dom/test-utils'; -jest.mock( - '../../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); const fetchUserActions = jest.fn(); const onUpdateField = jest.fn(); diff --git a/x-pack/legacy/plugins/siem/public/shared_imports.ts b/x-pack/legacy/plugins/siem/public/shared_imports.ts index c83433ef129c9..0c0ac637a4229 100644 --- a/x-pack/legacy/plugins/siem/public/shared_imports.ts +++ b/x-pack/legacy/plugins/siem/public/shared_imports.ts @@ -8,6 +8,7 @@ export { getUseField, getFieldValidityAndErrorMessage, FieldHook, + FieldValidateResponse, FIELD_TYPES, Form, FormData, @@ -17,6 +18,7 @@ export { UseField, useForm, ValidationFunc, + VALIDATION_TYPES, } from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field, diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts new file mode 100644 index 0000000000000..797141b0996af --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts @@ -0,0 +1,44 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +import { LicenseType } from '../../../licensing/common/types'; + +const platinumLicense: LicenseType = 'platinum'; + +export const PLUGIN = { + ID: 'crossClusterReplication', + TITLE: i18n.translate('xpack.crossClusterReplication.appTitle', { + defaultMessage: 'Cross-Cluster Replication', + }), + minimumLicenseType: platinumLicense, +}; + +export const APPS = { + CCR_APP: 'ccr', + REMOTE_CLUSTER_APP: 'remote_cluster', +}; + +export const MANAGEMENT_ID = 'cross_cluster_replication'; +export const BASE_PATH = `/management/elasticsearch/${MANAGEMENT_ID}`; +export const BASE_PATH_REMOTE_CLUSTERS = '/management/elasticsearch/remote_clusters'; +export const API_BASE_PATH = '/api/cross_cluster_replication'; +export const API_REMOTE_CLUSTERS_BASE_PATH = '/api/remote_clusters'; +export const API_INDEX_MANAGEMENT_BASE_PATH = '/api/index_management'; + +export const FOLLOWER_INDEX_ADVANCED_SETTINGS = { + maxReadRequestOperationCount: 5120, + maxOutstandingReadRequests: 12, + maxReadRequestSize: '32mb', + maxWriteRequestOperationCount: 5120, + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: 9, + maxWriteBufferCount: 2147483647, + maxWriteBufferSize: '512mb', + maxRetryDelay: '500ms', + readPollTimeout: '1m', +}; diff --git a/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap new file mode 100644 index 0000000000000..c20556fe1434d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/__snapshots__/follower_index_serialization.test.ts.snap @@ -0,0 +1,128 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CCR] follower index serialization deserializeFollowerIndex() deserializes Elasticsearch follower index object 1`] = ` +Object { + "leaderIndex": "leader 1", + "maxOutstandingReadRequests": 1, + "maxOutstandingWriteRequests": 1, + "maxReadRequestOperationCount": 1, + "maxReadRequestSize": "1b", + "maxRetryDelay": "1s", + "maxWriteBufferCount": 1, + "maxWriteBufferSize": "1b", + "maxWriteRequestOperationCount": 1, + "maxWriteRequestSize": "1b", + "name": "follower index 1", + "readPollTimeout": "1s", + "remoteCluster": "cluster 1", + "shards": Array [ + Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader 1", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [], + "remoteCluster": "cluster 1", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, + }, + Object { + "bytesReadCount": undefined, + "failedReadRequestsCount": undefined, + "failedWriteRequestsCount": undefined, + "followerGlobalCheckpoint": undefined, + "followerMappingVersion": undefined, + "followerMaxSequenceNum": undefined, + "followerSettingsVersion": undefined, + "id": "shard 2", + "lastRequestedSequenceNum": undefined, + "leaderGlobalCheckpoint": undefined, + "leaderIndex": "leader_index 2", + "leaderMaxSequenceNum": undefined, + "operationsReadCount": undefined, + "operationsWrittenCount": undefined, + "outstandingReadRequestsCount": undefined, + "outstandingWriteRequestsCount": undefined, + "readExceptions": undefined, + "remoteCluster": "remote_cluster 2", + "successfulReadRequestCount": undefined, + "successfulWriteRequestsCount": undefined, + "timeSinceLastReadMs": undefined, + "totalReadRemoteExecTimeMs": undefined, + "totalReadTimeMs": undefined, + "totalWriteTimeMs": undefined, + "writeBufferOperationsCount": undefined, + "writeBufferSizeBytes": undefined, + }, + ], + "status": "active", +} +`; + +exports[`[CCR] follower index serialization deserializeShard() deserializes shard 1`] = ` +Object { + "bytesReadCount": 1, + "failedReadRequestsCount": 1, + "failedWriteRequestsCount": 1, + "followerGlobalCheckpoint": 1, + "followerMappingVersion": 1, + "followerMaxSequenceNum": 1, + "followerSettingsVersion": 1, + "id": 1, + "lastRequestedSequenceNum": 1, + "leaderGlobalCheckpoint": 1, + "leaderIndex": "leader index", + "leaderMaxSequenceNum": 1, + "operationsReadCount": 1, + "operationsWrittenCount": 1, + "outstandingReadRequestsCount": 1, + "outstandingWriteRequestsCount": 1, + "readExceptions": Array [ + "read exception", + ], + "remoteCluster": "remote cluster", + "successfulReadRequestCount": 1, + "successfulWriteRequestsCount": 1, + "timeSinceLastReadMs": 1, + "totalReadRemoteExecTimeMs": 1, + "totalReadTimeMs": 1, + "totalWriteTimeMs": 1, + "writeBufferOperationsCount": 1, + "writeBufferSizeBytes": 1, +} +`; + +exports[`[CCR] follower index serialization serializeFollowerIndex() serializes object to Elasticsearch follower index object 1`] = ` +Object { + "leader_index": "leader index", + "max_outstanding_read_requests": 1, + "max_outstanding_write_requests": 1, + "max_read_request_operation_count": 1, + "max_read_request_size": "1b", + "max_retry_delay": "1s", + "max_write_buffer_count": 1, + "max_write_buffer_size": "1b", + "max_write_request_operation_count": 1, + "max_write_request_size": "1b", + "read_poll_timeout": "1s", + "remote_cluster": "remote cluster", +} +`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts similarity index 85% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js rename to x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts index eef87a6cc4c89..fe3e59f21ee23 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.js +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AutoFollowPattern, AutoFollowPatternFromEs } from '../types'; + import { deserializeAutoFollowPattern, deserializeListAutoFollowPatterns, @@ -12,13 +14,10 @@ import { describe('[CCR] auto-follow_serialization', () => { describe('deserializeAutoFollowPattern()', () => { - it('should return empty object if name or esObject are not provided', () => { - expect(deserializeAutoFollowPattern()).toEqual({}); - }); - it('should deserialize Elasticsearch object', () => { const expected = { name: 'some-name', + active: true, remoteCluster: 'foo', leaderIndexPatterns: ['foo-*'], followIndexPattern: 'bar', @@ -27,13 +26,14 @@ describe('[CCR] auto-follow_serialization', () => { const esObject = { name: 'some-name', pattern: { + active: true, remote_cluster: expected.remoteCluster, leader_index_patterns: expected.leaderIndexPatterns, follow_index_pattern: expected.followIndexPattern, }, }; - expect(deserializeAutoFollowPattern(esObject)).toEqual(expected); + expect(deserializeAutoFollowPattern(esObject as AutoFollowPatternFromEs)).toEqual(expected); }); }); @@ -78,7 +78,9 @@ describe('[CCR] auto-follow_serialization', () => { ], }; - expect(deserializeListAutoFollowPatterns(esObjects.patterns)).toEqual(expected); + expect( + deserializeListAutoFollowPatterns(esObjects.patterns as AutoFollowPatternFromEs[]) + ).toEqual(expected); }); }); @@ -96,7 +98,7 @@ describe('[CCR] auto-follow_serialization', () => { followIndexPattern: expected.follow_index_pattern, }; - expect(serializeAutoFollowPattern(object)).toEqual(expected); + expect(serializeAutoFollowPattern(object as AutoFollowPattern)).toEqual(expected); }); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts new file mode 100644 index 0000000000000..265af0ede1462 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/auto_follow_pattern_serialization.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AutoFollowPattern, AutoFollowPatternFromEs, AutoFollowPatternToEs } from '../types'; + +export const deserializeAutoFollowPattern = ( + autoFollowPattern: AutoFollowPatternFromEs +): AutoFollowPattern => { + const { + name, + pattern: { active, remote_cluster, leader_index_patterns, follow_index_pattern }, + } = autoFollowPattern; + + return { + name, + active, + remoteCluster: remote_cluster, + leaderIndexPatterns: leader_index_patterns, + followIndexPattern: follow_index_pattern, + }; +}; + +export const deserializeListAutoFollowPatterns = ( + autoFollowPatterns: AutoFollowPatternFromEs[] +): AutoFollowPattern[] => autoFollowPatterns.map(deserializeAutoFollowPattern); + +export const serializeAutoFollowPattern = ({ + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}: AutoFollowPattern): AutoFollowPatternToEs => ({ + remote_cluster: remoteCluster, + leader_index_patterns: leaderIndexPatterns, + follow_index_pattern: followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts new file mode 100644 index 0000000000000..bfe3e1b3443e6 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { ShardFromEs, FollowerIndexFromEs, FollowerIndex } from '../types'; + +import { + deserializeShard, + deserializeFollowerIndex, + deserializeListFollowerIndices, + serializeFollowerIndex, +} from './follower_index_serialization'; + +describe('[CCR] follower index serialization', () => { + describe('deserializeShard()', () => { + it('deserializes shard', () => { + const serializedShard = { + remote_cluster: 'remote cluster', + leader_index: 'leader index', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: ['read exception'], + time_since_last_read_millis: 1, + }; + + expect(deserializeShard(serializedShard as ShardFromEs)).toMatchSnapshot(); + }); + }); + + describe('deserializeFollowerIndex()', () => { + it('deserializes Elasticsearch follower index object', () => { + const serializedFollowerIndex = { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [ + { + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + shard_id: 1, + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + // This is an array of exception objects + read_exceptions: [], + time_since_last_read_millis: 1, + }, + { + remote_cluster: 'remote_cluster 2', + leader_index: 'leader_index 2', + shard_id: 'shard 2', + }, + ], + }; + + expect( + deserializeFollowerIndex(serializedFollowerIndex as FollowerIndexFromEs) + ).toMatchSnapshot(); + }); + }); + + describe('deserializeListFollowerIndices()', () => { + it('deserializes list of Elasticsearch follower index objects', () => { + const serializedFollowerIndexList = [ + { + follower_index: 'follower index 1', + remote_cluster: 'cluster 1', + leader_index: 'leader 1', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + shards: [], + }, + { + follower_index: 'follower index 2', + remote_cluster: 'cluster 2', + leader_index: 'leader 2', + status: 'paused', + parameters: { + max_read_request_operation_count: 2, + max_outstanding_read_requests: 2, + max_read_request_size: '2b', + max_write_request_operation_count: 2, + max_write_request_size: '2b', + max_outstanding_write_requests: 2, + max_write_buffer_count: 2, + max_write_buffer_size: '2b', + max_retry_delay: '2s', + read_poll_timeout: '2s', + }, + shards: [], + }, + ]; + + const deserializedFollowerIndexList = [ + { + name: 'follower index 1', + remoteCluster: 'cluster 1', + leaderIndex: 'leader 1', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [], + }, + { + name: 'follower index 2', + remoteCluster: 'cluster 2', + leaderIndex: 'leader 2', + status: 'paused', + maxReadRequestOperationCount: 2, + maxOutstandingReadRequests: 2, + maxReadRequestSize: '2b', + maxWriteRequestOperationCount: 2, + maxWriteRequestSize: '2b', + maxOutstandingWriteRequests: 2, + maxWriteBufferCount: 2, + maxWriteBufferSize: '2b', + maxRetryDelay: '2s', + readPollTimeout: '2s', + shards: [], + }, + ]; + + expect(deserializeListFollowerIndices(serializedFollowerIndexList)).toEqual( + deserializedFollowerIndexList + ); + }); + }); + + describe('serializeFollowerIndex()', () => { + it('serializes object to Elasticsearch follower index object', () => { + const deserializedFollowerIndex = { + name: 'test', + status: 'active', + shards: [], + remoteCluster: 'remote cluster', + leaderIndex: 'leader index', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + }; + + expect(serializeFollowerIndex(deserializedFollowerIndex as FollowerIndex)).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts similarity index 87% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js rename to x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts index c41fde8f7818d..df476a0b2db89 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/follower_index_serialization.js +++ b/x-pack/plugins/cross_cluster_replication/common/services/follower_index_serialization.ts @@ -4,7 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable camelcase */ +import { + ShardFromEs, + Shard, + FollowerIndexFromEs, + FollowerIndex, + FollowerIndexToEs, + FollowerIndexAdvancedSettings, + FollowerIndexAdvancedSettingsToEs, +} from '../types'; + export const deserializeShard = ({ remote_cluster, leader_index, @@ -32,7 +41,7 @@ export const deserializeShard = ({ operations_written, read_exceptions, time_since_last_read_millis, -}) => ({ +}: ShardFromEs): Shard => ({ id: shard_id, remoteCluster: remote_cluster, leaderIndex: leader_index, @@ -61,9 +70,7 @@ export const deserializeShard = ({ readExceptions: read_exceptions, timeSinceLastReadMs: time_since_last_read_millis, }); -/* eslint-enable camelcase */ -/* eslint-disable camelcase */ export const deserializeFollowerIndex = ({ follower_index, remote_cluster, @@ -82,7 +89,7 @@ export const deserializeFollowerIndex = ({ read_poll_timeout, } = {}, shards, -}) => ({ +}: FollowerIndexFromEs): FollowerIndex => ({ name: follower_index, remoteCluster: remote_cluster, leaderIndex: leader_index, @@ -99,10 +106,10 @@ export const deserializeFollowerIndex = ({ readPollTimeout: read_poll_timeout, shards: shards && shards.map(deserializeShard), }); -/* eslint-enable camelcase */ -export const deserializeListFollowerIndices = followerIndices => - followerIndices.map(deserializeFollowerIndex); +export const deserializeListFollowerIndices = ( + followerIndices: FollowerIndexFromEs[] +): FollowerIndex[] => followerIndices.map(deserializeFollowerIndex); export const serializeAdvancedSettings = ({ maxReadRequestOperationCount, @@ -115,7 +122,7 @@ export const serializeAdvancedSettings = ({ maxWriteBufferSize, maxRetryDelay, readPollTimeout, -}) => ({ +}: FollowerIndexAdvancedSettings): FollowerIndexAdvancedSettingsToEs => ({ max_read_request_operation_count: maxReadRequestOperationCount, max_outstanding_read_requests: maxOutstandingReadRequests, max_read_request_size: maxReadRequestSize, @@ -128,7 +135,7 @@ export const serializeAdvancedSettings = ({ read_poll_timeout: readPollTimeout, }); -export const serializeFollowerIndex = followerIndex => ({ +export const serializeFollowerIndex = (followerIndex: FollowerIndex): FollowerIndexToEs => ({ remote_cluster: followerIndex.remoteCluster, leader_index: followerIndex.leaderIndex, ...serializeAdvancedSettings(followerIndex), diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/common/services/utils.test.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts similarity index 62% rename from x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js rename to x-pack/plugins/cross_cluster_replication/common/services/utils.ts index 3d8c97f45327c..dda6732254cc3 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/services/utils.js +++ b/x-pack/plugins/cross_cluster_replication/common/services/utils.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -export const arrify = val => (Array.isArray(val) ? val : [val]); +export const arrify = (val: any): any[] => (Array.isArray(val) ? val : [val]); /** * Utilty to add some latency in a Promise chain * * @param {number} time Time in millisecond to wait */ -export const wait = (time = 1000) => data => { +export const wait = (time = 1000) => (data: any): Promise => { return new Promise(resolve => { setTimeout(() => resolve(data), time); }); @@ -19,8 +19,11 @@ export const wait = (time = 1000) => data => { /** * Utility to remove empty fields ("") from a request body */ -export const removeEmptyFields = body => - Object.entries(body).reduce((acc, [key, value]) => { +export const removeEmptyFields = (body: Record): Record => + Object.entries(body).reduce((acc: Record, [key, value]: [string, any]): Record< + string, + any + > => { if (value !== '') { acc[key] = value; } diff --git a/x-pack/plugins/cross_cluster_replication/common/types.ts b/x-pack/plugins/cross_cluster_replication/common/types.ts new file mode 100644 index 0000000000000..4932d6c570297 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/common/types.ts @@ -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. + */ + +export interface AutoFollowPattern { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +} + +export interface AutoFollowPatternFromEs { + name: string; + pattern: { + active: boolean; + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; + }; +} + +export interface AutoFollowPatternToEs { + remote_cluster: string; + leader_index_patterns: string[]; + follow_index_pattern: string; +} + +export interface ShardFromEs { + remote_cluster: string; + leader_index: string; + shard_id: number; + leader_global_checkpoint: number; + leader_max_seq_no: number; + follower_global_checkpoint: number; + follower_max_seq_no: number; + last_requested_seq_no: number; + outstanding_read_requests: number; + outstanding_write_requests: number; + write_buffer_operation_count: number; + write_buffer_size_in_bytes: number; + follower_mapping_version: number; + follower_settings_version: number; + total_read_time_millis: number; + total_read_remote_exec_time_millis: number; + successful_read_requests: number; + failed_read_requests: number; + operations_read: number; + bytes_read: number; + total_write_time_millis: number; + successful_write_requests: number; + failed_write_requests: number; + operations_written: number; + // This is an array of exception objects + read_exceptions: any[]; + time_since_last_read_millis: number; +} + +export interface Shard { + remoteCluster: string; + leaderIndex: string; + id: number; + leaderGlobalCheckpoint: number; + leaderMaxSequenceNum: number; + followerGlobalCheckpoint: number; + followerMaxSequenceNum: number; + lastRequestedSequenceNum: number; + outstandingReadRequestsCount: number; + outstandingWriteRequestsCount: number; + writeBufferOperationsCount: number; + writeBufferSizeBytes: number; + followerMappingVersion: number; + followerSettingsVersion: number; + totalReadTimeMs: number; + totalReadRemoteExecTimeMs: number; + successfulReadRequestCount: number; + failedReadRequestsCount: number; + operationsReadCount: number; + bytesReadCount: number; + totalWriteTimeMs: number; + successfulWriteRequestsCount: number; + failedWriteRequestsCount: number; + operationsWrittenCount: number; + // This is an array of exception objects + readExceptions: any[]; + timeSinceLastReadMs: number; +} + +export interface FollowerIndexFromEs { + follower_index: string; + remote_cluster: string; + leader_index: string; + status: string; + // Once https://github.com/elastic/elasticsearch/issues/54996 is resolved so that paused follower + // indices contain this information, we can removed this optional typing as well as the optional + // typing in FollowerIndexAdvancedSettings and FollowerIndexAdvancedSettingsToEs. + parameters?: FollowerIndexAdvancedSettingsToEs; + shards: ShardFromEs[]; +} + +export interface FollowerIndex extends FollowerIndexAdvancedSettings { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; + shards: Shard[]; +} + +export interface FollowerIndexToEs extends FollowerIndexAdvancedSettingsToEs { + remote_cluster: string; + leader_index: string; +} + +export interface FollowerIndexAdvancedSettings { + maxReadRequestOperationCount?: number; + maxOutstandingReadRequests?: number; + maxReadRequestSize?: string; // byte value + maxWriteRequestOperationCount?: number; + maxWriteRequestSize?: string; // byte value + maxOutstandingWriteRequests?: number; + maxWriteBufferCount?: number; + maxWriteBufferSize?: string; // byte value + maxRetryDelay?: string; // time value + readPollTimeout?: string; // time value +} + +export interface FollowerIndexAdvancedSettingsToEs { + max_read_request_operation_count?: number; + max_outstanding_read_requests?: number; + max_read_request_size?: string; // byte value + max_write_request_operation_count?: number; + max_write_request_size?: string; // byte value + max_outstanding_write_requests?: number; + max_write_buffer_count?: number; + max_write_buffer_size?: string; // byte value + max_retry_delay?: string; // time value + read_poll_timeout?: string; // time value +} + +export interface RecentAutoFollowError { + timestamp: number; + leaderIndex: string; + autoFollowException: { + type: string; + reason: string; + }; +} + +export interface RecentAutoFollowErrorFromEs { + timestamp: number; + leader_index: string; + auto_follow_exception: { + type: string; + reason: string; + }; +} + +export interface AutoFollowedCluster { + clusterName: string; + timeSinceLastCheckMillis: number; + lastSeenMetadataVersion: number; +} + +export interface AutoFollowedClusterFromEs { + cluster_name: string; + time_since_last_check_millis: number; + last_seen_metadata_version: number; +} + +export interface AutoFollowStats { + numberOfFailedFollowIndices: number; + numberOfFailedRemoteClusterStateRequests: number; + numberOfSuccessfulFollowIndices: number; + recentAutoFollowErrors: RecentAutoFollowError[]; + autoFollowedClusters: AutoFollowedCluster[]; +} + +export interface AutoFollowStatsFromEs { + number_of_failed_follow_indices: number; + number_of_failed_remote_cluster_state_requests: number; + number_of_successful_follow_indices: number; + recent_auto_follow_errors: RecentAutoFollowErrorFromEs[]; + auto_followed_clusters: AutoFollowedClusterFromEs[]; +} diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json new file mode 100644 index 0000000000000..ccf98f41def47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -0,0 +1,17 @@ +{ + "id": "crossClusterReplication", + "version": "kibana", + "server": true, + "ui": true, + "requiredPlugins": [ + "home", + "licensing", + "management", + "remoteClusters", + "indexManagement" + ], + "optionalPlugins": [ + "usageCollection" + ], + "configPath": ["xpack", "ccr"] +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js index 2be00e70f6f84..db1430d157183 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_add.test.js @@ -3,11 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { indexPatterns } from '../../../../../../src/plugins/data/public'; -jest.mock('ui/new_platform'); +import { indexPatterns } from '../../../../../../src/plugins/data/public'; +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; const { setup } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js index abc3e5dc9def2..170bce7b82085 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_edit.test.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { AutoFollowPatternForm } from '../../public/np_ready/app/components/auto_follow_pattern_form'; +import { AutoFollowPatternForm } from '../../app/components/auto_follow_pattern_form'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; import { AUTO_FOLLOW_PATTERN_EDIT } from './helpers/constants'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternEdit; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js index 20e982856dc19..190400e988634 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/auto_follow_pattern_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/auto_follow_pattern_list.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import { getAutoFollowPatternMock } from './fixtures/auto_follow_pattern'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getAutoFollowPatternClientMock } from '../../fixtures/auto_follow_pattern'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.autoFollowPatternList; describe('', () => { @@ -79,11 +76,11 @@ describe('', () => { const testPrefix = 'prefix_'; const testSuffix = '_suffix'; - const autoFollowPattern1 = getAutoFollowPatternClientMock({ + const autoFollowPattern1 = getAutoFollowPatternMock({ name: `a${getRandomString()}`, followIndexPattern: `${testPrefix}{{leader_index}}${testSuffix}`, }); - const autoFollowPattern2 = getAutoFollowPatternClientMock({ + const autoFollowPattern2 = getAutoFollowPatternMock({ name: `b${getRandomString()}`, followIndexPattern: '{{leader_index}}', // no prefix nor suffix }); @@ -305,10 +302,12 @@ describe('', () => { const message = 'bar'; const recentAutoFollowErrors = [ { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern1.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, { + timestamp: 1587081600021, leaderIndex: `${autoFollowPattern2.name}:my-leader-test`, autoFollowException: { type: 'exception', reason: message }, }, @@ -327,7 +326,7 @@ describe('', () => { expect(exists('autoFollowPatternDetail.errors')).toBe(true); expect(exists('autoFollowPatternDetail.titleErrors')).toBe(true); expect(find('autoFollowPatternDetail.recentError').map(error => error.text())).toEqual([ - message, + 'April 16th, 2020 8:00:00 PM: bar', ]); }); }); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.ts new file mode 100644 index 0000000000000..e6444c37e8590 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/auto_follow_pattern.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 { getRandomString } from '../../../../../../test_utils'; +import { AutoFollowPattern } from '../../../../common/types'; + +export const getAutoFollowPatternMock = ({ + name = getRandomString(), + active = false, + remoteCluster = getRandomString(), + leaderIndexPatterns = [`${getRandomString()}-*`], + followIndexPattern = getRandomString(), +}: { + name: string; + active: boolean; + remoteCluster: string; + leaderIndexPatterns: string[]; + followIndexPattern: string; +}): AutoFollowPattern => ({ + name, + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, +}); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts new file mode 100644 index 0000000000000..ff051d470531b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/fixtures/follower_index.ts @@ -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 { getRandomString } from '../../../../../../test_utils'; +import { FollowerIndex } from '../../../../common/types'; + +const Chance = require('chance'); // eslint-disable-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires +const chance = new Chance(); + +interface FollowerIndexMock { + name: string; + remoteCluster: string; + leaderIndex: string; + status: string; +} + +export const getFollowerIndexMock = ({ + name = getRandomString(), + remoteCluster = getRandomString(), + leaderIndex = getRandomString(), + status = 'Active', +}: FollowerIndexMock): FollowerIndex => ({ + name, + remoteCluster, + leaderIndex, + status, + maxReadRequestOperationCount: chance.integer(), + maxOutstandingReadRequests: chance.integer(), + maxReadRequestSize: getRandomString({ length: 5 }), + maxWriteRequestOperationCount: chance.integer(), + maxWriteRequestSize: '9223372036854775807b', + maxOutstandingWriteRequests: chance.integer(), + maxWriteBufferCount: chance.integer(), + maxWriteBufferSize: getRandomString({ length: 5 }), + maxRetryDelay: getRandomString({ length: 5 }), + readPollTimeout: getRandomString({ length: 5 }), + shards: [ + { + id: 0, + remoteCluster, + leaderIndex, + leaderGlobalCheckpoint: chance.integer(), + leaderMaxSequenceNum: chance.integer(), + followerGlobalCheckpoint: chance.integer(), + followerMaxSequenceNum: chance.integer(), + lastRequestedSequenceNum: chance.integer(), + outstandingReadRequestsCount: chance.integer(), + outstandingWriteRequestsCount: chance.integer(), + writeBufferOperationsCount: chance.integer(), + writeBufferSizeBytes: chance.integer(), + followerMappingVersion: chance.integer(), + followerSettingsVersion: chance.integer(), + totalReadTimeMs: chance.integer(), + totalReadRemoteExecTimeMs: chance.integer(), + successfulReadRequestCount: chance.integer(), + failedReadRequestsCount: chance.integer(), + operationsReadCount: chance.integer(), + bytesReadCount: chance.integer(), + totalWriteTimeMs: chance.integer(), + successfulWriteRequestsCount: chance.integer(), + failedWriteRequestsCount: chance.integer(), + operationsWrittenCount: chance.integer(), + readExceptions: [], + timeSinceLastReadMs: chance.integer(), + }, + ], +}); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js index 7680be9d858a4..4c99339e16952 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_add.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_add.test.js @@ -4,13 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { RemoteClustersFormField } from '../../public/np_ready/app/components'; - import { indexPatterns } from '../../../../../../src/plugins/data/public'; - -jest.mock('ui/new_platform'); +import './mocks'; +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; +import { RemoteClustersFormField } from '../../app/components'; const { setup } = pageHelpers.followerIndexAdd; const { setup: setupAutoFollowPatternAdd } = pageHelpers.autoFollowPatternAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js index cfa37ff2e0358..f4bda2af653aa 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_index_edit.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_index_edit.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; -import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -import { FollowerIndexForm } from '../../public/np_ready/app/components/follower_index_form/follower_index_form'; +import { FollowerIndexForm } from '../../app/components/follower_index_form/follower_index_form'; +import './mocks'; import { FOLLOWER_INDEX_EDIT } from './helpers/constants'; - -jest.mock('ui/new_platform'); +import { setupEnvironment, pageHelpers, nextTick } from './helpers'; const { setup } = pageHelpers.followerIndexEdit; const { setup: setupFollowerIndexAdd } = pageHelpers.followerIndexAdd; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index dde31d1d166f9..f98a1dafbbcbf 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -4,12 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFollowerIndexMock } from './fixtures/follower_index'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick, getRandomString } from './helpers'; -import { getFollowerIndexMock } from '../../fixtures/follower_index'; - -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.followerIndexList; describe('', () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js similarity index 76% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js index 1f64e589bc4c1..1cb4e7c7725df 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternAdd } from '../../../public/np_ready/app/sections/auto_follow_pattern_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternAdd } from '../../../app/sections/auto_follow_pattern_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js similarity index 82% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js index 2b110c6552072..9cad61893c409 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { AutoFollowPatternEdit } from '../../../public/np_ready/app/sections/auto_follow_pattern_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternEdit } from '../../../app/sections/auto_follow_pattern_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { AUTO_FOLLOW_PATTERN_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js index 1d3e8ad6dff83..450feed49f9f2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/auto_follow_pattern_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { AutoFollowPatternList } from '../../../public/np_ready/app/sections/home/auto_follow_pattern_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { AutoFollowPatternList } from '../../../app/sections/home/auto_follow_pattern_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/constants.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/constants.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js similarity index 79% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js index f74baa1b2ad0a..856b09f3f3cba 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_add.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_add.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexAdd } from '../../../public/np_ready/app/sections/follower_index_add'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexAdd } from '../../../app/sections/follower_index_add'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js similarity index 84% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js index 47f8539bb593b..893d01f151bc2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_edit.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_edit.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { FollowerIndexEdit } from '../../../public/np_ready/app/sections/follower_index_edit'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndexEdit } from '../../../app/sections/follower_index_edit'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; import { FOLLOWER_INDEX_EDIT_NAME } from './constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js similarity index 90% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js index 2154e11e17b1f..52f4267594cc1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/follower_index_list.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/follower_index_list.helpers.js @@ -5,9 +5,9 @@ */ import { registerTestBed, findTestSubject } from '../../../../../../test_utils'; -import { FollowerIndicesList } from '../../../public/np_ready/app/sections/home/follower_indices_list'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; +import { FollowerIndicesList } from '../../../app/sections/home/follower_indices_list'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js similarity index 68% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js index 664ad909ba8e7..56dfa765bfa4f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/home.helpers.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/home.helpers.js @@ -5,10 +5,10 @@ */ import { registerTestBed } from '../../../../../../test_utils'; -import { CrossClusterReplicationHome } from '../../../public/np_ready/app/sections/home/home'; -import { ccrStore } from '../../../public/np_ready/app/store'; -import routing from '../../../public/np_ready/app/services/routing'; -import { BASE_PATH } from '../../../common/constants'; +import { BASE_PATH } from '../../../../common/constants'; +import { CrossClusterReplicationHome } from '../../../app/sections/home/home'; +import { ccrStore } from '../../../app/store'; +import { routing } from '../../../app/services/routing'; const testBedConfig = { store: ccrStore, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/http_requests.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/http_requests.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/index.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js similarity index 91% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js index 3562ad0df5b51..6dedbbfa79b19 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/helpers/setup_environment.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/helpers/setup_environment.js @@ -7,7 +7,7 @@ import axios from 'axios'; import axiosXhrAdapter from 'axios/lib/adapters/xhr'; -import { setHttpClient } from '../../../public/np_ready/app/services/api'; +import { setHttpClient } from '../../../app/services/api'; import { init as initHttpRequests } from './http_requests'; export const setupEnvironment = () => { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js index 2c536d069ef53..18d8b4eb9dbe0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/__jest__/client_integration/home.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/home.test.js @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import '../../public/np_ready/app/services/breadcrumbs.mock'; +import './mocks'; import { setupEnvironment, pageHelpers, nextTick } from './helpers'; -jest.mock('ui/new_platform'); - const { setup } = pageHelpers.home; describe('', () => { @@ -36,7 +34,7 @@ describe('', () => { ({ exists, find, component } = setup()); }); - test('should set the correct an app title', () => { + test('should set the correct app title', () => { expect(exists('appTitle')).toBe(true); expect(find('appTitle').text()).toEqual('Cross-Cluster Replication'); }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts similarity index 70% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts rename to x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts index b7c75108d4ef0..60a196254d408 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.mock.ts +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/breadcrumbs.mock.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('./breadcrumbs', () => ({ - ...jest.requireActual('./breadcrumbs'), +jest.mock('../../../app/services/breadcrumbs', () => ({ + ...jest.requireActual('../../../app/services/breadcrumbs'), setBreadcrumbs: jest.fn(), })); diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts new file mode 100644 index 0000000000000..cff9c003f3e80 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * 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 './breadcrumbs.mock'; +import './track_ui_metric.mock'; diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts new file mode 100644 index 0000000000000..016e259343285 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/mocks/track_ui_metric.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../../app/services/track_ui_metric', () => ({ + ...jest.requireActual('../../../app/services/track_ui_metric'), + trackUiMetric: jest.fn(), + trackUserRequest: (request: Promise) => { + return request.then(response => response); + }, +})); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx similarity index 89% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js rename to x-pack/plugins/cross_cluster_replication/public/app/app.tsx index 968646a4bd1b0..ec349ccd6f2c7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/app.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/app.tsx @@ -5,8 +5,8 @@ */ import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { Route, Switch, Redirect, withRouter } from 'react-router-dom'; +import { Route, Switch, Redirect, withRouter, RouteComponentProps } from 'react-router-dom'; +import { History } from 'history'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -20,12 +20,14 @@ import { EuiTitle, } from '@elastic/eui'; -import { BASE_PATH } from '../../../common/constants'; +import { BASE_PATH } from '../../common/constants'; import { getFatalErrors } from './services/notifications'; import { SectionError } from './components'; -import routing from './services/routing'; +import { routing } from './services/routing'; +// @ts-ignore import { loadPermissions } from './services/api'; +// @ts-ignore import { CrossClusterReplicationHome, AutoFollowPatternAdd, @@ -34,16 +36,21 @@ import { FollowerIndexEdit, } from './sections'; -class AppComponent extends Component { - static propTypes = { - history: PropTypes.shape({ - push: PropTypes.func.isRequired, - createHref: PropTypes.func.isRequired, - }).isRequired, - }; +interface AppProps { + history: History; + location: any; +} + +interface AppState { + isFetchingPermissions: boolean; + fetchPermissionError: any; + hasPermission: boolean; + missingClusterPrivileges: any[]; +} - constructor(...args) { - super(...args); +class AppComponent extends Component { + constructor(props: any) { + super(props); this.registerRouter(); this.state = { @@ -54,18 +61,10 @@ class AppComponent extends Component { }; } - UNSAFE_componentWillMount() { - routing.userHasLeftApp = false; - } - componentDidMount() { this.checkPermissions(); } - componentWillUnmount() { - routing.userHasLeftApp = true; - } - async checkPermissions() { this.setState({ isFetchingPermissions: true, @@ -163,7 +162,6 @@ class AppComponent extends Component {
- + ( diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js b/x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/remote_clusters_provider.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/remote_clusters_provider.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_error.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_error.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_loading.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_loading.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js b/x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/components/section_unauthorized.js rename to x-pack/plugins/cross_cluster_replication/public/app/components/section_unauthorized.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/api.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/index.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/sections.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/sections.ts diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js b/x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/constants/ui_metric.js rename to x-pack/plugins/cross_cluster_replication/public/app/constants/ui_metric.ts diff --git a/x-pack/plugins/cross_cluster_replication/public/app/index.tsx b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx new file mode 100644 index 0000000000000..79569b587f97f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/index.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Provider } from 'react-redux'; +import { HashRouter } from 'react-router-dom'; +import { I18nStart } from 'kibana/public'; +import { UnmountCallback } from 'src/core/public'; + +import { init as initBreadcrumbs, SetBreadcrumbs } from './services/breadcrumbs'; +import { init as initDocumentation } from './services/documentation_links'; +import { App } from './app'; +import { ccrStore } from './store'; + +const renderApp = (element: Element, I18nContext: I18nStart['Context']): UnmountCallback => { + render( + + + + + + + , + element + ); + + return () => unmountComponentAtNode(element); +}; + +export async function mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, +}: { + element: Element; + setBreadcrumbs: SetBreadcrumbs; + I18nContext: I18nStart['Context']; + ELASTIC_WEBSITE_URL: string; + DOC_LINK_VERSION: string; +}): Promise { + // Import and initialize additional services here instead of in plugin.ts to reduce the size of the + // initial bundle as much as possible. + initBreadcrumbs(setBreadcrumbs); + initDocumentation(`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`); + + return renderApp(element, I18nContext); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/auto_follow_pattern_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js similarity index 80% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js index 2c90456076f85..be470edc07537 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.container.js @@ -39,8 +39,23 @@ const mapStateToProps = state => ({ const mapDispatchToProps = dispatch => ({ getAutoFollowPattern: id => dispatch(getAutoFollowPattern(id)), selectAutoFollowPattern: id => dispatch(selectEditAutoFollowPattern(id)), - saveAutoFollowPattern: (id, autoFollowPattern) => - dispatch(saveAutoFollowPattern(id, autoFollowPattern, true)), + saveAutoFollowPattern: (id, autoFollowPattern) => { + // Strip out errors. + const { active, remoteCluster, leaderIndexPatterns, followIndexPattern } = autoFollowPattern; + + dispatch( + saveAutoFollowPattern( + id, + { + active, + remoteCluster, + leaderIndexPatterns, + followIndexPattern, + }, + true + ) + ); + }, clearApiError: () => { dispatch(clearApiError(`${scope}-get`)); dispatch(clearApiError(`${scope}-save`)); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js index 4cd3617abd989..387d7817a0357 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/auto_follow_pattern_edit.js @@ -12,7 +12,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiPageContent, EuiSpacer } from '@elastic/eui'; import { listBreadcrumb, editBreadcrumb, setBreadcrumbs } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternForm, AutoFollowPatternPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/auto_follow_pattern_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/auto_follow_pattern_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/follower_index_add.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/follower_index_add.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_add/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_add/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js index 21493602c12a7..22f9a7338384b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/follower_index_edit.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/follower_index_edit.js @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import { setBreadcrumbs, listBreadcrumb, editBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { FollowerIndexForm, FollowerIndexPageTitle, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/follower_index_edit/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/follower_index_edit/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js index e9e14f57e814f..c8cf94842aa68 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/auto_follow_pattern_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_AUTO_FOLLOW_PATTERN_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js index 956a9f10d810b..eb90e59e99fee 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/auto_follow_pattern_table.js @@ -20,8 +20,8 @@ import { AutoFollowPatternDeleteProvider, AutoFollowPatternActionMenu, } from '../../../../../components'; -import routing from '../../../../../services/routing'; -import { trackUiMetric, METRIC_TYPE } from '../../../../../services/track_ui_metric'; +import { routing } from '../../../../../services/routing'; +import { trackUiMetric } from '../../../../../services/track_ui_metric'; export class AutoFollowPatternTable extends PureComponent { static propTypes = { @@ -86,7 +86,7 @@ export class AutoFollowPatternTable extends PureComponent { return ( { - trackUiMetric(METRIC_TYPE.CLICK, UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_AUTO_FOLLOW_PATTERN_SHOW_DETAILS_CLICK); selectAutoFollowPattern(name); }} data-test-subj="autoFollowPatternLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/auto_follow_pattern_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js index 1a6d5e6efe35a..3f2ed82420ff1 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/detail_panel.js @@ -7,7 +7,9 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; +import moment from 'moment'; + +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; import { EuiButtonEmpty, @@ -247,6 +249,7 @@ export class DetailPanel extends Component {
    {autoFollowPattern.errors.map((error, i) => (
  • + {moment(error.timestamp).format('MMMM Do, YYYY h:mm:ss A')}:{' '} {error.autoFollowException.reason}
  • ))} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/detail_panel/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/auto_follow_pattern_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/auto_follow_pattern_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js index 0f6ef75522ff7..4a66f7b717bac 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/context_menu.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/context_menu.js @@ -15,7 +15,7 @@ import { EuiPopoverTitle, } from '@elastic/eui'; -import routing from '../../../../../services/routing'; +import { routing } from '../../../../../services/routing'; import { FollowerIndexPauseProvider, FollowerIndexResumeProvider, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/context_menu/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/context_menu/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js index 3e8cf6d3e2f78..4436d76643e6c 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/detail_panel/detail_panel.js @@ -31,7 +31,7 @@ import { } from '@elastic/eui'; import 'brace/theme/textmate'; -import { getIndexListUri } from '../../../../../../../../../../../plugins/index_management/public'; +import { getIndexListUri } from '../../../../../../../../../plugins/index_management/public'; import { API_STATUS } from '../../../../../constants'; import { ContextMenu } from '../context_menu'; @@ -489,7 +489,6 @@ export class DetailPanel extends Component { return ( { - trackUiMetric(METRIC_TYPE.CLICK, UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); + trackUiMetric('click', UIM_FOLLOWER_INDEX_SHOW_DETAILS_CLICK); selectFollowerIndex(name); }} data-test-subj="followerIndexLink" diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/follower_indices_table/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/follower_indices_table/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/components/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/components/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js similarity index 99% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js index b7e04721f4748..7b843d08cefd3 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/follower_indices_list.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/follower_indices_list.js @@ -17,7 +17,7 @@ import { EuiSpacer, } from '@elastic/eui'; -import routing from '../../../services/routing'; +import { routing } from '../../../services/routing'; import { extractQueryParams } from '../../../services/query_params'; import { trackUiMetric, METRIC_TYPE } from '../../../services/track_ui_metric'; import { API_STATUS, UIM_FOLLOWER_INDEX_LIST_LOAD } from '../../../constants'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/follower_indices_list/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/follower_indices_list/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.container.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.container.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js similarity index 96% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js index 88db909612245..bcd9dad114862 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/home.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/home.js @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPageBody, EuiPageContent, EuiSpacer, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; -import { BASE_PATH } from '../../../../../common/constants'; +import { BASE_PATH } from '../../../../common/constants'; import { setBreadcrumbs, listBreadcrumb } from '../../services/breadcrumbs'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import { AutoFollowPatternList } from './auto_follow_pattern_list'; import { FollowerIndicesList } from './follower_indices_list'; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/home/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/home/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts similarity index 50% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts index 300afb4e2d2ff..b7c1f495604be 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/index.ts +++ b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.d.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export * from './plugin'; -export * from './base_path'; -export * from './app'; -export * from './settings'; +export declare const CrossClusterReplicationHome: any; +export declare const AutoFollowPatternAdd: any; +export declare const AutoFollowPatternEdit: any; +export declare const FollowerIndexAdd: any; +export declare const FollowerIndexEdit: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js b/x-pack/plugins/cross_cluster_replication/public/app/sections/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/sections/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/sections/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap b/x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap rename to x-pack/plugins/cross_cluster_replication/public/app/services/__snapshots__/auto_follow_pattern_validators.test.js.snap diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js similarity index 98% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/api.js index 24bc7e17356e2..adff40ef29be6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/api.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/api.js @@ -7,8 +7,8 @@ import { API_BASE_PATH, API_REMOTE_CLUSTERS_BASE_PATH, API_INDEX_MANAGEMENT_BASE_PATH, -} from '../../../../common/constants'; -import { arrify } from '../../../../common/services/utils'; +} from '../../../common/constants'; +import { arrify } from '../../../common/services/utils'; import { UIM_FOLLOWER_INDEX_CREATE, UIM_FOLLOWER_INDEX_UPDATE, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js similarity index 92% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js index 95aa3f0ebc3e4..70311d5ba1e4d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.js @@ -9,11 +9,12 @@ export const parseAutoFollowError = error => { return null; } - const { leaderIndex, autoFollowException } = error; + const { leaderIndex, autoFollowException, timestamp } = error; const id = leaderIndex.substring(0, leaderIndex.lastIndexOf(':')); return { id, + timestamp, leaderIndex, autoFollowException, }; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_errors.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_errors.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js index 1b5a39658ee46..cf394d4b3c7d8 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.js @@ -8,8 +8,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; -import { indexPatterns } from '../../../../../../../../src/plugins/data/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; +import { indexPatterns } from '../../../../../../src/plugins/data/public'; const { indexNameBeginsWithPeriod, diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/auto_follow_pattern_validators.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/auto_follow_pattern_validators.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts similarity index 62% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts rename to x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts index dc64cdee07f7d..84ac9356462ad 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/breadcrumbs.ts +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/breadcrumbs.ts @@ -3,26 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ + import { i18n } from '@kbn/i18n'; import { ChromeBreadcrumb } from 'src/core/public'; -import { ManagementAppMountParams } from '../../../../../../../../src/plugins/management/public'; +import { ManagementAppMountParams } from '../../../../../../src/plugins/management/public'; + +import { BASE_PATH } from '../../../common/constants'; -import { BASE_PATH } from '../../../../common/constants'; +export type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; -let setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; +let setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; -export const setBreadcrumbSetter = ({ - __LEGACY, -}: { - __LEGACY: { - chrome: any; - MANAGEMENT_BREADCRUMB: ChromeBreadcrumb; - }; -}): void => { - setBreadcrumbs = (crumbs: ChromeBreadcrumb[]) => { - __LEGACY.chrome.breadcrumbs.set([__LEGACY.MANAGEMENT_BREADCRUMB, ...crumbs]); - }; +export const init = (_setBreadcrumbs: SetBreadcrumbs): void => { + setBreadcrumbs = _setBreadcrumbs; }; export const listBreadcrumb = { diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts new file mode 100644 index 0000000000000..c8b00f6e246b5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/documentation_links.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +let _esBase: string; + +export const init = (esBase: string) => { + _esBase = esBase; +}; + +export const getAutoFollowPatternUrl = (): string => `${_esBase}/ccr-put-auto-follow-pattern.html`; +export const getFollowerIndexUrl = (): string => `${_esBase}/ccr-put-follow.html`; +export const getByteUnitsUrl = (): string => `${_esBase}/common-options.html#byte-units`; +export const getTimeUnitsUrl = (): string => `${_esBase}/common-options.html#time-units`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js similarity index 89% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js index d20fa76ef5451..118a54887d404 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/follower_index_default_settings.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/follower_index_default_settings.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../common/constants'; export const getSettingDefault = name => { if (!FOLLOWER_INDEX_ADVANCED_SETTINGS[name]) { diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js b/x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/get_remote_cluster_name.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/get_remote_cluster_name.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js index 64c3e8412437e..7e2b45b625c1f 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/input_validation.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/input_validation.js @@ -6,7 +6,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { indices } from '../../../../../../../../src/plugins/es_ui_shared/public'; +import { indices } from '../../../../../../src/plugins/es_ui_shared/public'; const isEmpty = value => { return !value || !value.trim().length; diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts new file mode 100644 index 0000000000000..66fc9de00995c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/notifications.ts @@ -0,0 +1,18 @@ +/* + * 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 { IToasts, FatalErrorsSetup } from 'src/core/public'; + +let _toasts: IToasts; +let _fatalErrors: FatalErrorsSetup; + +export const init = (toasts: IToasts, fatalErrors: FatalErrorsSetup) => { + _toasts = toasts; + _fatalErrors = fatalErrors; +}; + +export const getToasts = () => _toasts; +export const getFatalErrors = () => _fatalErrors; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js b/x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/query_params.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/query_params.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts similarity index 87% rename from x-pack/legacy/plugins/cross_cluster_replication/public/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts index e92c44da34474..9e96ea12856f6 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.d.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './register_routes'; +export declare const routing: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/routing.js index 965aeaaad22ad..124c61e1ba19e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/routing.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/routing.js @@ -10,7 +10,7 @@ import { createLocation } from 'history'; import { stringify } from 'query-string'; -import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../../common/constants'; +import { APPS, BASE_PATH, BASE_PATH_REMOTE_CLUSTERS } from '../../../common/constants'; const isModifiedEvent = event => !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); @@ -32,7 +32,6 @@ const appToBasePathMap = { }; class Routing { - _userHasLeftApp = false; _reactRouter = null; /** @@ -97,14 +96,6 @@ class Routing { set reactRouter(router) { this._reactRouter = router; } - - get userHasLeftApp() { - return this._userHasLeftApp; - } - - set userHasLeftApp(hasLeft) { - this._userHasLeftApp = hasLeft; - } } -export default new Routing(); +export const routing = new Routing(); diff --git a/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.ts new file mode 100644 index 0000000000000..aecc4eb83893f --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/app/services/track_ui_metric.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 { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { UiStatsMetricType, METRIC_TYPE } from '@kbn/analytics'; + +import { UIM_APP_NAME } from '../constants'; + +export { METRIC_TYPE }; + +// usageCollection is an optional dependency, so we default to a no-op. +export let trackUiMetric = (metricType: UiStatsMetricType, eventName: string) => {}; + +export function init(usageCollection: UsageCollectionSetup): void { + trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, UIM_APP_NAME); +} + +/** + * Transparently return provided request Promise, while allowing us to track + * a successful completion of the request. + */ +export function trackUserRequest(request: Promise, actionType: string) { + // Only track successful actions. + return request.then(response => { + // It looks like we're using the wrong type here, added via + // https://github.com/elastic/kibana/pull/41113/files#diff-e65a0a6696a9d723969afd871cbd60cdR19 + // but we'll keep it for now to avoid discontinuity in our telemetry data. + trackUiMetric(METRIC_TYPE.LOADED, actionType); + + // We return the response immediately without waiting for the tracking request to resolve, + // to avoid adding additional latency. + return response; + }); +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js b/x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/services/utils.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/services/utils.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js b/x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/action_types.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/action_types.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js index b81cd30f3977a..52a22cb17d0a9 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/auto_follow_pattern.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/auto_follow_pattern.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { getNotifications } from '../../services/notifications'; +import { getToasts } from '../../services/notifications'; import { SECTIONS, API_STATUS } from '../../constants'; import { loadAutoFollowPatterns as loadAutoFollowPatternsRequest, @@ -15,7 +15,7 @@ import { pauseAutoFollowPattern as pauseAutoFollowPatternRequest, resumeAutoFollowPattern as resumeAutoFollowPatternRequest, } from '../../services/api'; -import routing from '../../services/routing'; +import { routing } from '../../services/routing'; import * as t from '../action_types'; import { sendApiRequest } from './api'; import { getSelectedAutoFollowPatternId } from '../selectors'; @@ -75,7 +75,7 @@ export const saveAutoFollowPattern = (id, autoFollowPattern, isUpdating = false) } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); routing.navigate(`/auto_follow_patterns`, undefined, { pattern: encodeURIComponent(id), }); @@ -111,7 +111,7 @@ export const deleteAutoFollowPattern = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsDeleted.length) { @@ -133,7 +133,7 @@ export const deleteAutoFollowPattern = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); // If we've just deleted a pattern we were looking at, we need to close the panel. const autoFollowPatternId = getSelectedAutoFollowPatternId('detail')(getState()); @@ -173,7 +173,7 @@ export const pauseAutoFollowPattern = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsPaused.length) { @@ -195,7 +195,7 @@ export const pauseAutoFollowPattern = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); } }, }); @@ -229,7 +229,7 @@ export const resumeAutoFollowPattern = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsResumed.length) { @@ -251,7 +251,7 @@ export const resumeAutoFollowPattern = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); } }, }); diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/ccr.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/ccr.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js similarity index 94% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js index ebdee067ced75..d081e0444eb58 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/follower_index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/follower_index.js @@ -5,8 +5,8 @@ */ import { i18n } from '@kbn/i18n'; -import routing from '../../services/routing'; -import { getNotifications } from '../../services/notifications'; +import { routing } from '../../services/routing'; +import { getToasts } from '../../services/notifications'; import { SECTIONS, API_STATUS } from '../../constants'; import { loadFollowerIndices as loadFollowerIndicesRequest, @@ -76,7 +76,7 @@ export const saveFollowerIndex = (name, followerIndex, isUpdating = false) => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); routing.navigate(`/follower_indices`, undefined, { name: encodeURIComponent(name), }); @@ -112,7 +112,7 @@ export const pauseFollowerIndex = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsPaused.length) { @@ -134,7 +134,7 @@ export const pauseFollowerIndex = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); // Refresh list dispatch(loadFollowerIndices(true)); @@ -149,6 +149,7 @@ export const resumeFollowerIndex = id => scope, handler: async () => resumeFollowerIndexRequest(id), onSuccess(response, dispatch) { + console.log('response', response); /** * We can have 1 or more follower index resume operation * that can fail or succeed. We will show 1 toast notification for each. @@ -171,7 +172,7 @@ export const resumeFollowerIndex = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsResumed.length) { @@ -193,7 +194,7 @@ export const resumeFollowerIndex = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); } // Refresh list @@ -230,7 +231,7 @@ export const unfollowLeaderIndex = id => } ); - getNotifications().addDanger(errorMessage); + getToasts().addDanger(errorMessage); } if (response.itemsUnfollowed.length) { @@ -252,7 +253,7 @@ export const unfollowLeaderIndex = id => } ); - getNotifications().addSuccess(successMessage); + getToasts().addSuccess(successMessage); } if (response.itemsNotOpen.length) { @@ -274,7 +275,7 @@ export const unfollowLeaderIndex = id => } ); - getNotifications().addWarning(warningMessage); + getToasts().addWarning(warningMessage); } // If we've just unfollowed a follower index we were looking at, we need to close the panel. diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/actions/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/actions/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts similarity index 83% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts index f2c070fd44b6e..6d35dfeddfd46 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/check_license/index.js +++ b/x-pack/plugins/cross_cluster_replication/public/app/store/index.d.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { checkLicense } from './check_license'; +export declare const ccrStore: any; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/api.test.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/api.test.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/auto_follow_pattern.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/auto_follow_pattern.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/follower_index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/follower_index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js b/x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/reducers/stats.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/reducers/stats.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js b/x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/selectors/index.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/selectors/index.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js b/x-pack/plugins/cross_cluster_replication/public/app/store/store.js similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/app/store/store.js rename to x-pack/plugins/cross_cluster_replication/public/app/store/store.js diff --git a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts b/x-pack/plugins/cross_cluster_replication/public/index.ts similarity index 61% rename from x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts rename to x-pack/plugins/cross_cluster_replication/public/index.ts index 11aea6b7b5de4..e3e2d860e526d 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/public/np_ready/index.ts +++ b/x-pack/plugins/cross_cluster_replication/public/index.ts @@ -6,6 +6,7 @@ import { PluginInitializerContext } from 'src/core/public'; -import { CrossClusterReplicationUIPlugin } from './plugin'; +import { CrossClusterReplicationPlugin } from './plugin'; -export const plugin = (ctx: PluginInitializerContext) => new CrossClusterReplicationUIPlugin(ctx); +export const plugin = (initializerContext: PluginInitializerContext) => + new CrossClusterReplicationPlugin(initializerContext); diff --git a/x-pack/plugins/cross_cluster_replication/public/plugin.ts b/x-pack/plugins/cross_cluster_replication/public/plugin.ts new file mode 100644 index 0000000000000..bdaa04e9d53ee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/plugin.ts @@ -0,0 +1,102 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { first } from 'rxjs/operators'; +import { CoreSetup, Plugin, PluginInitializerContext } from 'src/core/public'; + +import { PLUGIN, MANAGEMENT_ID } from '../common/constants'; +import { init as initUiMetric } from './app/services/track_ui_metric'; +import { init as initNotification } from './app/services/notifications'; +import { PluginDependencies, ClientConfigType } from './types'; + +// @ts-ignore; +import { setHttpClient } from './app/services/api'; + +export class CrossClusterReplicationPlugin implements Plugin { + constructor(private readonly initializerContext: PluginInitializerContext) {} + + public setup(coreSetup: CoreSetup, plugins: PluginDependencies) { + const { licensing, remoteClusters, usageCollection, management, indexManagement } = plugins; + const esSection = management.sections.getSection('elasticsearch'); + + const { + http, + notifications: { toasts }, + fatalErrors, + getStartServices, + } = coreSetup; + + // Initialize services even if the app isn't mounted, because they're used by index management extensions. + setHttpClient(http); + initUiMetric(usageCollection); + initNotification(toasts, fatalErrors); + + const ccrApp = esSection!.registerApp({ + id: MANAGEMENT_ID, + title: PLUGIN.TITLE, + order: 4, + mount: async ({ element, setBreadcrumbs }) => { + const { mountApp } = await import('./app'); + + const [coreStart] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION }, + } = coreStart; + + return mountApp({ + element, + setBreadcrumbs, + I18nContext, + ELASTIC_WEBSITE_URL, + DOC_LINK_VERSION, + }); + }, + }); + + ccrApp.disable(); + + licensing.license$ + .pipe(first()) + .toPromise() + .then(license => { + const licenseStatus = license.check(PLUGIN.ID, PLUGIN.minimumLicenseType); + const isLicenseOk = licenseStatus.state === 'valid'; + const config = this.initializerContext.config.get(); + + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + if (isLicenseOk && isCcrUiEnabled) { + ccrApp.enable(); + + if (indexManagement) { + const propertyPath = 'isFollowerIndex'; + + const followerBadgeExtension = { + matchIndex: (index: any) => { + return get(index, propertyPath); + }, + label: i18n.translate('xpack.crossClusterReplication.indexMgmtBadge.followerLabel', { + defaultMessage: 'Follower', + }), + color: 'default', + filterExpression: 'isFollowerIndex:true', + }; + + indexManagement.extensionsService.addBadge(followerBadgeExtension); + } + } + }); + } + + public start() {} + public stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/public/types.ts b/x-pack/plugins/cross_cluster_replication/public/types.ts new file mode 100644 index 0000000000000..aac174b7524d3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/public/types.ts @@ -0,0 +1,25 @@ +/* + * 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 { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { IndexManagementPluginSetup } from '../../index_management/public'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/public'; +import { LicensingPluginSetup } from '../../licensing/public'; + +export interface PluginDependencies { + usageCollection: UsageCollectionSetup; + management: ManagementSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; + licensing: LicensingPluginSetup; +} + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts similarity index 97% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js rename to x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts index 91527b8eb7cc5..d4de54391286b 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/client/elasticsearch_ccr.js +++ b/x-pack/plugins/cross_cluster_replication/server/client/elasticsearch_ccr.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export const elasticsearchJsPlugin = (Client, config, components) => { +export const elasticsearchJsPlugin = (Client: any, config: any, components: any) => { const ca = components.clientAction.factory; Client.prototype.ccr = components.clientAction.namespaceFactory(); diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts new file mode 100644 index 0000000000000..17999d37c76b7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/config.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), +}); + +export type CrossClusterReplicationConfig = TypeOf; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts new file mode 100644 index 0000000000000..597c039ad202e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -0,0 +1,19 @@ +/* + * 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 { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { CrossClusterReplicationServerPlugin } from './plugin'; +import { configSchema, CrossClusterReplicationConfig } from './config'; + +export const plugin = (pluginInitializerContext: PluginInitializerContext) => + new CrossClusterReplicationServerPlugin(pluginInitializerContext); + +export const config: PluginConfigDescriptor = { + schema: configSchema, + exposeToBrowser: { + ui: true, + }, +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap similarity index 93% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap rename to x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap index 92ac6070904b5..3eced37112a35 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/__snapshots__/ccr_stats_serialization.test.js.snap +++ b/x-pack/plugins/cross_cluster_replication/server/lib/__snapshots__/ccr_stats_serialization.test.ts.snap @@ -19,6 +19,7 @@ Object { "type": "exception", }, "leaderIndex": "pattern-1:kibana_sample_1", + "timestamp": 1587081600021, }, Object { "autoFollowException": Object { @@ -26,6 +27,7 @@ Object { "type": "exception", }, "leaderIndex": "pattern-2:kibana_sample_1", + "timestamp": 1587081600021, }, ], } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts similarity index 95% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js rename to x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts index 5120c56701e5b..5141aa56c1d7e 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.test.js +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.test.ts @@ -15,6 +15,7 @@ describe('[CCR] auto-follow stats serialization', () => { recent_auto_follow_errors: [ { leader_index: 'pattern-1:kibana_sample_1', + timestamp: 1587081600021, auto_follow_exception: { type: 'exception', reason: @@ -23,6 +24,7 @@ describe('[CCR] auto-follow stats serialization', () => { }, { leader_index: 'pattern-2:kibana_sample_1', + timestamp: 1587081600021, auto_follow_exception: { type: 'exception', reason: diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts similarity index 77% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js rename to x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts index e4d2f8d64d1bb..7e2b088919842 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/ccr_stats_serialization.js +++ b/x-pack/plugins/cross_cluster_replication/server/lib/ccr_stats_serialization.ts @@ -4,11 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable camelcase */ +import { + RecentAutoFollowError, + RecentAutoFollowErrorFromEs, + AutoFollowedCluster, + AutoFollowedClusterFromEs, + AutoFollowStats, + AutoFollowStatsFromEs, +} from '../../common/types'; + export const deserializeRecentAutoFollowErrors = ({ + timestamp, leader_index, auto_follow_exception: { type, reason }, -}) => ({ +}: RecentAutoFollowErrorFromEs): RecentAutoFollowError => ({ + timestamp, leaderIndex: leader_index, autoFollowException: { type, @@ -20,7 +30,7 @@ export const deserializeAutoFollowedClusters = ({ cluster_name, time_since_last_check_millis, last_seen_metadata_version, -}) => ({ +}: AutoFollowedClusterFromEs): AutoFollowedCluster => ({ clusterName: cluster_name, timeSinceLastCheckMillis: time_since_last_check_millis, lastSeenMetadataVersion: last_seen_metadata_version, @@ -32,11 +42,10 @@ export const deserializeAutoFollowStats = ({ number_of_successful_follow_indices, recent_auto_follow_errors, auto_followed_clusters, -}) => ({ +}: AutoFollowStatsFromEs): AutoFollowStats => ({ numberOfFailedFollowIndices: number_of_failed_follow_indices, numberOfFailedRemoteClusterStateRequests: number_of_failed_remote_cluster_state_requests, numberOfSuccessfulFollowIndices: number_of_successful_follow_indices, recentAutoFollowErrors: recent_auto_follow_errors.map(deserializeRecentAutoFollowErrors), autoFollowedClusters: auto_followed_clusters.map(deserializeAutoFollowedClusters), }); -/* eslint-enable camelcase */ diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts similarity index 90% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts rename to x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts index 8afd5f1a018eb..9dde027cd6949 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/error_wrappers/wrap_es_error.ts +++ b/x-pack/plugins/cross_cluster_replication/server/lib/format_es_error.ts @@ -63,3 +63,16 @@ export function wrapEsError( const message = statusCodeToMessageMap[statusCode]; return { message, statusCode }; } + +export function formatEsError(err: any): any { + const { statusCode, message, body } = wrapEsError(err); + return { + statusCode, + body: { + message, + attributes: { + cause: body?.cause, + }, + }, + }; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts similarity index 100% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error.ts rename to x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts new file mode 100644 index 0000000000000..25c99803480f3 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts @@ -0,0 +1,139 @@ +/* + * 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. + */ + +declare module 'src/core/server' { + interface RequestHandlerContext { + crossClusterReplication?: CrossClusterReplicationContext; + } +} + +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + Plugin, + Logger, + PluginInitializerContext, + APICaller, + IScopedClusterClient, +} from 'src/core/server'; + +import { Index } from '../../index_management/server'; +import { PLUGIN } from '../common/constants'; +import { Dependencies } from './types'; +import { registerApiRoutes } from './routes'; +import { License } from './services'; +import { elasticsearchJsPlugin } from './client/elasticsearch_ccr'; +import { CrossClusterReplicationConfig } from './config'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +interface CrossClusterReplicationContext { + client: IScopedClusterClient; +} + +const ccrDataEnricher = async (indicesList: Index[], callWithRequest: APICaller) => { + if (!indicesList?.length) { + return indicesList; + } + const params = { + path: '/_all/_ccr/info', + method: 'GET', + }; + try { + const { follower_indices: followerIndices } = await callWithRequest( + 'transport.request', + params + ); + return indicesList.map(index => { + const isFollowerIndex = !!followerIndices.find( + (followerIndex: { follower_index: string }) => { + return followerIndex.follower_index === index.name; + } + ); + return { + ...index, + isFollowerIndex, + }; + }); + } catch (e) { + return indicesList; + } +}; + +export class CrossClusterReplicationServerPlugin implements Plugin { + private readonly config$: Observable; + private readonly license: License; + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + this.config$ = initializerContext.config.create(); + this.license = new License(); + } + + setup( + { http, elasticsearch }: CoreSetup, + { licensing, indexManagement, remoteClusters }: Dependencies + ) { + this.config$ + .pipe(first()) + .toPromise() + .then(config => { + // remoteClusters.isUiEnabled is driven by the xpack.remote_clusters.ui.enabled setting. + // The CCR UI depends upon the Remote Clusters UI (e.g. by cross-linking to it), so if + // the Remote Clusters UI is disabled we can't show the CCR UI. + const isCcrUiEnabled = config.ui.enabled && remoteClusters.isUiEnabled; + + // If the UI isn't enabled, then we don't want to expose any CCR concepts in the UI, including + // "follower" badges for follower indices. + if (isCcrUiEnabled) { + if (indexManagement.indexDataEnricher) { + indexManagement.indexDataEnricher.add(ccrDataEnricher); + } + } + }); + + this.license.setup( + { + pluginId: PLUGIN.ID, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate( + 'xpack.crossClusterReplication.licenseCheckErrorMessage', + { + defaultMessage: 'License check failed', + } + ), + }, + { + licensing, + logger: this.logger, + } + ); + + // Extend the elasticsearchJs client with additional endpoints. + const esClientConfig = { plugins: [elasticsearchJsPlugin] }; + const ccrEsClient = elasticsearch.createClient('crossClusterReplication', esClientConfig); + http.registerRouteHandlerContext('crossClusterReplication', (ctx, request) => { + return { + client: ccrEsClient.asScoped(request), + }; + }); + + registerApiRoutes({ + router: http.createRouter(), + license: this.license, + lib: { + isEsError, + formatEsError, + }, + }); + } + + start() {} + stop() {} +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts new file mode 100644 index 0000000000000..4cbdc7703a694 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerDeleteRoute } from './register_delete_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerAutoFollowPatternRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerDeleteRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts new file mode 100644 index 0000000000000..b41b52e1764c8 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should throw a 409 conflict error if id already exists', async () => { + const routeContextMock = mockRouteContext({ + // Fail the uniqueness check. + callAsCurrentUser: jest.fn().mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(409); + }); + + it('should return 200 status when the id does not exist', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // Pass the uniqueness check. + .mockRejectedValueOnce({ statusCode: 404 }) + .mockResolvedValueOnce(true), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + id: 'some-id', + foo: 'bar', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts new file mode 100644 index 0000000000000..12503e3532a47 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.ts @@ -0,0 +1,77 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Create an auto-follow pattern + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + id: schema.string(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id, ...rest } = request.body; + const body = serializeAutoFollowPattern(rest as AutoFollowPattern); + + /** + * First let's make sure that an auto-follow pattern with + * the same id does not exist. + */ + try { + await context.crossClusterReplication!.client.callAsCurrentUser('ccr.autoFollowPattern', { + id, + }); + // If we get here it means that an auto-follow pattern with the same id exists + return response.conflict({ + body: `An auto-follow pattern with the name "${id}" already exists.`, + }); + } catch (err) { + if (err.statusCode !== 404) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + } + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts new file mode 100644 index 0000000000000..e610d09b44275 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerDeleteRoute } from './register_delete_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Delete auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerDeleteRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.delete.mock.calls[0][1]; + }); + + it('deletes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('deletes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsDeleted).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.itemsDeleted).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts new file mode 100644 index 0000000000000..ed2633a4a469e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.ts @@ -0,0 +1,67 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Delete an auto-follow pattern + */ +export const registerDeleteRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.delete( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsDeleted: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.deleteAutoFollowPattern', { + id: _id, + }) + .then(() => itemsDeleted.push(_id)) + .catch((err: any) => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsDeleted, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts new file mode 100644 index 0000000000000..dd102c45665cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all auto-follow patterns', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.patterns).toEqual([ + { + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts new file mode 100644 index 0000000000000..70d8ae4d51e3b --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.ts @@ -0,0 +1,43 @@ +/* + * 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 { deserializeListAutoFollowPatterns } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a list of all auto-follow patterns + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/auto_follow_patterns'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPatterns' + ); + return response.ok({ + body: { + patterns: deserializeListAutoFollowPatterns(result.patterns), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts new file mode 100644 index 0000000000000..d5889074651f5 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts @@ -0,0 +1,67 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrAutoFollowPatternResponseMock = { + patterns: [ + { + name: 'autoFollowPattern', + pattern: { + active: true, + remote_cluster: 'remoteCluster', + leader_index_patterns: ['leader*'], + follow_index_pattern: 'follow', + }, + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce(ccrAutoFollowPatternResponseMock), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload).toEqual({ + active: true, + followIndexPattern: 'follow', + leaderIndexPatterns: ['leader*'], + name: 'autoFollowPattern', + remoteCluster: 'remoteCluster', + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts new file mode 100644 index 0000000000000..1edbf7e8806c7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; + +import { deserializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Get a single auto-follow pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const result = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.autoFollowPattern', + { id } + ); + const autoFollowPattern = result.patterns[0]; + + return response.ok({ + body: deserializeAutoFollowPattern(autoFollowPattern), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts new file mode 100644 index 0000000000000..1eaac02918b88 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts new file mode 100644 index 0000000000000..325939709e751 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.ts @@ -0,0 +1,66 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pause auto-follow pattern(s) + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(_id => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseAutoFollowPattern', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts new file mode 100644 index 0000000000000..9839761e701fc --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume auto-follow pattern(s)', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts new file mode 100644 index 0000000000000..f5e917773704c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.ts @@ -0,0 +1,66 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resume auto-follow pattern(s) + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.post( + { + path: addBasePath('/auto_follow_patterns/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeAutoFollowPattern', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts new file mode 100644 index 0000000000000..85f2270ec3aee --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts @@ -0,0 +1,64 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUpdateRoute } from './register_update_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Update auto-follow pattern', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerUpdateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('should serialize the payload before sending it to Elasticsearch', async () => { + const routeContextMock = mockRouteContext({ + // Just echo back what we send so we can inspect it. + callAsCurrentUser: jest.fn().mockImplementation((endpoint, payload) => payload), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'foo' }, + body: { + remoteCluster: 'bar1', + leaderIndexPatterns: ['bar2'], + followIndexPattern: 'bar3', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + id: 'foo', + body: { + remote_cluster: 'bar1', + leader_index_patterns: ['bar2'], + follow_index_pattern: 'bar3', + }, + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts new file mode 100644 index 0000000000000..836e5f55c5a48 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.ts @@ -0,0 +1,60 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { serializeAutoFollowPattern } from '../../../../common/services/auto_follow_pattern_serialization'; +import { AutoFollowPattern } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update an auto-follow pattern + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + const bodySchema = schema.object({ + active: schema.boolean(), + remoteCluster: schema.string(), + leaderIndexPatterns: schema.arrayOf(schema.string()), + followIndexPattern: schema.string(), + }); + + router.put( + { + path: addBasePath('/auto_follow_patterns/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const body = serializeAutoFollowPattern(request.body as AutoFollowPattern); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveAutoFollowPattern', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.ts new file mode 100644 index 0000000000000..45c5729535e58 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/index.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 { RouteDependencies } from '../../../types'; +import { registerPermissionsRoute } from './register_permissions_route'; +import { registerStatsRoute } from './register_stats_route'; + +export function registerCrossClusterReplicationRoutes(dependencies: RouteDependencies) { + registerPermissionsRoute(dependencies); + registerStatsRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts new file mode 100644 index 0000000000000..b8eb5ae14750e --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_permissions_route.ts @@ -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 { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns whether the user has CCR permissions + */ +export const registerPermissionsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/permissions'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + if (!license.isEsSecurityEnabled) { + // If security has been disabled in elasticsearch.yml. we'll just let the user use CCR + // because permissions are irrelevant. + return response.ok({ + body: { + hasPermission: true, + missingClusterPrivileges: [], + }, + }); + } + + try { + const { + has_all_requested: hasPermission, + cluster, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.permissions', { + body: { + cluster: ['manage', 'manage_ccr'], + }, + }); + + const missingClusterPrivileges = Object.keys(cluster).reduce( + (permissions: any, permissionName: any) => { + if (!cluster[permissionName]) { + permissions.push(permissionName); + return permissions; + } + }, + [] as any[] + ); + + return response.ok({ + body: { + hasPermission, + missingClusterPrivileges, + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts new file mode 100644 index 0000000000000..d4288cf7303e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/cross_cluster_replication/register_stats_route.ts @@ -0,0 +1,42 @@ +/* + * 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 { addBasePath } from '../../../services'; +import { deserializeAutoFollowStats } from '../../../lib/ccr_stats_serialization'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns Auto-follow stats + */ +export const registerStatsRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/stats/auto_follow'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + auto_follow_stats: autoFollowStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + return response.ok({ + body: deserializeAutoFollowStats(autoFollowStats), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts new file mode 100644 index 0000000000000..f5d8c7a4f5bda --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { RouteDependencies } from '../../../types'; +import { registerCreateRoute } from './register_create_route'; +import { registerFetchRoute } from './register_fetch_route'; +import { registerGetRoute } from './register_get_route'; +import { registerPauseRoute } from './register_pause_route'; +import { registerResumeRoute } from './register_resume_route'; +import { registerUnfollowRoute } from './register_unfollow_route'; +import { registerUpdateRoute } from './register_update_route'; + +export function registerFollowerIndexRoutes(dependencies: RouteDependencies) { + registerCreateRoute(dependencies); + registerFetchRoute(dependencies); + registerGetRoute(dependencies); + registerPauseRoute(dependencies); + registerResumeRoute(dependencies); + registerUnfollowRoute(dependencies); + registerUpdateRoute(dependencies); +} diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts new file mode 100644 index 0000000000000..bba82b04ce9a0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerCreateRoute } from './register_create_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Create follower index', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerCreateRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.post.mock.calls[0][1]; + }); + + it('should return 200 status when follower index is created', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + body: { + name: 'follower_index', + remoteCluster: 'remote_cluster', + leaderIndex: 'leader_index', + }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts new file mode 100644 index 0000000000000..acaeedacfdb2a --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.ts @@ -0,0 +1,65 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { serializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndex } from '../../../../common/types'; +import { addBasePath } from '../../../services'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { RouteDependencies } from '../../../types'; + +/** + * Create a follower index + */ +export const registerCreateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const bodySchema = schema.object({ + name: schema.string(), + remoteCluster: schema.string(), + leaderIndex: schema.string(), + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.post( + { + path: addBasePath('/follower_indices'), + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { name, ...rest } = request.body; + const body = removeEmptyFields(serializeFollowerIndex(rest as FollowerIndex)); + + try { + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.saveFollowerIndex', + { name, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts new file mode 100644 index 0000000000000..151ab84fabf4c --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts @@ -0,0 +1,160 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerFetchRoute } from './register_fetch_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Fetch all follower indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerFetchRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('deserializes the response from Elasticsearch', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrStatsMockResponse = { + follow_stats: { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }, + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest(); + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload.indices).toEqual([ + { + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }, + ]); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts new file mode 100644 index 0000000000000..a78901ce174e4 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.ts @@ -0,0 +1,62 @@ +/* + * 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 { deserializeListFollowerIndices } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a list of all follower indices + */ +export const registerFetchRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + router.get( + { + path: addBasePath('/follower_indices'), + validate: false, + }, + license.guardApiRoute(async (context, request, response) => { + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { + id: '_all', + }); + + const { + follow_stats: { indices: followerIndicesStats }, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.stats'); + + const followerIndicesStatsMap = followerIndicesStats.reduce((map: any, stats: any) => { + map[stats.index] = stats; + return map; + }, {}); + + const collatedFollowerIndices = followerIndices.map((followerIndex: any) => { + return { + ...followerIndex, + ...followerIndicesStatsMap[followerIndex.follower_index], + }; + }); + + return response.ok({ + body: { + indices: deserializeListFollowerIndices(collatedFollowerIndices), + }, + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts new file mode 100644 index 0000000000000..42d04ca65b1cb --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts @@ -0,0 +1,159 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerGetRoute } from './register_get_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Get one follower index', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerGetRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.get.mock.calls[0][1]; + }); + + it('should return a single resource even though ES returns an array with 1 item', async () => { + const ccrInfoMockResponse = { + follower_indices: [ + { + follower_index: 'followerIndexName', + remote_cluster: 'remoteCluster', + leader_index: 'leaderIndex', + status: 'active', + parameters: { + max_read_request_operation_count: 1, + max_outstanding_read_requests: 1, + max_read_request_size: '1b', + max_write_request_operation_count: 1, + max_write_request_size: '1b', + max_outstanding_write_requests: 1, + max_write_buffer_count: 1, + max_write_buffer_size: '1b', + max_retry_delay: '1s', + read_poll_timeout: '1s', + }, + }, + ], + }; + + // These stats correlate to the above follower indices. + const ccrFollowerIndexStatsMockResponse = { + indices: [ + { + index: 'followerIndexName', + shards: [ + { + shard_id: 1, + leader_index: 'leaderIndex', + leader_global_checkpoint: 1, + leader_max_seq_no: 1, + follower_global_checkpoint: 1, + follower_max_seq_no: 1, + last_requested_seq_no: 1, + outstanding_read_requests: 1, + outstanding_write_requests: 1, + write_buffer_operation_count: 1, + write_buffer_size_in_bytes: 1, + follower_mapping_version: 1, + follower_settings_version: 1, + total_read_time_millis: 1, + total_read_remote_exec_time_millis: 1, + successful_read_requests: 1, + failed_read_requests: 1, + operations_read: 1, + bytes_read: 1, + total_write_time_millis: 1, + successful_write_requests: 1, + failed_write_requests: 1, + operations_written: 1, + read_exceptions: 1, + time_since_last_read_millis: 1, + }, + ], + }, + ], + }; + + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce(ccrInfoMockResponse) + .mockResolvedValueOnce(ccrFollowerIndexStatsMockResponse), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'doesnt_matter' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + + expect(response.payload).toEqual({ + name: 'followerIndexName', + remoteCluster: 'remoteCluster', + leaderIndex: 'leaderIndex', + status: 'active', + maxReadRequestOperationCount: 1, + maxOutstandingReadRequests: 1, + maxReadRequestSize: '1b', + maxWriteRequestOperationCount: 1, + maxWriteRequestSize: '1b', + maxOutstandingWriteRequests: 1, + maxWriteBufferCount: 1, + maxWriteBufferSize: '1b', + maxRetryDelay: '1s', + readPollTimeout: '1s', + shards: [ + { + id: 1, + leaderIndex: 'leaderIndex', + leaderGlobalCheckpoint: 1, + leaderMaxSequenceNum: 1, + followerGlobalCheckpoint: 1, + followerMaxSequenceNum: 1, + lastRequestedSequenceNum: 1, + outstandingReadRequestsCount: 1, + outstandingWriteRequestsCount: 1, + writeBufferOperationsCount: 1, + writeBufferSizeBytes: 1, + followerMappingVersion: 1, + followerSettingsVersion: 1, + totalReadTimeMs: 1, + totalReadRemoteExecTimeMs: 1, + successfulReadRequestCount: 1, + failedReadRequestsCount: 1, + operationsReadCount: 1, + bytesReadCount: 1, + totalWriteTimeMs: 1, + successfulWriteRequestsCount: 1, + failedWriteRequestsCount: 1, + operationsWrittenCount: 1, + readExceptions: 1, + timeSinceLastReadMs: 1, + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts new file mode 100644 index 0000000000000..98a182fc15681 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { deserializeFollowerIndex } from '../../../../common/services/follower_index_serialization'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Returns a single follower index pattern + */ +export const registerGetRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ + id: schema.string(), + }); + + router.get( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ + body: `The follower index "${id}" does not exist.`, + }); + } + + // If this follower is paused, skip call to ES stats api since it will return 404 + if (followerIndexInfo.status === 'paused') { + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + }), + }); + } else { + const { + indices: followerIndicesStats, + } = await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.followerIndexStats', + { id } + ); + + return response.ok({ + body: deserializeFollowerIndex({ + ...followerIndexInfo, + ...(followerIndicesStats ? followerIndicesStats[0] : {}), + }), + }); + } + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts new file mode 100644 index 0000000000000..82cb88cbacea7 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerPauseRoute } from './register_pause_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Pause follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerPauseRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('pauses a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('pauses multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsPaused).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts new file mode 100644 index 0000000000000..7432ea7ca5c82 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.ts @@ -0,0 +1,64 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Pauses a follower index + */ +export const registerPauseRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/pause'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsPaused: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.pauseFollowerIndex', { + id: _id, + }) + .then(() => itemsPaused.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsPaused, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts new file mode 100644 index 0000000000000..04167c5db3162 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerResumeRoute } from './register_resume_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Resume follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerResumeRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('resumes a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest.fn().mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('resumes multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsResumed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts new file mode 100644 index 0000000000000..ca8f3a9f5fe9d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.ts @@ -0,0 +1,64 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Resumes a follower index + */ +export const registerResumeRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/resume'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsResumed: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map((_id: string) => + context + .crossClusterReplication!.client.callAsCurrentUser('ccr.resumeFollowerIndex', { + id: _id, + }) + .then(() => itemsResumed.push(_id)) + .catch(err => { + errors.push({ id: _id, error: formatError(err) }); + }) + ) + ); + + return response.ok({ + body: { + itemsResumed, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts new file mode 100644 index 0000000000000..6302d5868b0db --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts @@ -0,0 +1,109 @@ +/* + * 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 { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; +import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server'; + +import { isEsError } from '../../../lib/is_es_error'; +import { formatEsError } from '../../../lib/format_es_error'; +import { License } from '../../../services'; +import { mockRouteContext } from '../test_lib'; +import { registerUnfollowRoute } from './register_unfollow_route'; + +const httpService = httpServiceMock.createSetupContract(); + +describe('[CCR API] Unfollow follower index/indices', () => { + let routeHandler: RequestHandler; + + beforeEach(() => { + const router = httpService.createRouter('') as jest.Mocked; + + registerUnfollowRoute({ + router, + license: { + guardApiRoute: (route: any) => route, + } as License, + lib: { + isEsError, + formatEsError, + }, + }); + + routeHandler = router.put.mock.calls[0][1]; + }); + + it('unfollows a single item', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors).toEqual([]); + }); + + it('unfollows multiple items', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // c + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b,c' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a', 'b', 'c']); + expect(response.payload.errors).toEqual([]); + }); + + it('returns partial errors', async () => { + const routeContextMock = mockRouteContext({ + callAsCurrentUser: jest + .fn() + // a + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + .mockResolvedValueOnce({ acknowledge: true }) + // b + .mockResolvedValueOnce({ acknowledge: true }) + .mockRejectedValueOnce({ response: { error: {} } }), + }); + + const request = httpServerMock.createKibanaRequest({ + params: { id: 'a,b' }, + }); + + const response = await routeHandler(routeContextMock, request, kibanaResponseFactory); + expect(response.payload.itemsUnfollowed).toEqual(['a']); + expect(response.payload.errors[0].id).toEqual('b'); + }); +}); diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts new file mode 100644 index 0000000000000..282fead02bbe0 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.ts @@ -0,0 +1,95 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Unfollow follower index's leader index + */ +export const registerUnfollowRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + router.put( + { + path: addBasePath('/follower_indices/{id}/unfollow'), + validate: { + params: paramsSchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + const ids = id.split(','); + + const itemsUnfollowed: string[] = []; + const itemsNotOpen: string[] = []; + const errors: Array<{ id: string; error: any }> = []; + + const formatError = (err: any) => { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + }; + + await Promise.all( + ids.map(async (_id: string) => { + try { + // Try to pause follower, let it fail silently since it may already be paused + try { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { id: _id } + ); + } catch (e) { + // Swallow errors + } + + // Close index + await context.crossClusterReplication!.client.callAsCurrentUser('indices.close', { + index: _id, + }); + + // Unfollow leader + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.unfollowLeaderIndex', + { id: _id } + ); + + // Try to re-open the index, store failures in a separate array to surface warnings in the UI + // This will allow users to query their index normally after unfollowing + try { + await context.crossClusterReplication!.client.callAsCurrentUser('indices.open', { + index: _id, + }); + } catch (e) { + itemsNotOpen.push(_id); + } + + // Push success + itemsUnfollowed.push(_id); + } catch (err) { + errors.push({ id: _id, error: formatError(err) }); + } + }) + ); + + return response.ok({ + body: { + itemsUnfollowed, + itemsNotOpen, + errors, + }, + }); + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts new file mode 100644 index 0000000000000..521de77180974 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_update_route.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema } from '@kbn/config-schema'; +import { serializeAdvancedSettings } from '../../../../common/services/follower_index_serialization'; +import { FollowerIndexAdvancedSettings } from '../../../../common/types'; +import { removeEmptyFields } from '../../../../common/services/utils'; +import { addBasePath } from '../../../services'; +import { RouteDependencies } from '../../../types'; + +/** + * Update a follower index + */ +export const registerUpdateRoute = ({ + router, + license, + lib: { isEsError, formatEsError }, +}: RouteDependencies) => { + const paramsSchema = schema.object({ id: schema.string() }); + + const bodySchema = schema.object({ + maxReadRequestOperationCount: schema.maybe(schema.number()), + maxOutstandingReadRequests: schema.maybe(schema.number()), + maxReadRequestSize: schema.maybe(schema.string()), // byte value + maxWriteRequestOperationCount: schema.maybe(schema.number()), + maxWriteRequestSize: schema.maybe(schema.string()), // byte value + maxOutstandingWriteRequests: schema.maybe(schema.number()), + maxWriteBufferCount: schema.maybe(schema.number()), + maxWriteBufferSize: schema.maybe(schema.string()), // byte value + maxRetryDelay: schema.maybe(schema.string()), // time value + readPollTimeout: schema.maybe(schema.string()), // time value + }); + + router.put( + { + path: addBasePath('/follower_indices/{id}'), + validate: { + params: paramsSchema, + body: bodySchema, + }, + }, + license.guardApiRoute(async (context, request, response) => { + const { id } = request.params; + + // We need to first pause the follower and then resume it by passing the advanced settings + try { + const { + follower_indices: followerIndices, + } = await context.crossClusterReplication!.client.callAsCurrentUser('ccr.info', { id }); + + const followerIndexInfo = followerIndices && followerIndices[0]; + + if (!followerIndexInfo) { + return response.notFound({ body: `The follower index "${id}" does not exist.` }); + } + + // Retrieve paused state instead of pulling it from the payload to ensure it's not stale. + const isPaused = followerIndexInfo.status === 'paused'; + + // Pause follower if not already paused + if (!isPaused) { + await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.pauseFollowerIndex', + { + id, + } + ); + } + + // Resume follower + const body = removeEmptyFields( + serializeAdvancedSettings(request.body as FollowerIndexAdvancedSettings) + ); + + return response.ok({ + body: await context.crossClusterReplication!.client.callAsCurrentUser( + 'ccr.resumeFollowerIndex', + { id, body } + ), + }); + } catch (err) { + if (isEsError(err)) { + return response.customError(formatEsError(err)); + } + // Case: default + return response.internalError({ body: err }); + } + }) + ); +}; diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts new file mode 100644 index 0000000000000..9b4fb134ed230 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/test_lib.ts @@ -0,0 +1,23 @@ +/* + * 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 { RequestHandlerContext } from 'src/core/server'; + +export function mockRouteContext({ + callAsCurrentUser, +}: { + callAsCurrentUser: any; +}): RequestHandlerContext { + const routeContextMock = ({ + crossClusterReplication: { + client: { + callAsCurrentUser, + }, + }, + } as unknown) as RequestHandlerContext; + + return routeContextMock; +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts similarity index 52% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts rename to x-pack/plugins/cross_cluster_replication/server/routes/index.ts index 7e59417550691..84abfb369e002 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/register_routes.ts +++ b/x-pack/plugins/cross_cluster_replication/server/routes/index.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { RouteDependencies } from '../types'; + import { registerAutoFollowPatternRoutes } from './api/auto_follow_pattern'; import { registerFollowerIndexRoutes } from './api/follower_index'; -import { registerCcrRoutes } from './api/ccr'; -import { RouteDependencies } from './types'; +import { registerCrossClusterReplicationRoutes } from './api/cross_cluster_replication'; -export function registerRoutes(deps: RouteDependencies) { - registerAutoFollowPatternRoutes(deps); - registerFollowerIndexRoutes(deps); - registerCcrRoutes(deps); +export function registerApiRoutes(dependencies: RouteDependencies) { + registerAutoFollowPatternRoutes(dependencies); + registerFollowerIndexRoutes(dependencies); + registerCrossClusterReplicationRoutes(dependencies); } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts similarity index 64% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts rename to x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts index 4ce0a2f5644f3..3f3dd131df7c7 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/app.ts +++ b/x-pack/plugins/cross_cluster_replication/server/services/add_base_path.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -export const APPS = { - CCR_APP: 'ccr', - REMOTE_CLUSTER_APP: 'remote_cluster', -}; +import { API_BASE_PATH } from '../../common/constants'; + +export const addBasePath = (uri: string): string => `${API_BASE_PATH}${uri}`; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts b/x-pack/plugins/cross_cluster_replication/server/services/index.ts similarity index 74% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts rename to x-pack/plugins/cross_cluster_replication/server/services/index.ts index 0743e443955f4..d7b544b290c39 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/license_pre_routing_factory/index.ts +++ b/x-pack/plugins/cross_cluster_replication/server/services/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { licensePreRoutingFactory } from './license_pre_routing_factory'; +export { License } from './license'; +export { addBasePath } from './add_base_path'; diff --git a/x-pack/plugins/cross_cluster_replication/server/services/license.ts b/x-pack/plugins/cross_cluster_replication/server/services/license.ts new file mode 100644 index 0000000000000..bfd357867c3e2 --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/services/license.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + private _isEsSecurityEnabled: boolean = false; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === 'valid'; + + // Retrieving security checks the results of GET /_xpack as well as license state, + // so we're also checking whether the security is disabled in elasticsearch.yml. + this._isEsSecurityEnabled = license.getFeature('security').isEnabled; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + get isEsSecurityEnabled() { + return this._isEsSecurityEnabled; + } +} diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts new file mode 100644 index 0000000000000..049d440e3d85d --- /dev/null +++ b/x-pack/plugins/cross_cluster_replication/server/types.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 { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { IndexManagementPluginSetup } from '../../index_management/server'; +import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; +import { License } from './services'; +import { isEsError } from './lib/is_es_error'; +import { formatEsError } from './lib/format_es_error'; + +export interface Dependencies { + licensing: LicensingPluginSetup; + indexManagement: IndexManagementPluginSetup; + remoteClusters: RemoteClustersPluginSetup; +} + +export interface RouteDependencies { + router: IRouter; + license: License; + lib: { + isEsError: typeof isEsError; + formatEsError: typeof formatEsError; + }; +} diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts index 95f2c9e477064..a7d6aa894d91d 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts @@ -45,9 +45,11 @@ describe('Async search strategy', () => { it('stops polling when the response is complete', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, @@ -67,10 +69,39 @@ describe('Async search strategy', () => { expect(mockSearch).toBeCalledTimes(2); }); + it('stops polling when the response is an error', async () => { + mockSearch + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })) + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: true })); + + const asyncSearch = asyncSearchStrategyProvider({ + core: mockCoreStart, + getSearchStrategy: jest.fn().mockImplementation(() => { + return () => { + return { + search: mockSearch, + }; + }; + }), + }); + + expect(mockSearch).toBeCalledTimes(0); + + await asyncSearch + .search(mockRequest, mockOptions) + .toPromise() + .catch(() => { + expect(mockSearch).toBeCalledTimes(2); + }); + }); + it('only sends the ID and server strategy after the first request', async () => { mockSearch - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1 })) - .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 2 })); + .mockReturnValueOnce(of({ id: 1, total: 2, loaded: 1, is_running: true, is_partial: true })) + .mockReturnValueOnce( + of({ id: 1, total: 2, loaded: 2, is_running: false, is_partial: false }) + ); const asyncSearch = asyncSearchStrategyProvider({ core: mockCoreStart, diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts index 6271d7fcbeaac..18b5b976b3c1b 100644 --- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts @@ -14,7 +14,7 @@ import { SYNC_SEARCH_STRATEGY, TSearchStrategyProvider, } from '../../../../../src/plugins/data/public'; -import { IAsyncSearchRequest, IAsyncSearchOptions } from './types'; +import { IAsyncSearchRequest, IAsyncSearchOptions, IAsyncSearchResponse } from './types'; export const ASYNC_SEARCH_STRATEGY = 'ASYNC_SEARCH_STRATEGY'; @@ -52,9 +52,14 @@ export const asyncSearchStrategyProvider: TSearchStrategyProvider { + expand((response: IAsyncSearchResponse) => { + // If the response indicates of an error, stop polling and complete the observable + if (!response || (response.is_partial && !response.is_running)) { + return throwError(new AbortError()); + } + // If the response indicates it is complete, stop polling and complete the observable - if ((response.loaded ?? 0) >= (response.total ?? 0)) return EMPTY; + if (!response.is_running) return EMPTY; id = response.id; diff --git a/x-pack/plugins/data_enhanced/public/search/types.ts b/x-pack/plugins/data_enhanced/public/search/types.ts index edaaf1b22654d..8ffc8eddda052 100644 --- a/x-pack/plugins/data_enhanced/public/search/types.ts +++ b/x-pack/plugins/data_enhanced/public/search/types.ts @@ -4,7 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISearchOptions, ISyncSearchRequest } from '../../../../../src/plugins/data/public'; +import { + IKibanaSearchResponse, + ISearchOptions, + ISyncSearchRequest, +} from '../../../../../src/plugins/data/public'; export interface IAsyncSearchRequest extends ISyncSearchRequest { /** @@ -19,3 +23,14 @@ export interface IAsyncSearchOptions extends ISearchOptions { */ pollInterval?: number; } + +export interface IAsyncSearchResponse extends IKibanaSearchResponse { + /** + * Indicates whether async search is still in flight + */ + is_running?: boolean; + /** + * Indicates whether the results returned are complete or partial + */ + is_partial?: boolean; +} diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 6b329bccab4a7..bf502889ffa4f 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -23,6 +23,8 @@ import { shimHitsTotal } from './shim_hits_total'; export interface AsyncSearchResponse { id: string; + is_partial: boolean; + is_running: boolean; response: SearchResponse; } @@ -71,13 +73,19 @@ async function asyncSearch( // Wait up to 1s for the response to return const query = toSnakeCase({ waitForCompletionTimeout: '1s', ...queryParams }); - const { response, id } = (await caller( + const { id, response, is_partial, is_running } = (await caller( 'transport.request', { method, path, body, query }, options )) as AsyncSearchResponse; - return { id, rawResponse: shimHitsTotal(response), ...getTotalLoaded(response._shards) }; + return { + id, + is_partial, + is_running, + rawResponse: shimHitsTotal(response), + ...getTotalLoaded(response._shards), + }; } async function rollupSearch( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx index 6c294d9c86548..7475229853698 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/header_navigation.tsx @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEvent, useMemo } from 'react'; +import React, { memo, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiTabs, EuiTab } from '@elastic/eui'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useLocation } from 'react-router-dom'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { Immutable } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface NavTabs { name: string; @@ -48,33 +49,30 @@ const navTabs: Immutable = [ }, ]; -export const HeaderNavigation: React.FunctionComponent = React.memo(() => { - const history = useHistory(); - const location = useLocation(); +const NavTab = memo<{ tab: NavTabs }>(({ tab }) => { + const { pathname } = useLocation(); const { services } = useKibana(); + const onClickHandler = useNavigateByRouterEventHandler(tab.href); const BASE_PATH = services.application.getUrlForApp('endpoint'); + return ( + + {tab.name} + + ); +}); + +export const HeaderNavigation: React.FunctionComponent = React.memo(() => { const tabList = useMemo(() => { return navTabs.map((tab, index) => { - return ( - { - event.preventDefault(); - history.push(tab.href); - }} - isSelected={ - tab.href === location.pathname || - (tab.href !== '/' && location.pathname.startsWith(tab.href)) - } - > - {tab.name} - - ); + return ; }); - }, [BASE_PATH, history, location.pathname]); + }, []); return {tabList}; }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx index d0a8f9690dafb..2d4d1ca8a1b5b 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/components/link_to_app.test.tsx @@ -110,7 +110,7 @@ describe('LinkToApp component', () => { const clickEventArg = spyOnClickHandler.mock.calls[0][0]; expect(clickEventArg.isDefaultPrevented()).toBe(true); }); - it('should not navigate if onClick callback prevents defalut', () => { + it('should not navigate if onClick callback prevents default', () => { const spyOnClickHandler: LinkToAppOnClickMock = jest.fn(ev => { ev.preventDefault(); }); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx new file mode 100644 index 0000000000000..b1f09617f0174 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.test.tsx @@ -0,0 +1,90 @@ +/* + * 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 { AppContextTestRender, createAppRootMockRenderer } from '../../mocks'; +import { useNavigateByRouterEventHandler } from './use_navigate_by_router_event_handler'; +import { act, fireEvent, cleanup } from '@testing-library/react'; + +type ClickHandlerMock = jest.Mock< + Return, + [React.MouseEvent] +>; + +describe('useNavigateByRouterEventHandler hook', () => { + let render: AppContextTestRender['render']; + let history: AppContextTestRender['history']; + let renderResult: ReturnType; + let linkEle: HTMLAnchorElement; + let clickHandlerSpy: ClickHandlerMock; + const Link = React.memo<{ + routeTo: Parameters[0]; + onClick?: Parameters[1]; + }>(({ routeTo, onClick }) => { + const onClickHandler = useNavigateByRouterEventHandler(routeTo, onClick); + return ( + + mock link + + ); + }); + + beforeEach(async () => { + ({ render, history } = createAppRootMockRenderer()); + clickHandlerSpy = jest.fn(); + renderResult = render(); + linkEle = (await renderResult.findByText('mock link')) as HTMLAnchorElement; + }); + afterEach(cleanup); + + it('should navigate to path via Router', () => { + const containerClickSpy = jest.fn(); + renderResult.container.addEventListener('click', containerClickSpy); + expect(history.location.pathname).not.toEqual('/mock/path'); + act(() => { + fireEvent.click(linkEle); + }); + expect(containerClickSpy.mock.calls[0][0].defaultPrevented).toBe(true); + expect(history.location.pathname).toEqual('/mock/path'); + renderResult.container.removeEventListener('click', containerClickSpy); + }); + it('should support onClick prop', () => { + act(() => { + fireEvent.click(linkEle); + }); + expect(clickHandlerSpy).toHaveBeenCalled(); + expect(history.location.pathname).toEqual('/mock/path'); + }); + it('should not navigate if preventDefault is true', () => { + clickHandlerSpy.mockImplementation(event => { + event.preventDefault(); + }); + act(() => { + fireEvent.click(linkEle); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if click was not the primary mouse button', async () => { + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not navigate via router if anchor has target', () => { + linkEle.setAttribute('target', '_top'); + act(() => { + fireEvent.click(linkEle, { button: 2 }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + it('should not to navigate if meta|alt|ctrl|shift keys are pressed', () => { + ['meta', 'alt', 'ctrl', 'shift'].forEach(key => { + act(() => { + fireEvent.click(linkEle, { [`${key}Key`]: true }); + }); + expect(history.location.pathname).not.toEqual('/mock/path'); + }); + }); +}); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts new file mode 100644 index 0000000000000..dc33f0befaf35 --- /dev/null +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hooks/use_navigate_by_router_event_handler.ts @@ -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 { MouseEventHandler, useCallback } from 'react'; +import { useHistory } from 'react-router-dom'; +import { LocationDescriptorObject } from 'history'; + +type EventHandlerCallback = MouseEventHandler; + +/** + * Provides an event handler that can be used with (for example) `onClick` props to prevent the + * event's default behaviour and instead navigate to to a route via the Router + * + * @param routeTo + * @param onClick + */ +export const useNavigateByRouterEventHandler = ( + routeTo: string | [string, unknown] | LocationDescriptorObject, // Cover the calling signature of `history.push()` + + /** Additional onClick callback */ + onClick?: EventHandlerCallback +): EventHandlerCallback => { + const history = useHistory(); + return useCallback( + ev => { + try { + if (onClick) { + onClick(ev); + } + } catch (error) { + ev.preventDefault(); + throw error; + } + + if (ev.defaultPrevented) { + return; + } + + if (ev.button !== 0) { + return; + } + + if ( + ev.currentTarget instanceof HTMLAnchorElement && + ev.currentTarget.target !== '' && + ev.currentTarget.target !== '_self' + ) { + return; + } + + if (ev.metaKey || ev.altKey || ev.ctrlKey || ev.shiftKey) { + return; + } + + ev.preventDefault(); + + if (Array.isArray(routeTo)) { + history.push(...routeTo); + } else if (typeof routeTo === 'string') { + history.push(routeTo); + } else { + history.push(routeTo); + } + }, + [history, onClick, routeTo] + ); +}; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx index 26f2203790a9e..02f91307c988e 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/components/flyout_sub_header.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { memo } from 'react'; +import React, { memo, MouseEventHandler } from 'react'; import { EuiFlyoutHeader, CommonProps, EuiButtonEmpty } from '@elastic/eui'; import styled from 'styled-components'; @@ -12,7 +12,7 @@ export type FlyoutSubHeaderProps = CommonProps & { children: React.ReactNode; backButton?: { title: string; - onClick: (event: React.MouseEvent) => void; + onClick: MouseEventHandler; href?: string; }; }; diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx index 4710c8f3115f0..d7f11d51a9a8f 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/host_details.tsx @@ -16,13 +16,13 @@ import { import React, { memo, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { useHistory } from 'react-router-dom'; import { HostMetadata } from '../../../../../../common/types'; import { FormattedDateAndTime } from '../../formatted_date_time'; import { LinkToApp } from '../../components/link_to_app'; import { useHostListSelector, useHostLogsUrl } from '../hooks'; import { urlFromQueryParams } from '../url_from_query_params'; import { policyResponseStatus, uiQueryParams } from '../../../store/hosts/selectors'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; const HostIds = styled(EuiListGroupItem)` margin-top: 0; @@ -43,7 +43,6 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { const policyStatus = useHostListSelector( policyResponseStatus ) as keyof typeof POLICY_STATUS_TO_HEALTH_COLOR; - const history = useHistory(); const detailsResultsUpper = useMemo(() => { return [ { @@ -74,6 +73,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { show: 'policy_response', }); }, [details.host.id, queryParams]); + const policyStatusClickHandler = useNavigateByRouterEventHandler(policyResponseUri); const detailsResultsLower = useMemo(() => { return [ @@ -93,10 +93,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => { { - ev.preventDefault(); - history.push(policyResponseUri); - }} + onClick={policyStatusClickHandler} > { details.endpoint.policy.id, details.host.hostname, details.host.ip, - history, - policyResponseUri, + policyResponseUri.search, + policyStatusClickHandler, policyStatus, ]); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx index a41d4a968f177..0c43e18822508 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/details/index.tsx @@ -24,6 +24,7 @@ import { HostDetails } from './host_details'; import { PolicyResponse } from './policy_response'; import { HostMetadata } from '../../../../../../common/types'; import { FlyoutSubHeader, FlyoutSubHeaderProps } from './components/flyout_sub_header'; +import { useNavigateByRouterEventHandler } from '../../hooks/use_navigate_by_router_event_handler'; export const HostDetailsFlyout = memo(() => { const history = useHistory(); @@ -92,24 +93,25 @@ export const HostDetailsFlyout = memo(() => { const PolicyResponseFlyoutPanel = memo<{ hostMeta: HostMetadata; }>(({ hostMeta }) => { - const history = useHistory(); const { show, ...queryParams } = useHostListSelector(uiQueryParams); + const detailsUri = useMemo( + () => + urlFromQueryParams({ + ...queryParams, + selected_host: hostMeta.host.id, + }), + [hostMeta.host.id, queryParams] + ); + const backToDetailsClickHandler = useNavigateByRouterEventHandler(detailsUri); const backButtonProp = useMemo((): FlyoutSubHeaderProps['backButton'] => { - const detailsUri = urlFromQueryParams({ - ...queryParams, - selected_host: hostMeta.host.id, - }); return { title: i18n.translate('xpack.endpoint.host.policyResponse.backLinkTitle', { defaultMessage: 'Endpoint Details', }), href: '?' + detailsUri.search, - onClick: ev => { - ev.preventDefault(); - history.push(detailsUri); - }, + onClick: backToDetailsClickHandler, }; - }, [history, hostMeta.host.id, queryParams]); + }, [backToDetailsClickHandler, detailsUri.search]); return ( <> diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx index 1d81d6e8a16db..e662bafed6492 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.tsx @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useMemo, useCallback } from 'react'; +import React, { useMemo, useCallback, memo } from 'react'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { EuiPage, EuiPageBody, @@ -31,11 +30,26 @@ import { useHostListSelector } from './hooks'; import { CreateStructuredSelector } from '../../types'; import { urlFromQueryParams } from './url_from_query_params'; import { HostMetadata, Immutable } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; + +const HostLink = memo<{ + name: string; + href: string; + route: ReturnType; +}>(({ name, href, route }) => { + const clickHandler = useNavigateByRouterEventHandler(route); + + return ( + // eslint-disable-next-line @elastic/eui/href-or-on-click + + {name} + + ); +}); const selector = (createStructuredSelector as CreateStructuredSelector)(selectors); export const HostList = () => { const dispatch = useDispatch<(a: HostAction) => void>(); - const history = useHistory(); const { listData, pageIndex, @@ -75,18 +89,9 @@ export const HostList = () => { defaultMessage: 'Hostname', }), render: ({ host: { hostname, id } }: { host: { hostname: string; id: string } }) => { + const newQueryParams = urlFromQueryParams({ ...queryParams, selected_host: id }); return ( - // eslint-disable-next-line @elastic/eui/href-or-on-click - { - ev.preventDefault(); - history.push(urlFromQueryParams({ ...queryParams, selected_host: id })); - }} - > - {hostname} - + ); }, }, @@ -150,7 +155,7 @@ export const HostList = () => { }, }, ]; - }, [queryParams, history]); + }, [queryParams]); return ( diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx index 2ecc2b117bf01..d780b7bde8af3 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.test.tsx @@ -101,7 +101,7 @@ describe('Policy Details', () => { 'EuiPageHeaderSection[data-test-subj="pageViewHeaderLeft"] EuiButtonEmpty' ); expect(history.location.pathname).toEqual('/policy/1'); - backToListButton.simulate('click'); + backToListButton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display agent stats', async () => { @@ -130,7 +130,7 @@ describe('Policy Details', () => { 'EuiButtonEmpty[data-test-subj="policyDetailsCancelButton"]' ); expect(history.location.pathname).toEqual('/policy/1'); - cancelbutton.simulate('click'); + cancelbutton.simulate('click', { button: 0 }); expect(history.location.pathname).toEqual('/policy'); }); it('should display save button', async () => { diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx index 076de7b57b44b..ea9eb292dba1a 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_details.tsx @@ -20,7 +20,6 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { useDispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { usePolicyDetailsSelector } from './policy_hooks'; import { policyDetails, @@ -36,11 +35,11 @@ import { AgentsSummary } from './agents_summary'; import { VerticalDivider } from './vertical_divider'; import { WindowsEvents, MacEvents, LinuxEvents } from './policy_forms/events'; import { MalwareProtections } from './policy_forms/protections/malware'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); const { notifications, services } = useKibana(); - const history = useHistory(); // Store values const policyItem = usePolicyDetailsSelector(policyDetails); @@ -82,13 +81,7 @@ export const PolicyDetails = React.memo(() => { } }, [notifications.toasts, policyItem, policyName, policyUpdateStatus]); - const handleBackToListOnClick: React.MouseEventHandler = useCallback( - ev => { - ev.preventDefault(); - history.push(`/policy`); - }, - [history] - ); + const handleBackToListOnClick = useNavigateByRouterEventHandler('/policy'); const handleSaveOnClick = useCallback(() => { setShowConfirm(true); diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx index 062c7afb6706d..f7eafff137f51 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/policy/policy_list.tsx @@ -24,30 +24,26 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { PageView } from '../components/page_view'; import { LinkToApp } from '../components/link_to_app'; import { Immutable, PolicyData } from '../../../../../common/types'; +import { useNavigateByRouterEventHandler } from '../hooks/use_navigate_by_router_event_handler'; interface TableChangeCallbackArguments { page: { index: number; size: number }; } -const PolicyLink: React.FC<{ name: string; route: string }> = ({ name, route }) => { - const history = useHistory(); - +const PolicyLink: React.FC<{ name: string; route: string; href: string }> = ({ + name, + route, + href, +}) => { + const clickHandler = useNavigateByRouterEventHandler(route); return ( - { - event.preventDefault(); - history.push(route); - }} - > + // eslint-disable-next-line @elastic/eui/href-or-on-click + {name} ); }; -const renderPolicyNameLink = (value: string, item: Immutable) => { - return ; -}; - export const PolicyList = React.memo(() => { const { services, notifications } = useKibana(); const history = useHistory(); @@ -95,7 +91,16 @@ export const PolicyList = React.memo(() => { name: i18n.translate('xpack.endpoint.policyList.nameField', { defaultMessage: 'Policy Name', }), - render: renderPolicyNameLink, + render: (value: string, item: Immutable) => { + const routeUri = `/policy/${item.id}`; + return ( + + ); + }, truncateText: true, }, { diff --git a/x-pack/plugins/endpoint/server/mocks.ts b/x-pack/plugins/endpoint/server/mocks.ts index 903aa19cd8843..3881840efe9df 100644 --- a/x-pack/plugins/endpoint/server/mocks.ts +++ b/x-pack/plugins/endpoint/server/mocks.ts @@ -4,6 +4,9 @@ * 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'; + /** * Creates a mock IndexPatternRetriever for use in tests. * @@ -28,6 +31,15 @@ export const createMockMetadataIndexPatternRetriever = () => { return createMockIndexPatternRetriever(MetadataIndexPattern); }; +/** + * Creates a mock AgentService + */ +export const createMockAgentService = (): jest.Mocked => { + return { + getAgentStatusById: jest.fn(), + }; +}; + /** * Creates a mock IndexPatternService for use in tests that need to interact with the Ingest Manager's * ESIndexPatternService. @@ -35,10 +47,13 @@ export const createMockMetadataIndexPatternRetriever = () => { * @param indexPattern a string index pattern to return when called by a test * @returns the same value as `indexPattern` parameter */ -export const createMockIndexPatternService = (indexPattern: string) => { +export const createMockIngestManagerSetupContract = ( + indexPattern: string +): IngestManagerSetupContract => { return { esIndexPatternService: { getESIndexPattern: jest.fn().mockResolvedValue(indexPattern), }, + agentService: createMockAgentService(), }; }; diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 8d55e64f16dcf..c380bc5c3e3d0 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -7,7 +7,7 @@ import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; -import { createMockIndexPatternService } from './mocks'; +import { createMockIngestManagerSetupContract } from './mocks'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; @@ -31,7 +31,7 @@ describe('test endpoint plugin', () => { }; mockedEndpointPluginSetupDependencies = { features: mockedPluginSetupContract, - ingestManager: createMockIndexPatternService(''), + ingestManager: createMockIngestManagerSetupContract(''), }; }); diff --git a/x-pack/plugins/endpoint/server/plugin.ts b/x-pack/plugins/endpoint/server/plugin.ts index 6a42014e91130..ce6be5aeaf6db 100644 --- a/x-pack/plugins/endpoint/server/plugin.ts +++ b/x-pack/plugins/endpoint/server/plugin.ts @@ -70,6 +70,7 @@ export class EndpointPlugin plugins.ingestManager.esIndexPatternService, this.initializerContext.logger ), + agentService: plugins.ingestManager.agentService, logFactory: this.initializerContext.logger, config: (): Promise => { return createConfig$(this.initializerContext) 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 6be7b26898206..39fc2ba4c74bb 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/alerts.test.ts @@ -12,7 +12,7 @@ import { import { registerAlertRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import { alertingIndexGetQuerySchema } from '../../../common/schema/alert_index'; -import { createMockIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockIndexPatternRetriever } from '../../mocks'; describe('test alerts route', () => { let routerMock: jest.Mocked; @@ -26,6 +26,7 @@ describe('test alerts route', () => { routerMock = httpServiceMock.createRouter(); registerAlertRoutes(routerMock, { indexPatternRetriever: createMockIndexPatternRetriever('events-endpoint-*'), + agentService: createMockAgentService(), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.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 86e9f55da5697..9055ee4110fbb 100644 --- a/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts +++ b/x-pack/plugins/endpoint/server/routes/alerts/details/handlers.ts @@ -37,7 +37,13 @@ export const alertDetailsHandlerWrapper = function( indexPattern ); - const currentHostInfo = await getHostData(ctx, response._source.host.id, indexPattern); + const currentHostInfo = await getHostData( + { + endpointAppContext, + requestHandlerContext: ctx, + }, + response._source.host.id + ); return res.ok({ body: { diff --git a/x-pack/plugins/endpoint/server/routes/metadata/index.ts b/x-pack/plugins/endpoint/server/routes/metadata/index.ts index 883bb88204fd4..bc79b828576e0 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/index.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/index.ts @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IRouter, RequestHandlerContext } from 'kibana/server'; +import { IRouter, Logger, RequestHandlerContext } from 'kibana/server'; import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; -import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; +import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { HostInfo, HostMetadata, HostResultList, HostStatus } from '../../../common/types'; import { EndpointAppContext } from '../../types'; +import { AgentStatus } from '../../../../ingest_manager/common/types/models'; interface HitSource { _source: HostMetadata; } +interface MetadataRequestContext { + requestHandlerContext: RequestHandlerContext; + endpointAppContext: EndpointAppContext; +} + +const HOST_STATUS_MAPPING = new Map([ + ['online', HostStatus.ONLINE], + ['offline', HostStatus.OFFLINE], +]); + export function registerEndpointRoutes(router: IRouter, endpointAppContext: EndpointAppContext) { router.post( { @@ -62,7 +73,12 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp 'search', queryParams )) as SearchResponse; - return res.ok({ body: mapToHostResultList(queryParams, response) }); + return res.ok({ + body: await mapToHostResultList(queryParams, response, { + endpointAppContext, + requestHandlerContext: context, + }), + }); } catch (err) { return res.internalError({ body: err }); } @@ -79,11 +95,13 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp }, async (context, req, res) => { try { - const index = await endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( - context + const doc = await getHostData( + { + endpointAppContext, + requestHandlerContext: context, + }, + req.params.id ); - - const doc = await getHostData(context, req.params.id, index); if (doc) { return res.ok({ body: doc }); } @@ -96,12 +114,14 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp } export async function getHostData( - context: RequestHandlerContext, - id: string, - index: string + metadataRequestContext: MetadataRequestContext, + id: string ): Promise { + const index = await metadataRequestContext.endpointAppContext.indexPatternRetriever.getMetadataIndexPattern( + metadataRequestContext.requestHandlerContext + ); const query = getESQueryHostMetadataByID(id, index); - const response = (await context.core.elasticsearch.dataClient.callAsCurrentUser( + const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.dataClient.callAsCurrentUser( 'search', query )) as SearchResponse; @@ -110,22 +130,25 @@ export async function getHostData( return undefined; } - return enrichHostMetadata(response.hits.hits[0]._source); + return await enrichHostMetadata(response.hits.hits[0]._source, metadataRequestContext); } -function mapToHostResultList( +async function mapToHostResultList( queryParams: Record, - searchResponse: SearchResponse -): HostResultList { + searchResponse: SearchResponse, + metadataRequestContext: MetadataRequestContext +): Promise { const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0; if (searchResponse.hits.hits.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, - hosts: searchResponse.hits.hits - .map(response => response.inner_hits.most_recent.hits.hits) - .flatMap(data => data as HitSource) - .map(entry => enrichHostMetadata(entry._source)), + hosts: await Promise.all( + searchResponse.hits.hits + .map(response => response.inner_hits.most_recent.hits.hits) + .flatMap(data => data as HitSource) + .map(async entry => enrichHostMetadata(entry._source, metadataRequestContext)) + ), total: totalNumberOfHosts, }; } else { @@ -138,9 +161,43 @@ function mapToHostResultList( } } -function enrichHostMetadata(hostMetadata: HostMetadata): HostInfo { +async function enrichHostMetadata( + hostMetadata: HostMetadata, + metadataRequestContext: MetadataRequestContext +): Promise { + let hostStatus = HostStatus.ERROR; + let elasticAgentId = hostMetadata?.elastic?.agent?.id; + const log = logger(metadataRequestContext.endpointAppContext); + try { + /** + * Get agent status by elastic agent id if available or use the host id. + * https://github.com/elastic/endpoint-app-team/issues/354 + */ + + if (!elasticAgentId) { + elasticAgentId = hostMetadata.host.id; + log.warn(`Missing elastic agent id, using host id instead ${elasticAgentId}`); + } + + const status = await metadataRequestContext.endpointAppContext.agentService.getAgentStatusById( + metadataRequestContext.requestHandlerContext.core.savedObjects.client, + elasticAgentId + ); + hostStatus = HOST_STATUS_MAPPING.get(status) || HostStatus.ERROR; + } catch (e) { + if (e.isBoom && e.output.statusCode === 404) { + log.warn(`agent with id ${elasticAgentId} not found`); + } else { + log.error(e); + throw e; + } + } return { metadata: hostMetadata, - host_status: HostStatus.ERROR, + host_status: hostStatus, }; } + +const logger = (endpointAppContext: EndpointAppContext): Logger => { + return endpointAppContext.logFactory.get('metadata'); +}; 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 9a7d3fb3188a6..a1186aabc7a66 100644 --- a/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/endpoint/server/routes/metadata/metadata.test.ts @@ -25,7 +25,9 @@ import { SearchResponse } from 'elasticsearch'; import { registerEndpointRoutes } from './index'; import { EndpointConfigSchema } from '../../config'; import * as data from '../../test_data/all_metadata_data.json'; -import { createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { createMockAgentService, createMockMetadataIndexPatternRetriever } from '../../mocks'; +import { AgentService } from '../../../../ingest_manager/common/types'; +import Boom from 'boom'; describe('test endpoint route', () => { let routerMock: jest.Mocked; @@ -35,6 +37,7 @@ describe('test endpoint route', () => { let mockSavedObjectClient: jest.Mocked; let routeHandler: RequestHandler; let routeConfig: RouteConfig; + let mockAgentService: jest.Mocked; beforeEach(() => { mockClusterClient = elasticsearchServiceMock.createClusterClient() as jest.Mocked< @@ -45,8 +48,10 @@ describe('test endpoint route', () => { mockClusterClient.asScoped.mockReturnValue(mockScopedClient); routerMock = httpServiceMock.createRouter(); mockResponse = httpServerMock.createResponseFactory(); + mockAgentService = createMockAgentService(); registerEndpointRoutes(routerMock, { indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + agentService: mockAgentService, logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }); @@ -83,7 +88,7 @@ describe('test endpoint route', () => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -113,6 +118,8 @@ describe('test endpoint route', () => { ], }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse) ); @@ -154,6 +161,8 @@ describe('test endpoint route', () => { filter: 'not host.ip:10.140.73.246', }, }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve((data as unknown) as SearchResponse) ); @@ -216,10 +225,10 @@ describe('test endpoint route', () => { }, }) ); + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('error'); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') )!; - await routeHandler( createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), mockRequest, @@ -233,13 +242,14 @@ describe('test endpoint route', () => { expect(message).toEqual('Endpoint Not Found'); }); - it('should return a single endpoint with status error', async () => { + it('should return a single endpoint with status online', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: (data as any).hits.hits[0]._id }, }); const response: SearchResponse = (data as unknown) as SearchResponse< HostMetadata >; + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('online'); mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => path.startsWith('/api/endpoint/metadata') @@ -256,6 +266,64 @@ describe('test endpoint route', () => { expect(mockResponse.ok).toBeCalled(); const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result).toHaveProperty('metadata.endpoint'); + expect(result.host_status).toEqual(HostStatus.ONLINE); + }); + + it('should return a single endpoint with status error when AgentService throw 404', async () => { + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockImplementation(() => { + throw Boom.notFound('Agent not found'); + }); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; + expect(result.host_status).toEqual(HostStatus.ERROR); + }); + + it('should return a single endpoint with status error when status is not offline or online', async () => { + const response: SearchResponse = (data as unknown) as SearchResponse< + HostMetadata + >; + + const mockRequest = httpServerMock.createKibanaRequest({ + params: { id: response.hits.hits[0]._id }, + }); + + mockAgentService.getAgentStatusById = jest.fn().mockReturnValue('warning'); + mockScopedClient.callAsCurrentUser.mockImplementationOnce(() => Promise.resolve(response)); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith('/api/endpoint/metadata') + )!; + + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(mockScopedClient.callAsCurrentUser).toBeCalled(); + expect(routeConfig.options).toEqual({ authRequired: true }); + expect(mockResponse.ok).toBeCalled(); + const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo; expect(result.host_status).toEqual(HostStatus.ERROR); }); }); 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 c8143fbdda1ea..7e6e3f875cd4c 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,7 +6,11 @@ import { httpServerMock, loggingServiceMock } from '../../../../../../src/core/server/mocks'; import { EndpointConfigSchema } from '../../config'; import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; -import { createMockMetadataIndexPatternRetriever, MetadataIndexPattern } from '../../mocks'; +import { + createMockAgentService, + createMockMetadataIndexPatternRetriever, + MetadataIndexPattern, +} from '../../mocks'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -18,6 +22,7 @@ describe('query builder', () => { mockRequest, { indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + agentService: createMockAgentService(), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, @@ -69,6 +74,7 @@ describe('query builder', () => { mockRequest, { indexPatternRetriever: createMockMetadataIndexPatternRetriever(), + agentService: createMockAgentService(), logFactory: loggingServiceMock.create(), config: () => Promise.resolve(EndpointConfigSchema.validate({})), }, diff --git a/x-pack/plugins/endpoint/server/types.ts b/x-pack/plugins/endpoint/server/types.ts index 46a23060339f4..d43ec58aec428 100644 --- a/x-pack/plugins/endpoint/server/types.ts +++ b/x-pack/plugins/endpoint/server/types.ts @@ -6,12 +6,14 @@ import { LoggerFactory } from 'kibana/server'; import { EndpointConfigType } from './config'; import { IndexPatternRetriever } from './index_pattern'; +import { AgentService } from '../../ingest_manager/common/types'; /** * The context for Endpoint apps. */ export interface EndpointAppContext { indexPatternRetriever: IndexPatternRetriever; + agentService: AgentService; logFactory: LoggerFactory; config(): Promise; } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts index 7d24bc31006b4..aa3ac9ea75c22 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/services/notification.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export let toasts: any; -export let fatalErrors: any; +import { IToasts, FatalErrorsSetup } from 'src/core/public'; -export function init(_toasts: any, _fatalErrors: any): void { +export let toasts: IToasts; +export let fatalErrors: FatalErrorsSetup; + +export function init(_toasts: IToasts, _fatalErrors: FatalErrorsSetup): void { toasts = _toasts; fatalErrors = _fatalErrors; } diff --git a/x-pack/plugins/infra/public/apps/start_app.tsx b/x-pack/plugins/infra/public/apps/start_app.tsx index ebf9562c38d7a..4c213700b62e6 100644 --- a/x-pack/plugins/infra/public/apps/start_app.tsx +++ b/x-pack/plugins/infra/public/apps/start_app.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { createBrowserHistory } from 'history'; import React from 'react'; import ReactDOM from 'react-dom'; import { ApolloProvider } from 'react-apollo'; @@ -25,6 +24,7 @@ import { AppRouter } from '../routers'; import { TriggersAndActionsUIPublicPluginSetup } from '../../../triggers_actions_ui/public'; import { TriggersActionsProvider } from '../utils/triggers_actions_context'; import '../index.scss'; +import { NavigationWarningPromptProvider } from '../utils/navigation_warning_prompt'; export const CONTAINER_CLASSNAME = 'infra-container-element'; @@ -36,8 +36,8 @@ export async function startApp( Router: AppRouter, triggersActionsUI: TriggersAndActionsUIPublicPluginSetup ) { - const { element, appBasePath } = params; - const history = createBrowserHistory({ basename: appBasePath }); + const { element, history } = params; + const InfraPluginRoot: React.FunctionComponent = () => { const [darkMode] = useUiSetting$('theme:darkMode'); @@ -49,7 +49,9 @@ export async function startApp( - + + + diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index 36645fa3f1f35..7f248cd103003 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -17,7 +17,6 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useCallback, useContext, useMemo } from 'react'; -import { Prompt } from 'react-router-dom'; import { Source } from '../../containers/source'; import { FieldsConfigurationPanel } from './fields_configuration_panel'; @@ -26,6 +25,7 @@ import { NameConfigurationPanel } from './name_configuration_panel'; import { LogColumnsConfigurationPanel } from './log_columns_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; +import { Prompt } from '../../utils/navigation_warning_prompt'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; @@ -100,10 +100,13 @@ export const SourceConfigurationSettings = ({ data-test-subj="sourceConfigurationContent" > { const INTERNAL_APP = 'metrics'; -// Note: Memory history doesn't support basename, -// we'll work around this by re-assigning 'createHref' so that -// it includes a basename, this then acts as our browserHistory instance would. const history = createMemoryHistory(); -const originalCreateHref = history.createHref; -history.createHref = (location: LocationDescriptorObject): string => { - return `${PREFIX}${INTERNAL_APP}${originalCreateHref.call(history, location)}`; -}; +history.push(`${PREFIX}${INTERNAL_APP}`); +const scopedHistory = new ScopedHistory(history, `${PREFIX}${INTERNAL_APP}`); const ProviderWrapper: React.FC = ({ children }) => { return ( - + {children}; ); @@ -111,7 +107,7 @@ describe('useLinkProps hook', () => { pathname: '/', }); expect(result.current.href).toBe('/test-basepath/s/test-space/app/ml/'); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with pathname options', () => { @@ -127,7 +123,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with hash options', () => { @@ -143,7 +139,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host&id=some-id&count=12345' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with more complex encoding', () => { @@ -161,7 +157,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/ml#/explorer?type=host%20%2B%20host&name=this%20name%20has%20spaces%20and%20**%20and%20%25&id=some-id&count=12345&animals=dog,cat,bear' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); it('Provides the correct props with a consumer using Rison encoding for search', () => { @@ -180,7 +176,7 @@ describe('useLinkProps hook', () => { expect(result.current.href).toBe( '/test-basepath/s/test-space/app/rison-app#rison-route?type=host%20%2B%20host&state=(refreshInterval:(pause:!t,value:0),time:(from:12345,to:54321))' ); - expect(result.current.onClick).not.toBeDefined(); + expect(result.current.onClick).toBeDefined(); }); }); }); diff --git a/x-pack/plugins/infra/public/hooks/use_link_props.tsx b/x-pack/plugins/infra/public/hooks/use_link_props.tsx index e60ab32046832..8c522bb7fa764 100644 --- a/x-pack/plugins/infra/public/hooks/use_link_props.tsx +++ b/x-pack/plugins/infra/public/hooks/use_link_props.tsx @@ -9,7 +9,8 @@ import { stringify } from 'query-string'; import url from 'url'; import { url as urlUtils } from '../../../../../src/plugins/kibana_utils/public'; import { usePrefixPathWithBasepath } from './use_prefix_path_with_basepath'; -import { useHistory } from '../utils/history_context'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useNavigationWarningPrompt } from '../utils/navigation_warning_prompt'; type Search = Record; @@ -28,31 +29,26 @@ interface LinkProps { export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): LinkProps => { validateParams({ app, pathname, hash, search }); - const history = useHistory(); + const { prompt } = useNavigationWarningPrompt(); const prefixer = usePrefixPathWithBasepath(); + const navigateToApp = useKibana().services.application?.navigateToApp; const encodedSearch = useMemo(() => { return search ? encodeSearch(search) : undefined; }, [search]); - const internalLinkResult = useMemo(() => { - // When the logs / metrics apps are first mounted a history instance is setup with a 'basename' equal to the - // 'appBasePath' received from Core's 'AppMountParams', e.g. /BASE_PATH/s/SPACE_ID/app/APP_ID. With internal - // linking we are using 'createHref' and 'push' on top of this history instance. So a pathname of /inventory used within - // the metrics app will ultimatey end up as /BASE_PATH/s/SPACE_ID/app/metrics/inventory. React-router responds to this - // as it is instantiated with the same history instance. - return history?.createHref({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); - }, [history, pathname, encodedSearch]); - - const externalLinkResult = useMemo(() => { + const mergedHash = useMemo(() => { // The URI spec defines that the query should appear before the fragment // https://tools.ietf.org/html/rfc3986#section-3 (e.g. url.format()). However, in Kibana, apps that use // hash based routing expect the query to be part of the hash. This will handle that. - const mergedHash = hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + return hash && encodedSearch ? `${hash}?${encodedSearch}` : hash; + }, [hash, encodedSearch]); + + const mergedPathname = useMemo(() => { + return pathname && encodedSearch ? `${pathname}?${encodedSearch}` : pathname; + }, [pathname, encodedSearch]); + const href = useMemo(() => { const link = url.format({ pathname, hash: mergedHash, @@ -60,28 +56,36 @@ export const useLinkProps = ({ app, pathname, hash, search }: LinkDescriptor): L }); return prefixer(app, link); - }, [hash, encodedSearch, pathname, prefixer, app]); + }, [mergedHash, hash, encodedSearch, pathname, prefixer, app]); const onClick = useMemo(() => { - // If these results are equal we know we're trying to navigate within the same application - // that the current history instance is representing - if (internalLinkResult && linksAreEquivalent(externalLinkResult, internalLinkResult)) { - return (e: React.MouseEvent | React.MouseEvent) => { - e.preventDefault(); - if (history) { - history.push({ - pathname: pathname ? formatPathname(pathname) : undefined, - search: encodedSearch, - }); + return (e: React.MouseEvent | React.MouseEvent) => { + e.preventDefault(); + + const navigate = () => { + if (navigateToApp) { + const navigationPath = mergedHash ? `#${mergedHash}` : mergedPathname; + navigateToApp(app, { path: navigationPath ? navigationPath : undefined }); } }; - } else { - return undefined; - } - }, [internalLinkResult, externalLinkResult, history, pathname, encodedSearch]); + + // A component somewhere within the app hierarchy is requesting that we + // prompt the user before navigating. + if (prompt) { + const wantsToNavigate = window.confirm(prompt); + if (wantsToNavigate) { + navigate(); + } else { + return; + } + } else { + navigate(); + } + }; + }, [navigateToApp, mergedHash, mergedPathname, app, prompt]); return { - href: externalLinkResult, + href, onClick, }; }; @@ -90,10 +94,6 @@ const encodeSearch = (search: Search) => { return stringify(urlUtils.encodeQuery(search), { sort: false, encode: false }); }; -const formatPathname = (pathname: string) => { - return pathname[0] === '/' ? pathname : `/${pathname}`; -}; - const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { if (!app && hash) { throw new Error( @@ -101,9 +101,3 @@ const validateParams = ({ app, pathname, hash, search }: LinkDescriptor) => { ); } }; - -const linksAreEquivalent = (externalLink: string, internalLink: string): boolean => { - // Compares with trailing slashes removed. This handles the case where the pathname is '/' - // and 'createHref' will include the '/' but Kibana's 'getUrlForApp' will remove it. - return externalLink.replace(/\/$/, '') === internalLink.replace(/\/$/, ''); -}; diff --git a/x-pack/plugins/infra/public/utils/history_context.ts b/x-pack/plugins/infra/public/utils/history_context.ts index fe036e3179ec1..844d5b5e8e76f 100644 --- a/x-pack/plugins/infra/public/utils/history_context.ts +++ b/x-pack/plugins/infra/public/utils/history_context.ts @@ -5,9 +5,9 @@ */ import { createContext, useContext } from 'react'; -import { History } from 'history'; +import { ScopedHistory } from 'src/core/public'; -export const HistoryContext = createContext(undefined); +export const HistoryContext = createContext(undefined); export const useHistory = () => { return useContext(HistoryContext); diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx new file mode 100644 index 0000000000000..10f8fb9e71f43 --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/context.tsx @@ -0,0 +1,31 @@ +/* + * 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 { createContext, useContext } from 'react'; + +interface ContextValues { + prompt?: string; + setPrompt: (prompt: string | undefined) => void; +} + +export const NavigationWarningPromptContext = createContext({ + setPrompt: (prompt: string | undefined) => {}, +}); + +export const useNavigationWarningPrompt = () => { + return useContext(NavigationWarningPromptContext); +}; + +export const NavigationWarningPromptProvider: React.FC = ({ children }) => { + const [prompt, setPrompt] = useState(undefined); + + return ( + + {children} + + ); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts similarity index 80% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts rename to x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts index 441648a8701e0..dcdbf8e912a83 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/lib/is_es_error_factory/index.ts +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/index.ts @@ -4,4 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isEsErrorFactory } from './is_es_error_factory'; +export * from './context'; +export * from './prompt'; diff --git a/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx new file mode 100644 index 0000000000000..65ec4729c036d --- /dev/null +++ b/x-pack/plugins/infra/public/utils/navigation_warning_prompt/prompt.tsx @@ -0,0 +1,25 @@ +/* + * 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, { useEffect } from 'react'; +import { useNavigationWarningPrompt } from './context'; + +interface Props { + prompt?: string; +} + +export const Prompt: React.FC = ({ prompt }) => { + const { setPrompt } = useNavigationWarningPrompt(); + + useEffect(() => { + setPrompt(prompt); + return () => { + setPrompt(undefined); + }; + }, [prompt, setPrompt]); + + return null; +}; diff --git a/x-pack/plugins/ingest_manager/README.md b/x-pack/plugins/ingest_manager/README.md index 07acdf8affd49..9cd4821c2a727 100644 --- a/x-pack/plugins/ingest_manager/README.md +++ b/x-pack/plugins/ingest_manager/README.md @@ -1,9 +1,9 @@ # Ingest Manager ## Plugin - - No features enabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/feature-ingest/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L19) - - Setting `xpack.ingestManager.enabled=true` is required to enable the plugin. It adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) - - Adding `--xpack.ingestManager.epm.enabled=true` will add the EPM API & UI - - Adding `--xpack.ingestManager.fleet.enabled=true` will add the Fleet API & UI + - The plugin is disabled by default. See the TypeScript type for the [the available plugin configuration options](https://github.com/elastic/kibana/blob/master/x-pack/plugins/ingest_manager/common/types/index.ts#L9-L27) + - Setting `xpack.ingestManager.enabled=true` enables the plugin including the EPM and Fleet features. It also adds the `DATASOURCE_API_ROUTES` and `AGENT_CONFIG_API_ROUTES` values in [`common/constants/routes.ts`](./common/constants/routes.ts) + - Adding `--xpack.ingestManager.epm.enabled=false` will disable the EPM API & UI + - Adding `--xpack.ingestManager.fleet.enabled=false` will disable the Fleet API & UI - [code for adding the routes](https://github.com/elastic/kibana/blob/1f27d349533b1c2865c10c45b2cf705d7416fb36/x-pack/plugins/ingest_manager/server/plugin.ts#L115-L133) - [Integration tests](server/integration_tests/router.test.ts) - Both EPM and Fleet require `ingestManager` be enabled. They are not standalone features. @@ -25,7 +25,7 @@ One common development workflow is: ``` - Start Kibana in another shell ``` - yarn start --xpack.ingestManager.enabled=true --xpack.ingestManager.epm.enabled=true --xpack.ingestManager.fleet.enabled=true --no-base-path --xpack.endpoint.enabled=true + yarn start --xpack.ingestManager.enabled=true --no-base-path --xpack.endpoint.enabled=true ``` This plugin follows the `common`, `server`, `public` structure from the [Architecture Style Guide diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts index 3496ea782ee99..17509571f1985 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.test.ts @@ -3,11 +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. */ -import { NewDatasource, DatasourceInput } from '../types'; +import { Datasource, NewDatasource, DatasourceInput } from '../types'; import { storedDatasourceToAgentDatasource } from './datasource_to_agent_datasource'; describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { - const mockDatasource: NewDatasource = { + const mockNewDatasource: NewDatasource = { name: 'mock-datasource', description: '', config_id: '', @@ -17,6 +17,12 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [], }; + const mockDatasource: Datasource = { + ...mockNewDatasource, + id: 'some-uuid', + revision: 1, + }; + const mockInput: DatasourceInput = { type: 'test-logs', enabled: true, @@ -70,7 +76,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { it('returns agent datasource config for datasource with no inputs', () => { expect(storedDatasourceToAgentDatasource(mockDatasource)).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -87,7 +94,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }, }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -99,9 +107,21 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { }); }); + it('uses name for id when id is not provided in case of new datasource', () => { + expect(storedDatasourceToAgentDatasource(mockNewDatasource)).toEqual({ + id: 'mock-datasource', + name: 'mock-datasource', + namespace: 'default', + enabled: true, + use_output: 'default', + inputs: [], + }); + }); + it('returns agent datasource config with flattened input and package stream', () => { expect(storedDatasourceToAgentDatasource({ ...mockDatasource, inputs: [mockInput] })).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -140,7 +160,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { ], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', @@ -169,7 +190,8 @@ describe('Ingest Manager - storedDatasourceToAgentDatasource', () => { inputs: [{ ...mockInput, enabled: false }], }) ).toEqual({ - id: 'mock-datasource', + id: 'some-uuid', + name: 'mock-datasource', namespace: 'default', enabled: true, use_output: 'default', diff --git a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts index b509878b7f945..9e09d3fa3153a 100644 --- a/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts +++ b/x-pack/plugins/ingest_manager/common/services/datasource_to_agent_datasource.ts @@ -12,7 +12,8 @@ export const storedDatasourceToAgentDatasource = ( const { name, namespace, enabled, package: pkg, inputs } = datasource; const fullDatasource: FullAgentConfigDatasource = { - id: name, + id: 'id' in datasource ? datasource.id : name, + name, namespace, enabled, use_output: DEFAULT_OUTPUT.name, // TODO: hardcoded to default output for now diff --git a/x-pack/plugins/ingest_manager/common/types/index.ts b/x-pack/plugins/ingest_manager/common/types/index.ts index 42f7a9333118e..150a4c9d60280 100644 --- a/x-pack/plugins/ingest_manager/common/types/index.ts +++ b/x-pack/plugins/ingest_manager/common/types/index.ts @@ -3,9 +3,24 @@ * 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 { AgentStatus } from './models'; + export * from './models'; export * from './rest_spec'; +/** + * A service that provides exported functions that return information about an Agent + */ +export interface AgentService { + /** + * Return the status by the Agent's id + * @param soClient + * @param agentId + */ + getAgentStatusById(soClient: SavedObjectsClientContract, agentId: string): Promise; +} + export interface IngestManagerConfigType { enabled: boolean; epm: { diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts index 002c3784446a8..2372caee512af 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent_config.ts @@ -34,8 +34,10 @@ export interface AgentConfig extends NewAgentConfig, SavedObjectAttributes { revision: number; } -export type FullAgentConfigDatasource = Pick & { - id: string; +export type FullAgentConfigDatasource = Pick< + Datasource, + 'id' | 'name' | 'namespace' | 'enabled' +> & { package?: Pick; use_output: string; inputs: Array< diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 53ad0310ea613..c750aa99204fa 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -57,6 +57,7 @@ export interface RegistryPackage { icons?: RegistryImage[]; assets?: string[]; internal?: boolean; + removable?: boolean; format_version: string; datasets?: Dataset[]; datasources?: RegistryDatasource[]; @@ -256,6 +257,10 @@ export enum DefaultPackages { endpoint = 'endpoint', } +export interface IndexTemplateMappings { + properties: any; +} + export interface IndexTemplate { order: number; index_patterns: string[]; @@ -263,3 +268,8 @@ export interface IndexTemplate { mappings: object; aliases: object; } + +export interface TemplateRef { + templateName: string; + indexTemplate: IndexTemplate; +} diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx index 56b109a9bc062..ad27c590d5eaa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/components/yaml/index.tsx @@ -27,7 +27,9 @@ import { Loading } from '../../../../../components'; const CONFIG_KEYS_ORDER = [ 'id', + 'name', 'revision', + 'type', 'outputs', 'datasources', 'enabled', @@ -52,7 +54,7 @@ export const ConfigYamlView = memo<{ config: AgentConfig }>(({ config }) => { return ( - + {dump(fullConfigRequest.data.item, { sortKeys: (keyA: string, keyB: string) => { const indexA = CONFIG_KEYS_ORDER.indexOf(keyA); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx index 0d4b395895322..a3d24e7806f34 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx @@ -50,10 +50,18 @@ export function Content(props: ContentProps) { type ContentPanelProps = PackageInfo & Pick; export function ContentPanel(props: ContentPanelProps) { - const { panel, name, version, assets, title } = props; + const { panel, name, version, assets, title, removable } = props; switch (panel) { case 'settings': - return ; + return ( + + ); case 'data-sources': return ; case 'overview': diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx index ff7ecf97714b6..f947466caf4b0 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx @@ -13,8 +13,14 @@ import { InstallStatus, PackageInfo } from '../../../../types'; import { InstallationButton } from './installation_button'; import { useGetDatasources } from '../../../../hooks'; +const NoteLabel = () => ( + +); export const SettingsPanel = ( - props: Pick + props: Pick ) => { const getPackageInstallStatus = useGetPackageInstallStatus(); const { data: datasourcesData } = useGetDatasources({ @@ -22,10 +28,9 @@ export const SettingsPanel = ( page: 1, kuery: `datasources.package.name:${props.name}`, }); - const { name, title } = props; + const { name, title, removable } = props; const packageInstallStatus = getPackageInstallStatus(name); const packageHasDatasources = !!datasourcesData?.total; - return ( @@ -89,12 +94,12 @@ export const SettingsPanel = (

    - {packageHasDatasources && ( + {packageHasDatasources && removable === true && (

    - + + + ), + }} + /> +

    + )} + {removable === false && ( +

    + + ), }} diff --git a/x-pack/plugins/ingest_manager/server/index.ts b/x-pack/plugins/ingest_manager/server/index.ts index 7859c44ccfd89..d99eb2a9bb4bb 100644 --- a/x-pack/plugins/ingest_manager/server/index.ts +++ b/x-pack/plugins/ingest_manager/server/index.ts @@ -18,11 +18,11 @@ export const config = { schema: schema.object({ enabled: schema.boolean({ defaultValue: false }), epm: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), registryUrl: schema.uri({ defaultValue: 'https://epr-staging.elastic.co' }), }), fleet: schema.object({ - enabled: schema.boolean({ defaultValue: false }), + enabled: schema.boolean({ defaultValue: true }), kibana: schema.object({ host: schema.maybe(schema.string()), ca_sha256: schema.maybe(schema.string()), diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4dd070a7414f0..075a0917b9fae 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -39,18 +39,20 @@ import { registerInstallScriptRoutes, } from './routes'; -import { IngestManagerConfigType } from '../common'; +import { AgentService, IngestManagerConfigType } from '../common'; import { appContextService, ESIndexPatternService, 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 { @@ -148,6 +150,9 @@ export class IngestManagerPlugin implements Plugin { } return deepFreeze({ esIndexPatternService: new ESIndexPatternSavedObjectService(), + agentService: { + getAgentStatusById, + }, }); } diff --git a/x-pack/plugins/ingest_manager/server/saved_objects.ts b/x-pack/plugins/ingest_manager/server/saved_objects.ts index dc0b4695603e4..0a7229b1f2807 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects.ts @@ -150,6 +150,7 @@ export const savedObjectMappings = { name: { type: 'keyword' }, version: { type: 'keyword' }, internal: { type: 'boolean' }, + removable: { type: 'boolean' }, es_index_patterns: { dynamic: false, type: 'object', diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts new file mode 100644 index 0000000000000..d19fe883a7780 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { savedObjectsClientMock } from '../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { getAgentStatusById } from './status'; +import { AGENT_TYPE_PERMANENT } from '../../../common/constants'; +import { AgentSOAttributes } from '../../../common/types/models'; +import { SavedObject } from 'kibana/server'; + +describe('Agent status service', () => { + it('should return inactive when agent is not active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: false, + local_metadata: '{}', + user_provided_metadata: '{}', + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('inactive'); + }); + + it('should return online when agent is active', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + mockSavedObjectsClient.get = jest.fn().mockReturnValue({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + attributes: { + active: true, + local_metadata: '{}', + user_provided_metadata: '{}', + }, + } as SavedObject); + const status = await getAgentStatusById(mockSavedObjectsClient, 'id'); + expect(status).toEqual('online'); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.ts index 21e200d701e69..001b6d01f078e 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/status.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/status.ts @@ -5,7 +5,7 @@ */ import { SavedObjectsClientContract } from 'src/core/server'; -import { listAgents } from './crud'; +import { getAgent, listAgents } from './crud'; import { AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../constants'; import { AgentStatus, Agent } from '../../types'; @@ -17,6 +17,14 @@ import { } from '../../constants'; import { AgentStatusKueryHelper } from '../../../common/services'; +export async function getAgentStatusById( + soClient: SavedObjectsClientContract, + agentId: string +): Promise { + const agent = await getAgent(soClient, agentId); + return getAgentStatus(agent); +} + export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus { const { type, last_checkin: lastCheckIn } = agent; const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap index 166983fbccc35..5cf1f241a709f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap @@ -28,9 +28,6 @@ exports[`tests loading base.yml: base.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { @@ -123,9 +120,6 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { @@ -218,9 +212,6 @@ exports[`tests loading system.yml: system.yml 1`] = ` } }, "mappings": { - "_meta": { - "package": "foo" - }, "dynamic_templates": [ { "strings_as_keyword": { diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts index 560ddfc1f6885..4df626259ece7 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/install.ts @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - AssetReference, - Dataset, - RegistryPackage, - IngestAssetType, - ElasticsearchAssetType, -} from '../../../../types'; +import { Dataset, RegistryPackage, ElasticsearchAssetType, TemplateRef } from '../../../../types'; import { CallESAsCurrentUser } from '../../../../types'; import { Field, loadFieldsFromYaml, processFields } from '../../fields/field'; import { getPipelineNameForInstallation } from '../ingest_pipeline/install'; @@ -22,7 +16,7 @@ export const installTemplates = async ( callCluster: CallESAsCurrentUser, pkgName: string, pkgVersion: string -) => { +): Promise => { // install any pre-built index template assets, // atm, this is only the base package's global template installPreBuiltTemplates(pkgName, pkgVersion, callCluster); @@ -30,7 +24,7 @@ export const installTemplates = async ( // build templates per dataset from yml files const datasets = registryPackage.datasets; if (datasets) { - const templates = datasets.reduce>>((acc, dataset) => { + const installTemplatePromises = datasets.reduce>>((acc, dataset) => { acc.push( installTemplateForDataset({ pkg: registryPackage, @@ -40,7 +34,9 @@ export const installTemplates = async ( ); return acc; }, []); - return Promise.all(templates).then(results => results.flat()); + + const res = await Promise.all(installTemplatePromises); + return res.flat(); } return []; }; @@ -84,7 +80,7 @@ export async function installTemplateForDataset({ pkg: RegistryPackage; callCluster: CallESAsCurrentUser; dataset: Dataset; -}): Promise { +}): Promise { const fields = await loadFieldsFromYaml(pkg, dataset.path); return installTemplate({ callCluster, @@ -104,7 +100,7 @@ export async function installTemplate({ fields: Field[]; dataset: Dataset; packageVersion: string; -}): Promise { +}): Promise { const mappings = generateMappings(processFields(fields)); const templateName = generateTemplateName(dataset); let pipelineName; @@ -122,6 +118,8 @@ export async function installTemplate({ body: template, }); - // The id of a template is its name - return { id: templateName, type: IngestAssetType.IndexTemplate }; + return { + templateName, + indexTemplate: template, + }; } diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts index 22a61d2bdfb7c..46b6923962462 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts @@ -5,24 +5,30 @@ */ import { Field, Fields } from '../../fields/field'; -import { Dataset, IndexTemplate } from '../../../../types'; +import { + Dataset, + CallESAsCurrentUser, + TemplateRef, + IndexTemplate, + IndexTemplateMappings, +} from '../../../../types'; import { getDatasetAssetBaseName } from '../index'; interface Properties { [key: string]: any; } -interface Mappings { - properties: any; -} - -interface Mapping { - [key: string]: any; -} interface MultiFields { [key: string]: object; } +export interface IndexTemplateMapping { + [key: string]: any; +} +export interface CurrentIndex { + indexName: string; + indexTemplate: IndexTemplate; +} const DEFAULT_SCALING_FACTOR = 1000; const DEFAULT_IGNORE_ABOVE = 1024; @@ -34,7 +40,7 @@ const DEFAULT_IGNORE_ABOVE = 1024; export function getTemplate( type: string, templateName: string, - mappings: Mappings, + mappings: IndexTemplateMappings, pipelineName?: string | undefined ): IndexTemplate { const template = getBaseTemplate(type, templateName, mappings); @@ -52,7 +58,7 @@ export function getTemplate( * * @param fields */ -export function generateMappings(fields: Field[]): Mappings { +export function generateMappings(fields: Field[]): IndexTemplateMappings { const props: Properties = {}; // TODO: this can happen when the fields property in fields.yml is present but empty // Maybe validation should be moved to fields/field.ts @@ -140,8 +146,8 @@ function generateMultiFields(fields: Fields): MultiFields { return multiFields; } -function generateKeywordMapping(field: Field): Mapping { - const mapping: Mapping = { +function generateKeywordMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = { ignore_above: DEFAULT_IGNORE_ABOVE, }; if (field.ignore_above) { @@ -150,8 +156,8 @@ function generateKeywordMapping(field: Field): Mapping { return mapping; } -function generateTextMapping(field: Field): Mapping { - const mapping: Mapping = {}; +function generateTextMapping(field: Field): IndexTemplateMapping { + const mapping: IndexTemplateMapping = {}; if (field.analyzer) { mapping.analyzer = field.analyzer; } @@ -200,7 +206,11 @@ export function generateESIndexPatterns(datasets: Dataset[] | undefined): Record return patterns; } -function getBaseTemplate(type: string, templateName: string, mappings: Mappings): IndexTemplate { +function getBaseTemplate( + type: string, + templateName: string, + mappings: IndexTemplateMappings +): IndexTemplate { return { // We need to decide which order we use for the templates order: 1, @@ -234,10 +244,6 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings) }, }, mappings: { - // To be filled with interesting information about this specific index - _meta: { - package: 'foo', - }, // All the dynamic field mappings dynamic_templates: [ // This makes sure all mappings are keywords by default @@ -261,3 +267,112 @@ function getBaseTemplate(type: string, templateName: string, mappings: Mappings) aliases: {}, }; } + +export const updateCurrentWriteIndices = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise => { + if (!templates) return; + + const allIndices = await queryIndicesFromTemplates(callCluster, templates); + return updateAllIndices(allIndices, callCluster); +}; + +const queryIndicesFromTemplates = async ( + callCluster: CallESAsCurrentUser, + templates: TemplateRef[] +): Promise => { + const indexPromises = templates.map(template => { + return getIndices(callCluster, template); + }); + const indexObjects = await Promise.all(indexPromises); + return indexObjects.filter(item => item !== undefined).flat(); +}; + +const getIndices = async ( + callCluster: CallESAsCurrentUser, + template: TemplateRef +): Promise => { + const { templateName, indexTemplate } = template; + const res = await callCluster('search', getIndexQuery(templateName)); + const indices: any[] = res?.aggregations?.index.buckets; + if (indices) { + return indices.map(index => ({ + indexName: index.key, + indexTemplate, + })); + } +}; + +const updateAllIndices = async ( + indexNameWithTemplates: CurrentIndex[], + callCluster: CallESAsCurrentUser +): Promise => { + const updateIndexPromises = indexNameWithTemplates.map(({ indexName, indexTemplate }) => { + return updateExistingIndex({ indexName, callCluster, indexTemplate }); + }); + await Promise.all(updateIndexPromises); +}; +const updateExistingIndex = async ({ + indexName, + callCluster, + indexTemplate, +}: { + indexName: string; + callCluster: CallESAsCurrentUser; + indexTemplate: IndexTemplate; +}) => { + const { settings, mappings } = indexTemplate; + // try to update the mappings first + // for now we assume updates are compatible + try { + await callCluster('indices.putMapping', { + index: indexName, + body: mappings, + }); + } catch (err) { + throw new Error('incompatible mappings update'); + } + // update settings after mappings was successful to ensure + // pointing to theme new pipeline is safe + // for now, only update the pipeline + if (!settings.index.default_pipeline) return; + try { + await callCluster('indices.putSettings', { + index: indexName, + body: { index: { default_pipeline: settings.index.default_pipeline } }, + }); + } catch (err) { + throw new Error('incompatible settings update'); + } +}; + +const getIndexQuery = (templateName: string) => ({ + index: `${templateName}-*`, + size: 0, + body: { + query: { + bool: { + must: [ + { + exists: { + field: 'stream.namespace', + }, + }, + { + exists: { + field: 'stream.dataset', + }, + }, + ], + }, + }, + aggs: { + index: { + terms: { + field: '_index', + }, + }, + }, + }, +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index 0a7642752b3e9..06f3decdbbe6f 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -12,15 +12,19 @@ import { KibanaAssetType, CallESAsCurrentUser, DefaultPackages, + ElasticsearchAssetType, + IngestAssetType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; import { getObject } from './get_objects'; -import { getInstallation } from './index'; +import { getInstallation, getInstallationObject } from './index'; import { installTemplates } from '../elasticsearch/template/install'; import { generateESIndexPatterns } from '../elasticsearch/template/template'; import { installPipelines } from '../elasticsearch/ingest_pipeline/install'; import { installILMPolicy } from '../elasticsearch/ilm/install'; +import { deleteAssetsByType, deleteKibanaSavedObjectsAssets } from './remove'; +import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -89,44 +93,84 @@ export async function installPackage(options: { const { savedObjectsClient, pkgkey, callCluster } = options; // TODO: change epm API to /packageName/version so we don't need to do this const [pkgName, pkgVersion] = pkgkey.split('-'); + // see if some version of this package is already installed + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const reinstall = pkgVersion === installedPkg?.attributes.version; + const registryPackageInfo = await Registry.fetchInfo(pkgName, pkgVersion); - const { internal = false } = registryPackageInfo; + const { internal = false, removable = true } = registryPackageInfo; - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, - pkgName, - pkgVersion, - }); - const installPipelinePromises = installPipelines(registryPackageInfo, callCluster); - const installTemplatePromises = installTemplates( + // delete the previous version's installation's SO kibana assets before installing new ones + // in case some assets were removed in the new version + if (installedPkg) { + try { + await deleteKibanaSavedObjectsAssets(savedObjectsClient, installedPkg.attributes.installed); + } catch (err) { + // some assets may not exist if deleting during a failed update + } + } + + const [installedKibanaAssets, installedPipelines] = await Promise.all([ + installKibanaAssets({ + savedObjectsClient, + pkgName, + pkgVersion, + }), + installPipelines(registryPackageInfo, callCluster), + // index patterns and ilm policies are not currently associated with a particular package + // so we do not save them in the package saved object state. + installIndexPatterns(savedObjectsClient, pkgName, pkgVersion), + // currenly only the base package has an ILM policy + // at some point ILM policies can be installed/modified + // per dataset and we should then save them + installILMPolicy(pkgName, pkgVersion, callCluster), + ]); + + // install or update the templates + const installedTemplates = await installTemplates( registryPackageInfo, callCluster, pkgName, pkgVersion ); + const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // index patterns and ilm policies are not currently associated with a particular package - // so we do not save them in the package saved object state. at some point ILM policies can be installed/modified - // per dataset and we should then save them - await installIndexPatterns(savedObjectsClient, pkgName, pkgVersion); - // currenly only the base package has an ILM policy - await installILMPolicy(pkgName, pkgVersion, callCluster); - - const res = await Promise.all([ - installKibanaAssetsPromise, - installPipelinePromises, - installTemplatePromises, - ]); + // get template refs to save + const installedTemplateRefs = installedTemplates.map(template => ({ + id: template.templateName, + type: IngestAssetType.IndexTemplate, + })); - const toSaveAssetRefs: AssetReference[] = res.flat(); - const toSaveESIndexPatterns = generateESIndexPatterns(registryPackageInfo.datasets); - // Save those references in the package manager's state saved object - return await saveInstallationReferences({ + if (installedPkg) { + // update current index for every index template created + await updateCurrentWriteIndices(callCluster, installedTemplates); + if (!reinstall) { + try { + // delete the previous version's installation's pipelines + // this must happen after the template is updated + await deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects: installedPkg.attributes.installed, + assetType: ElasticsearchAssetType.ingestPipeline, + }); + } catch (err) { + throw new Error(err.message); + } + } + } + const toSaveAssetRefs: AssetReference[] = [ + ...installedKibanaAssets, + ...installedPipelines, + ...installedTemplateRefs, + ]; + // Save references to installed assets in the package's saved object state + return saveInstallationReferences({ savedObjectsClient, - pkgkey, pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, }); @@ -154,10 +198,10 @@ export async function installKibanaAssets(options: { export async function saveInstallationReferences(options: { savedObjectsClient: SavedObjectsClientContract; - pkgkey: string; pkgName: string; pkgVersion: string; internal: boolean; + removable: boolean; toSaveAssetRefs: AssetReference[]; toSaveESIndexPatterns: Record; }) { @@ -166,36 +210,25 @@ export async function saveInstallationReferences(options: { pkgName, pkgVersion, internal, + removable, toSaveAssetRefs, toSaveESIndexPatterns, } = options; - const installation = await getInstallation({ savedObjectsClient, pkgName }); - const savedAssetRefs = installation?.installed || []; - const toInstallESIndexPatterns = Object.assign( - installation?.es_index_patterns || {}, - toSaveESIndexPatterns - ); - - const mergeRefsReducer = (current: AssetReference[], pending: AssetReference) => { - const hasRef = current.find(c => c.id === pending.id && c.type === pending.type); - if (!hasRef) current.push(pending); - return current; - }; - const toInstallAssetsRefs = toSaveAssetRefs.reduce(mergeRefsReducer, savedAssetRefs); await savedObjectsClient.create( PACKAGES_SAVED_OBJECT_TYPE, { - installed: toInstallAssetsRefs, - es_index_patterns: toInstallESIndexPatterns, + installed: toSaveAssetRefs, + es_index_patterns: toSaveESIndexPatterns, name: pkgName, version: pkgVersion, internal, + removable, }, { id: pkgName, overwrite: true } ); - return toInstallAssetsRefs; + return toSaveAssetRefs; } async function installKibanaSavedObjects({ diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index a30acb97b99cf..498796438c6c8 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -20,7 +20,10 @@ export async function removeInstallation(options: { // TODO: the epm api should change to /name/version so we don't need to do this const [pkgName] = pkgkey.split('-'); const installation = await getInstallation({ savedObjectsClient, pkgName }); - const installedObjects = installation?.installed || []; + if (!installation) throw new Error('integration does not exist'); + if (installation.removable === false) + throw new Error(`The ${pkgName} integration is installed by default and cannot be removed`); + const installedObjects = installation.installed || []; // Delete the manager saved object with references to the asset objects // could also update with [] or some other state @@ -29,7 +32,17 @@ export async function removeInstallation(options: { // recreate or delete index patterns when a package is uninstalled await installIndexPatterns(savedObjectsClient); - // Delete the installed assets + // Delete the installed asset + await deleteAssets(installedObjects, savedObjectsClient, callCluster); + + // successful delete's in SO client return {}. return something more useful + return installedObjects; +} +async function deleteAssets( + installedObjects: AssetReference[], + savedObjectsClient: SavedObjectsClientContract, + callCluster: CallESAsCurrentUser +) { const deletePromises = installedObjects.map(async ({ id, type }) => { const assetType = type as AssetType; if (savedObjectTypes.includes(assetType)) { @@ -40,22 +53,62 @@ export async function removeInstallation(options: { deleteTemplate(callCluster, id); } }); - await Promise.all([...deletePromises]); - - // successful delete's in SO client return {}. return something more useful - return installedObjects; + try { + await Promise.all([...deletePromises]); + } catch (err) { + throw new Error(err.message); + } } - async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise { // '*' shouldn't ever appear here, but it still would delete all ingest pipelines if (id && id !== '*') { - await callCluster('ingest.deletePipeline', { id }); + try { + await callCluster('ingest.deletePipeline', { id }); + } catch (err) { + throw new Error(`error deleting pipeline ${id}`); + } } } async function deleteTemplate(callCluster: CallESAsCurrentUser, name: string): Promise { // '*' shouldn't ever appear here, but it still would delete all templates if (name && name !== '*') { - await callCluster('indices.deleteTemplate', { name }); + try { + await callCluster('indices.deleteTemplate', { name }); + } catch { + throw new Error(`error deleting template ${name}`); + } } } + +export async function deleteAssetsByType({ + savedObjectsClient, + callCluster, + installedObjects, + assetType, +}: { + savedObjectsClient: SavedObjectsClientContract; + callCluster: CallESAsCurrentUser; + installedObjects: AssetReference[]; + assetType: ElasticsearchAssetType; +}) { + const toDelete = installedObjects.filter(asset => asset.type === assetType); + try { + await deleteAssets(toDelete, savedObjectsClient, callCluster); + } catch (err) { + throw new Error(err.message); + } +} + +export async function deleteKibanaSavedObjectsAssets( + savedObjectsClient: SavedObjectsClientContract, + installedObjects: AssetReference[] +) { + const deletePromises = installedObjects.map(({ id, type }) => { + const assetType = type as AssetType; + if (savedObjectTypes.includes(assetType)) { + savedObjectsClient.delete(assetType, id); + } + }); + await Promise.all(deletePromises); +} diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index 1cd5622c0c7b0..105f9039f1e98 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -47,6 +47,8 @@ export { RegistrySearchResults, RegistrySearchResult, DefaultPackages, + TemplateRef, + IndexTemplateMappings, } from '../../common'; export type CallESAsCurrentUser = ScopedClusterClient['callAsCurrentUser']; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx index f12a0e5b907c7..d6b6de479acfb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx @@ -28,7 +28,7 @@ import { import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EmbeddableVisTriggerContext } from '../../../../../src/plugins/embeddable/public'; +import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public'; import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public'; import { LensMultiTable, FormatFactory } from '../types'; import { XYArgs, SeriesType, visualizationTypes } from './types'; @@ -277,7 +277,7 @@ export function XYChart({ const timeFieldName = xDomain && xAxisFieldName; - const context: EmbeddableVisTriggerContext = { + const context: ValueClickTriggerContext = { data: { data: points.map(point => ({ row: point.row, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index a4006732224ce..6f9c0985f5f4a 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -166,6 +166,7 @@ export enum STYLE_TYPE { export enum LAYER_STYLE_TYPE { VECTOR = 'VECTOR', HEATMAP = 'HEATMAP', + TILE = 'TILE', } export const COLOR_MAP_TYPE = { diff --git a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts index f8175b0ed3f10..6980f14d0788a 100644 --- a/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/descriptor_types.d.ts @@ -5,8 +5,9 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ +import { Query } from 'src/plugins/data/public'; import { AGG_TYPE, GRID_RESOLUTION, RENDER_AS, SORT_ORDER, SCALING_TYPES } from '../constants'; -import { VectorStyleDescriptor } from './style_property_descriptor_types'; +import { StyleDescriptor, VectorStyleDescriptor } from './style_property_descriptor_types'; import { DataRequestDescriptor } from './data_request_descriptor_types'; export type AttributionDescriptor = { @@ -17,6 +18,7 @@ export type AttributionDescriptor = { export type AbstractSourceDescriptor = { id?: string; type: string; + applyGlobalQuery?: boolean; }; export type EMSTMSSourceDescriptor = AbstractSourceDescriptor & { @@ -71,17 +73,15 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { term: string; // term field name }; -export type KibanaRegionmapSourceDescriptor = { - type: string; +export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & { name: string; }; -export type KibanaTilemapSourceDescriptor = { - type: string; -}; +// This is for symmetry with other sources only. +// It takes no additional configuration since all params are in the .yml. +export type KibanaTilemapSourceDescriptor = AbstractSourceDescriptor; -export type WMSSourceDescriptor = { - type: string; +export type WMSSourceDescriptor = AbstractSourceDescriptor & { serviceUrl: string; layers: string; styles: string; @@ -111,6 +111,8 @@ export type JoinDescriptor = { right: ESTermSourceDescriptor; }; +// todo : this union type is incompatible with dynamic extensibility of sources. +// Reconsider using SourceDescriptor in type signatures for top-level classes export type SourceDescriptor = | XYZTMSSourceDescriptor | WMSSourceDescriptor @@ -121,7 +123,9 @@ export type SourceDescriptor = | ESGeoGridSourceDescriptor | EMSFileSourceDescriptor | ESPewPewSourceDescriptor - | TiledSingleLayerVectorSourceDescriptor; + | TiledSingleLayerVectorSourceDescriptor + | EMSTMSSourceDescriptor + | EMSFileSourceDescriptor; export type LayerDescriptor = { __dataRequests?: DataRequestDescriptor[]; @@ -129,12 +133,14 @@ export type LayerDescriptor = { __errorMessage?: string; alpha?: number; id: string; - label?: string; + label?: string | null; minZoom?: number; maxZoom?: number; - sourceDescriptor: SourceDescriptor; + sourceDescriptor: SourceDescriptor | null; type?: string; visible?: boolean; + style?: StyleDescriptor | null; + query?: Query; }; export type VectorLayerDescriptor = LayerDescriptor & { diff --git a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts index 47e56ff96d623..381bc5bba01c0 100644 --- a/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts +++ b/x-pack/plugins/maps/common/descriptor_types/style_property_descriptor_types.d.ts @@ -182,7 +182,11 @@ export type VectorStylePropertiesDescriptor = { [VECTOR_STYLES.LABEL_BORDER_SIZE]?: LabelBorderSizeStylePropertyDescriptor; }; -export type VectorStyleDescriptor = { +export type StyleDescriptor = { + type: string; +}; + +export type VectorStyleDescriptor = StyleDescriptor & { type: LAYER_STYLE_TYPE.VECTOR; properties: VectorStylePropertiesDescriptor; }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.d.ts b/x-pack/plugins/maps/public/actions/map_actions.d.ts index debead3ad5c45..c8db284a5c4f1 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.d.ts @@ -14,6 +14,7 @@ import { MapCenterAndZoom, MapRefreshConfig, } from '../../common/descriptor_types'; +import { MapSettings } from '../reducers/map'; export type SyncContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -62,3 +63,14 @@ export function hideViewControl(): AnyAction; export function setHiddenLayers(hiddenLayerIds: string[]): AnyAction; export function addLayerWithoutDataSync(layerDescriptor: unknown): AnyAction; + +export function setMapSettings(settings: MapSettings): AnyAction; + +export function rollbackMapSettings(): AnyAction; + +export function trackMapSettings(): AnyAction; + +export function updateMapSetting( + settingKey: string, + settingValue: string | boolean | number +): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/map_actions.js b/x-pack/plugins/maps/public/actions/map_actions.js index 572385d628b16..da6ba6b481054 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.js +++ b/x-pack/plugins/maps/public/actions/map_actions.js @@ -76,6 +76,10 @@ export const HIDE_TOOLBAR_OVERLAY = 'HIDE_TOOLBAR_OVERLAY'; export const HIDE_LAYER_CONTROL = 'HIDE_LAYER_CONTROL'; export const HIDE_VIEW_CONTROL = 'HIDE_VIEW_CONTROL'; export const SET_WAITING_FOR_READY_HIDDEN_LAYERS = 'SET_WAITING_FOR_READY_HIDDEN_LAYERS'; +export const SET_MAP_SETTINGS = 'SET_MAP_SETTINGS'; +export const ROLLBACK_MAP_SETTINGS = 'ROLLBACK_MAP_SETTINGS'; +export const TRACK_MAP_SETTINGS = 'TRACK_MAP_SETTINGS'; +export const UPDATE_MAP_SETTING = 'UPDATE_MAP_SETTING'; function getLayerLoadingCallbacks(dispatch, getState, layerId) { return { @@ -145,6 +149,29 @@ export function setMapInitError(errorMessage) { }; } +export function setMapSettings(settings) { + return { + type: SET_MAP_SETTINGS, + settings, + }; +} + +export function rollbackMapSettings() { + return { type: ROLLBACK_MAP_SETTINGS }; +} + +export function trackMapSettings() { + return { type: TRACK_MAP_SETTINGS }; +} + +export function updateMapSetting(settingKey, settingValue) { + return { + type: UPDATE_MAP_SETTING, + settingKey, + settingValue, + }; +} + export function trackCurrentLayerState(layerId) { return { type: TRACK_CURRENT_LAYER_STATE, diff --git a/x-pack/plugins/maps/public/actions/ui_actions.d.ts b/x-pack/plugins/maps/public/actions/ui_actions.d.ts index e087dc70256f0..43cdcff7d2d69 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.d.ts +++ b/x-pack/plugins/maps/public/actions/ui_actions.d.ts @@ -5,6 +5,7 @@ */ import { AnyAction } from 'redux'; +import { FLYOUT_STATE } from '../reducers/ui'; export const UPDATE_FLYOUT: string; export const CLOSE_SET_VIEW: string; @@ -17,6 +18,8 @@ export const SHOW_TOC_DETAILS: string; export const HIDE_TOC_DETAILS: string; export const UPDATE_INDEXING_STAGE: string; +export function updateFlyout(display: FLYOUT_STATE): AnyAction; + export function setOpenTOCDetails(layerIds?: string[]): AnyAction; export function setIsLayerTOCOpen(open: boolean): AnyAction; diff --git a/x-pack/plugins/maps/public/actions/ui_actions.js b/x-pack/plugins/maps/public/actions/ui_actions.js index 77fdf6b0f12d2..e2a36e33e7db0 100644 --- a/x-pack/plugins/maps/public/actions/ui_actions.js +++ b/x-pack/plugins/maps/public/actions/ui_actions.js @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getFlyoutDisplay } from '../selectors/ui_selectors'; +import { FLYOUT_STATE } from '../reducers/ui'; +import { setSelectedLayer, trackMapSettings } from './map_actions'; + export const UPDATE_FLYOUT = 'UPDATE_FLYOUT'; export const CLOSE_SET_VIEW = 'CLOSE_SET_VIEW'; export const OPEN_SET_VIEW = 'OPEN_SET_VIEW'; @@ -28,6 +32,17 @@ export function updateFlyout(display) { display, }; } +export function openMapSettings() { + return (dispatch, getState) => { + const flyoutDisplay = getFlyoutDisplay(getState()); + if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + return; + } + dispatch(setSelectedLayer(null)); + dispatch(trackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.MAP_SETTINGS_PANEL)); + }; +} export function closeSetView() { return { type: CLOSE_SET_VIEW, diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js index f41ed26b2a05d..4b5cad8d19260 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.test.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.test.js @@ -52,7 +52,7 @@ describe('kibana.yml configured with map.tilemap.url', () => { sourceDescriptor: { type: 'KIBANA_TILEMAP', }, - style: {}, + style: { type: 'TILE' }, type: 'TILE', visible: true, }, @@ -96,7 +96,7 @@ describe('EMS is enabled', () => { isAutoSelect: true, type: 'EMS_TMS', }, - style: {}, + style: { type: 'TILE' }, type: 'VECTOR_TILE', visible: true, }, diff --git a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js index 1c47e0ab7dc2a..1a58b0cefaed9 100644 --- a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js @@ -15,6 +15,7 @@ import { getRefreshConfig, getQuery, getFilters, + getMapSettings, } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; @@ -98,6 +99,7 @@ export function createSavedGisMapClass(services) { refreshConfig: getRefreshConfig(state), query: _.omit(getQuery(state), 'queryLastTriggeredAt'), filters: getFilters(state), + settings: getMapSettings(state), }); this.uiStateJSON = JSON.stringify({ diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/plugins/maps/public/connected_components/gis_map/index.js index c825fdab75ca7..f8769d0bb898a 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/index.js +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.js @@ -6,8 +6,6 @@ import { connect } from 'react-redux'; import { GisMap } from './view'; - -import { FLYOUT_STATE } from '../../reducers/ui'; import { exitFullScreen } from '../../actions/ui_actions'; import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; @@ -22,12 +20,9 @@ import { import { getCoreChrome } from '../../kibana_services'; function mapStateToProps(state = {}) { - const flyoutDisplay = getFlyoutDisplay(state); return { areLayersLoaded: areLayersLoaded(state), - layerDetailsVisible: flyoutDisplay === FLYOUT_STATE.LAYER_PANEL, - addLayerVisible: flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD, - noFlyoutVisible: flyoutDisplay === FLYOUT_STATE.NONE, + flyoutDisplay: getFlyoutDisplay(state), isFullScreen: getIsFullScreen(state), refreshConfig: getRefreshConfig(state), mapInitError: getMapInitError(state), diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js index 28ad12133d611..6eb173a001d01 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -6,6 +6,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; +import classNames from 'classnames'; import { MBMapContainer } from '../map/mb'; import { WidgetOverlay } from '../widget_overlay'; import { ToolbarOverlay } from '../toolbar_overlay'; @@ -19,6 +20,8 @@ import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; import { indexPatterns as indexPatternsUtils } from '../../../../../../src/plugins/data/public'; import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapSettingsPanel } from '../map_settings_panel'; const RENDER_COMPLETE_EVENT = 'renderComplete'; @@ -147,9 +150,7 @@ export class GisMap extends Component { render() { const { addFilters, - layerDetailsVisible, - addLayerVisible, - noFlyoutVisible, + flyoutDisplay, isFullScreen, exitFullScreen, mapInitError, @@ -174,16 +175,13 @@ export class GisMap extends Component { ); } - let currentPanel; - let currentPanelClassName; - if (noFlyoutVisible) { - currentPanel = null; - } else if (addLayerVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ; - } else if (layerDetailsVisible) { - currentPanelClassName = 'mapMapLayerPanel-isVisible'; - currentPanel = ; + let flyoutPanel = null; + if (flyoutDisplay === FLYOUT_STATE.ADD_LAYER_WIZARD) { + flyoutPanel = ; + } else if (flyoutDisplay === FLYOUT_STATE.LAYER_PANEL) { + flyoutPanel = ; + } else if (flyoutDisplay === FLYOUT_STATE.MAP_SETTINGS_PANEL) { + flyoutPanel = ; } let exitFullScreenButton; @@ -210,8 +208,13 @@ export class GisMap extends Component { - - {currentPanel} + + {flyoutPanel} {exitFullScreenButton} 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 d864b60eb433b..459b38d422694 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, + getMapSettings, } from '../../../selectors/map_selectors'; import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; @@ -30,6 +31,7 @@ import { getInspectorAdapters } from '../../../reducers/non_serializable_instanc function mapStateToProps(state = {}) { return { isMapReady: getMapReady(state), + settings: getMapSettings(state), layerList: getLayerList(state), goto: getGoto(state), inspectorAdapters: getInspectorAdapters(state), 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 2d95de184f0f4..71c1af44e493b 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 @@ -12,14 +12,8 @@ import { removeOrphanedSourcesAndLayers, addSpritesheetToMap, } from './utils'; - import { getGlyphUrl, isRetina } from '../../../meta'; -import { - DECIMAL_DEGREES_PRECISION, - MAX_ZOOM, - MIN_ZOOM, - ZOOM_PRECISION, -} from '../../../../common/constants'; +import { DECIMAL_DEGREES_PRECISION, ZOOM_PRECISION } from '../../../../common/constants'; import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; @@ -80,7 +74,7 @@ export class MBMapContainer extends React.Component { } _debouncedSync = _.debounce(() => { - if (this._isMounted) { + if (this._isMounted || !this.props.isMapReady) { if (!this.state.hasSyncedLayerList) { this.setState( { @@ -92,6 +86,7 @@ export class MBMapContainer extends React.Component { } ); } + this._syncSettings(); } }, 256); @@ -133,8 +128,8 @@ export class MBMapContainer extends React.Component { scrollZoom: this.props.scrollZoom, preserveDrawingBuffer: getInjectedVarFunc()('preserveDrawingBuffer', false), interactive: !this.props.disableInteractive, - minZoom: MIN_ZOOM, - maxZoom: MAX_ZOOM, + maxZoom: this.props.settings.maxZoom, + minZoom: this.props.settings.minZoom, }; const initialView = _.get(this.props.goto, 'center'); if (initialView) { @@ -265,17 +260,13 @@ export class MBMapContainer extends React.Component { }; _syncMbMapWithLayerList = () => { - if (!this.props.isMapReady) { - return; - } - removeOrphanedSourcesAndLayers(this.state.mbMap, this.props.layerList); this.props.layerList.forEach(layer => layer.syncLayerWithMB(this.state.mbMap)); syncLayerOrderForSingleLayer(this.state.mbMap, this.props.layerList); }; _syncMbMapWithInspector = () => { - if (!this.props.isMapReady || !this.props.inspectorAdapters.map) { + if (!this.props.inspectorAdapters.map) { return; } @@ -289,6 +280,27 @@ export class MBMapContainer extends React.Component { }); }; + _syncSettings() { + let zoomRangeChanged = false; + if (this.props.settings.minZoom !== this.state.mbMap.getMinZoom()) { + this.state.mbMap.setMinZoom(this.props.settings.minZoom); + zoomRangeChanged = true; + } + if (this.props.settings.maxZoom !== this.state.mbMap.getMaxZoom()) { + this.state.mbMap.setMaxZoom(this.props.settings.maxZoom); + zoomRangeChanged = true; + } + + // 'moveend' event not fired when map moves from setMinZoom or setMaxZoom + // https://github.com/mapbox/mapbox-gl-js/issues/9610 + // hack to update extent after zoom update finishes moving map. + if (zoomRangeChanged) { + setTimeout(() => { + this.props.extentChanged(this._getMapState()); + }, 300); + } + } + render() { let drawControl; let tooltipControl; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts new file mode 100644 index 0000000000000..329fac28d7d2e --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/index.ts @@ -0,0 +1,39 @@ +/* + * 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 { AnyAction, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import { FLYOUT_STATE } from '../../reducers/ui'; +import { MapStoreState } from '../../reducers/store'; +import { MapSettingsPanel } from './map_settings_panel'; +import { rollbackMapSettings, updateMapSetting } from '../../actions/map_actions'; +import { getMapSettings, hasMapSettingsChanges } from '../../selectors/map_selectors'; +import { updateFlyout } from '../../actions/ui_actions'; + +function mapStateToProps(state: MapStoreState) { + return { + settings: getMapSettings(state), + hasMapSettingsChanges: hasMapSettingsChanges(state), + }; +} + +function mapDispatchToProps(dispatch: Dispatch) { + return { + cancelChanges: () => { + dispatch(rollbackMapSettings()); + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + keepChanges: () => { + dispatch(updateFlyout(FLYOUT_STATE.NONE)); + }, + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => { + dispatch(updateMapSetting(settingKey, settingValue)); + }, + }; +} + +const connectedMapSettingsPanel = connect(mapStateToProps, mapDispatchToProps)(MapSettingsPanel); +export { connectedMapSettingsPanel as MapSettingsPanel }; 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 new file mode 100644 index 0000000000000..36ed29e92cf69 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/map_settings_panel.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { NavigationPanel } from './navigation_panel'; + +interface Props { + cancelChanges: () => void; + hasMapSettingsChanges: boolean; + keepChanges: () => void; + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function MapSettingsPanel({ + cancelChanges, + hasMapSettingsChanges, + keepChanges, + settings, + updateMapSetting, +}: Props) { + // TODO move common text like Cancel and Close to common i18n translation + const closeBtnLabel = hasMapSettingsChanges + ? i18n.translate('xpack.maps.mapSettingsPanel.cancelLabel', { + defaultMessage: 'Cancel', + }) + : i18n.translate('xpack.maps.mapSettingsPanel.closeLabel', { + defaultMessage: 'Close', + }); + + return ( + + + +

    + +

    + + + +
    +
    + +
    +
    + + + + + + {closeBtnLabel} + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx new file mode 100644 index 0000000000000..ed83e838f44f6 --- /dev/null +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/navigation_panel.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MapSettings } from '../../reducers/map'; +import { ValidatedDualRange, Value } from '../../../../../../src/plugins/kibana_react/public'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +interface Props { + settings: MapSettings; + updateMapSetting: (settingKey: string, settingValue: string | number | boolean) => void; +} + +export function NavigationPanel({ settings, updateMapSetting }: Props) { + const onZoomChange = (value: Value) => { + updateMapSetting('minZoom', Math.max(MIN_ZOOM, parseInt(value[0] as string, 10))); + updateMapSetting('maxZoom', Math.min(MAX_ZOOM, parseInt(value[1] as string, 10))); + }; + + return ( + + +
    + +
    +
    + + + +
    + ); +} diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js index 2b6fae26098be..c3cc4090ab952 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/index.js @@ -7,12 +7,13 @@ import { connect } from 'react-redux'; import { SetViewControl } from './set_view_control'; import { setGotoWithCenter } from '../../../actions/map_actions'; -import { getMapZoom, getMapCenter } from '../../../selectors/map_selectors'; +import { getMapZoom, getMapCenter, getMapSettings } from '../../../selectors/map_selectors'; import { closeSetView, openSetView } from '../../../actions/ui_actions'; import { getIsSetViewOpen } from '../../../selectors/ui_selectors'; function mapStateToProps(state = {}) { return { + settings: getMapSettings(state), isSetViewOpen: getIsSetViewOpen(state), zoom: getMapZoom(state), center: getMapCenter(state), diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js index 9c983447bfbf6..2c10728f78e5c 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/set_view_control/set_view_control.js @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MAX_ZOOM, MIN_ZOOM } from '../../../../common/constants'; function getViewString(lat, lon, zoom) { return `${lat},${lon},${zoom}`; @@ -118,8 +117,8 @@ export class SetViewControl extends Component { const { isInvalid: isZoomInvalid, component: zoomFormRow } = this._renderNumberFormRow({ value: this.state.zoom, - min: MIN_ZOOM, - max: MAX_ZOOM, + min: this.props.settings.minZoom, + max: this.props.settings.maxZoom, onChange: this._onZoomChange, label: i18n.translate('xpack.maps.setViewControl.zoomLabel', { defaultMessage: 'Zoom', diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap index 560ebad89c50e..0af4eb0793f03 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/__snapshots__/view.test.js.snap @@ -65,7 +65,7 @@ exports[`LayerControl is rendered 1`] = ` data-test-subj="addLayerButton" fill={true} fullWidth={true} - isDisabled={true} + isDisabled={false} onClick={[Function]} > `; + +exports[`LayerControl should disable buttons when flyout is open 1`] = ` + + + + + + +

    + +

    +
    +
    + + + + + +
    +
    + + + +
    + + + + +
    +`; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js index 8780bac59e4b7..915f808b8e358 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/index.js @@ -22,7 +22,7 @@ function mapStateToProps(state = {}) { isReadOnly: getIsReadOnly(state), isLayerTOCOpen: getIsLayerTOCOpen(state), layerList: getLayerList(state), - isAddButtonActive: getFlyoutDisplay(state) === FLYOUT_STATE.NONE, + isFlyoutOpen: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, }; } diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js index 537a676287042..180dc2e3933c3 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/view.js @@ -57,7 +57,7 @@ export function LayerControl({ closeLayerTOC, openLayerTOC, layerList, - isAddButtonActive, + isFlyoutOpen, }) { if (!isLayerTOCOpen) { const hasErrors = layerList.some(layer => { @@ -86,7 +86,7 @@ export function LayerControl({ {}, isLayerTOCOpen: true, layerList: [], + isFlyoutOpen: false, }; describe('LayerControl', () => { @@ -30,6 +31,12 @@ describe('LayerControl', () => { expect(component).toMatchSnapshot(); }); + test('should disable buttons when flyout is open', () => { + const component = shallow(); + + expect(component).toMatchSnapshot(); + }); + test('isReadOnly', () => { const component = shallow(); diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index dbd48d614e99b..467cf4727edb7 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -28,6 +28,7 @@ import { } from '../../../../../src/plugins/data/public'; import { GisMap } from '../connected_components/gis_map'; import { createMapStore, MapStore } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; import { setGotoWithCenter, replaceLayerList, @@ -40,6 +41,7 @@ import { hideLayerControl, hideViewControl, setHiddenLayers, + setMapSettings, } from '../actions/map_actions'; import { MapCenterAndZoom } from '../../common/descriptor_types'; import { setReadOnly, setIsLayerTOCOpen, setOpenTOCDetails } from '../actions/ui_actions'; @@ -60,6 +62,7 @@ interface MapEmbeddableConfig { editable: boolean; title?: string; layerList: unknown[]; + settings?: MapSettings; } export interface MapEmbeddableInput extends EmbeddableInput { @@ -97,6 +100,7 @@ export class MapEmbeddable extends Embeddable this.onContainerStateChanged(input)); @@ -194,6 +199,10 @@ export class MapEmbeddable extends Embeddable; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; -} - -export interface ILayerArguments { - layerDescriptor: LayerDescriptor; - source: ISource; -} - -export class AbstractLayer implements ILayer { - static createDescriptor(options: Partial, mapColors?: string[]): LayerDescriptor; - constructor(layerArguments: ILayerArguments); - getBounds(mapFilters: MapFilters): Promise; - getDataRequest(id: string): DataRequest | undefined; - getDisplayName(source?: ISource): Promise; - getId(): string; - getSourceDataRequest(): DataRequest | undefined; - getSource(): ISource; - getSourceForEditing(): ISource; - syncData(syncContext: SyncContext): Promise; - isVisible(): boolean; - showAtZoomLevel(zoomLevel: number): boolean; - getMinZoom(): number; - getMaxZoom(): number; - getMinSourceZoom(): number; - getQuery(): MapQuery; - _removeStaleMbSourcesAndLayers(mbMap: unknown): void; - _requiresPrevSourceCleanup(mbMap: unknown): boolean; -} diff --git a/x-pack/plugins/maps/public/layers/layer.js b/x-pack/plugins/maps/public/layers/layer.js deleted file mode 100644 index 9362ce2c028e6..0000000000000 --- a/x-pack/plugins/maps/public/layers/layer.js +++ /dev/null @@ -1,373 +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 _ from 'lodash'; -import React from 'react'; -import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; -import { DataRequest } from './util/data_request'; -import { - MAX_ZOOM, - MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, - MIN_ZOOM, - SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; -import uuid from 'uuid/v4'; - -import { copyPersistentState } from '../reducers/util.js'; -import { i18n } from '@kbn/i18n'; - -export class AbstractLayer { - constructor({ layerDescriptor, source }) { - this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); - this._source = source; - if (this._descriptor.__dataRequests) { - this._dataRequests = this._descriptor.__dataRequests.map( - dataRequest => new DataRequest(dataRequest) - ); - } else { - this._dataRequests = []; - } - } - - static getBoundDataForSource(mbMap, sourceId) { - const mbStyle = mbMap.getStyle(); - return mbStyle.sources[sourceId].data; - } - - static createDescriptor(options = {}) { - const layerDescriptor = { ...options }; - - layerDescriptor.__dataRequests = _.get(options, '__dataRequests', []); - layerDescriptor.id = _.get(options, 'id', uuid()); - layerDescriptor.label = options.label && options.label.length > 0 ? options.label : null; - layerDescriptor.minZoom = _.get(options, 'minZoom', MIN_ZOOM); - layerDescriptor.maxZoom = _.get(options, 'maxZoom', MAX_ZOOM); - layerDescriptor.alpha = _.get(options, 'alpha', 0.75); - layerDescriptor.visible = _.get(options, 'visible', true); - layerDescriptor.style = _.get(options, 'style', {}); - - return layerDescriptor; - } - - destroy() { - if (this._source) { - this._source.destroy(); - } - } - - async cloneDescriptor() { - const clonedDescriptor = copyPersistentState(this._descriptor); - // layer id is uuid used to track styles/layers in mapbox - clonedDescriptor.id = uuid(); - const displayName = await this.getDisplayName(); - clonedDescriptor.label = `Clone of ${displayName}`; - clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); - if (clonedDescriptor.joins) { - clonedDescriptor.joins.forEach(joinDescriptor => { - // right.id is uuid used to track requests in inspector - joinDescriptor.right.id = uuid(); - }); - } - return clonedDescriptor; - } - - makeMbLayerId(layerNameSuffix) { - return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; - } - - isJoinable() { - return this.getSource().isJoinable(); - } - - supportsElasticsearchFilters() { - return this.getSource().isESSource(); - } - - async supportsFitToBounds() { - return await this.getSource().supportsFitToBounds(); - } - - async getDisplayName(source) { - if (this._descriptor.label) { - return this._descriptor.label; - } - - const sourceDisplayName = source - ? await source.getDisplayName() - : await this.getSource().getDisplayName(); - return sourceDisplayName || `Layer ${this._descriptor.id}`; - } - - async getAttributions() { - if (!this.hasErrors()) { - return await this.getSource().getAttributions(); - } - return []; - } - - getLabel() { - return this._descriptor.label ? this._descriptor.label : ''; - } - - getCustomIconAndTooltipContent() { - return { - icon: , - }; - } - - getIconAndTooltipContent(zoomLevel, isUsingSearch) { - let icon; - let tooltipContent = null; - const footnotes = []; - if (this.hasErrors()) { - icon = ( - - ); - tooltipContent = this.getErrors(); - } else if (this.isLayerLoading()) { - icon = ; - } else if (!this.isVisible()) { - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { - defaultMessage: `Layer is hidden.`, - }); - } else if (!this.showAtZoomLevel(zoomLevel)) { - const minZoom = this.getMinZoom(); - const maxZoom = this.getMaxZoom(); - icon = ; - tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { - defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, - values: { minZoom, maxZoom }, - }); - } else { - const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); - if (customIconAndTooltipContent) { - icon = customIconAndTooltipContent.icon; - if (!customIconAndTooltipContent.areResultsTrimmed) { - tooltipContent = customIconAndTooltipContent.tooltipContent; - } else { - footnotes.push({ - icon: , - message: customIconAndTooltipContent.tooltipContent, - }); - } - } - - if (isUsingSearch && this.getQueryableIndexPatternIds().length) { - footnotes.push({ - icon: , - message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { - defaultMessage: 'Results narrowed by search bar', - }), - }); - } - } - - return { - icon, - tooltipContent, - footnotes, - }; - } - - async hasLegendDetails() { - return false; - } - - renderLegendDetails() { - return null; - } - - getId() { - return this._descriptor.id; - } - - getSource() { - return this._source; - } - - getSourceForEditing() { - return this._source; - } - - isVisible() { - return this._descriptor.visible; - } - - showAtZoomLevel(zoom) { - return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); - } - - getMinZoom() { - return this._descriptor.minZoom; - } - - getMaxZoom() { - return this._descriptor.maxZoom; - } - - getMinSourceZoom() { - return this._source.getMinZoom(); - } - - _requiresPrevSourceCleanup() { - return false; - } - - _removeStaleMbSourcesAndLayers(mbMap) { - if (this._requiresPrevSourceCleanup(mbMap)) { - const mbStyle = mbMap.getStyle(); - mbStyle.layers.forEach(mbLayer => { - if (this.ownsMbLayerId(mbLayer.id)) { - mbMap.removeLayer(mbLayer.id); - } - }); - Object.keys(mbStyle.sources).some(mbSourceId => { - if (this.ownsMbSourceId(mbSourceId)) { - mbMap.removeSource(mbSourceId); - } - }); - } - } - - getAlpha() { - return this._descriptor.alpha; - } - - getQuery() { - return this._descriptor.query; - } - - getCurrentStyle() { - return this._style; - } - - getStyleForEditing() { - return this._style; - } - - async getImmutableSourceProperties() { - return this.getSource().getImmutableProperties(); - } - - renderSourceSettingsEditor = ({ onChange }) => { - return this.getSourceForEditing().renderSourceSettingsEditor({ onChange }); - }; - - getPrevRequestToken(dataId) { - const prevDataRequest = this.getDataRequest(dataId); - if (!prevDataRequest) { - return; - } - - return prevDataRequest.getRequestToken(); - } - - getInFlightRequestTokens() { - if (!this._dataRequests) { - return []; - } - - const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); - return _.compact(requestTokens); - } - - getSourceDataRequest() { - return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); - } - - getDataRequest(id) { - return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); - } - - isLayerLoading() { - return this._dataRequests.some(dataRequest => dataRequest.isLoading()); - } - - hasErrors() { - return _.get(this._descriptor, '__isInErrorState', false); - } - - getErrors() { - return this.hasErrors() ? this._descriptor.__errorMessage : ''; - } - - toLayerDescriptor() { - return this._descriptor; - } - - async syncData() { - //no-op by default - } - - getMbLayerIds() { - throw new Error('Should implement AbstractLayer#getMbLayerIds'); - } - - ownsMbLayerId() { - throw new Error('Should implement AbstractLayer#ownsMbLayerId'); - } - - ownsMbSourceId() { - throw new Error('Should implement AbstractLayer#ownsMbSourceId'); - } - - canShowTooltip() { - return false; - } - - syncLayerWithMB() { - throw new Error('Should implement AbstractLayer#syncLayerWithMB'); - } - - getLayerTypeIconName() { - throw new Error('should implement Layer#getLayerTypeIconName'); - } - - isDataLoaded() { - const sourceDataRequest = this.getSourceDataRequest(); - return sourceDataRequest && sourceDataRequest.hasData(); - } - - async getBounds(/* mapFilters: MapFilters */) { - return { - minLon: -180, - maxLon: 180, - minLat: -89, - maxLat: 89, - }; - } - - renderStyleEditor({ onStyleDescriptorChange }) { - const style = this.getStyleForEditing(); - if (!style) { - return null; - } - return style.renderEditor({ layer: this, onStyleDescriptorChange }); - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - syncVisibilityWithMb(mbMap, mbLayerId) { - mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); - } - - getType() { - return this._descriptor.type; - } -} diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/layers/layer.tsx new file mode 100644 index 0000000000000..ce48793e1481b --- /dev/null +++ b/x-pack/plugins/maps/public/layers/layer.tsx @@ -0,0 +1,490 @@ +/* + * 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. + */ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { Query } from 'src/plugins/data/public'; +import _ from 'lodash'; +import React, { ReactElement } from 'react'; +import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; +import uuid from 'uuid/v4'; +import { i18n } from '@kbn/i18n'; +import { FeatureCollection } from 'geojson'; +import { DataRequest } from './util/data_request'; +import { + MAX_ZOOM, + MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, + MIN_ZOOM, + SOURCE_DATA_ID_ORIGIN, +} from '../../common/constants'; +// @ts-ignore +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { copyPersistentState } from '../reducers/util.js'; +import { + LayerDescriptor, + MapExtent, + MapFilters, + StyleDescriptor, +} from '../../common/descriptor_types'; +import { Attribution, ImmutableSourceProperty, ISource } from './sources/source'; +import { SyncContext } from '../actions/map_actions'; +import { IStyle } from './styles/style'; + +export interface ILayer { + getBounds(mapFilters: MapFilters): Promise; + getDataRequest(id: string): DataRequest | undefined; + getDisplayName(source?: ISource): Promise; + getId(): string; + getSourceDataRequest(): DataRequest | undefined; + getSource(): ISource; + getSourceForEditing(): ISource; + syncData(syncContext: SyncContext): void; + supportsElasticsearchFilters(): boolean; + supportsFitToBounds(): Promise; + getAttributions(): Promise; + getLabel(): string; + getCustomIconAndTooltipContent(): IconAndTooltipContent; + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent; + renderLegendDetails(): ReactElement | null; + showAtZoomLevel(zoom: number): boolean; + getMinZoom(): number; + getMaxZoom(): number; + getMinSourceZoom(): number; + getAlpha(): number; + getQuery(): Query | null; + getStyle(): IStyle; + getStyleForEditing(): IStyle; + getCurrentStyle(): IStyle; + getImmutableSourceProperties(): Promise; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + isLayerLoading(): boolean; + hasErrors(): boolean; + getErrors(): string; + toLayerDescriptor(): LayerDescriptor; + getMbLayerIds(): string[]; + ownsMbLayerId(mbLayerId: string): boolean; + ownsMbSourceId(mbSourceId: string): boolean; + canShowTooltip(): boolean; + syncLayerWithMB(mbMap: unknown): void; + getLayerTypeIconName(): string; + isDataLoaded(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getType(): string | undefined; + isVisible(): boolean; + cloneDescriptor(): Promise; + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null; +} +export type Footnote = { + icon: ReactElement; + message?: string | null; +}; +export type IconAndTooltipContent = { + icon?: ReactElement | null; + tooltipContent?: string | null; + footnotes?: Footnote[] | null; + areResultsTrimmed?: boolean; +}; + +export interface ILayerArguments { + layerDescriptor: LayerDescriptor; + source: ISource; + style: IStyle; +} + +export class AbstractLayer implements ILayer { + protected readonly _descriptor: LayerDescriptor; + protected readonly _source: ISource; + protected readonly _style: IStyle; + protected readonly _dataRequests: DataRequest[]; + + static createDescriptor(options: Partial): LayerDescriptor { + return { + ...options, + sourceDescriptor: options.sourceDescriptor ? options.sourceDescriptor : null, + __dataRequests: _.get(options, '__dataRequests', []), + id: _.get(options, 'id', uuid()), + label: options.label && options.label.length > 0 ? options.label : null, + minZoom: _.get(options, 'minZoom', MIN_ZOOM), + maxZoom: _.get(options, 'maxZoom', MAX_ZOOM), + alpha: _.get(options, 'alpha', 0.75), + visible: _.get(options, 'visible', true), + style: _.get(options, 'style', null), + }; + } + + destroy() { + if (this._source) { + this._source.destroy(); + } + } + + constructor({ layerDescriptor, source, style }: ILayerArguments) { + this._descriptor = AbstractLayer.createDescriptor(layerDescriptor); + this._source = source; + this._style = style; + if (this._descriptor.__dataRequests) { + this._dataRequests = this._descriptor.__dataRequests.map( + dataRequest => new DataRequest(dataRequest) + ); + } else { + this._dataRequests = []; + } + } + + static getBoundDataForSource(mbMap: unknown, sourceId: string): FeatureCollection { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + return mbStyle.sources[sourceId].data; + } + + async cloneDescriptor(): Promise { + // @ts-ignore + const clonedDescriptor = copyPersistentState(this._descriptor); + // layer id is uuid used to track styles/layers in mapbox + clonedDescriptor.id = uuid(); + const displayName = await this.getDisplayName(); + clonedDescriptor.label = `Clone of ${displayName}`; + clonedDescriptor.sourceDescriptor = this.getSource().cloneDescriptor(); + + // todo: remove this + // This should not be in AbstractLayer. It relies on knowledge of VectorLayerDescriptor + // @ts-ignore + if (clonedDescriptor.joins) { + // @ts-ignore + clonedDescriptor.joins.forEach(joinDescriptor => { + // right.id is uuid used to track requests in inspector + // @ts-ignore + joinDescriptor.right.id = uuid(); + }); + } + return clonedDescriptor; + } + + makeMbLayerId(layerNameSuffix: string): string { + return `${this.getId()}${MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER}${layerNameSuffix}`; + } + + isJoinable(): boolean { + return this.getSource().isJoinable(); + } + + supportsElasticsearchFilters(): boolean { + return this.getSource().isESSource(); + } + + async supportsFitToBounds(): Promise { + return await this.getSource().supportsFitToBounds(); + } + + async getDisplayName(source?: ISource): Promise { + if (this._descriptor.label) { + return this._descriptor.label; + } + + const sourceDisplayName = source + ? await source.getDisplayName() + : await this.getSource().getDisplayName(); + return sourceDisplayName || `Layer ${this._descriptor.id}`; + } + + async getAttributions(): Promise { + if (!this.hasErrors()) { + return await this.getSource().getAttributions(); + } + return []; + } + + getStyleForEditing(): IStyle { + return this._style; + } + + getStyle() { + return this._style; + } + + getLabel(): string { + return this._descriptor.label ? this._descriptor.label : ''; + } + + getCustomIconAndTooltipContent(): IconAndTooltipContent { + return { + icon: , + }; + } + + getIconAndTooltipContent(zoomLevel: number, isUsingSearch: boolean): IconAndTooltipContent { + let icon; + let tooltipContent = null; + const footnotes = []; + if (this.hasErrors()) { + icon = ( + + ); + tooltipContent = this.getErrors(); + } else if (this.isLayerLoading()) { + icon = ; + } else if (!this.isVisible()) { + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.layerHiddenTooltip', { + defaultMessage: `Layer is hidden.`, + }); + } else if (!this.showAtZoomLevel(zoomLevel)) { + const minZoom = this.getMinZoom(); + const maxZoom = this.getMaxZoom(); + icon = ; + tooltipContent = i18n.translate('xpack.maps.layer.zoomFeedbackTooltip', { + defaultMessage: `Layer is visible between zoom levels {minZoom} and {maxZoom}.`, + values: { minZoom, maxZoom }, + }); + } else { + const customIconAndTooltipContent = this.getCustomIconAndTooltipContent(); + if (customIconAndTooltipContent) { + icon = customIconAndTooltipContent.icon; + if (!customIconAndTooltipContent.areResultsTrimmed) { + tooltipContent = customIconAndTooltipContent.tooltipContent; + } else { + footnotes.push({ + icon: , + message: customIconAndTooltipContent.tooltipContent, + }); + } + } + + if (isUsingSearch && this.getQueryableIndexPatternIds().length) { + footnotes.push({ + icon: , + message: i18n.translate('xpack.maps.layer.isUsingSearchMsg', { + defaultMessage: 'Results narrowed by search bar', + }), + }); + } + } + + return { + icon, + tooltipContent, + footnotes, + }; + } + + async hasLegendDetails(): Promise { + return false; + } + + renderLegendDetails(): ReactElement | null { + return null; + } + + getId(): string { + return this._descriptor.id; + } + + getSource(): ISource { + return this._source; + } + + getSourceForEditing(): ISource { + return this._source; + } + + isVisible(): boolean { + return !!this._descriptor.visible; + } + + showAtZoomLevel(zoom: number): boolean { + return zoom >= this.getMinZoom() && zoom <= this.getMaxZoom(); + } + + getMinZoom(): number { + return typeof this._descriptor.minZoom === 'number' ? this._descriptor.minZoom : MIN_ZOOM; + } + + getMaxZoom(): number { + return typeof this._descriptor.maxZoom === 'number' ? this._descriptor.maxZoom : MAX_ZOOM; + } + + getMinSourceZoom(): number { + return this._source.getMinZoom(); + } + + _requiresPrevSourceCleanup(mbMap: unknown) { + return false; + } + + _removeStaleMbSourcesAndLayers(mbMap: unknown) { + if (this._requiresPrevSourceCleanup(mbMap)) { + // @ts-ignore + const mbStyle = mbMap.getStyle(); + // @ts-ignore + mbStyle.layers.forEach(mbLayer => { + // @ts-ignore + if (this.ownsMbLayerId(mbLayer.id)) { + // @ts-ignore + mbMap.removeLayer(mbLayer.id); + } + }); + // @ts-ignore + Object.keys(mbStyle.sources).some(mbSourceId => { + // @ts-ignore + if (this.ownsMbSourceId(mbSourceId)) { + // @ts-ignore + mbMap.removeSource(mbSourceId); + } + }); + } + } + + getAlpha(): number { + return typeof this._descriptor.alpha === 'number' ? this._descriptor.alpha : 1; + } + + getQuery(): Query | null { + return this._descriptor.query ? this._descriptor.query : null; + } + + getCurrentStyle(): IStyle { + return this._style; + } + + async getImmutableSourceProperties() { + const source = this.getSource(); + return await source.getImmutableProperties(); + } + + renderSourceSettingsEditor({ onChange }: { onChange: () => void }) { + const source = this.getSourceForEditing(); + return source.renderSourceSettingsEditor({ onChange }); + } + + getPrevRequestToken(dataId: string): symbol | undefined { + const prevDataRequest = this.getDataRequest(dataId); + if (!prevDataRequest) { + return; + } + + return prevDataRequest.getRequestToken(); + } + + getInFlightRequestTokens(): symbol[] { + if (!this._dataRequests) { + return []; + } + + const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); + + // Compact removes all the undefineds + // @ts-ignore + return _.compact(requestTokens); + } + + getSourceDataRequest(): DataRequest | undefined { + return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); + } + + getDataRequest(id: string): DataRequest | undefined { + return this._dataRequests.find(dataRequest => dataRequest.getDataId() === id); + } + + isLayerLoading(): boolean { + return this._dataRequests.some(dataRequest => dataRequest.isLoading()); + } + + hasErrors(): boolean { + return _.get(this._descriptor, '__isInErrorState', false); + } + + getErrors(): string { + return this.hasErrors() && this._descriptor.__errorMessage + ? this._descriptor.__errorMessage + : ''; + } + + toLayerDescriptor(): LayerDescriptor { + return this._descriptor; + } + + async syncData(syncContext: SyncContext) { + // no-op by default + } + + getMbLayerIds(): string[] { + throw new Error('Should implement AbstractLayer#getMbLayerIds'); + } + + ownsMbLayerId(layerId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbLayerId'); + } + + ownsMbSourceId(sourceId: string): boolean { + throw new Error('Should implement AbstractLayer#ownsMbSourceId'); + } + + canShowTooltip() { + return false; + } + + syncLayerWithMB(mbMap: unknown) { + throw new Error('Should implement AbstractLayer#syncLayerWithMB'); + } + + getLayerTypeIconName(): string { + throw new Error('should implement Layer#getLayerTypeIconName'); + } + + isDataLoaded(): boolean { + const sourceDataRequest = this.getSourceDataRequest(); + return sourceDataRequest ? sourceDataRequest.hasData() : false; + } + + async getBounds(mapFilters: MapFilters): Promise { + return { + minLon: -180, + maxLon: 180, + minLat: -89, + maxLat: 89, + }; + } + + renderStyleEditor({ + onStyleDescriptorChange, + }: { + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null { + const style = this.getStyleForEditing(); + if (!style) { + return null; + } + return style.renderEditor({ layer: this, onStyleDescriptorChange }); + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + syncVisibilityWithMb(mbMap: unknown, mbLayerId: string) { + // @ts-ignore + mbMap.setLayoutProperty(mbLayerId, 'visibility', this.isVisible() ? 'visible' : 'none'); + } + + getType(): string | undefined { + return this._descriptor.type; + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js index 137513ad7c612..36f898f723757 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js +++ b/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js @@ -21,8 +21,6 @@ import { registerSource } from '../source_registry'; export class GeojsonFileSource extends AbstractVectorSource { static type = SOURCE_TYPES.GEOJSON_FILE; - static isIndexingSource = true; - static createDescriptor(geoJson, name) { // Wrap feature as feature collection if needed let featureCollection; @@ -70,7 +68,7 @@ export class GeojsonFileSource extends AbstractVectorSource { } shouldBeIndexed() { - return GeojsonFileSource.isIndexingSource; + return true; } } diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts index 96347c444dd5b..51ee15e7ea5af 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts @@ -6,7 +6,7 @@ import { AbstractESAggSource } from '../es_agg_source'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; -import { GRID_RESOLUTION, RENDER_AS } from '../../../../common/constants'; +import { GRID_RESOLUTION } from '../../../../common/constants'; export class ESGeoGridSource extends AbstractESAggSource { static createDescriptor({ @@ -14,12 +14,7 @@ export class ESGeoGridSource extends AbstractESAggSource { geoField, requestType, resolution, - }: { - indexPatternId: string; - geoField: string; - requestType: RENDER_AS; - resolution?: GRID_RESOLUTION; - }): ESGeoGridSourceDescriptor; + }: Partial): ESGeoGridSourceDescriptor; constructor(sourceDescriptor: ESGeoGridSourceDescriptor, inspectorAdapters: unknown); diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts index 092dc3bf0d5a8..3b41ae6bfd86b 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts @@ -8,6 +8,8 @@ import { AbstractVectorSource } from '../vector_source'; import { IVectorSource } from '../vector_source'; import { IndexPattern, SearchSource } from '../../../../../../../src/plugins/data/public'; import { VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; export interface IESSource extends IVectorSource { getId(): string; @@ -20,6 +22,13 @@ export interface IESSource extends IVectorSource { limit: number, initialSearchContext?: object ): Promise; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise; } export class AbstractESSource extends AbstractVectorSource implements IESSource { @@ -33,4 +42,11 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource limit: number, initialSearchContext?: object ): Promise; + loadStylePropsMeta( + layerName: string, + style: VectorStyle, + dynamicStyleProps: IDynamicStyleProperty[], + registerCancelCallback: (requestToken: symbol, callback: () => void) => void, + searchFilters: VectorSourceRequestMeta + ): Promise; } diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts index 701bd5e2c8b5e..248ca2b9212b4 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts +++ b/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts @@ -9,4 +9,5 @@ import { IESAggSource } from '../es_agg_source'; export interface IESTermSource extends IESAggSource { getTermField(): IField; + hasCompleteConfig(): boolean; } diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts index 0bfda6be72203..a73cfbdc0d043 100644 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts @@ -15,9 +15,9 @@ import { IField } from '../../fields/field'; import { registerSource } from '../source_registry'; import { getDataSourceLabel, getUrlLabel } from '../../../../common/i18n_getters'; import { - LayerDescriptor, MapExtent, TiledSingleLayerVectorSourceDescriptor, + VectorLayerDescriptor, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; @@ -66,12 +66,15 @@ export class MVTSingleLayerVectorSource extends AbstractSource return []; } - createDefaultLayer(options: LayerDescriptor): TiledVectorLayer { - const layerDescriptor = { + createDefaultLayer(options?: Partial): TiledVectorLayer { + const layerDescriptor: Partial = { sourceDescriptor: this._descriptor, ...options, }; - const normalizedLayerDescriptor = TiledVectorLayer.createDescriptor(layerDescriptor, []); + const normalizedLayerDescriptor: VectorLayerDescriptor = TiledVectorLayer.createDescriptor( + layerDescriptor, + [] + ); const vectorLayerArguments: VectorLayerArguments = { layerDescriptor: normalizedLayerDescriptor, source: this, diff --git a/x-pack/plugins/maps/public/layers/sources/source.d.ts b/x-pack/plugins/maps/public/layers/sources/source.d.ts deleted file mode 100644 index 5a01da02adaae..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.d.ts +++ /dev/null @@ -1,56 +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. - */ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ - -import { AbstractSourceDescriptor, LayerDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; - -export type ImmutableSourceProperty = { - label: string; - value: string; -}; - -export type Attribution = { - url: string; - label: string; -}; - -export interface ISource { - createDefaultLayer(options?: LayerDescriptor): ILayer; - destroy(): void; - getDisplayName(): Promise; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise; - isTimeAware(): Promise; - getImmutableProperties(): Promise; - getAttributions(): Promise; - getMinZoom(): number; - getMaxZoom(): number; -} - -export class AbstractSource implements ISource { - readonly _descriptor: AbstractSourceDescriptor; - constructor(sourceDescriptor: AbstractSourceDescriptor, inspectorAdapters?: object); - - destroy(): void; - createDefaultLayer(options?: LayerDescriptor, mapColors?: string[]): ILayer; - getDisplayName(): Promise; - getInspectorAdapters(): object; - isFieldAware(): boolean; - isFilterByMapBounds(): boolean; - isGeoGridPrecisionAware(): boolean; - isQueryAware(): boolean; - isRefreshTimerAware(): Promise; - isTimeAware(): Promise; - getImmutableProperties(): Promise; - getAttributions(): Promise; - getMinZoom(): number; - getMaxZoom(): number; -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.js b/x-pack/plugins/maps/public/layers/sources/source.js deleted file mode 100644 index fd93daf249b26..0000000000000 --- a/x-pack/plugins/maps/public/layers/sources/source.js +++ /dev/null @@ -1,159 +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 { copyPersistentState } from '../../reducers/util'; -import { MIN_ZOOM, MAX_ZOOM } from '../../../common/constants'; - -export class AbstractSource { - static isIndexingSource = false; - - static renderEditor() { - throw new Error('Must implement Source.renderEditor'); - } - - static createDescriptor() { - throw new Error('Must implement Source.createDescriptor'); - } - - constructor(descriptor, inspectorAdapters) { - this._descriptor = descriptor; - this._inspectorAdapters = inspectorAdapters; - } - - destroy() {} - - cloneDescriptor() { - return copyPersistentState(this._descriptor); - } - - async supportsFitToBounds() { - return true; - } - - /** - * return list of immutable source properties. - * Immutable source properties are properties that can not be edited by the user. - */ - async getImmutableProperties() { - return []; - } - - getInspectorAdapters() { - return this._inspectorAdapters; - } - - _createDefaultLayerDescriptor() { - throw new Error(`Source#createDefaultLayerDescriptor not implemented`); - } - - createDefaultLayer() { - throw new Error(`Source#createDefaultLayer not implemented`); - } - - async getDisplayName() { - console.warn('Source should implement Source#getDisplayName'); - return ''; - } - - /** - * return attribution for this layer as array of objects with url and label property. - * e.g. [{ url: 'example.com', label: 'foobar' }] - * @return {Promise} - */ - async getAttributions() { - return []; - } - - isFieldAware() { - return false; - } - - isRefreshTimerAware() { - return false; - } - - isGeoGridPrecisionAware() { - return false; - } - - async isTimeAware() { - return false; - } - - getFieldNames() { - return []; - } - - hasCompleteConfig() { - throw new Error(`Source#hasCompleteConfig not implemented`); - } - - renderSourceSettingsEditor() { - return null; - } - - getApplyGlobalQuery() { - return !!this._descriptor.applyGlobalQuery; - } - - getIndexPatternIds() { - return []; - } - - getQueryableIndexPatternIds() { - return []; - } - - isFilterByMapBounds() { - return false; - } - - isQueryAware() { - return false; - } - - getGeoGridPrecision() { - return 0; - } - - isJoinable() { - return false; - } - - shouldBeIndexed() { - return AbstractSource.isIndexingSource; - } - - isESSource() { - return false; - } - - // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes - async getPreIndexedShape(/* properties */) { - return null; - } - - // Returns function used to format value - async createFieldFormatter(/* field */) { - return null; - } - - async loadStylePropsMeta() { - throw new Error(`Source#loadStylePropsMeta not implemented`); - } - - async getValueSuggestions(/* field, query */) { - return []; - } - - getMinZoom() { - return MIN_ZOOM; - } - - getMaxZoom() { - return MAX_ZOOM; - } -} diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/layers/sources/source.ts new file mode 100644 index 0000000000000..1cd84010159ab --- /dev/null +++ b/x-pack/plugins/maps/public/layers/sources/source.ts @@ -0,0 +1,195 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/consistent-type-definitions */ + +import { ReactElement } from 'react'; + +import { Adapters } from 'src/plugins/inspector/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +// @ts-ignore +import { copyPersistentState } from '../../reducers/util'; + +import { LayerDescriptor, SourceDescriptor } from '../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IField } from '../fields/field'; +import { MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; + +export type ImmutableSourceProperty = { + label: string; + value: string; +}; + +export type Attribution = { + url: string; + label: string; +}; + +export type PreIndexedShape = { + index: string; + id: string | number; + path: string; +}; + +export type FieldFormatter = (value: string | number | null | undefined | boolean) => string; + +export interface ISource { + createDefaultLayer(options?: Partial): ILayer; + destroy(): void; + getDisplayName(): Promise; + getInspectorAdapters(): Adapters | undefined; + isFieldAware(): boolean; + isFilterByMapBounds(): boolean; + isGeoGridPrecisionAware(): boolean; + isQueryAware(): boolean; + isRefreshTimerAware(): boolean; + isTimeAware(): Promise; + getImmutableProperties(): Promise; + getAttributions(): Promise; + isESSource(): boolean; + renderSourceSettingsEditor({ onChange }: { onChange: () => void }): ReactElement | null; + supportsFitToBounds(): Promise; + isJoinable(): boolean; + cloneDescriptor(): SourceDescriptor; + getFieldNames(): string[]; + getApplyGlobalQuery(): boolean; + getIndexPatternIds(): string[]; + getQueryableIndexPatternIds(): string[]; + getGeoGridPrecision(zoom: number): number; + shouldBeIndexed(): boolean; + getPreIndexedShape(): Promise; + createFieldFormatter(field: IField): Promise; + getValueSuggestions(field: IField, query: string): Promise; + getMinZoom(): number; + getMaxZoom(): number; +} + +export class AbstractSource implements ISource { + readonly _descriptor: SourceDescriptor; + readonly _inspectorAdapters?: Adapters | undefined; + + constructor(descriptor: SourceDescriptor, inspectorAdapters?: Adapters) { + this._descriptor = descriptor; + this._inspectorAdapters = inspectorAdapters; + } + + destroy(): void {} + + cloneDescriptor(): SourceDescriptor { + // @ts-ignore + return copyPersistentState(this._descriptor); + } + + async supportsFitToBounds(): Promise { + return true; + } + + /** + * return list of immutable source properties. + * Immutable source properties are properties that can not be edited by the user. + */ + async getImmutableProperties(): Promise { + return []; + } + + getInspectorAdapters(): Adapters | undefined { + return this._inspectorAdapters; + } + + createDefaultLayer(options?: Partial): ILayer { + throw new Error(`Source#createDefaultLayer not implemented`); + } + + async getDisplayName(): Promise { + return ''; + } + + async getAttributions(): Promise { + return []; + } + + isFieldAware(): boolean { + return false; + } + + isRefreshTimerAware(): boolean { + return false; + } + + isGeoGridPrecisionAware(): boolean { + return false; + } + + isQueryAware(): boolean { + return false; + } + + getFieldNames(): string[] { + return []; + } + + renderSourceSettingsEditor() { + return null; + } + + getApplyGlobalQuery(): boolean { + return !!this._descriptor.applyGlobalQuery; + } + + getIndexPatternIds(): string[] { + return []; + } + + getQueryableIndexPatternIds(): string[] { + return []; + } + + getGeoGridPrecision(zoom: number): number { + return 0; + } + + isJoinable(): boolean { + return false; + } + + shouldBeIndexed(): boolean { + return false; + } + + isESSource(): boolean { + return false; + } + + // Returns geo_shape indexed_shape context for spatial quering by pre-indexed shapes + async getPreIndexedShape(/* properties */): Promise { + return null; + } + + // Returns function used to format value + async createFieldFormatter(field: IField): Promise { + return null; + } + + async getValueSuggestions(field: IField, query: string): Promise { + return []; + } + + async isTimeAware(): Promise { + return false; + } + + isFilterByMapBounds(): boolean { + return false; + } + + getMinZoom() { + return MIN_ZOOM; + } + + getMaxZoom() { + return MAX_ZOOM; + } +} diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts index 8b64480f92961..77f8d88a8c0ab 100644 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts +++ b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts @@ -13,6 +13,7 @@ import { AbstractTMSSource } from '../tms_source'; import { LayerDescriptor, XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; import { Attribution, ImmutableSourceProperty } from '../source'; import { XYZTMSSourceConfig } from './xyz_tms_editor'; +import { ILayer } from '../../layer'; export const sourceTitle = i18n.translate('xpack.maps.source.ems_xyzTitle', { defaultMessage: 'Tile Map Service', @@ -48,7 +49,7 @@ export class XYZTMSSource extends AbstractTMSSource { ]; } - createDefaultLayer(options?: LayerDescriptor): TileLayer { + createDefaultLayer(options?: LayerDescriptor): ILayer { const layerDescriptor: LayerDescriptor = TileLayer.createDescriptor({ sourceDescriptor: this._descriptor, ...options, diff --git a/x-pack/plugins/maps/public/layers/styles/abstract_style.js b/x-pack/plugins/maps/public/layers/styles/abstract_style.js deleted file mode 100644 index 3e7a3dbf7ed20..0000000000000 --- a/x-pack/plugins/maps/public/layers/styles/abstract_style.js +++ /dev/null @@ -1,29 +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. - */ - -export class AbstractStyle { - getDescriptorWithMissingStylePropsRemoved(/* nextOrdinalFields */) { - return { - hasChanges: false, - }; - } - - async pluckStyleMetaFromSourceDataRequest(/* sourceDataRequest */) { - return {}; - } - - getDescriptor() { - return this._descriptor; - } - - renderEditor(/* { layer, onStyleDescriptorChange } */) { - return null; - } - - getSourceFieldNames() { - return []; - } -} diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js index d769fe0da9ec2..1fa24943c5e51 100644 --- a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js +++ b/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js @@ -5,7 +5,7 @@ */ import React from 'react'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { HeatmapStyleEditor } from './components/heatmap_style_editor'; import { HeatmapLegend } from './components/legend/heatmap_legend'; import { DEFAULT_HEATMAP_COLOR_RAMP_NAME } from './components/heatmap_constants'; diff --git a/x-pack/plugins/maps/public/layers/styles/style.ts b/x-pack/plugins/maps/public/layers/styles/style.ts new file mode 100644 index 0000000000000..38fdc36904412 --- /dev/null +++ b/x-pack/plugins/maps/public/layers/styles/style.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactElement } from 'react'; +import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IField } from '../fields/field'; +import { DataRequest } from '../util/data_request'; + +export interface IStyle { + getDescriptor(): StyleDescriptor | null; + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor }; + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor; + renderEditor({ + layer, + onStyleDescriptorChange, + }: { + layer: ILayer; + onStyleDescriptorChange: (styleDescriptor: StyleDescriptor) => void; + }): ReactElement | null; + getSourceFieldNames(): string[]; +} + +export class AbstractStyle implements IStyle { + readonly _descriptor: StyleDescriptor | null; + + constructor(descriptor: StyleDescriptor | null) { + this._descriptor = descriptor; + } + + getDescriptorWithMissingStylePropsRemoved( + nextFields: IField[] + ): { hasChanges: boolean; nextStyleDescriptor?: StyleDescriptor } { + return { + hasChanges: false, + }; + } + + pluckStyleMetaFromSourceDataRequest(sourceDataRequest: DataRequest): StyleMetaDescriptor { + return { fieldMeta: {} }; + } + + getDescriptor(): StyleDescriptor | null { + return this._descriptor; + } + + renderEditor(/* { layer, onStyleDescriptorChange } */) { + return null; + } + + getSourceFieldNames(): string[] { + return []; + } +} diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts similarity index 51% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts rename to x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts index 7a38d024d99a2..f658d0821edf2 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/index.ts +++ b/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts @@ -4,8 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PluginInitializerContext } from 'src/core/server'; -import { CrossClusterReplicationServerPlugin } from './plugin'; +import { AbstractStyle } from '../style'; +import { LAYER_STYLE_TYPE } from '../../../../common/constants'; -export const plugin = (ctx: PluginInitializerContext) => - new CrossClusterReplicationServerPlugin(ctx); +export class TileStyle extends AbstractStyle { + constructor() { + super({ + type: LAYER_STYLE_TYPE.TILE, + }); + } +} diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts index e010d5ac7d7a3..762322b8e09f9 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts @@ -7,24 +7,23 @@ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; import { IVectorLayer } from '../../vector_layer'; import { IVectorSource } from '../../sources/vector_source'; +import { AbstractStyle, IStyle } from '../style'; import { VectorStyleDescriptor, VectorStylePropertiesDescriptor, } from '../../../../common/descriptor_types'; -export interface IVectorStyle { +export interface IVectorStyle extends IStyle { getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; getSourceFieldNames(): string[]; } -export class VectorStyle implements IVectorStyle { +export class VectorStyle extends AbstractStyle implements IVectorStyle { static createDescriptor(properties: VectorStylePropertiesDescriptor): VectorStyleDescriptor; static createDefaultStyleProperties(mapColors: string[]): VectorStylePropertiesDescriptor; constructor(descriptor: VectorStyleDescriptor, source: IVectorSource, layer: IVectorLayer); getSourceFieldNames(): string[]; getAllStyleProperties(): IStyleProperty[]; - getDescriptor(): VectorStyleDescriptor; getDynamicPropertiesArray(): IDynamicStyleProperty[]; } diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js index b044c98d44d41..5a4edd9c93a05 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js +++ b/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js @@ -8,7 +8,7 @@ import _ from 'lodash'; import React from 'react'; import { VectorStyleEditor } from './components/vector_style_editor'; import { getDefaultProperties, LINE_STYLES, POLYGON_STYLES } from './vector_style_defaults'; -import { AbstractStyle } from '../abstract_style'; +import { AbstractStyle } from '../style'; import { GEO_JSON_TYPE, FIELD_ORIGIN, @@ -60,6 +60,7 @@ export class VectorStyle extends AbstractStyle { constructor(descriptor = {}, source, layer) { super(); + descriptor = descriptor === null ? {} : descriptor; this._source = source; this._layer = layer; this._descriptor = { diff --git a/x-pack/plugins/maps/public/layers/tile_layer.d.ts b/x-pack/plugins/maps/public/layers/tile_layer.d.ts index 53e8c388ee4c2..8a1ef0f172717 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.d.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractLayer, ILayerArguments } from './layer'; +import { AbstractLayer } from './layer'; import { ITMSSource } from './sources/tms_source'; import { LayerDescriptor } from '../../common/descriptor_types'; -interface ITileLayerArguments extends ILayerArguments { +interface ITileLayerArguments { source: ITMSSource; layerDescriptor: LayerDescriptor; } diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/layers/tile_layer.js index 2ac60e12d137a..baded3c287637 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/plugins/maps/public/layers/tile_layer.js @@ -6,7 +6,8 @@ import { AbstractLayer } from './layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; +import { TileStyle } from './styles/tile/tile_style'; export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; @@ -15,9 +16,14 @@ export class TileLayer extends AbstractLayer { const tileLayerDescriptor = super.createDescriptor(options, mapColors); tileLayerDescriptor.type = TileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } + constructor({ source, layerDescriptor }) { + super({ source, layerDescriptor, style: new TileStyle() }); + } + async syncData({ startLoading, stopLoading, onLoadError, dataFilters }) { if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) { return; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/layers/tile_layer.test.ts index f8c2fd9db60fa..a7e8be9fc4b46 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/layers/tile_layer.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TileLayer } from './tile_layer'; +// eslint-disable-next-line max-classes-per-file +import { ITileLayerArguments, TileLayer } from './tile_layer'; import { SOURCE_TYPES } from '../../common/constants'; import { XYZTMSSourceDescriptor } from '../../common/descriptor_types'; import { ITMSSource, AbstractTMSSource } from './sources/tms_source'; @@ -38,10 +39,13 @@ class MockTileSource extends AbstractTMSSource implements ITMSSource { describe('TileLayer', () => { it('should use display-label from source', async () => { const source = new MockTileSource(sourceDescriptor); - const layer: ILayer = new TileLayer({ + + const args: ITileLayerArguments = { source, layerDescriptor: { id: 'layerid', sourceDescriptor }, - }); + }; + + const layer: ILayer = new TileLayer(args); expect(await source.getDisplayName()).toEqual(await layer.getDisplayName()); }); diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx index c47cae5641e56..06c5ef579b221 100644 --- a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx @@ -20,7 +20,7 @@ export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; static createDescriptor( - descriptor: VectorLayerDescriptor, + descriptor: Partial, mapColors: string[] ): VectorLayerDescriptor { const layerDescriptor = super.createDescriptor(descriptor, mapColors); diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/layers/vector_layer.d.ts index 3d5b8054ff3fd..efc1f3011c687 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/layers/vector_layer.d.ts @@ -19,7 +19,7 @@ import { IVectorStyle } from './styles/vector/vector_style'; import { IField } from './fields/field'; import { SyncContext } from '../actions/map_actions'; -type VectorLayerArguments = { +export type VectorLayerArguments = { source: IVectorSource; joins?: IJoin[]; layerDescriptor: VectorLayerDescriptor; @@ -33,14 +33,12 @@ export interface IVectorLayer extends ILayer { } export class VectorLayer extends AbstractLayer implements IVectorLayer { + protected readonly _style: IVectorStyle; static createDescriptor( options: Partial, mapColors?: string[] ): VectorLayerDescriptor; - protected readonly _source: IVectorSource; - protected readonly _style: IVectorStyle; - constructor(options: VectorLayerArguments); getLayerTypeIconName(): string; getFields(): Promise; diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/layers/vector_layer.js index c5947a63587ea..17b7f8152d76d 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_layer.js @@ -484,6 +484,8 @@ export class VectorLayer extends AbstractLayer { try { startLoading(dataRequestId, requestToken, nextMeta); const layerName = await this.getDisplayName(source); + + //todo: cast source to ESSource when migrating to TS const styleMeta = await source.loadStylePropsMeta( layerName, style, diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/layers/vector_tile_layer.js index c620ec6c56dc3..fc7812a2c86c7 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/layers/vector_tile_layer.js @@ -6,7 +6,7 @@ import { TileLayer } from './tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; import { isRetina } from '../meta'; import { addSpriteSheetToMapFromImageData, @@ -28,6 +28,7 @@ export class VectorTileLayer extends TileLayer { const tileLayerDescriptor = super.createDescriptor(options); tileLayerDescriptor.type = VectorTileLayer.type; tileLayerDescriptor.alpha = _.get(options, 'alpha', 1); + tileLayerDescriptor.style = { type: LAYER_STYLE_TYPE.TILE }; return tileLayerDescriptor; } diff --git a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts similarity index 54% rename from x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts rename to x-pack/plugins/maps/public/reducers/default_map_settings.ts index 7f57c20c536e0..81622ea9581b0 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/server/np_ready/routes/types.ts +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -3,11 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { IRouter } from 'src/core/server'; -export interface RouteDependencies { - router: IRouter; - __LEGACY: { - server: any; +import { MAX_ZOOM, MIN_ZOOM } from '../../common/constants'; +import { MapSettings } from './map'; + +export function getDefaultMapSettings(): MapSettings { + return { + maxZoom: MAX_ZOOM, + minZoom: MIN_ZOOM, }; } diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 30271d4d5fa8b..af2d96eb75562 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -39,6 +39,11 @@ export type MapContext = { hideViewControl: boolean; }; +export type MapSettings = { + maxZoom: number; + minZoom: number; +}; + export type MapState = { ready: boolean; mapInitError?: string | null; @@ -49,4 +54,6 @@ export type MapState = { __transientLayerId: string | null; layerList: LayerDescriptor[]; waitingForMapReadyLayerList: LayerDescriptor[]; + settings: MapSettings; + __rollbackSettings: MapSettings | null; }; diff --git a/x-pack/plugins/maps/public/reducers/map.js b/x-pack/plugins/maps/public/reducers/map.js index 251a2304538ed..a76267bb7095e 100644 --- a/x-pack/plugins/maps/public/reducers/map.js +++ b/x-pack/plugins/maps/public/reducers/map.js @@ -46,8 +46,13 @@ import { HIDE_LAYER_CONTROL, HIDE_VIEW_CONTROL, SET_WAITING_FOR_READY_HIDDEN_LAYERS, + SET_MAP_SETTINGS, + ROLLBACK_MAP_SETTINGS, + TRACK_MAP_SETTINGS, + UPDATE_MAP_SETTING, } from '../actions/map_actions'; +import { getDefaultMapSettings } from './default_map_settings'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from './util'; import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; @@ -124,6 +129,8 @@ const INITIAL_STATE = { __transientLayerId: null, layerList: [], waitingForMapReadyLayerList: [], + settings: getDefaultMapSettings(), + __rollbackSettings: null, }; export function map(state = INITIAL_STATE, action) { @@ -179,6 +186,32 @@ export function map(state = INITIAL_STATE, action) { ...state, goto: null, }; + case SET_MAP_SETTINGS: + return { + ...state, + settings: { ...getDefaultMapSettings(), ...action.settings }, + }; + case ROLLBACK_MAP_SETTINGS: + return state.__rollbackSettings + ? { + ...state, + settings: { ...state.__rollbackSettings }, + __rollbackSettings: null, + } + : state; + case TRACK_MAP_SETTINGS: + return { + ...state, + __rollbackSettings: state.settings, + }; + case UPDATE_MAP_SETTING: + return { + ...state, + settings: { + ...(state.settings ? state.settings : {}), + [action.settingKey]: action.settingValue, + }, + }; case SET_LAYER_ERROR_STATUS: const { layerList } = state; const layerIdx = getLayerIndex(layerList, action.layerId); diff --git a/x-pack/plugins/maps/public/reducers/ui.ts b/x-pack/plugins/maps/public/reducers/ui.ts index f577618c74ffe..537ea7fc7b24b 100644 --- a/x-pack/plugins/maps/public/reducers/ui.ts +++ b/x-pack/plugins/maps/public/reducers/ui.ts @@ -22,6 +22,7 @@ export enum FLYOUT_STATE { NONE = 'NONE', LAYER_PANEL = 'LAYER_PANEL', ADD_LAYER_WIZARD = 'ADD_LAYER_WIZARD', + MAP_SETTINGS_PANEL = 'MAP_SETTINGS_PANEL', } export enum INDEXING_STAGE { 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 32579d036590e..fed344b7744fe 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -6,8 +6,8 @@ import { AnyAction } from 'redux'; import { MapCenter } from '../../common/descriptor_types'; - import { MapStoreState } from '../reducers/store'; +import { MapSettings } from '../reducers/map'; export function getHiddenLayerIds(state: MapStoreState): string[]; @@ -16,3 +16,7 @@ export function getMapZoom(state: MapStoreState): number; export function getMapCenter(state: MapStoreState): MapCenter; export function getQueryableUniqueIndexPatternIds(state: MapStoreState): string[]; + +export function getMapSettings(state: MapStoreState): MapSettings; + +export function hasMapSettingsChanges(state: MapStoreState): boolean; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index c7073efa96cd5..20d1f7e08c910 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -64,6 +64,18 @@ function createSourceInstance(sourceDescriptor, inspectorAdapters) { return new source.ConstructorFunction(sourceDescriptor, inspectorAdapters); } +export const getMapSettings = ({ map }) => map.settings; + +const getRollbackMapSettings = ({ map }) => map.__rollbackSettings; + +export const hasMapSettingsChanges = createSelector( + getMapSettings, + getRollbackMapSettings, + (settings, rollbackSettings) => { + return rollbackSettings ? !_.isEqual(settings, rollbackSettings) : false; + } +); + export const getOpenTooltips = ({ map }) => { return map && map.openTooltips ? map.openTooltips : []; }; diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index dfe8ab31f972c..3a3ec6ac799d2 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -16,8 +16,33 @@ import { CLUSTER_ALERTS_ADDRESS_CONFIG_KEY } from '../common/constants'; * major version! * @return {Array} array of rename operations and callback function for rename logging */ -export const deprecations = ({ rename }: ConfigDeprecationFactory): ConfigDeprecation[] => { +export const deprecations = ({ + rename, + renameFromRoot, +}: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ + // This order matters. The "blanket rename" needs to happen at the end + renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), + renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), + renameFromRoot( + 'xpack.monitoring.show_license_expiration', + 'monitoring.ui.show_license_expiration' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.elasticsearch.enabled', + 'monitoring.ui.container.elasticsearch.enabled' + ), + renameFromRoot( + 'xpack.monitoring.ui.container.logstash.enabled', + 'monitoring.ui.container.logstash.enabled' + ), + renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'), + renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), + renameFromRoot( + 'xpack.monitoring.elasticsearch.logFetchCount', + 'monitoring.ui.elasticsearch.logFetchCount' + ), + renameFromRoot('xpack.monitoring', 'monitoring'), (config, fromPath, logger) => { const clusterAlertsEnabled = get(config, 'cluster_alerts.enabled'); const emailNotificationsEnabled = diff --git a/x-pack/plugins/remote_clusters/public/index.ts b/x-pack/plugins/remote_clusters/public/index.ts index 6ba021b157c3e..127ec2a670645 100644 --- a/x-pack/plugins/remote_clusters/public/index.ts +++ b/x-pack/plugins/remote_clusters/public/index.ts @@ -3,8 +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. */ + import { PluginInitializerContext } from 'kibana/public'; import { RemoteClustersUIPlugin } from './plugin'; +export { RemoteClustersPluginSetup } from './plugin'; + export const plugin = (initializerContext: PluginInitializerContext) => new RemoteClustersUIPlugin(initializerContext); diff --git a/x-pack/plugins/remote_clusters/public/plugin.ts b/x-pack/plugins/remote_clusters/public/plugin.ts index d110c461c1e3f..22f98e94748d8 100644 --- a/x-pack/plugins/remote_clusters/public/plugin.ts +++ b/x-pack/plugins/remote_clusters/public/plugin.ts @@ -14,7 +14,12 @@ import { init as initNotification } from './application/services/notification'; import { init as initRedirect } from './application/services/redirect'; import { Dependencies, ClientConfigType } from './types'; -export class RemoteClustersUIPlugin implements Plugin { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersUIPlugin + implements Plugin { constructor(private readonly initializerContext: PluginInitializerContext) {} setup( @@ -55,6 +60,10 @@ export class RemoteClustersUIPlugin implements Plugin new RemoteClustersServerPlugin(ctx); diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index fca4a5dbc5f94..a7ca30a6bf96d 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/server'; import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; @@ -18,19 +19,26 @@ import { registerDeleteRoute, } from './routes/api'; -export class RemoteClustersServerPlugin implements Plugin { +export interface RemoteClustersPluginSetup { + isUiEnabled: boolean; +} + +export class RemoteClustersServerPlugin + implements Plugin { licenseStatus: LicenseStatus; log: Logger; - config: Observable; + config$: Observable; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); - this.config = config.create(); + this.config$ = config.create(); this.licenseStatus = { valid: false }; } async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { const router = http.createRouter(); + const config = await this.config$.pipe(first()).toPromise(); + const routeDependencies: RouteDependencies = { router, getLicenseStatus: () => this.licenseStatus, @@ -64,6 +72,10 @@ export class RemoteClustersServerPlugin implements Plugin } } }); + + return { + isUiEnabled: config.ui.enabled, + }; } start() {} diff --git a/x-pack/plugins/reporting/common/types.d.ts b/x-pack/plugins/reporting/common/types.d.ts new file mode 100644 index 0000000000000..34f0bc9ac8a36 --- /dev/null +++ b/x-pack/plugins/reporting/common/types.d.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ConfigType } from '../server/config'; diff --git a/x-pack/plugins/reporting/constants.ts b/x-pack/plugins/reporting/constants.ts index 5756d29face12..8079c5b1d9887 100644 --- a/x-pack/plugins/reporting/constants.ts +++ b/x-pack/plugins/reporting/constants.ts @@ -7,13 +7,6 @@ export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; -export const JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG = { - jobCompletionNotifier: { - interval: 10000, - intervalErrorMultiplier: 5, - }, -}; - // Routes export const API_BASE_URL = '/api/reporting'; export const API_LIST_URL = `${API_BASE_URL}/jobs`; diff --git a/x-pack/plugins/reporting/index.d.ts b/x-pack/plugins/reporting/index.d.ts index 26d661e29bd94..77faf837e6505 100644 --- a/x-pack/plugins/reporting/index.d.ts +++ b/x-pack/plugins/reporting/index.d.ts @@ -4,15 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - CoreSetup, - CoreStart, - HttpSetup, - Plugin, - PluginInitializerContext, - NotificationsStart, -} from '../../../src/core/public'; - export type JobId = string; export type JobStatus = | 'completed' @@ -21,9 +12,6 @@ export type JobStatus = | 'processing' | 'failed'; -export type HttpService = HttpSetup; -export type NotificationsService = NotificationsStart; - export interface SourceJob { _id: JobId; _source: { diff --git a/x-pack/plugins/reporting/public/components/report_listing.test.tsx b/x-pack/plugins/reporting/public/components/report_listing.test.tsx index 380a3b3295b9f..787279e6caf9b 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.test.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.test.tsx @@ -47,12 +47,24 @@ const toasts = { addDanger: jest.fn(), } as any; +const mockPollConfig = { + jobCompletionNotifier: { + interval: 5000, + intervalErrorMultiplier: 3, + }, + jobsRefresh: { + interval: 5000, + intervalErrorMultiplier: 3, + }, +}; + describe('ReportListing', () => { it('Report job listing with some items', () => { const wrapper = mountWithIntl( @@ -74,6 +86,7 @@ describe('ReportListing', () => { } + pollConfig={mockPollConfig} redirect={jest.fn()} toasts={toasts} /> diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 885e9577471a0..d8f9b7d37cfbf 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -16,14 +16,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import { get } from 'lodash'; import moment from 'moment'; -import { Component, Fragment, default as React } from 'react'; +import { Component, default as React, Fragment } from 'react'; import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; -import { JobStatuses, JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG } from '../../constants'; +import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; +import { ClientConfigType } from '../plugin'; import { ReportDeleteButton, ReportDownloadButton, @@ -53,6 +54,7 @@ export interface Props { intl: InjectedIntl; apiClient: ReportingAPIClient; license$: LicensingPluginSetup['license$']; + pollConfig: ClientConfigType['poll']; redirect: ApplicationStart['navigateToApp']; toasts: ToastsSetup; } @@ -167,12 +169,10 @@ class ReportListingUi extends Component { functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.interval, + pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG.jobCompletionNotifier.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index 3c121f1712685..eed6d5dd141e7 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; import * as Rx from 'rxjs'; import { catchError, map } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { NotificationsSetup } from 'src/core/public'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JOB_STATUS_COMPLETED, JOB_STATUS_FAILED, JOB_STATUS_WARNINGS, } from '../../constants'; - -import { - JobId, - JobSummary, - JobStatusBuckets, - NotificationsService, - SourceJob, -} from '../../index.d'; - +import { JobId, JobStatusBuckets, JobSummary, SourceJob } from '../../index.d'; import { - getSuccessToast, getFailureToast, + getGeneralErrorToast, + getSuccessToast, getWarningFormulasToast, getWarningMaxSizeToast, - getGeneralErrorToast, } from '../components'; import { ReportingAPIClient } from './reporting_api_client'; @@ -47,7 +40,7 @@ function summarizeJob(src: SourceJob): JobSummary { } export class ReportingNotifierStreamHandler { - constructor(private notifications: NotificationsService, private apiClient: ReportingAPIClient) {} + constructor(private notifications: NotificationsSetup, private apiClient: ReportingAPIClient) {} /* * Use Kibana Toast API to show our messages diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index 08ba10ff69207..c40e7ad373eaf 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -4,44 +4,42 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as Rx from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import ReactDOM from 'react-dom'; +import * as Rx from 'rxjs'; import { catchError, filter, map, mergeMap, takeUntil } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; +import { + CoreSetup, + CoreStart, + NotificationsSetup, + Plugin, + PluginInitializerContext, +} from 'src/core/public'; import { ManagementSetup } from 'src/plugins/management/public'; -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; -import { I18nProvider } from '@kbn/i18n/react'; import { UiActionsSetup } from 'src/plugins/ui_actions/public'; - -import { ReportListing } from './components/report_listing'; -import { getGeneralErrorToast } from './components'; - -import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; -import { ReportingAPIClient } from './lib/reporting_api_client'; -import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; -import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; -import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; - -import { LicensingPluginSetup } from '../../licensing/public'; +import { JobId, JobStatusBuckets } from '../'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; -import { SharePluginSetup } from '../../../../src/plugins/share/public'; - import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; +import { SharePluginSetup } from '../../../../src/plugins/share/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ConfigType } from '../common/types'; +import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; +import { getGeneralErrorToast } from './components'; +import { ReportListing } from './components/report_listing'; +import { ReportingAPIClient } from './lib/reporting_api_client'; +import { ReportingNotifierStreamHandler as StreamHandler } from './lib/stream_handler'; +import { GetCsvReportPanelAction } from './panel_actions/get_csv_panel_action'; +import { csvReportingProvider } from './share_context_menu/register_csv_reporting'; +import { reportingPDFPNGProvider } from './share_context_menu/register_pdf_png_reporting'; -import { - JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG, - JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, -} from '../constants'; - -import { JobId, JobStatusBuckets, NotificationsService } from '..'; - -const { - jobCompletionNotifier: { interval: JOBS_REFRESH_INTERVAL }, -} = JOB_COMPLETION_NOTIFICATIONS_POLLER_CONFIG; +export interface ClientConfigType { + poll: ConfigType['poll']; +} function getStored(): JobId[] { const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); @@ -49,7 +47,7 @@ function getStored(): JobId[] { } function handleError( - notifications: NotificationsService, + notifications: NotificationsSetup, err: Error ): Rx.Observable { notifications.toasts.addDanger( @@ -64,18 +62,19 @@ function handleError( return Rx.of({ completed: [], failed: [] }); } -export class ReportingPublicPlugin implements Plugin { +export class ReportingPublicPlugin implements Plugin { + private config: ClientConfigType; private readonly stop$ = new Rx.ReplaySubject(1); - private readonly title = i18n.translate('xpack.reporting.management.reportingTitle', { defaultMessage: 'Reporting', }); - private readonly breadcrumbText = i18n.translate('xpack.reporting.breadcrumb', { defaultMessage: 'Reporting', }); - constructor(initializerContext: PluginInitializerContext) {} + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } public setup( core: CoreSetup, @@ -130,6 +129,7 @@ export class ReportingPublicPlugin implements Plugin { @@ -163,8 +163,9 @@ export class ReportingPublicPlugin implements Plugin { const { http, notifications } = core; const apiClient = new ReportingAPIClient(http); const streamHandler = new StreamHandler(notifications, apiClient); + const { interval } = this.config.poll.jobsRefresh; - Rx.timer(0, JOBS_REFRESH_INTERVAL) + Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called map(() => getStored()), // read all pending job IDs from session storage diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index f0a0a093aa8c0..a0d7618322c65 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -10,6 +10,7 @@ import { ConfigSchema, ConfigType } from './schema'; export { createConfig$ } from './create_config'; export const config: PluginConfigDescriptor = { + exposeToBrowser: { poll: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ unused('capture.browser.chromium.maxScreenshotDimension'), diff --git a/x-pack/plugins/siem/common/default_index_pattern.ts b/x-pack/plugins/siem/common/default_index_pattern.ts deleted file mode 100644 index 4d53aeb000c55..0000000000000 --- a/x-pack/plugins/siem/common/default_index_pattern.ts +++ /dev/null @@ -1,15 +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. - */ - -/** The comma-delimited list of Elasticsearch indices from which the SIEM app collects events */ -export const defaultIndexPattern = [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', -]; diff --git a/x-pack/plugins/siem/common/utility_types.ts b/x-pack/plugins/siem/common/utility_types.ts index c7bbdbfccf082..b46ccdbbe3d05 100644 --- a/x-pack/plugins/siem/common/utility_types.ts +++ b/x-pack/plugins/siem/common/utility_types.ts @@ -6,12 +6,6 @@ import { ReactNode } from 'react'; -export type Pick3 = { - [P1 in K1]: { [P2 in K2]: { [P3 in K3]: T[K1][K2][P3] } }; -}; - -export type Omit = Pick>; - // This type is for typing EuiDescriptionList export interface DescriptionList { title: NonNullable; diff --git a/x-pack/plugins/siem/cypress/integration/cases.spec.ts b/x-pack/plugins/siem/cypress/integration/cases.spec.ts new file mode 100644 index 0000000000000..f541555d56440 --- /dev/null +++ b/x-pack/plugins/siem/cypress/integration/cases.spec.ts @@ -0,0 +1,115 @@ +/* + * 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 { case1 } from '../objects/case'; + +import { + ALL_CASES_CLOSE_ACTION, + ALL_CASES_CLOSED_CASES_COUNT, + ALL_CASES_CLOSED_CASES_STATS, + ALL_CASES_COMMENTS_COUNT, + ALL_CASES_DELETE_ACTION, + ALL_CASES_NAME, + ALL_CASES_OPEN_CASES_COUNT, + ALL_CASES_OPEN_CASES_STATS, + ALL_CASES_OPENED_ON, + ALL_CASES_PAGE_TITLE, + ALL_CASES_REPORTER, + ALL_CASES_REPORTERS_COUNT, + ALL_CASES_SERVICE_NOW_INCIDENT, + ALL_CASES_TAGS, + ALL_CASES_TAGS_COUNT, +} from '../screens/all_cases'; +import { + ACTION, + CASE_DETAILS_DESCRIPTION, + CASE_DETAILS_PAGE_TITLE, + CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN, + CASE_DETAILS_STATUS, + CASE_DETAILS_TAGS, + CASE_DETAILS_TIMELINE_MARKDOWN, + CASE_DETAILS_USER_ACTION, + CASE_DETAILS_USERNAMES, + PARTICIPANTS, + REPORTER, + USER, +} from '../screens/case_details'; +import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens/timeline'; + +import { goToCaseDetails, goToCreateNewCase } from '../tasks/all_cases'; +import { openCaseTimeline } from '../tasks/case_details'; +import { backToCases, createNewCase } from '../tasks/create_new_case'; +import { loginAndWaitForPageWithoutDateRange } from '../tasks/login'; +import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver'; + +import { CASES } from '../urls/navigation'; + +describe('Cases', () => { + before(() => { + esArchiverLoad('timeline'); + }); + + after(() => { + esArchiverUnload('timeline'); + }); + + it('Creates a new case with timeline and opens the timeline', () => { + loginAndWaitForPageWithoutDateRange(CASES); + goToCreateNewCase(); + createNewCase(case1); + backToCases(); + + cy.get(ALL_CASES_PAGE_TITLE).should('have.text', 'Cases Beta'); + cy.get(ALL_CASES_OPEN_CASES_STATS).should('have.text', 'Open cases1'); + cy.get(ALL_CASES_CLOSED_CASES_STATS).should('have.text', 'Closed cases0'); + cy.get(ALL_CASES_OPEN_CASES_COUNT).should('have.text', 'Open cases (1)'); + cy.get(ALL_CASES_CLOSED_CASES_COUNT).should('have.text', 'Closed cases (0)'); + cy.get(ALL_CASES_REPORTERS_COUNT).should('have.text', 'Reporter1'); + cy.get(ALL_CASES_TAGS_COUNT).should('have.text', 'Tags2'); + cy.get(ALL_CASES_NAME).should('have.text', case1.name); + cy.get(ALL_CASES_REPORTER).should('have.text', case1.reporter); + case1.tags.forEach((tag, index) => { + cy.get(ALL_CASES_TAGS(index)).should('have.text', tag); + }); + cy.get(ALL_CASES_COMMENTS_COUNT).should('have.text', '0'); + cy.get(ALL_CASES_OPENED_ON).should('include.text', 'ago'); + cy.get(ALL_CASES_SERVICE_NOW_INCIDENT).should('have.text', 'Not pushed'); + cy.get(ALL_CASES_DELETE_ACTION).should('exist'); + cy.get(ALL_CASES_CLOSE_ACTION).should('exist'); + + goToCaseDetails(); + + const expectedTags = case1.tags.join(''); + cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name); + cy.get(CASE_DETAILS_STATUS).should('have.text', 'open'); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(USER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USER_ACTION) + .eq(ACTION) + .should('have.text', 'added description'); + cy.get(CASE_DETAILS_DESCRIPTION).should( + 'have.text', + `${case1.description} ${case1.timeline.title}` + ); + cy.get(CASE_DETAILS_USERNAMES) + .eq(REPORTER) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_USERNAMES) + .eq(PARTICIPANTS) + .should('have.text', case1.reporter); + cy.get(CASE_DETAILS_TAGS).should('have.text', expectedTags); + cy.get(CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN).should('have.attr', 'disabled'); + cy.get(CASE_DETAILS_TIMELINE_MARKDOWN).then($element => { + const timelineLink = $element.prop('href').match(/http(s?):\/\/\w*:\w*(\S*)/)[0]; + openCaseTimeline(timelineLink); + + cy.get(TIMELINE_TITLE).should('have.attr', 'value', case1.timeline.title); + cy.get(TIMELINE_DESCRIPTION).should('have.attr', 'value', case1.timeline.description); + cy.get(TIMELINE_QUERY).should('have.attr', 'value', case1.timeline.query); + }); + }); +}); diff --git a/x-pack/plugins/siem/cypress/objects/case.ts b/x-pack/plugins/siem/cypress/objects/case.ts new file mode 100644 index 0000000000000..1c7bc34bca417 --- /dev/null +++ b/x-pack/plugins/siem/cypress/objects/case.ts @@ -0,0 +1,29 @@ +/* + * 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 { Timeline } from './timeline'; + +export interface TestCase { + name: string; + tags: string[]; + description: string; + timeline: Timeline; + reporter: string; +} + +const caseTimeline: Timeline = { + title: 'SIEM test', + description: 'description', + query: 'host.name:*', +}; + +export const case1: TestCase = { + name: 'This is the title of the case', + tags: ['Tag1', 'Tag2'], + description: 'This is the case description', + timeline: caseTimeline, + reporter: 'elastic', +}; diff --git a/x-pack/plugins/siem/cypress/objects/timeline.ts b/x-pack/plugins/siem/cypress/objects/timeline.ts index bca99bfa9266a..060a1376b46ce 100644 --- a/x-pack/plugins/siem/cypress/objects/timeline.ts +++ b/x-pack/plugins/siem/cypress/objects/timeline.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -interface Timeline { +export interface Timeline { title: string; + description: string; query: string; } diff --git a/x-pack/plugins/siem/cypress/screens/all_cases.ts b/x-pack/plugins/siem/cypress/screens/all_cases.ts new file mode 100644 index 0000000000000..b1e4c66515352 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/all_cases.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ALL_CASES_CLOSE_ACTION = '[data-test-subj="action-close"]'; + +export const ALL_CASES_CLOSED_CASES_COUNT = '[data-test-subj="closed-case-count"]'; + +export const ALL_CASES_CLOSED_CASES_STATS = '[data-test-subj="closedStatsHeader"]'; + +export const ALL_CASES_COMMENTS_COUNT = '[data-test-subj="case-table-column-commentCount"]'; + +export const ALL_CASES_CREATE_NEW_CASE_BTN = '[data-test-subj="createNewCaseBtn"]'; + +export const ALL_CASES_DELETE_ACTION = '[data-test-subj="action-delete"]'; + +export const ALL_CASES_NAME = '[data-test-subj="case-details-link"]'; + +export const ALL_CASES_OPEN_CASES_COUNT = '[data-test-subj="open-case-count"]'; + +export const ALL_CASES_OPEN_CASES_STATS = '[data-test-subj="openStatsHeader"]'; + +export const ALL_CASES_OPENED_ON = '[data-test-subj="case-table-column-createdAt"]'; + +export const ALL_CASES_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const ALL_CASES_REPORTER = '[data-test-subj="case-table-column-createdBy"]'; + +export const ALL_CASES_REPORTERS_COUNT = + '[data-test-subj="options-filter-popover-button-Reporter"]'; + +export const ALL_CASES_SERVICE_NOW_INCIDENT = + '[data-test-subj="case-table-column-external-notPushed"]'; + +export const ALL_CASES_TAGS = (index: number) => { + return `[data-test-subj="case-table-column-tags-${index}"]`; +}; + +export const ALL_CASES_TAGS_COUNT = '[data-test-subj="options-filter-popover-button-Tags"]'; diff --git a/x-pack/plugins/siem/cypress/screens/case_details.ts b/x-pack/plugins/siem/cypress/screens/case_details.ts new file mode 100644 index 0000000000000..3bd180b1d588f --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/case_details.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const ACTION = 2; + +export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="markdown-root"]'; + +export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]'; + +export const CASE_DETAILS_PUSH_AS_SERVICE_NOW_BTN = '[data-test-subj="push-to-service-now"]'; + +export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]'; + +export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]'; + +export const CASE_DETAILS_TIMELINE_MARKDOWN = '[data-test-subj="markdown-link"]'; + +export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem'; + +export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; + +export const PARTICIPANTS = 1; + +export const REPORTER = 0; + +export const USER = 1; diff --git a/x-pack/plugins/siem/cypress/screens/create_new_case.ts b/x-pack/plugins/siem/cypress/screens/create_new_case.ts new file mode 100644 index 0000000000000..6e2beb78fff19 --- /dev/null +++ b/x-pack/plugins/siem/cypress/screens/create_new_case.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]'; + +export const DESCRIPTION_INPUT = + '[data-test-subj="caseDescription"] [data-test-subj="textAreaInput"]'; + +export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]'; + +export const LOADING_SPINNER = '[data-test-subj="create-case-loading-spinner"]'; + +export const SUBMIT_BTN = '[data-test-subj="create-case-submit"]'; + +export const TAGS_INPUT = '[data-test-subj="caseTags"] [data-test-subj="comboBoxSearchInput"]'; + +export const TIMELINE = '[data-test-subj="timeline"]'; + +export const TIMELINE_SEARCHBOX = '[data-test-subj="timeline-super-select-search-box"]'; + +export const TITLE_INPUT = '[data-test-subj="caseTitle"] [data-test-subj="input"]'; diff --git a/x-pack/plugins/siem/cypress/screens/timeline.ts b/x-pack/plugins/siem/cypress/screens/timeline.ts index 53d8273d9ce6b..58d2568084f7c 100644 --- a/x-pack/plugins/siem/cypress/screens/timeline.ts +++ b/x-pack/plugins/siem/cypress/screens/timeline.ts @@ -42,6 +42,8 @@ export const TIMELINE_INSPECT_BUTTON = '[data-test-subj="inspect-empty-button"]' export const TIMELINE_NOT_READY_TO_DROP_BUTTON = '[data-test-subj="flyout-button-not-ready-to-drop"]'; +export const TIMELINE_QUERY = '[data-test-subj="timelineQueryInput"]'; + export const TIMELINE_SETTINGS_ICON = '[data-test-subj="settings-gear"]'; export const TIMELINE_TITLE = '[data-test-subj="timeline-title"]'; diff --git a/x-pack/plugins/siem/cypress/tasks/all_cases.ts b/x-pack/plugins/siem/cypress/tasks/all_cases.ts new file mode 100644 index 0000000000000..f374532201324 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/all_cases.ts @@ -0,0 +1,15 @@ +/* + * 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 { ALL_CASES_NAME, ALL_CASES_CREATE_NEW_CASE_BTN } from '../screens/all_cases'; + +export const goToCreateNewCase = () => { + cy.get(ALL_CASES_CREATE_NEW_CASE_BTN).click({ force: true }); +}; + +export const goToCaseDetails = () => { + cy.get(ALL_CASES_NAME).click({ force: true }); +}; diff --git a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts b/x-pack/plugins/siem/cypress/tasks/case_details.ts similarity index 52% rename from x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts rename to x-pack/plugins/siem/cypress/tasks/case_details.ts index bd5bb50514c01..a28f8b8010adb 100644 --- a/x-pack/legacy/plugins/cross_cluster_replication/common/constants/plugin.ts +++ b/x-pack/plugins/siem/cypress/tasks/case_details.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN = { - ID: 'cross_cluster_replication', +import { TIMELINE_TITLE } from '../screens/timeline'; + +export const openCaseTimeline = (link: string) => { + cy.visit('/app/kibana'); + cy.visit(link); + cy.contains('a', 'SIEM'); + cy.get(TIMELINE_TITLE).should('exist'); }; diff --git a/x-pack/plugins/siem/cypress/tasks/create_new_case.ts b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts new file mode 100644 index 0000000000000..b7078a1033de8 --- /dev/null +++ b/x-pack/plugins/siem/cypress/tasks/create_new_case.ts @@ -0,0 +1,42 @@ +/* + * 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 { TestCase } from '../objects/case'; + +import { + BACK_TO_CASES_BTN, + DESCRIPTION_INPUT, + SUBMIT_BTN, + INSERT_TIMELINE_BTN, + LOADING_SPINNER, + TAGS_INPUT, + TIMELINE, + TIMELINE_SEARCHBOX, + TITLE_INPUT, +} from '../screens/create_new_case'; + +export const backToCases = () => { + cy.get(BACK_TO_CASES_BTN).click({ force: true }); +}; + +export const createNewCase = (newCase: TestCase) => { + cy.get(TITLE_INPUT).type(newCase.name, { force: true }); + newCase.tags.forEach(tag => { + cy.get(TAGS_INPUT).type(`${tag}{enter}`, { force: true }); + }); + cy.get(DESCRIPTION_INPUT).type(`${newCase.description} `, { force: true }); + + cy.get(INSERT_TIMELINE_BTN).click({ force: true }); + cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`); + cy.get(TIMELINE).should('be.visible'); + cy.get(TIMELINE) + .eq(1) + .click({ force: true }); + + cy.get(SUBMIT_BTN).click({ force: true }); + cy.get(LOADING_SPINNER).should('exist'); + cy.get(LOADING_SPINNER).should('not.exist'); +}; diff --git a/x-pack/plugins/siem/cypress/urls/navigation.ts b/x-pack/plugins/siem/cypress/urls/navigation.ts index 5e65e5aa34c18..263469a4dbaed 100644 --- a/x-pack/plugins/siem/cypress/urls/navigation.ts +++ b/x-pack/plugins/siem/cypress/urls/navigation.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +export const CASES = '/app/siem#/case'; export const DETECTIONS = 'app/siem#/detections'; export const HOSTS_PAGE = '/app/siem#/hosts/allHosts'; export const HOSTS_PAGE_TAB_URLS = { diff --git a/x-pack/plugins/siem/package.json b/x-pack/plugins/siem/package.json index 1fcef46243628..31c930dce71c0 100644 --- a/x-pack/plugins/siem/package.json +++ b/x-pack/plugins/siem/package.json @@ -5,7 +5,7 @@ "private": true, "license": "Elastic-License", "scripts": { - "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js & node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", + "extract-mitre-attacks": "node scripts/extract_tactics_techniques_mitre.js && node ../../../scripts/eslint ../../legacy/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts --fix", "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:run": "cypress run --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-siem/cypress/results > ../../../target/kibana-siem/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-siem/cypress/results/output.json --reportDir ../../../target/kibana-siem/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-siem/cypress/results/*.xml ../../../target/junit/ && exit $status;", diff --git a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js index 478463b1a8064..145d9715970c8 100644 --- a/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js +++ b/x-pack/plugins/siem/scripts/extract_tactics_techniques_mitre.js @@ -123,7 +123,7 @@ async function main() { .replace(/}"/g, '}') .replace(/"{/g, '{')}; - export const techniques = ${JSON.stringify(techniques, null, 2)}; + export const technique = ${JSON.stringify(techniques, null, 2)}; export const techniquesOptions: MitreTechniquesOptions[] = ${JSON.stringify(getTechniquesOptions(techniques), null, 2) diff --git a/x-pack/plugins/siem/server/client/client.test.ts b/x-pack/plugins/siem/server/client/client.test.ts index 94ff2149b8c64..c0ae15cb73f4e 100644 --- a/x-pack/plugins/siem/server/client/client.test.ts +++ b/x-pack/plugins/siem/server/client/client.test.ts @@ -9,7 +9,7 @@ import { createMockConfig } from '../lib/detection_engine/routes/__mocks__'; import { SiemClient } from './client'; describe('SiemClient', () => { - describe('#signalsIndex', () => { + describe('#getSignalsIndex', () => { it('returns the index scoped to the specified spaceId', () => { const mockConfig = { ...createMockConfig(), @@ -18,7 +18,7 @@ describe('SiemClient', () => { const spaceId = 'fooSpace'; const client = new SiemClient(spaceId, mockConfig); - expect(client.signalsIndex).toEqual('mockSignalsIndex-fooSpace'); + expect(client.getSignalsIndex()).toEqual('mockSignalsIndex-fooSpace'); }); }); }); diff --git a/x-pack/plugins/siem/server/client/client.ts b/x-pack/plugins/siem/server/client/client.ts index 6cb0d4cfade77..5780bb4173f79 100644 --- a/x-pack/plugins/siem/server/client/client.ts +++ b/x-pack/plugins/siem/server/client/client.ts @@ -4,14 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export class SiemClient { - public readonly signalsIndex: string; + private readonly signalsIndex: string; constructor(private spaceId: string, private config: ConfigType) { const configuredSignalsIndex = this.config.signalsIndex; this.signalsIndex = `${configuredSignalsIndex}-${this.spaceId}`; } + + public getSignalsIndex = (): string => this.signalsIndex; } diff --git a/x-pack/plugins/siem/server/client/factory.ts b/x-pack/plugins/siem/server/client/factory.ts index d3d6b84e5b090..69db4d7eed98f 100644 --- a/x-pack/plugins/siem/server/client/factory.ts +++ b/x-pack/plugins/siem/server/client/factory.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SiemClient } from './client'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; interface SetupDependencies { getSpaceId?: (request: KibanaRequest) => string | undefined; diff --git a/x-pack/plugins/siem/server/index.ts b/x-pack/plugins/siem/server/index.ts index 83e2f900a3b90..e9cd78589fac9 100644 --- a/x-pack/plugins/siem/server/index.ts +++ b/x-pack/plugins/siem/server/index.ts @@ -5,7 +5,7 @@ */ import { PluginInitializerContext } from '../../../../src/core/server'; -import { Plugin } from './plugin'; +import { Plugin, PluginSetup, PluginStart } from './plugin'; import { configSchema, ConfigType } from './config'; export const plugin = (context: PluginInitializerContext) => { @@ -14,4 +14,4 @@ export const plugin = (context: PluginInitializerContext) => { export const config = { schema: configSchema }; -export { ConfigType }; +export { ConfigType, Plugin, PluginSetup, PluginStart }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts index 10efdb518f7b7..f3b4068f6dd2d 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -13,6 +13,7 @@ import { import { alertsClientMock } from '../../../../../../alerting/server/mocks'; import { actionsClientMock } from '../../../../../../actions/server/mocks'; import { licensingMock } from '../../../../../../licensing/server/mocks'; +import { siemMock } from '../../../../mocks'; const createMockClients = () => ({ actionsClient: actionsClientMock.create(), @@ -20,7 +21,7 @@ const createMockClients = () => ({ clusterClient: elasticsearchServiceMock.createScopedClusterClient(), licensing: { license: licensingMock.createLicenseMock() }, savedObjectsClient: savedObjectsClientMock.create(), - siemClient: { signalsIndex: 'mockSignalsIndex' }, + siemClient: siemMock.createClient(), }); const createRequestContextMock = ( diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts index cb48e35228858..20b8ad29d2715 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/create_index_route.ts @@ -37,7 +37,7 @@ export const createIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (indexExists) { return siemResponse.error({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts index 5eff38b778492..79cf4851f9ab8 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/delete_index_route.ts @@ -45,7 +45,7 @@ export const deleteIndexRoute = (router: IRouter) => { } const callCluster = clusterClient.callAsCurrentUser; - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(callCluster, index); if (!indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts index 8ff8d7461ecd1..2b418892f0f39 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/index/read_index_route.ts @@ -29,7 +29,7 @@ export const readIndexRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, index); if (indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 7dbbe837e656d..e3c41c555f297 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -36,7 +36,7 @@ export const readPrivilegesRoute = ( return siemResponse.error({ statusCode: 404 }); } - const index = siemClient.signalsIndex; + const index = siemClient.getSignalsIndex(); const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index); const privileges = merge(clusterPrivileges, { is_authenticated: security?.authc.isAuthenticated(request) ?? false, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index bfc8c9c54b2c0..3c6adce45f959 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -49,7 +49,7 @@ export const addPrepackedRulesRoute = (router: IRouter) => { const rulesToInstall = getRulesToInstall(rulesFromFileSystem, prepackagedRules); const rulesToUpdate = getRulesToUpdate(rulesFromFileSystem, prepackagedRules); - const { signalsIndex } = siemClient; + const signalsIndex = siemClient.getSignalsIndex(); if (rulesToInstall.length !== 0 || rulesToUpdate.length !== 0) { const signalsIndexExists = await getIndexExists( clusterClient.callAsCurrentUser, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 2d7ddb79e5af5..133c98a6af7b3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -92,7 +92,7 @@ export const createRulesBulkRoute = (router: IRouter) => { try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return createBulkErrorObject({ diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 1f0896686aca0..9f1cddb2051c9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -9,10 +9,8 @@ import uuid from 'uuid'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { createRules } from '../../rules/create_rules'; -import { IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; import { RuleAlertParamsRest } from '../../types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { createRulesSchema } from '../schemas/create_rules_schema'; @@ -23,6 +21,7 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const createRulesRoute = (router: IRouter): void => { router.post( @@ -82,7 +81,7 @@ export const createRulesRoute = (router: IRouter): void => { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { return siemResponse.error({ @@ -145,10 +144,7 @@ export const createRulesRoute = (router: IRouter): void => { name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusSavedObjectsClientFactory(savedObjectsClient).find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts index 38748e287ab45..b35ba27ef3561 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_bulk_route.ts @@ -11,14 +11,11 @@ import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; -import { - IRuleSavedAttributesSavedObjectAttributes, - DeleteRulesRequestParams, -} from '../../rules/types'; +import { DeleteRulesRequestParams } from '../../rules/types'; import { deleteRules } from '../../rules/delete_rules'; import { deleteNotifications } from '../../notifications/delete_notifications'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; type Config = RouteConfig; type Handler = RequestHandler; @@ -44,6 +41,8 @@ export const deleteRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const rules = await Promise.all( request.body.map(async payloadRule => { const { id, rule_id: ruleId } = payloadRule; @@ -61,17 +60,12 @@ export const deleteRulesBulkRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); return transformValidateBulkError(idOrRuleIdOrUnknown, rule, undefined, ruleStatuses); } else { return getIdBulkError({ id, ruleId }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts index 098d556741fed..2288633ee8d2e 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/delete_rules_route.ts @@ -11,13 +11,10 @@ import { queryRulesSchema } from '../schemas/query_rules_schema'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { - DeleteRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { DeleteRuleRequestParams } from '../../rules/types'; import { deleteNotifications } from '../../notifications/delete_notifications'; import { deleteRuleActionsSavedObject } from '../../rule_actions/delete_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const deleteRulesRoute = (router: IRouter) => { router.delete( @@ -44,6 +41,7 @@ export const deleteRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await deleteRules({ actionsClient, alertsClient, @@ -56,17 +54,12 @@ export const deleteRulesRoute = (router: IRouter) => { ruleAlertId: rule.id, savedObjectsClient, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 6, search: rule.id, searchFields: ['alertId'], }); - ruleStatuses.saved_objects.forEach(async obj => - savedObjectsClient.delete(ruleStatusSavedObjectType, obj.id) - ); + ruleStatuses.saved_objects.forEach(async obj => ruleStatusClient.delete(obj.id)); const [validated, errors] = transformValidate( rule, undefined, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts index 8433b74adf310..bc4568dd0a40b 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -6,7 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { ExportRulesRequestParams } from '../../rules/types'; import { getNonPackagedRulesCount } from '../../rules/get_existing_prepackaged_rules'; import { exportRulesSchema, exportRulesQuerySchema } from '../schemas/export_rules_schema'; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts index 9661fac81497c..f293b9e64a316 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_route.ts @@ -7,15 +7,12 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRules } from '../../rules/find_rules'; -import { - FindRulesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { FindRulesRequestParams } from '../../rules/types'; import { findRulesSchema } from '../schemas/find_rules_schema'; import { transformValidateFindAlerts } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesRoute = (router: IRouter) => { router.get( @@ -40,6 +37,7 @@ export const findRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await findRules({ alertsClient, perPage: query.per_page, @@ -50,10 +48,7 @@ export const findRulesRoute = (router: IRouter) => { }); const ruleStatuses = await Promise.all( rules.data.map(async rule => { - const results = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const results = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index 6b54a25a1b1c4..8e35fecf6a652 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -9,17 +9,16 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequestParams, - IRuleSavedAttributesSavedObjectAttributes, RuleStatusResponse, IRuleStatusAttributes, } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { buildRouteValidation, transformError, convertToSnakeCase, buildSiemResponse, } from '../utils'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const findRulesStatusesRoute = (router: IRouter) => { router.post( @@ -50,12 +49,10 @@ export const findRulesStatusesRoute = (router: IRouter) => { } */ try { + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const statuses = await body.ids.reduce>( async (acc, id) => { - const lastFiveErrorsForId = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const lastFiveErrorsForId = await ruleStatusClient.find({ perPage: 6, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 8c052cfdf4024..1233e01a67762 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -126,6 +126,7 @@ describe('import_rules_route', () => { }); test('returns an error if the index does not exist', async () => { + clients.siemClient.getSignalsIndex.mockReturnValue('mockSignalsIndex'); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getEmptyIndex()); const response = await server.inject(request, context); expect(response.status).toEqual(200); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 527fab786910f..202252da293ee 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -10,7 +10,7 @@ import { extname } from 'path'; import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { ConfigType } from '../../../..'; +import { ConfigType } from '../../../../config'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; @@ -147,7 +147,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { ruleType: type, }); - const signalsIndex = siemClient.signalsIndex; + const signalsIndex = siemClient.getSignalsIndex(); const indexExists = await getIndexExists( clusterClient.callAsCurrentUser, signalsIndex diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index e4236f4632dcd..534253db65d78 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - PatchRuleAlertParamsRest, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { transformBulkError, buildRouteValidation, @@ -21,8 +18,8 @@ import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesBulkRoute = (router: IRouter) => { router.patch( @@ -46,6 +43,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -131,10 +129,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 23469144e11f8..f7932cb016ba7 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -7,10 +7,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { patchRules } from '../../rules/patch_rules'; -import { - PatchRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { PatchRuleAlertParamsRest } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; import { buildRouteValidation, @@ -20,8 +17,8 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const patchRulesRoute = (router: IRouter) => { router.patch( @@ -83,6 +80,7 @@ export const patchRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ actionsClient, alertsClient, @@ -127,10 +125,7 @@ export const patchRulesRoute = (router: IRouter) => { throttle, name: rule.name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts index 4d23e0217f2e8..cedd7ccd1a411 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/read_rules_route.ts @@ -11,12 +11,9 @@ import { transformValidate } from './validate'; import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { readRules } from '../../rules/read_rules'; import { queryRulesSchema } from '../schemas/query_rules_schema'; -import { - ReadRuleRequestParams, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; +import { ReadRuleRequestParams } from '../../rules/types'; import { getRuleActionsSavedObject } from '../../rule_actions/get_rule_actions_saved_object'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const readRulesRoute = (router: IRouter) => { router.get( @@ -41,6 +38,7 @@ export const readRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await readRules({ alertsClient, id, @@ -51,10 +49,7 @@ export const readRulesRoute = (router: IRouter) => { savedObjectsClient, ruleAlertId: rule.id, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 6db91d74294fc..f929f2fb3f649 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - IRuleSavedAttributesSavedObjectAttributes, - UpdateRuleAlertParamsRest, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { @@ -19,10 +16,10 @@ import { validateLicenseForRuleType, } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesBulkRoute = (router: IRouter) => { router.put( @@ -47,6 +44,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { const { @@ -83,7 +81,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { version, exceptions_list, } = payloadRule; - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); @@ -134,10 +132,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7dbbe5a22ab46..dedc2c914410a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,10 +6,7 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - UpdateRuleAlertParamsRest, - IRuleSavedAttributesSavedObjectAttributes, -} from '../../rules/types'; +import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; import { buildRouteValidation, @@ -19,9 +16,9 @@ import { } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; -import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; export const updateRulesRoute = (router: IRouter) => { router.put( @@ -78,12 +75,13 @@ export const updateRulesRoute = (router: IRouter) => { const actionsClient = context.actions?.getActionsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.siem?.getSiemClient(); + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); if (!siemClient || !actionsClient || !alertsClient) { return siemResponse.error({ statusCode: 404 }); } - const finalIndex = outputIndex ?? siemClient.signalsIndex; + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, actionsClient, @@ -131,10 +129,7 @@ export const updateRulesRoute = (router: IRouter) => { throttle, name, }); - const ruleStatuses = await savedObjectsClient.find< - IRuleSavedAttributesSavedObjectAttributes - >({ - type: ruleStatusSavedObjectType, + const ruleStatuses = await ruleStatusClient.find({ perPage: 1, sortField: 'statusDate', sortOrder: 'desc', diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index c71761fcc39db..bcb70b6b4f0dd 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -44,7 +44,7 @@ export const setSignalsStatusRoute = (router: IRouter) => { } try { const result = await clusterClient.callAsCurrentUser('updateByQuery', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { script: { source: `ctx._source.signal.status = '${status}'`, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts index fd02b3371ed38..41896c725b903 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/signals/query_signals_route.ts @@ -29,7 +29,7 @@ export const querySignalsRoute = (router: IRouter) => { try { const result = await clusterClient.callAsCurrentUser('search', { - index: siemClient.signalsIndex, + index: siemClient.getSignalsIndex(), body: { query, aggs, _source, track_total_hits, size }, ignoreUnavailable: true, }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts index f54f43c41ef6e..d50c339c95266 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rule_actions/saved_object_mappings.ts @@ -4,37 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleActionsSavedObjectType = 'siem-detection-engine-rule-actions'; export const ruleActionsSavedObjectMappings = { - [ruleActionsSavedObjectType]: { - properties: { - alertThrottle: { - type: 'keyword', - }, - ruleAlertId: { - type: 'keyword', - }, - ruleThrottle: { - type: 'keyword', - }, - actions: { - properties: { - group: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - action_type_id: { - type: 'keyword', - }, - params: { - dynamic: true, - properties: {}, - }, + properties: { + alertThrottle: { + type: 'keyword', + }, + ruleAlertId: { + type: 'keyword', + }, + ruleThrottle: { + type: 'keyword', + }, + actions: { + properties: { + group: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + action_type_id: { + type: 'keyword', + }, + params: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dynamic: true as any, + properties: {}, }, }, }, }, }; + +export const type: SavedObjectsType = { + name: ruleActionsSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleActionsSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index c23f539b58160..85b13ed9cf4ed 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -7,10 +7,10 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { PatchRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; +import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion, calculateName, calculateInterval } from './utils'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const patchRules = async ({ alertsClient, @@ -134,22 +134,22 @@ export const patchRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } else { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts index 1d91def5fa6cc..2dcc90240ad40 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/saved_object_mappings.ts @@ -4,44 +4,51 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsType } from '../../../../../../../src/core/server'; + export const ruleStatusSavedObjectType = 'siem-detection-engine-rule-status'; export const ruleStatusSavedObjectMappings = { - [ruleStatusSavedObjectType]: { - properties: { - alertId: { - type: 'keyword', - }, - status: { - type: 'keyword', - }, - statusDate: { - type: 'date', - }, - lastFailureAt: { - type: 'date', - }, - lastSuccessAt: { - type: 'date', - }, - lastFailureMessage: { - type: 'text', - }, - lastSuccessMessage: { - type: 'text', - }, - lastLookBackDate: { - type: 'date', - }, - gap: { - type: 'text', - }, - bulkCreateTimeDurations: { - type: 'float', - }, - searchAfterTimeDurations: { - type: 'float', - }, + properties: { + alertId: { + type: 'keyword', + }, + status: { + type: 'keyword', + }, + statusDate: { + type: 'date', + }, + lastFailureAt: { + type: 'date', + }, + lastSuccessAt: { + type: 'date', + }, + lastFailureMessage: { + type: 'text', + }, + lastSuccessMessage: { + type: 'text', + }, + lastLookBackDate: { + type: 'date', + }, + gap: { + type: 'text', + }, + bulkCreateTimeDurations: { + type: 'float', + }, + searchAfterTimeDurations: { + type: 'float', }, }, }; + +export const type: SavedObjectsType = { + name: ruleStatusSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: ruleStatusSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 7ddbbd76b0661..29c2cfdf91076 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,11 +7,11 @@ import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerting/server'; import { readRules } from './read_rules'; -import { IRuleSavedAttributesSavedObjectAttributes, UpdateRuleParams } from './types'; +import { UpdateRuleParams } from './types'; import { addTags } from './add_tags'; -import { ruleStatusSavedObjectType } from './saved_object_mappings'; import { calculateVersion } from './utils'; import { hasListsFeature } from '../feature_flags'; +import { ruleStatusSavedObjectsClientFactory } from '../signals/rule_status_saved_objects_client'; export const updateRules = async ({ alertsClient, @@ -129,22 +129,22 @@ export const updateRules = async ({ await alertsClient.disable({ id: rule.id }); } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); - const ruleCurrentStatus = savedObjectsClient - ? await savedObjectsClient.find({ - type: ruleStatusSavedObjectType, - perPage: 1, - sortField: 'statusDate', - sortOrder: 'desc', - search: rule.id, - searchFields: ['alertId'], - }) - : null; + + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); + const ruleCurrentStatus = await ruleStatusClient.find({ + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + // set current status for this rule to be 'going to run' if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; - currentStatusToDisable.attributes.status = 'going to run'; - await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + await ruleStatusClient.update(currentStatusToDisable.id, { ...currentStatusToDisable.attributes, + status: 'going to run', }); } } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts index 731fffcac1bb0..251a1e6d118ff 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -13,7 +13,7 @@ import { import { loggingServiceMock } from '../../../../../../../../src/core/server/mocks'; import { RuleTypeParams, OutputRuleAlertRest } from '../../types'; import { IRuleStatusAttributes } from '../../rules/types'; -import { ruleStatusSavedObjectType } from '../../../../saved_objects'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const sampleRuleAlertParams = ( maxSignals?: number | undefined, diff --git a/x-pack/plugins/siem/server/lib/note/saved_object.ts b/x-pack/plugins/siem/server/lib/note/saved_object.ts index 2b94fd4516786..3eae30625e422 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object.ts @@ -25,9 +25,9 @@ import { import { FrameworkRequest } from '../framework'; import { SavedNote, NoteSavedObjectRuntimeType, NoteSavedObject } from './types'; import { noteSavedObjectType } from './saved_object_mappings'; -import { timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; export class Note { public async deleteNote(request: FrameworkRequest, noteIds: string[]) { diff --git a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts index b001e30e52336..0f079571b868b 100644 --- a/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/note/saved_object_mappings.ts @@ -4,37 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedNote } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const noteSavedObjectType = 'siem-ui-timeline-note'; -export const noteSavedObjectMappings: { - [noteSavedObjectType]: ElasticsearchMappingOf; -} = { - [noteSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - note: { - type: 'text', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const noteSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + note: { + type: 'text', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: noteSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: noteSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts index 7fc23d86d8218..1e3a481e17106 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object.ts @@ -20,9 +20,10 @@ import { SavedPinnedEvent, } from './types'; import { PageInfoNote, SortNote, PinnedEvent as PinnedEventResponse } from '../../graphql/types'; -import { pinnedEventSavedObjectType, timelineSavedObjectType } from '../../saved_objects'; import { pickSavedTimeline } from '../timeline/pick_saved_timeline'; import { convertSavedObjectToSavedTimeline } from '../timeline/convert_saved_object_to_savedtimeline'; +import { pinnedEventSavedObjectType } from './saved_object_mappings'; +import { timelineSavedObjectType } from '../timeline/saved_object_mappings'; export class PinnedEvent { public async deletePinnedEventOnTimeline(request: FrameworkRequest, pinnedEventIds: string[]) { diff --git a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts index 322f585ae8ff2..1a4cd3fce575d 100644 --- a/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/pinned_event/saved_object_mappings.ts @@ -4,34 +4,36 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedPinnedEvent } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const pinnedEventSavedObjectType = 'siem-ui-timeline-pinned-event'; -export const pinnedEventSavedObjectMappings: { - [pinnedEventSavedObjectType]: ElasticsearchMappingOf; -} = { - [pinnedEventSavedObjectType]: { - properties: { - timelineId: { - type: 'keyword', - }, - eventId: { - type: 'keyword', - }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', - }, +export const pinnedEventSavedObjectMappings = { + properties: { + timelineId: { + type: 'keyword', + }, + eventId: { + type: 'keyword', + }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', }, }, }; + +export const type: SavedObjectsType = { + name: pinnedEventSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: pinnedEventSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts index c59f6eb6ce3da..e0eefbf811a56 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/export_timelines_route.ts @@ -8,7 +8,7 @@ import { set as _set } from 'lodash/fp'; import { TIMELINE_EXPORT_URL } from '../../../../common/constants'; import { IRouter } from '../../../../../../../src/core/server'; -import { ConfigType } from '../../..'; +import { ConfigType } from '../../../config'; import { transformError, buildSiemResponse } from '../../detection_engine/routes/utils'; import { getExportTimelineByObjectIds } from './utils/export_timelines'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts index 258ef9faf671b..9d148abf82cdd 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/import_timelines_route.ts @@ -32,7 +32,7 @@ import { IRouter } from '../../../../../../../src/core/server'; import { SetupPlugins } from '../../../plugin'; import { ImportTimelinesPayloadSchemaRt } from './schemas/import_timelines_schema'; import { importRulesSchema } from '../../detection_engine/routes/schemas/response/import_rules_schema'; -import { ConfigType } from '../../..'; +import { ConfigType } from '../../../config'; import { Timeline } from '../saved_object'; import { validate } from '../../detection_engine/routes/rules/validate'; diff --git a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts index edd4abe0d76b5..677891fa16c02 100644 --- a/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts +++ b/x-pack/plugins/siem/server/lib/timeline/routes/utils/export_timelines.ts @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set as _set } from 'lodash/fp'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, -} from '../../../../saved_objects'; import { NoteSavedObject } from '../../../note/types'; import { PinnedEventSavedObject } from '../../../pinned_event/types'; import { convertSavedObjectToSavedTimeline } from '../../convert_saved_object_to_savedtimeline'; @@ -30,6 +24,9 @@ import { TimelineSavedObject, } from '../../types'; import { transformDataToNdjson } from '../../../../utils/read_stream/create_stream_from_ndjson'; +import { pinnedEventSavedObjectType } from '../../../pinned_event/saved_object_mappings'; +import { noteSavedObjectType } from '../../../note/saved_object_mappings'; +import { timelineSavedObjectType } from '../../saved_object_mappings'; export type TimelineSavedObjectsClient = Pick< SavedObjectsClient, diff --git a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts index 8fc12fd56a8f6..b956e0f98fcb6 100644 --- a/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts +++ b/x-pack/plugins/siem/server/lib/timeline/saved_object_mappings.ts @@ -4,272 +4,274 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ElasticsearchMappingOf } from '../../utils/typed_elasticsearch_mappings'; -import { SavedTimeline } from './types'; +import { SavedObjectsType } from '../../../../../../src/core/server'; export const timelineSavedObjectType = 'siem-ui-timeline'; -export const timelineSavedObjectMappings: { - [timelineSavedObjectType]: ElasticsearchMappingOf; -} = { - [timelineSavedObjectType]: { - properties: { - columns: { - properties: { - aggregatable: { - type: 'boolean', - }, - category: { - type: 'keyword', - }, - columnHeaderType: { - type: 'keyword', - }, - description: { - type: 'text', - }, - example: { - type: 'text', - }, - indexes: { - type: 'keyword', - }, - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - placeholder: { - type: 'text', - }, - searchable: { - type: 'boolean', - }, - type: { - type: 'keyword', - }, +export const timelineSavedObjectMappings = { + properties: { + columns: { + properties: { + aggregatable: { + type: 'boolean', + }, + category: { + type: 'keyword', + }, + columnHeaderType: { + type: 'keyword', + }, + description: { + type: 'text', + }, + example: { + type: 'text', + }, + indexes: { + type: 'keyword', + }, + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + placeholder: { + type: 'text', + }, + searchable: { + type: 'boolean', + }, + type: { + type: 'keyword', }, }, - dataProviders: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + dataProviders: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, - and: { - properties: { - id: { - type: 'keyword', - }, - name: { - type: 'text', - }, - enabled: { - type: 'boolean', - }, - excluded: { - type: 'boolean', - }, - kqlQuery: { - type: 'text', - }, - queryMatch: { - properties: { - field: { - type: 'text', - }, - displayField: { - type: 'text', - }, - value: { - type: 'text', - }, - displayValue: { - type: 'text', - }, - operator: { - type: 'text', - }, + }, + and: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + enabled: { + type: 'boolean', + }, + excluded: { + type: 'boolean', + }, + kqlQuery: { + type: 'text', + }, + queryMatch: { + properties: { + field: { + type: 'text', + }, + displayField: { + type: 'text', + }, + value: { + type: 'text', + }, + displayValue: { + type: 'text', + }, + operator: { + type: 'text', }, }, }, }, }, }, - description: { - type: 'text', - }, - eventType: { - type: 'keyword', - }, - favorite: { - properties: { - keySearch: { - type: 'text', - }, - fullName: { - type: 'text', - }, - userName: { - type: 'text', - }, - favoriteDate: { - type: 'date', - }, + }, + description: { + type: 'text', + }, + eventType: { + type: 'keyword', + }, + favorite: { + properties: { + keySearch: { + type: 'text', + }, + fullName: { + type: 'text', + }, + userName: { + type: 'text', + }, + favoriteDate: { + type: 'date', }, }, - filters: { - properties: { - meta: { - properties: { - alias: { - type: 'text', - }, - controlledBy: { - type: 'text', - }, - disabled: { - type: 'boolean', - }, - field: { - type: 'text', - }, - formattedValue: { - type: 'text', - }, - index: { - type: 'keyword', - }, - key: { - type: 'keyword', - }, - negate: { - type: 'boolean', - }, - params: { - type: 'text', - }, - type: { - type: 'keyword', - }, - value: { - type: 'text', - }, + }, + filters: { + properties: { + meta: { + properties: { + alias: { + type: 'text', + }, + controlledBy: { + type: 'text', + }, + disabled: { + type: 'boolean', + }, + field: { + type: 'text', + }, + formattedValue: { + type: 'text', + }, + index: { + type: 'keyword', + }, + key: { + type: 'keyword', + }, + negate: { + type: 'boolean', + }, + params: { + type: 'text', + }, + type: { + type: 'keyword', + }, + value: { + type: 'text', }, - }, - exists: { - type: 'text', - }, - match_all: { - type: 'text', - }, - missing: { - type: 'text', - }, - query: { - type: 'text', - }, - range: { - type: 'text', - }, - script: { - type: 'text', }, }, + exists: { + type: 'text', + }, + match_all: { + type: 'text', + }, + missing: { + type: 'text', + }, + query: { + type: 'text', + }, + range: { + type: 'text', + }, + script: { + type: 'text', + }, }, - kqlMode: { - type: 'keyword', - }, - kqlQuery: { - properties: { - filterQuery: { - properties: { - kuery: { - properties: { - kind: { - type: 'keyword', - }, - expression: { - type: 'text', - }, + }, + kqlMode: { + type: 'keyword', + }, + kqlQuery: { + properties: { + filterQuery: { + properties: { + kuery: { + properties: { + kind: { + type: 'keyword', + }, + expression: { + type: 'text', }, }, - serializedQuery: { - type: 'text', - }, + }, + serializedQuery: { + type: 'text', }, }, }, }, - title: { - type: 'text', - }, - dateRange: { - properties: { - start: { - type: 'date', - }, - end: { - type: 'date', - }, + }, + title: { + type: 'text', + }, + dateRange: { + properties: { + start: { + type: 'date', }, - }, - savedQueryId: { - type: 'keyword', - }, - sort: { - properties: { - columnId: { - type: 'keyword', - }, - sortDirection: { - type: 'keyword', - }, + end: { + type: 'date', }, }, - created: { - type: 'date', - }, - createdBy: { - type: 'text', - }, - updated: { - type: 'date', - }, - updatedBy: { - type: 'text', + }, + savedQueryId: { + type: 'keyword', + }, + sort: { + properties: { + columnId: { + type: 'keyword', + }, + sortDirection: { + type: 'keyword', + }, }, }, + created: { + type: 'date', + }, + createdBy: { + type: 'text', + }, + updated: { + type: 'date', + }, + updatedBy: { + type: 'text', + }, }, }; + +export const type: SavedObjectsType = { + name: timelineSavedObjectType, + hidden: false, + namespaceType: 'single', + mappings: timelineSavedObjectMappings, +}; diff --git a/x-pack/plugins/siem/server/lib/types.ts b/x-pack/plugins/siem/server/lib/types.ts index a74fe8f778ba9..2a897806dc628 100644 --- a/x-pack/plugins/siem/server/lib/types.ts +++ b/x-pack/plugins/siem/server/lib/types.ts @@ -6,7 +6,7 @@ import { AuthenticatedUser } from '../../../security/public'; import { RequestHandlerContext } from '../../../../../src/core/server'; -export { ConfigType as Configuration } from '../'; +export { ConfigType as Configuration } from '../config'; import { Authentications } from './authentications'; import { Events } from './events'; diff --git a/x-pack/plugins/siem/server/mocks.ts b/x-pack/plugins/siem/server/mocks.ts new file mode 100644 index 0000000000000..44c41be86b6ff --- /dev/null +++ b/x-pack/plugins/siem/server/mocks.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SiemClient } from './types'; + +type SiemClientMock = jest.Mocked; +const createSiemClientMock = (): SiemClientMock => + (({ + getSignalsIndex: jest.fn(), + } as unknown) as SiemClientMock); + +export const siemMock = { + createClient: createSiemClientMock, +}; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index b9ec1c2e92438..3988fbec05de4 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, + Plugin as IPlugin, PluginInitializerContext, Logger, } from '../../../../src/core/server'; @@ -33,15 +34,10 @@ import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule import { rulesNotificationAlertType } from './lib/detection_engine/notifications/rules_notification_alert_type'; import { isNotificationAlertExecutor } from './lib/detection_engine/notifications/types'; import { hasListsFeature, listsEnvFeatureFlagName } from './lib/detection_engine/feature_flags'; -import { - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, -} from './saved_objects'; +import { initSavedObjects, savedObjectTypes } from './saved_objects'; import { SiemClientFactory } from './client'; import { createConfig$, ConfigType } from './config'; +import { initUiSettings } from './ui_settings'; export { CoreSetup, CoreStart }; @@ -60,7 +56,12 @@ export interface StartPlugins { alerting: AlertingStart; } -export class Plugin { +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface PluginStart {} + +export class Plugin implements IPlugin { readonly name = 'siem'; private readonly logger: Logger; private readonly config$: Observable; @@ -86,6 +87,9 @@ export class Plugin { ); } + initSavedObjects(core.savedObjects); + initUiSettings(core.uiSettings); + const router = core.http.createRouter(); core.http.registerRouteHandlerContext(this.name, (context, request, response) => ({ getSiemClient: () => this.siemClientFactory.create(request), @@ -125,15 +129,11 @@ export class Plugin { 'alert', 'action', 'action_task_params', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], read: ['config'], }, @@ -156,15 +156,11 @@ export class Plugin { all: ['alert', 'action', 'action_task_params'], read: [ 'config', - noteSavedObjectType, - pinnedEventSavedObjectType, - timelineSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, 'cases', 'cases-comments', 'cases-configure', 'cases-user-actions', + ...savedObjectTypes, ], }, ui: [ @@ -201,7 +197,11 @@ export class Plugin { const libs = compose(core, plugins, this.context.env.mode.prod); initServer(libs); + + return {}; } - public start(core: CoreStart, plugins: StartPlugins) {} + public start(core: CoreStart, plugins: StartPlugins) { + return {}; + } } diff --git a/x-pack/plugins/siem/server/routes/index.ts b/x-pack/plugins/siem/server/routes/index.ts index 64b232a2686b8..1c03823e85fd7 100644 --- a/x-pack/plugins/siem/server/routes/index.ts +++ b/x-pack/plugins/siem/server/routes/index.ts @@ -31,7 +31,7 @@ import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/r import { importTimelinesRoute } from '../lib/timeline/routes/import_timelines_route'; import { exportTimelinesRoute } from '../lib/timeline/routes/export_timelines_route'; import { SetupPlugins } from '../plugin'; -import { ConfigType } from '..'; +import { ConfigType } from '../config'; export const initRoutes = ( router: IRouter, diff --git a/x-pack/plugins/siem/server/saved_objects.ts b/x-pack/plugins/siem/server/saved_objects.ts index 7b097eefedb46..66a470099d649 100644 --- a/x-pack/plugins/siem/server/saved_objects.ts +++ b/x-pack/plugins/siem/server/saved_objects.ts @@ -4,35 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { noteSavedObjectType, noteSavedObjectMappings } from './lib/note/saved_object_mappings'; -import { - pinnedEventSavedObjectType, - pinnedEventSavedObjectMappings, -} from './lib/pinned_event/saved_object_mappings'; -import { - timelineSavedObjectType, - timelineSavedObjectMappings, -} from './lib/timeline/saved_object_mappings'; -import { - ruleStatusSavedObjectMappings, - ruleStatusSavedObjectType, -} from './lib/detection_engine/rules/saved_object_mappings'; -import { - ruleActionsSavedObjectMappings, - ruleActionsSavedObjectType, -} from './lib/detection_engine/rule_actions/saved_object_mappings'; +import { CoreSetup } from '../../../../src/core/server'; -export { - noteSavedObjectType, - pinnedEventSavedObjectType, - ruleStatusSavedObjectType, - ruleActionsSavedObjectType, - timelineSavedObjectType, -}; -export const savedObjectMappings = { - ...timelineSavedObjectMappings, - ...noteSavedObjectMappings, - ...pinnedEventSavedObjectMappings, - ...ruleStatusSavedObjectMappings, - ...ruleActionsSavedObjectMappings, +import { type as noteType } from './lib/note/saved_object_mappings'; +import { type as pinnedEventType } from './lib/pinned_event/saved_object_mappings'; +import { type as timelineType } from './lib/timeline/saved_object_mappings'; +import { type as ruleStatusType } from './lib/detection_engine/rules/saved_object_mappings'; +import { type as ruleActionsType } from './lib/detection_engine/rule_actions/saved_object_mappings'; + +const types = [noteType, pinnedEventType, ruleActionsType, ruleStatusType, timelineType]; + +export const savedObjectTypes = types.map(type => type.name); + +export const initSavedObjects = (savedObjects: CoreSetup['savedObjects']) => { + types.forEach(type => savedObjects.registerType(type)); }; diff --git a/x-pack/plugins/siem/server/ui_settings.ts b/x-pack/plugins/siem/server/ui_settings.ts new file mode 100644 index 0000000000000..26b7fd72571af --- /dev/null +++ b/x-pack/plugins/siem/server/ui_settings.ts @@ -0,0 +1,141 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; + +import { CoreSetup } from '../../../../src/core/server'; +import { + DEFAULT_INDEX_KEY, + DEFAULT_INDEX_PATTERN, + DEFAULT_ANOMALY_SCORE, + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_FROM, + DEFAULT_TO, + ENABLE_NEWS_FEED_SETTING, + NEWS_FEED_URL_SETTING, + NEWS_FEED_URL_SETTING_DEFAULT, + IP_REPUTATION_LINKS_SETTING, + IP_REPUTATION_LINKS_SETTING_DEFAULT, +} from '../common/constants'; + +export const initUiSettings = (uiSettings: CoreSetup['uiSettings']) => { + uiSettings.register({ + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalLabel', { + defaultMessage: 'Time filter refresh interval', + }), + value: `{ + "pause": ${DEFAULT_INTERVAL_PAUSE}, + "value": ${DEFAULT_INTERVAL_VALUE} +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultRefreshIntervalDescription', { + defaultMessage: + '

    Default refresh interval for the SIEM time filter, in milliseconds.

    ', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + value: schema.number(), + pause: schema.boolean(), + }), + }, + [DEFAULT_SIEM_TIME_RANGE]: { + type: 'json', + name: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeLabel', { + defaultMessage: 'Time filter period', + }), + value: `{ + "from": "${DEFAULT_FROM}", + "to": "${DEFAULT_TO}" +}`, + description: i18n.translate('xpack.siem.uiSettings.defaultTimeRangeDescription', { + defaultMessage: '

    Default period of time in the SIEM time filter.

    ', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.object({ + from: schema.string(), + to: schema.string(), + }), + }, + [DEFAULT_INDEX_KEY]: { + name: i18n.translate('xpack.siem.uiSettings.defaultIndexLabel', { + defaultMessage: 'Elasticsearch indices', + }), + value: DEFAULT_INDEX_PATTERN, + description: i18n.translate('xpack.siem.uiSettings.defaultIndexDescription', { + defaultMessage: + '

    Comma-delimited list of Elasticsearch indices from which the SIEM app collects events.

    ', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf(schema.string()), + }, + [DEFAULT_ANOMALY_SCORE]: { + name: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreLabel', { + defaultMessage: 'Anomaly threshold', + }), + value: 50, + type: 'number', + description: i18n.translate('xpack.siem.uiSettings.defaultAnomalyScoreDescription', { + defaultMessage: + '

    Value above which Machine Learning job anomalies are displayed in the SIEM app.

    Valid values: 0 to 100.

    ', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.number(), + }, + [ENABLE_NEWS_FEED_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.enableNewsFeedLabel', { + defaultMessage: 'News feed', + }), + value: true, + description: i18n.translate('xpack.siem.uiSettings.enableNewsFeedDescription', { + defaultMessage: '

    Enables the News feed

    ', + }), + type: 'boolean', + category: ['siem'], + requiresPageReload: true, + schema: schema.boolean(), + }, + [NEWS_FEED_URL_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.newsFeedUrl', { + defaultMessage: 'News feed URL', + }), + value: NEWS_FEED_URL_SETTING_DEFAULT, + description: i18n.translate('xpack.siem.uiSettings.newsFeedUrlDescription', { + defaultMessage: '

    News feed content will be retrieved from this URL

    ', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.string(), + }, + [IP_REPUTATION_LINKS_SETTING]: { + name: i18n.translate('xpack.siem.uiSettings.ipReputationLinks', { + defaultMessage: 'IP Reputation Links', + }), + value: IP_REPUTATION_LINKS_SETTING_DEFAULT, + type: 'json', + description: i18n.translate('xpack.siem.uiSettings.ipReputationLinksDescription', { + defaultMessage: + 'Array of URL templates to build the list of reputation URLs to be displayed on the IP Details page.', + }), + category: ['siem'], + requiresPageReload: true, + schema: schema.arrayOf( + schema.object({ + name: schema.string(), + url_template: schema.string(), + }) + ), + }, + }); +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8a606e230dc36..68463b2f44259 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -548,6 +548,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "ダッシュボードが読み込めません。", "dashboard.factory.displayName": "ダッシュボード", "dashboard.panel.removePanel.replacePanel": "パネルの交換", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} と {lt} {to}", "data.common.kql.errors.endOfInputText": "インプットの終わり", "data.common.kql.errors.fieldNameText": "フィールド名", @@ -2002,8 +2004,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "にアクセスして有効な別のドキュメントを選択してください。", "kbn.context.unableToLoadAnchorDocumentDescription": "別のドキュメントが読み込めません", "kbn.context.unableToLoadDocumentDescription": "ドキュメントが読み込めません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "「6.1.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルには想定された列または行フィールドがありません", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "「6.3.0」のダッシュボードの互換性のため、パネルデータを移行できませんでした。パネルに必要なフィールドがありません: {key}", "kbn.dashboardTitle": "ダッシュボード", "kbn.devToolsTitle": "開発ツール", "kbn.discover.backToTopLinkText": "最上部へ戻る。", @@ -2295,6 +2295,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "インデックス、インデックスパターン、保存されたオブジェクト、Kibana の設定、その他を管理します。", "kbn.management.landing.text": "すべてのツールの一覧は、左のメニューにあります。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可視化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "上書き", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "「{title}」に上書きしてよろしいですか?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "{type} を上書きしますか?", @@ -2416,8 +2418,6 @@ "savedObjectsManagement.breadcrumb.index": "保存されたオブジェクト", "savedObjectsManagement.field.offLabel": "オフ", "savedObjectsManagement.field.onLabel": "オン", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可視化", "kibana_legacy.bigUrlWarningNotificationMessage": "{advancedSettingsLink}で{storeInSessionStorageParam}オプションを有効にするか、オンスクリーンビジュアルを簡素化してください。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高度な設定", "kibana_legacy.bigUrlWarningNotificationTitle": "URLが大きく、Kibanaの動作が停止する可能性があります", @@ -6098,9 +6098,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "一時停止中", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "ステータス", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "フォロワーインデックスの接尾辞", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "{licenseType} ライセンスが期限切れのため {pluginName} を使用できません", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "現在ライセンス情報が利用できないため {pluginName} を使用できません。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "ご使用の {licenseType} ライセンスは {pluginName} をサポートしていません。ライセンスをアップグレードしてください。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "キャンセル", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "削除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "{count} 個の自動フォローパターンを削除しますか?", @@ -16738,4 +16735,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。", "xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index faee95b8172b7..0964ab53b7fee 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -548,6 +548,8 @@ "dashboard.dashboardGrid.toast.unableToLoadDashboardDangerMessage": "无法加载仪表板。", "dashboard.factory.displayName": "仪表板", "dashboard.panel.removePanel.replacePanel": "替换面板", + "dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", + "dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "data.aggTypes.buckets.ranges.rangesFormatMessage": "{gte} {from} 和 {lt} {to}", "data.common.kql.errors.endOfInputText": "输入结束", "data.common.kql.errors.fieldNameText": "字段名称", @@ -2003,8 +2005,6 @@ "kbn.context.reloadPageDescription.selectValidAnchorDocumentTextMessage": "以选择有效地定位点文档。", "kbn.context.unableToLoadAnchorDocumentDescription": "无法加载该定位点文档", "kbn.context.unableToLoadDocumentDescription": "无法加载文档", - "kbn.dashboard.panel.unableToMigratePanelDataForSixOneZeroErrorMessage": "无法迁移用于“6.1.0”向后兼容的面板数据,面板不包含所需的列和/或行字段", - "kbn.dashboard.panel.unableToMigratePanelDataForSixThreeZeroErrorMessage": "无法迁移用于“6.3.0”向后兼容的面板数据,面板不包含预期字段:{key}", "kbn.dashboardTitle": "仪表板", "kbn.devToolsTitle": "开发工具", "kbn.discover.backToTopLinkText": "返至顶部。", @@ -2296,6 +2296,8 @@ "kbn.management.landing.header": "Kibana {version} 管理", "kbn.management.landing.subhead": "管理您的索引、索引模式、已保存对象、Kibana 设置等等。", "kbn.management.landing.text": "应用的完整列表位于左侧菜单中。", + "kbn.managementTitle": "管理", + "kbn.visualizeTitle": "可视化", "savedObjectsManagement.indexPattern.confirmOverwriteButton": "覆盖", "savedObjectsManagement.indexPattern.confirmOverwriteLabel": "确定要覆盖 “{title}”?", "savedObjectsManagement.indexPattern.confirmOverwriteTitle": "覆盖“{type}”?", @@ -2417,8 +2419,6 @@ "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", "savedObjectsManagement.breadcrumb.edit": "编辑 {savedObjectType}", "savedObjectsManagement.breadcrumb.index": "已保存对象", - "kbn.managementTitle": "管理", - "kbn.visualizeTitle": "可视化", "kibana_legacy.bigUrlWarningNotificationMessage": "在{advancedSettingsLink}中启用“{storeInSessionStorageParam}”选项或简化屏幕视觉效果。", "kibana_legacy.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置", "kibana_legacy.bigUrlWarningNotificationTitle": "URL 过长,Kibana 可能无法工作", @@ -6100,9 +6100,6 @@ "xpack.crossClusterReplication.autoFollowPatternList.table.statusTextPaused": "已暂停", "xpack.crossClusterReplication.autoFollowPatternList.table.statusTitle": "状态", "xpack.crossClusterReplication.autoFollowPatternList.table.suffixColumnTitle": "Follower 索引后缀", - "xpack.crossClusterReplication.checkLicense.errorExpiredMessage": "您不能使用 {pluginName},因为您的 {licenseType} 许可证已过期", - "xpack.crossClusterReplication.checkLicense.errorUnavailableMessage": "您不能使用 {pluginName},因为许可证信息当前不可用。", - "xpack.crossClusterReplication.checkLicense.errorUnsupportedMessage": "您的 {licenseType} 许可证不支持 {pluginName}。请升级您的许可。", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.cancelButtonText": "取消", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.confirmButtonText": "删除", "xpack.crossClusterReplication.deleteAutoFollowPattern.confirmModal.deleteMultipleTitle": "是否删除 {count} 个自动跟随模式?", @@ -16743,4 +16740,4 @@ "xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。", "xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx index 658a0e869548f..fdfaf70648694 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.test.tsx @@ -4,12 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { FunctionComponent } from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { act } from 'react-dom/test-utils'; import { TypeRegistry } from '../../type_registry'; import { registerBuiltInActionTypes } from './index'; import { ActionTypeModel, ActionParamsProps } from '../../../types'; import { IndexActionParams, EsIndexActionConnector } from './types'; import { coreMock } from '../../../../../../../src/core/public/mocks'; +jest.mock('../../../common/index_controls', () => ({ + firstFieldOption: jest.fn(), + getFields: jest.fn(), + getIndexOptions: jest.fn(), + getIndexPatterns: jest.fn(), +})); const ACTION_TYPE_ID = '.index'; let actionTypeModel: ActionTypeModel; @@ -91,13 +98,40 @@ describe('action params validation', () => { }); describe('IndexActionConnectorFields renders', () => { - test('all connector fields is rendered', () => { + test('all connector fields is rendered', async () => { const mocks = coreMock.createSetup(); expect(actionTypeModel.actionConnectorFields).not.toBeNull(); if (!actionTypeModel.actionConnectorFields) { return; } + + const { getIndexPatterns } = jest.requireMock('../../../common/index_controls'); + getIndexPatterns.mockResolvedValueOnce([ + { + id: 'indexPattern1', + attributes: { + title: 'indexPattern1', + }, + }, + { + id: 'indexPattern2', + attributes: { + title: 'indexPattern2', + }, + }, + ]); + const { getFields } = jest.requireMock('../../../common/index_controls'); + getFields.mockResolvedValueOnce([ + { + type: 'date', + name: 'test1', + }, + { + type: 'text', + name: 'test2', + }, + ]); const ConnectorFields = actionTypeModel.actionConnectorFields; const actionConnector = { secrets: {}, @@ -119,8 +153,38 @@ describe('IndexActionConnectorFields renders', () => { http={mocks.http} /> ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(wrapper.find('[data-test-subj="connectorIndexesComboBox"]').length > 0).toBeTruthy(); expect(wrapper.find('[data-test-subj="indexRefreshCheckbox"]').length > 0).toBeTruthy(); + + const indexSearchBoxValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); + expect(indexSearchBoxValue.first().props().value).toEqual(''); + + const indexComboBox = wrapper.find('#indexConnectorSelectSearchBox'); + indexComboBox.first().simulate('click'); + const event = { target: { value: 'indexPattern1' } }; + indexComboBox + .find('input') + .first() + .simulate('change', event); + + const indexSearchBoxValueBeforeEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueBeforeEnterData.first().props().value).toEqual('indexPattern1'); + + const indexComboBoxClear = wrapper.find('[data-test-subj="comboBoxClearButton"]'); + indexComboBoxClear.first().simulate('click'); + + const indexSearchBoxValueAfterEnterData = wrapper.find( + '[data-test-subj="comboBoxSearchInput"]' + ); + expect(indexSearchBoxValueAfterEnterData.first().props().value).toEqual('indexPattern1'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx index 15f68e6a9f441..55a219ca94aea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index.tsx @@ -151,7 +151,7 @@ const IndexActionConnectorFields: React.FunctionComponent { - editActionConfig('index', selected[0].value); + editActionConfig('index', selected.length > 0 ? selected[0].value : ''); const indices = selected.map(s => s.value as string); // reset time field and expression fields if indices are deleted diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts index 2af1a9ac38e44..24347b7799871 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.test.ts @@ -51,6 +51,7 @@ describe('Upgrade Assistant Usage Collector', () => { 'ui_reindex.open': 4, 'ui_reindex.start': 2, 'ui_reindex.stop': 1, + 'ui_reindex.not_defined': 1, }, }; }, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts index 9c2946db7f084..0c2e3a1e43f4a 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/telemetry/usage_collector.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { set } from 'lodash'; +import { get } from 'lodash'; import { APICaller, ElasticsearchServiceStart, @@ -84,16 +84,19 @@ export async function fetchUpgradeAssistantMetrics( return defaultTelemetrySavedObject; } - const upgradeAssistantTelemetrySOAttrsKeys = Object.keys( - upgradeAssistantTelemetrySavedObjectAttrs - ); - const telemetryObj = defaultTelemetrySavedObject; - - upgradeAssistantTelemetrySOAttrsKeys.forEach((key: string) => { - set(telemetryObj, key, upgradeAssistantTelemetrySavedObjectAttrs[key]); - }); - - return telemetryObj as UpgradeAssistantTelemetrySavedObject; + return { + ui_open: { + overview: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.overview', 0), + cluster: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.cluster', 0), + indices: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_open.indices', 0), + }, + ui_reindex: { + close: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.close', 0), + open: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.open', 0), + start: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.start', 0), + stop: get(upgradeAssistantTelemetrySavedObjectAttrs, 'ui_reindex.stop', 0), + }, + } as UpgradeAssistantTelemetrySavedObject; }; return { diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js index 22f0bde50b073..b9a0bfd40a8d6 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.helpers.js @@ -5,33 +5,27 @@ */ import { API_BASE_PATH } from './constants'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; export const registerHelpers = supertest => { let autoFollowPatternsCreated = []; const loadAutoFollowPatterns = () => supertest.get(`${API_BASE_PATH}/auto_follow_patterns`); - const getAutoFollowPattern = name => - supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${name}`); + const getAutoFollowPattern = id => supertest.get(`${API_BASE_PATH}/auto_follow_patterns/${id}`); - const createAutoFollowPattern = ( - name = getRandomString(), - payload = getAutoFollowIndexPayload() - ) => { - autoFollowPatternsCreated.push(name); + const createAutoFollowPattern = payload => { + autoFollowPatternsCreated.push(payload.id); return supertest .post(`${API_BASE_PATH}/auto_follow_patterns`) .set('kbn-xsrf', 'xxx') - .send({ ...payload, id: name }); + .send(payload); }; - const deleteAutoFollowPattern = name => { - autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== name); + const deleteAutoFollowPattern = id => { + autoFollowPatternsCreated = autoFollowPatternsCreated.filter(c => c !== id); - return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${name}`).set('kbn-xsrf', 'xxx'); + return supertest.delete(`${API_BASE_PATH}/auto_follow_patterns/${id}`).set('kbn-xsrf', 'xxx'); }; const deleteAllAutoFollowPatterns = () => diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js index 3efb4d6600f7f..7a95ba7fcd981 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/auto_follow_pattern.js @@ -6,8 +6,7 @@ import expect from '@kbn/expect'; -import { getRandomString } from './lib'; -import { getAutoFollowIndexPayload } from './fixtures'; +import { REMOTE_CLUSTER_NAME } from './constants'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; import { registerHelpers as registerAutoFollowPatternHelpers } from './auto_follow_pattern.helpers'; @@ -37,44 +36,60 @@ export default function({ getService }) { describe('when remote cluster does not exist', () => { it('should throw a 404 error when cluster is unknown', async () => { - const payload = getAutoFollowIndexPayload(); - payload.remoteCluster = 'unknown-cluster'; + const { body } = await createAutoFollowPattern({ + id: 'pattern0', + remoteCluster: 'unknown-cluster', + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); - const { body } = await createAutoFollowPattern(undefined, payload).expect(404); + expect(body.statusCode).to.be(404); expect(body.attributes.cause[0]).to.contain('no such remote cluster'); }); }); describe('when remote cluster exists', () => { - before(() => addCluster()); + before(async () => addCluster()); describe('create()', () => { it('should create an auto-follow pattern when cluster is known', async () => { - const name = getRandomString(); - const { body } = await createAutoFollowPattern(name).expect(200); - console.log(body); - + const { body, statusCode } = await createAutoFollowPattern({ + id: 'pattern1', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + expect(statusCode).to.be(200); expect(body.acknowledged).to.eql(true); }); }); describe('get()', () => { it('should return a 404 when the auto-follow pattern is not found', async () => { - const name = getRandomString(); - const { body } = await getAutoFollowPattern(name).expect(404); - + const { body } = await getAutoFollowPattern('missing-pattern'); + expect(body.statusCode).to.be(404); expect(body.attributes.cause).not.to.be(undefined); }); it('should return an auto-follow pattern that was created', async () => { - const name = getRandomString(); - const autoFollowPattern = getAutoFollowIndexPayload(); - - await createAutoFollowPattern(name, autoFollowPattern); - - const { body } = await getAutoFollowPattern(name).expect(200); - - expect(body).to.eql({ ...autoFollowPattern, name }); + await createAutoFollowPattern({ + id: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); + + const { body, statusCode } = await getAutoFollowPattern('pattern2'); + + expect(statusCode).to.be(200); + expect(body).to.eql({ + name: 'pattern2', + remoteCluster: REMOTE_CLUSTER_NAME, + active: true, + leaderIndexPatterns: ['leader-*'], + followIndexPattern: '{{leader_index}}_follower', + }); }); }); }); diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js index de47f5d9ea85e..6e254b27356f2 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/fixtures.js @@ -7,13 +7,6 @@ import { REMOTE_CLUSTER_NAME } from './constants'; import { getRandomString } from './lib'; -export const getAutoFollowIndexPayload = (remoteCluster = REMOTE_CLUSTER_NAME, active = true) => ({ - active, - remoteCluster, - leaderIndexPatterns: ['leader-*'], - followIndexPattern: '{{leader_index}}_follower', -}); - export const getFollowerIndexPayload = ( leaderIndexName = getRandomString(), remoteCluster = REMOTE_CLUSTER_NAME, diff --git a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js index eabf474120f2b..d03b1f83fb404 100644 --- a/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js +++ b/x-pack/test/api_integration/apis/management/cross_cluster_replication/follower_indices.js @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../legacy/plugins/cross_cluster_replication/common/constants'; +import { FOLLOWER_INDEX_ADVANCED_SETTINGS } from '../../../../../plugins/cross_cluster_replication/common/constants'; import { getFollowerIndexPayload } from './fixtures'; import { registerHelpers as registerElasticSearchHelpers, getRandomString } from './lib'; import { registerHelpers as registerRemoteClustersHelpers } from './remote_clusters.helpers'; @@ -57,7 +57,8 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); - it('should create a follower index that follows an existing remote index', async () => { + // NOTE: If this test fails locally it's probably because you have another cluster running. + it('should create a follower index that follows an existing leader index', async () => { // First let's create an index to follow const leaderIndex = await createIndex(); @@ -65,7 +66,7 @@ export default function({ getService }) { const { body } = await createFollowerIndex(undefined, payload).expect(200); // There is a race condition in which Elasticsearch can respond without acknowledging, - // i.e. `body .follow_index_shards_acked` is sometimes true and sometimes false. + // i.e. `body.follow_index_shards_acked` is sometimes true and sometimes false. // By only asserting that `follow_index_created` is true, we eliminate this flakiness. expect(body.follow_index_created).to.eql(true); }); @@ -79,6 +80,7 @@ export default function({ getService }) { expect(body.attributes.cause[0]).to.contain('no such index'); }); + // NOTE: If this test fails locally it's probably because you have another cluster running. it('should return a follower index that was created', async () => { const leaderIndex = await createIndex(); diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index 7195b8680a286..d2d07eca475e7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -193,10 +193,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); }); @@ -219,10 +219,10 @@ export default function({ getService }) { 'size', 'isFrozen', 'aliases', - 'ilm', // data enricher - 'isRollupIndex', // data enricher // Cloud disables CCR, so wouldn't expect follower indices. 'isFollowerIndex', // data enricher + 'ilm', // data enricher + 'isRollupIndex', // data enricher ]; expect(Object.keys(body[0])).to.eql(expectedKeys); expect(body.length > 1).to.be(true); // to contrast it with the next test diff --git a/x-pack/test/api_integration/config.js b/x-pack/test/api_integration/config.js index 0eac7c58044e6..dda8c2d888d30 100644 --- a/x-pack/test/api_integration/config.js +++ b/x-pack/test/api_integration/config.js @@ -30,7 +30,6 @@ export async function getApiIntegrationConfig({ readConfigFile }) { '--telemetry.optIn=true', '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', '--xpack.endpoint.alertResultListDefaultDateRange.from=2018-01-10T00:00:00.000Z', ], }, diff --git a/x-pack/test/epm_api_integration/config.ts b/x-pack/test/epm_api_integration/config.ts index e95a389ef20ed..b04bc76ccb315 100644 --- a/x-pack/test/epm_api_integration/config.ts +++ b/x-pack/test/epm_api_integration/config.ts @@ -28,7 +28,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xPackAPITestsConfig.get('kbnTestServer'), serverArgs: [ ...xPackAPITestsConfig.get('kbnTestServer.serverArgs'), - '--xpack.ingestManager.epm.enabled=true', '--xpack.ingestManager.epm.registryUrl=http://localhost:6666', ], }, diff --git a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz index a71281c0ecfec..3d4f0e11a7cc6 100644 Binary files a/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz and b/x-pack/test/functional/es_archives/endpoint/alerts/host_api_feature/data.json.gz differ diff --git a/x-pack/test/functional_endpoint/config.ts b/x-pack/test/functional_endpoint/config.ts index 6ae78ab9d48ac..d7f1cc21828d1 100644 --- a/x-pack/test/functional_endpoint/config.ts +++ b/x-pack/test/functional_endpoint/config.ts @@ -30,8 +30,6 @@ export default async function({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), '--xpack.endpoint.enabled=true', '--xpack.ingestManager.enabled=true', - '--xpack.ingestManager.epm.enabled=true', - '--xpack.ingestManager.fleet.enabled=true', ], }, }; diff --git a/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz new file mode 100644 index 0000000000000..c7acb36992af3 Binary files /dev/null and b/x-pack/test/siem_cypress/es_archives/timeline/data.json.gz differ diff --git a/x-pack/test/siem_cypress/es_archives/timeline/mappings.json b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json new file mode 100644 index 0000000000000..d3412f9d43b57 --- /dev/null +++ b/x-pack/test/siem_cypress/es_archives/timeline/mappings.json @@ -0,0 +1,2976 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "agent_actions": "ed270b46812f0fa1439366c428a2cf17", + "agent_configs": "38abaf89513877745c359e7700c0c66a", + "agent_events": "3231653fafe4ef3196fe3b32ab774bf2", + "agents": "c3eeb7b9d97176f15f6d126370ab23c7", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "08b8b110dbca273d37e8aef131ecab61", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "datasources": "d4bc0c252b2b5683ff21ea32d00acffc", + "enrollment_api_keys": "28b91e20b105b6f928e2012600085d8f", + "epm-package": "0be91c6758421dd5d0f1a58e9e5bc7c3", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "9ecce5b58867403613d82fe496470b34", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "21c3ea0763beb1ecb0162529706b88c5", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "268da3a48066123fc5baf35abaa55014", + "metrics-explorer-view": "53c5365793677328df0ccb6138bf3cdd", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "outputs": "aee9782e0d500b867859650a36280165", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "server": "ec97f1c5da1a19609a60874e5af1100c", + "siem-detection-engine-rule-actions": "90eee2e4635260f4be0a1da8f5bc0aa0", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "ac8020190f5950dd3250b6499144e7fb", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "a53a20fe086b72c9a86da3cc12dad8a6", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "b6289473c8985c79b6c47eebc19a0ca5", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "agent_actions": { + "properties": { + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "flattened" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agent_configs": { + "properties": { + "datasources": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "text" + }, + "namespace": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "updated_on": { + "type": "keyword" + } + } + }, + "agent_events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "config_id": { + "type": "keyword" + }, + "config_newest_revision": { + "type": "integer" + }, + "config_revision": { + "type": "integer" + }, + "current_error_events": { + "type": "text" + }, + "default_api_key": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "text" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "text" + }, + "version": { + "type": "keyword" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "properties": { + "agents": { + "properties": { + "dotnet": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "go": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "java": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "js-base": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "nodejs": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "python": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "ruby": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "rum-js": { + "properties": { + "agent": { + "properties": { + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "service": { + "properties": { + "framework": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "language": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "runtime": { + "properties": { + "composite": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + } + } + }, + "cardinality": { + "properties": { + "transaction": { + "properties": { + "name": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + }, + "user_agent": { + "properties": { + "original": { + "properties": { + "all_agents": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "rum": { + "properties": { + "1d": { + "type": "long" + } + } + } + } + } + } + } + } + }, + "counts": { + "properties": { + "agent_configuration": { + "properties": { + "all": { + "type": "long" + } + } + }, + "error": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "max_error_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "max_transaction_groups_per_service": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "services": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "sourcemap": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "span": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + }, + "traces": { + "properties": { + "1d": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "1d": { + "type": "long" + }, + "all": { + "type": "long" + } + } + } + } + }, + "has_any_services": { + "type": "boolean" + }, + "indices": { + "properties": { + "all": { + "properties": { + "total": { + "properties": { + "docs": { + "properties": { + "count": { + "type": "long" + } + } + }, + "store": { + "properties": { + "size_in_bytes": { + "type": "long" + } + } + } + } + } + } + }, + "shards": { + "properties": { + "total": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "ml": { + "properties": { + "all_jobs_count": { + "type": "long" + } + } + } + } + }, + "retainment": { + "properties": { + "error": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "metric": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "onboarding": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "span": { + "properties": { + "ms": { + "type": "long" + } + } + }, + "transaction": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services_per_agent": { + "properties": { + "dotnet": { + "null_value": 0, + "type": "long" + }, + "go": { + "null_value": 0, + "type": "long" + }, + "java": { + "null_value": 0, + "type": "long" + }, + "js-base": { + "null_value": 0, + "type": "long" + }, + "nodejs": { + "null_value": 0, + "type": "long" + }, + "python": { + "null_value": 0, + "type": "long" + }, + "ruby": { + "null_value": 0, + "type": "long" + }, + "rum-js": { + "null_value": 0, + "type": "long" + } + } + }, + "tasks": { + "properties": { + "agent_configuration": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "agents": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "cardinality": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "groupings": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "indices_stats": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "integrations": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "processor_events": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "services": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + }, + "versions": { + "properties": { + "took": { + "properties": { + "ms": { + "type": "long" + } + } + } + } + } + } + }, + "version": { + "properties": { + "apm_server": { + "properties": { + "major": { + "type": "long" + }, + "minor": { + "type": "long" + }, + "patch": { + "type": "long" + } + } + } + } + } + } + }, + "application_usage_totals": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + } + } + }, + "application_usage_transactional": { + "properties": { + "appId": { + "type": "keyword" + }, + "minutesOnScreen": { + "type": "float" + }, + "numberOfClicks": { + "type": "long" + }, + "timestamp": { + "type": "date" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "datasources": { + "properties": { + "config_id": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "properties": { + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "keyword" + }, + "streams": { + "properties": { + "config": { + "type": "flattened" + }, + "dataset": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "processors": { + "type": "keyword" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "revision": { + "type": "integer" + } + } + }, + "enrollment_api_keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "config_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "epm-package": { + "properties": { + "installed": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "properties": { + "description": { + "type": "text" + }, + "fields": { + "properties": { + "container": { + "type": "keyword" + }, + "host": { + "type": "keyword" + }, + "pod": { + "type": "keyword" + }, + "tiebreaker": { + "type": "keyword" + }, + "timestamp": { + "type": "keyword" + } + } + }, + "logAlias": { + "type": "keyword" + }, + "logColumns": { + "properties": { + "fieldColumn": { + "properties": { + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } + }, + "messageColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + }, + "timestampColumn": { + "properties": { + "id": { + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "metricAlias": { + "type": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "inventory-view": { + "properties": { + "autoBounds": { + "type": "boolean" + }, + "autoReload": { + "type": "boolean" + }, + "boundsOverride": { + "properties": { + "max": { + "type": "integer" + }, + "min": { + "type": "integer" + } + } + }, + "customMetrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "customOptions": { + "properties": { + "field": { + "type": "keyword" + }, + "text": { + "type": "keyword" + } + }, + "type": "nested" + }, + "filterQuery": { + "properties": { + "expression": { + "type": "keyword" + }, + "kind": { + "type": "keyword" + } + } + }, + "groupBy": { + "properties": { + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + }, + "metric": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "label": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "nodeType": { + "type": "keyword" + }, + "time": { + "type": "integer" + }, + "view": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "expression": { + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "bounds": { + "type": "geo_shape" + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "properties": { + "attributesPerMap": { + "properties": { + "dataSourcesCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + }, + "emsVectorLayersCount": { + "dynamic": "true", + "type": "object" + }, + "layerTypesCount": { + "dynamic": "true", + "type": "object" + }, + "layersCount": { + "properties": { + "avg": { + "type": "long" + }, + "max": { + "type": "long" + }, + "min": { + "type": "long" + } + } + } + } + }, + "indexPatternsWithGeoFieldCount": { + "type": "long" + }, + "mapsTotalCount": { + "type": "long" + }, + "settings": { + "properties": { + "showMapVisualizationTypes": { + "type": "boolean" + } + } + }, + "timeCaptured": { + "type": "date" + } + } + }, + "metrics-explorer-view": { + "properties": { + "chartOptions": { + "properties": { + "stack": { + "type": "boolean" + }, + "type": { + "type": "keyword" + }, + "yAxisMode": { + "type": "keyword" + } + } + }, + "currentTimerange": { + "properties": { + "from": { + "type": "keyword" + }, + "interval": { + "type": "keyword" + }, + "to": { + "type": "keyword" + } + } + }, + "name": { + "type": "keyword" + }, + "options": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "filterQuery": { + "type": "keyword" + }, + "groupBy": { + "type": "keyword" + }, + "limit": { + "type": "integer" + }, + "metrics": { + "properties": { + "aggregation": { + "type": "keyword" + }, + "color": { + "type": "keyword" + }, + "field": { + "type": "keyword" + }, + "label": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "outputs": { + "properties": { + "api_key": { + "type": "keyword" + }, + "ca_sha256": { + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "dynamic": "true", + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "dynamic": "true", + "properties": { + "indexName": { + "type": "keyword" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "properties": { + "heartbeatIndices": { + "type": "keyword" + } + } + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchRefName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} \ No newline at end of file