diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 9a49c19b94df2..33ecfcd84fd3e 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -11,14 +11,14 @@ kibanaPipeline(timeoutMinutes: 120) { 'CI_PARALLEL_PROCESS_NUMBER=1' ]) { parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')() + 'oss-baseline': { + workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() } }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')() + 'xpack-baseline': { + workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() } }, ]) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d81f6af4cec28..7daa42af7024d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -59,7 +59,6 @@ # APM /x-pack/plugins/apm/ @elastic/apm-ui /x-pack/test/functional/apps/apm/ @elastic/apm-ui -/src/legacy/core_plugins/apm_oss/ @elastic/apm-ui /src/plugins/apm_oss/ @elastic/apm-ui /src/apm.js @watson @vigneshshanmugam @@ -83,9 +82,6 @@ /src/plugins/home/public @elastic/kibana-core-ui /src/plugins/home/server/*.ts @elastic/kibana-core-ui /src/plugins/home/server/services/ @elastic/kibana-core-ui -# Exclude tutorial resources folder for now because they are not owned by Kibana app and most will move out soon -/src/legacy/core_plugins/kibana/public/home/*.ts @elastic/kibana-core-ui -/src/legacy/core_plugins/kibana/public/home/np_ready/ @elastic/kibana-core-ui /x-pack/plugins/global_search_bar/ @elastic/kibana-core-ui # Observability UIs @@ -167,7 +163,6 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-platform -/x-pack/legacy/plugins/security/ @elastic/kibana-security /x-pack/legacy/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security @@ -286,8 +281,6 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib # Core design /src/plugins/dashboard/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/canvas/**/*.scss @elastic/kibana-core-ui-designers -/src/legacy/core_plugins/kibana/public/home/**/*.scss @elastic/kibana-core-ui-designers -/x-pack/legacy/plugins/security/**/*.scss @elastic/kibana-core-ui-designers /x-pack/legacy/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/spaces/**/*.scss @elastic/kibana-core-ui-designers /x-pack/plugins/security/**/*.scss @elastic/kibana-core-ui-designers @@ -297,7 +290,7 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib /x-pack/plugins/infra/**/*.scss @elastic/observability-design /x-pack/plugins/ingest_manager/**/*.scss @elastic/observability-design /x-pack/plugins/observability/**/*.scss @elastic/observability-design -/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design +/x-pack/plugins/monitoring/**/*.scss @elastic/observability-design # Ent. Search design /x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index e00a67f6c78a4..b4c9c6a4ec39e 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -49,7 +49,7 @@ GET /_template/apm-{version} *Using Logstash, Kafka, etc.* If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka), then the index template will not be set up automatically. Instead, you'll need to -{apm-server-ref}/configuration-template.html[load the template manually]. +{apm-server-ref}/apm-server-template.html[load the template manually]. *Using a custom index names* This problem can also occur if you've customized the index name that you write APM data to. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 9c8d753a2d668..3489dcd018293 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -104,15 +104,14 @@ security is enabled, `xpack.security.encryptionKey`. [cols="2*<"] |=== | `xpack.reporting.queue.pollInterval` - | Specifies the number of milliseconds that the reporting poller waits between polling the - index for any pending Reporting jobs. Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[time] that the reporting poller waits between polling the index for any + pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`. | [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon} - | How long each worker has to produce a report. If your machine is slow or under - heavy load, you might need to increase this timeout. Specified in milliseconds. - If a Reporting job execution time goes over this time limit, the job will be - marked as a failure and there will not be a download available. - Defaults to `120000` (two minutes). + | {ref}/common-options.html#time-units[How long] each worker has to produce a report. If your machine is slow or under heavy + load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a + failure and no download will be available. Can be specified as number of milliseconds. + Defaults to `2m`. |=== @@ -127,24 +126,24 @@ control the capturing process. |=== a| `xpack.reporting.capture.timeouts` `.openUrl` {ess-icon} - | Specify how long to allow the Reporting browser to wait for the "Loading..." screen - to dismiss and find the initial data for the Kibana page. If the time is - exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message. - Defaults to `60000` (1 minute). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for the "Loading..." screen + to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current + page, and the download link shows a warning message. Can be specified as number of milliseconds. + Defaults to `1m`. a| `xpack.reporting.capture.timeouts` `.waitForElements` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualization - panels to load on the Kibana page. If the time is exceeded, a page screenshot - is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30 - seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualization panels + to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows + a warning message. Can be specified as number of milliseconds. + Defaults to `30s`. a| `xpack.reporting.capture.timeouts` `.renderComplete` {ess-icon} - | Specify how long to allow the Reporting browser to wait for all visualizations to - fetch and render the data. If the time is exceeded, a - page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to - `30000` (30 seconds). + | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualizations to + fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a + warning message. Can be specified as number of milliseconds. + Defaults to `30s`. |=== @@ -163,11 +162,10 @@ available, but there will likely be errors in the visualizations in the report. job, as many times as this setting. Defaults to `3`. | `xpack.reporting.capture.loadDelay` - | When visualizations are not evented, this is the amount of time before - taking a screenshot. All visualizations that ship with {kib} are evented, so this - setting should not have much effect. If you are seeing empty images instead of - visualizations, try increasing this value. - Defaults to `3000` (3 seconds). + | Specify the {ref}/common-options.html#time-units[amount of time] before taking a screenshot when visualizations are not evented. + All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images + instead of visualizations, try increasing this value. + Defaults to `3s`. | [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon} | Specifies the browser to use to capture screenshots. This setting exists for @@ -213,9 +211,9 @@ a| `xpack.reporting.capture.browser` [cols="2*<"] |=== | [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon} - | The maximum size of a CSV file before being truncated. This setting exists to prevent - large exports from causing performance and storage issues. - Defaults to `10485760` (10mB). + | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to + prevent large exports from causing performance and storage issues. Can be specified as number of bytes. + Defaults to `10mb`. | `xpack.reporting.csv.scroll.size` | Number of documents retrieved from {es} for each scroll iteration during a CSV @@ -223,7 +221,7 @@ a| `xpack.reporting.capture.browser` Defaults to `500`. | `xpack.reporting.csv.scroll.duration` - | Amount of time allowed before {kib} cleans the scroll context during a CSV export. + | Amount of {ref}/common-options.html#time-units[time] allowed before {kib} cleans the scroll context during a CSV export. Defaults to `30s`. | `xpack.reporting.csv.checkForFormulas` diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc index eadca229bc19c..7022320328c85 100644 --- a/docs/user/alerting/action-types/server-log.asciidoc +++ b/docs/user/alerting/action-types/server-log.asciidoc @@ -2,7 +2,7 @@ [[server-log-action-type]] === Server log action -This action type writes and entry to the {kib} server log. +This action type writes an entry to the {kib} server log. [float] [[server-log-connector-configuration]] diff --git a/kibana.d.ts b/kibana.d.ts index 517bda374af9d..b707405ffbeaf 100644 --- a/kibana.d.ts +++ b/kibana.d.ts @@ -28,7 +28,6 @@ export { Public, Server }; /** * All exports from TS ambient definitions (where types are added for JS source in a .d.ts file). */ -import * as LegacyElasticsearch from './src/legacy/core_plugins/elasticsearch'; import * as LegacyKibanaPluginSpec from './src/legacy/plugin_discovery/plugin_spec/plugin_spec_options'; import * as LegacyKibanaServer from './src/legacy/server/kbn_server'; @@ -44,13 +43,4 @@ export namespace Legacy { export type InitPluginFunction = LegacyKibanaPluginSpec.InitPluginFunction; export type UiExports = LegacyKibanaPluginSpec.UiExports; export type PluginSpecOptions = LegacyKibanaPluginSpec.PluginSpecOptions; - - export namespace Plugins { - export namespace elasticsearch { - export type Plugin = LegacyElasticsearch.ElasticsearchPlugin; - export type Cluster = LegacyElasticsearch.Cluster; - export type ClusterConfig = LegacyElasticsearch.ClusterConfig; - export type CallClusterOptions = LegacyElasticsearch.CallClusterOptions; - } - } } diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts index b1d1335eb1888..78472bb3f517d 100644 --- a/src/cli/cluster/cluster_manager.ts +++ b/src/cli/cluster/cluster_manager.ts @@ -224,7 +224,6 @@ export class ClusterManager { new Set( [ fromRoot('src/core'), - fromRoot('src/legacy/core_plugins'), fromRoot('src/legacy/server'), fromRoot('src/legacy/ui'), fromRoot('src/legacy/utils'), diff --git a/src/cli/serve/serve.js b/src/cli/serve/serve.js index eeb5564667ec4..d8bd39b9dcdf4 100644 --- a/src/cli/serve/serve.js +++ b/src/cli/serve/serve.js @@ -178,7 +178,7 @@ export default function (program) { 'A path to scan for plugins, this can be specified multiple ' + 'times to specify multiple directories', pluginDirCollector, - [fromRoot('plugins'), fromRoot('src/legacy/core_plugins')] + [fromRoot('plugins')] ) .option( '--plugin-path ', diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 362c34d416743..19487efe1366c 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -40,7 +40,7 @@ export async function runDockerGenerator( ubi: boolean = false ) { // UBI var config - const baseOSImage = ubi ? 'registry.access.redhat.com/ubi8/ubi-minimal:latest' : 'centos:8'; + const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8'; const ubiVersionTag = 'ubi8'; const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : ''; diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index 486c8563c5456..5d31db63773fa 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -25,7 +25,6 @@ export default { '/src/plugins', '/src/legacy/ui', '/src/core', - '/src/legacy/core_plugins', '/src/legacy/server', '/src/cli', '/src/cli_keystore', @@ -51,14 +50,11 @@ export default { 'packages/kbn-ui-framework/src/services/**/*.js', '!packages/kbn-ui-framework/src/services/index.js', '!packages/kbn-ui-framework/src/services/**/*/index.js', - 'src/legacy/core_plugins/**/*.{js,mjs,jsx,ts,tsx}', - '!src/legacy/core_plugins/**/{__test__,__snapshots__}/**/*', ], moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '^src/plugins/(.*)': '/src/plugins/$1', - '^plugins/([^/.]*)(.*)': '/src/legacy/core_plugins/$1/public$2', '^uiExports/(.*)': '/src/dev/jest/mocks/file_mock.js', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index e22dc03cf57aa..ba58dcdfa4d58 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -132,11 +132,6 @@ export const REMOVE_EXTENSION = ['packages/kbn-plugin-generator/template/**/*.ej * @type {Array} */ export const TEMPORARILY_IGNORED_PATHS = [ - 'src/legacy/core_plugins/console/public/src/directives/helpExample.txt', - 'src/legacy/core_plugins/console/public/src/sense_editor/theme-sense-dark.js', - '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/fixtures/config_upgrade_from_4.0.0_to_4.0.1-snapshot.json', 'src/core/server/core_app/assets/favicons/android-chrome-192x192.png', 'src/core/server/core_app/assets/favicons/android-chrome-256x256.png', diff --git a/src/legacy/core_plugins/elasticsearch/index.d.ts b/src/legacy/core_plugins/elasticsearch/index.d.ts deleted file mode 100644 index 83e7bb19e57ba..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.d.ts +++ /dev/null @@ -1,526 +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 { - Client as ESClient, - GenericParams, - // root params - BulkIndexDocumentsParams, - ClearScrollParams, - CountParams, - CreateDocumentParams, - DeleteDocumentParams, - DeleteDocumentByQueryParams, - DeleteScriptParams, - DeleteTemplateParams, - ExistsParams, - ExplainParams, - FieldStatsParams, - GetParams, - GetResponse, - GetScriptParams, - GetSourceParams, - GetTemplateParams, - IndexDocumentParams, - InfoParams, - MGetParams, - MSearchParams, - MSearchTemplateParams, - MTermVectorsParams, - PingParams, - PutScriptParams, - PutTemplateParams, - ReindexParams, - ReindexRethrottleParams, - RenderSearchTemplateParams, - ScrollParams, - SearchParams, - SearchShardsParams, - SearchTemplateParams, - SuggestParams, - TermvectorsParams, - UpdateDocumentParams, - UpdateDocumentByQueryParams, - MGetResponse, - MSearchResponse, - SearchResponse, - // cat - CatAliasesParams, - CatAllocationParams, - CatFielddataParams, - CatHealthParams, - CatHelpParams, - CatIndicesParams, - CatCommonParams, - CatRecoveryParams, - CatSegmentsParams, - CatShardsParams, - CatSnapshotsParams, - CatTasksParams, - CatThreadPoolParams, - // cluster - ClusterAllocationExplainParams, - ClusterGetSettingsParams, - ClusterHealthParams, - ClusterPendingTasksParams, - ClusterPutSettingsParams, - ClusterRerouteParams, - ClusterStateParams, - ClusterStatsParams, - // indices - IndicesAnalyzeParams, - IndicesClearCacheParams, - IndicesCloseParams, - IndicesCreateParams, - IndicesDeleteParams, - IndicesDeleteAliasParams, - IndicesDeleteTemplateParams, - IndicesExistsParams, - IndicesExistsAliasParams, - IndicesExistsTemplateParams, - IndicesExistsTypeParams, - IndicesFlushParams, - IndicesFlushSyncedParams, - IndicesForcemergeParams, - IndicesGetParams, - IndicesGetAliasParams, - IndicesGetFieldMappingParams, - IndicesGetMappingParams, - IndicesGetSettingsParams, - IndicesGetTemplateParams, - IndicesGetUpgradeParams, - IndicesOpenParams, - IndicesPutAliasParams, - IndicesPutMappingParams, - IndicesPutSettingsParams, - IndicesPutTemplateParams, - IndicesRecoveryParams, - IndicesRefreshParams, - IndicesRolloverParams, - IndicesSegmentsParams, - IndicesShardStoresParams, - IndicesShrinkParams, - IndicesStatsParams, - IndicesUpdateAliasesParams, - IndicesUpgradeParams, - IndicesValidateQueryParams, - // ingest - IngestDeletePipelineParams, - IngestGetPipelineParams, - IngestPutPipelineParams, - IngestSimulateParams, - // nodes - NodesHotThreadsParams, - NodesInfoParams, - NodesStatsParams, - // snapshot - SnapshotCreateParams, - SnapshotCreateRepositoryParams, - SnapshotDeleteParams, - SnapshotDeleteRepositoryParams, - SnapshotGetParams, - SnapshotGetRepositoryParams, - SnapshotRestoreParams, - SnapshotStatusParams, - SnapshotVerifyRepositoryParams, - // tasks - TasksCancelParams, - TasksGetParams, - TasksListParams, -} from 'elasticsearch'; - -export class Cluster { - public callWithRequest: CallClusterWithRequest; - public callWithInternalUser: CallCluster; - constructor(config: ClusterConfig); -} - -export interface ClusterConfig { - [option: string]: any; -} - -export interface Request { - headers: RequestHeaders; -} - -interface RequestHeaders { - [name: string]: string; -} - -interface AssistantAPIClientParams extends GenericParams { - path: '/_migration/assistance'; - method: 'GET'; -} - -type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; -type MIGRATION_DEPRECATION_LEVEL = 'none' | 'info' | 'warning' | 'critical'; - -export interface AssistanceAPIResponse { - indices: { - [indexName: string]: { - action_required: MIGRATION_ASSISTANCE_INDEX_ACTION; - }; - }; -} - -interface DeprecationAPIClientParams extends GenericParams { - path: '/_migration/deprecations'; - method: 'GET'; -} - -export interface DeprecationInfo { - level: MIGRATION_DEPRECATION_LEVEL; - message: string; - url: string; - details?: string; -} - -export interface IndexSettingsDeprecationInfo { - [indexName: string]: DeprecationInfo[]; -} - -export interface DeprecationAPIResponse { - cluster_settings: DeprecationInfo[]; - ml_settings: DeprecationInfo[]; - node_settings: DeprecationInfo[]; - index_settings: IndexSettingsDeprecationInfo; -} - -export interface CallClusterOptions { - wrap401Errors?: boolean; - signal?: AbortSignal; -} - -export interface CallClusterWithRequest { - /* eslint-disable */ - (request: Request, endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (request: Request, endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (request: Request, endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (request: Request, endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (request: Request, endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (request: Request, endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namepsace - (request: Request, endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (request: Request, endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (request: Request, endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (request: Request, endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (request: Request, endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - ( - request: Request, - endpoint: 'transport.request', - clientParams: AssistantAPIClientParams, - options?: {} - ): Promise; - ( - request: Request, - endpoint: 'transport.request', - clientParams: DeprecationAPIClientParams, - options?: {} - ): Promise; - - // Catch-all definition - ( - request: Request, - endpoint: string, - clientParams?: any, - options?: CallClusterOptions - ): Promise; - /* eslint-enable */ -} - -export interface CallCluster { - /* eslint-disable */ - (endpoint: 'bulk', params: BulkIndexDocumentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'clearScroll', params: ClearScrollParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'count', params: CountParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'create', params: CreateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'delete', params: DeleteDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteByQuery', params: DeleteDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteScript', params: DeleteScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'deleteTemplate', params: DeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'exists', params: ExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'explain', params: ExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'fieldStats', params: FieldStatsParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'get', params: GetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'getScript', params: GetScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getSource', params: GetSourceParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'getTemplate', params: GetTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'index', params: IndexDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'info', params: InfoParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'mget', params: MGetParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearch', params: MSearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'msearchTemplate', params: MSearchTemplateParams, options?: CallClusterOptions): Promise>; - (endpoint: 'mtermvectors', params: MTermVectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ping', params: PingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putScript', params: PutScriptParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'putTemplate', params: PutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindex', params: ReindexParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'reindexRethrottle', params: ReindexRethrottleParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'renderSearchTemplate', params: RenderSearchTemplateParams, options?: CallClusterOptions): ReturnType; - // Generic types cannot be properly looked up with ReturnType. Hard code these explicitly. - (endpoint: 'scroll', params: ScrollParams, options?: CallClusterOptions): Promise>; - (endpoint: 'search', params: SearchParams, options?: CallClusterOptions): Promise>; - (endpoint: 'searchShards', params: SearchShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'searchTemplate', params: SearchTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'suggest', params: SuggestParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'termvectors', params: TermvectorsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'update', params: UpdateDocumentParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'updateByQuery', params: UpdateDocumentByQueryParams, options?: CallClusterOptions): ReturnType; - - // cat namespace - (endpoint: 'cat.aliases', params: CatAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.allocation', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.count', params: CatAllocationParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.fielddata', params: CatFielddataParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.health', params: CatHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.help', params: CatHelpParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.indices', params: CatIndicesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.master', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodeattrs', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.nodes', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.pendingTasks', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.plugins', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.recovery', params: CatRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.repositories', params: CatCommonParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.segments', params: CatSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.shards', params: CatShardsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.snapshots', params: CatSnapshotsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.tasks', params: CatTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cat.threadPool', params: CatThreadPoolParams, options?: CallClusterOptions): ReturnType; - - // cluster namespace - (endpoint: 'cluster.allocationExplain', params: ClusterAllocationExplainParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.getSettings', params: ClusterGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.health', params: ClusterHealthParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.pendingTasks', params: ClusterPendingTasksParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.putSettings', params: ClusterPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.reroute', params: ClusterRerouteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.state', params: ClusterStateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'cluster.stats', params: ClusterStatsParams, options?: CallClusterOptions): ReturnType; - - // indices namespace - (endpoint: 'indices.analyze', params: IndicesAnalyzeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.clearCache', params: IndicesClearCacheParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.close', params: IndicesCloseParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.create', params: IndicesCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.delete', params: IndicesDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteAlias', params: IndicesDeleteAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.deleteTemplate', params: IndicesDeleteTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.exists', params: IndicesExistsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsAlias', params: IndicesExistsAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsTemplate', params: IndicesExistsTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.existsType', params: IndicesExistsTypeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flush', params: IndicesFlushParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.flushSynced', params: IndicesFlushSyncedParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.forcemerge', params: IndicesForcemergeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.get', params: IndicesGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getAlias', params: IndicesGetAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getFieldMapping', params: IndicesGetFieldMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getMapping', params: IndicesGetMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getSettings', params: IndicesGetSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getTemplate', params: IndicesGetTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.getUpgrade', params: IndicesGetUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.open', params: IndicesOpenParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putAlias', params: IndicesPutAliasParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putMapping', params: IndicesPutMappingParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putSettings', params: IndicesPutSettingsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.putTemplate', params: IndicesPutTemplateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.recovery', params: IndicesRecoveryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.refresh', params: IndicesRefreshParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.rollover', params: IndicesRolloverParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.segments', params: IndicesSegmentsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shardStores', params: IndicesShardStoresParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.shrink', params: IndicesShrinkParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.stats', params: IndicesStatsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.updateAliases', params: IndicesUpdateAliasesParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.upgrade', params: IndicesUpgradeParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'indices.validateQuery', params: IndicesValidateQueryParams, options?: CallClusterOptions): ReturnType; - - // ingest namespace - (endpoint: 'ingest.deletePipeline', params: IngestDeletePipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.getPipeline', params: IngestGetPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.putPipeline', params: IngestPutPipelineParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'ingest.simulate', params: IngestSimulateParams, options?: CallClusterOptions): ReturnType; - - // nodes namespace - (endpoint: 'nodes.hotThreads', params: NodesHotThreadsParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.info', params: NodesInfoParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'nodes.stats', params: NodesStatsParams, options?: CallClusterOptions): ReturnType; - - // snapshot namespace - (endpoint: 'snapshot.create', params: SnapshotCreateParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.createRepository', params: SnapshotCreateRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.delete', params: SnapshotDeleteParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.deleteRepository', params: SnapshotDeleteRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.get', params: SnapshotGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.getRepository', params: SnapshotGetRepositoryParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.restore', params: SnapshotRestoreParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.status', params: SnapshotStatusParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'snapshot.verifyRepository', params: SnapshotVerifyRepositoryParams, options?: CallClusterOptions): ReturnType; - - // tasks namespace - (endpoint: 'tasks.cancel', params: TasksCancelParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.get', params: TasksGetParams, options?: CallClusterOptions): ReturnType; - (endpoint: 'tasks.list', params: TasksListParams, options?: CallClusterOptions): ReturnType; - - // other APIs accessed via transport.request - (endpoint: 'transport.request', clientParams: AssistantAPIClientParams, options?: {}): Promise< - AssistanceAPIResponse - >; - (endpoint: 'transport.request', clientParams: DeprecationAPIClientParams, options?: {}): Promise< - DeprecationAPIResponse - >; - - // Catch-all definition - (endpoint: string, clientParams?: any, options?: CallClusterOptions): Promise; - /* eslint-enable */ -} - -export interface ElasticsearchPlugin { - status: { on: (status: string, cb: () => void) => void }; - getCluster(name: string): Cluster; -} diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js deleted file mode 100644 index f90f490d68035..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ /dev/null @@ -1,58 +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 { Cluster } from './server/lib/cluster'; -import { createProxy } from './server/lib/create_proxy'; - -export default function (kibana) { - return new kibana.Plugin({ - require: [], - - async init(server) { - // All methods that ES plugin exposes are synchronous so we should get the first - // value from all observables here to be able to synchronously return and create - // cluster clients afterwards. - const { client } = server.newPlatform.setup.core.elasticsearch.legacy; - const adminCluster = new Cluster(client); - const dataCluster = new Cluster(client); - - const clusters = new Map(); - server.expose('getCluster', (name) => { - if (name === 'admin') { - return adminCluster; - } - - if (name === 'data') { - return dataCluster; - } - - return clusters.get(name); - }); - - server.events.on('stop', () => { - for (const cluster of clusters.values()) { - cluster.close(); - } - - clusters.clear(); - }); - - createProxy(server); - }, - }); -} diff --git a/src/legacy/core_plugins/elasticsearch/package.json b/src/legacy/core_plugins/elasticsearch/package.json deleted file mode 100644 index b5403e1f13de7..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "elasticsearch", - "version": "kibana", - "types": "index.d.ts" -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js deleted file mode 100644 index 0b8786f0c2841..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.js +++ /dev/null @@ -1,34 +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 { AbortController } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; - -/* - * A simple utility for generating a handler that provides a signal to the handler that signals when - * the client has closed the connection on this request. - */ -export function abortableRequestHandler(fn) { - return (req, ...args) => { - const controller = new AbortController(); - req.events.once('disconnect', () => { - controller.abort(); - }); - return fn(controller.signal, req, ...args); - }; -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js b/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js deleted file mode 100644 index d79dd4ae4e449..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/abortable_request_handler.test.js +++ /dev/null @@ -1,58 +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 { AbortSignal } from 'abortcontroller-polyfill/dist/cjs-ponyfill'; -import { abortableRequestHandler } from './abortable_request_handler'; - -describe('abortableRequestHandler', () => { - jest.useFakeTimers(); - - it('should call abort if disconnected', () => { - const eventHandlers = new Map(); - const mockRequest = { - events: { - once: jest.fn((key, fn) => eventHandlers.set(key, fn)), - }, - }; - - const handler = jest.fn(); - const onAborted = jest.fn(); - const abortableHandler = abortableRequestHandler(handler); - abortableHandler(mockRequest); - - const [signal, request] = handler.mock.calls[0]; - - expect(signal instanceof AbortSignal).toBe(true); - expect(request).toBe(mockRequest); - - signal.addEventListener('abort', onAborted); - - // Shouldn't be aborted or call onAborted prior to disconnecting - expect(signal.aborted).toBe(false); - expect(onAborted).not.toBeCalled(); - - expect(eventHandlers.has('disconnect')).toBe(true); - eventHandlers.get('disconnect')(); - jest.runAllTimers(); - - // Should be aborted and call onAborted after disconnecting - expect(signal.aborted).toBe(true); - expect(onAborted).toBeCalled(); - }); -}); diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts b/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts deleted file mode 100644 index 0e7692f6be755..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/cluster.ts +++ /dev/null @@ -1,51 +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 { Request } from 'hapi'; -import { errors } from 'elasticsearch'; -import { LegacyCallAPIOptions, LegacyClusterClient, FakeRequest } from 'kibana/server'; - -export class Cluster { - public readonly errors = errors; - - constructor(private readonly clusterClient: LegacyClusterClient) {} - - public callWithRequest = async ( - req: Request | FakeRequest, - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient - .asScoped(req) - .callAsCurrentUser(endpoint, clientParams, options); - }; - - public callWithInternalUser = async ( - endpoint: string, - clientParams?: Record, - options?: LegacyCallAPIOptions - ) => { - return await this.clusterClient.callAsInternalUser(endpoint, clientParams, options); - }; - - public close() { - this.clusterClient.close(); - } -} diff --git a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js b/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js deleted file mode 100644 index 7302241c46939..0000000000000 --- a/src/legacy/core_plugins/elasticsearch/server/lib/create_proxy.js +++ /dev/null @@ -1,79 +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 Joi from 'joi'; -import { abortableRequestHandler } from './abortable_request_handler'; - -export function createProxy(server) { - const { callWithRequest } = server.plugins.elasticsearch.getCluster('data'); - - server.route({ - method: 'POST', - path: '/elasticsearch/_msearch', - config: { - payload: { - parse: 'gunzip', - }, - }, - handler: abortableRequestHandler((signal, req, h) => { - const { query, payload } = req; - return callWithRequest( - req, - 'transport.request', - { - path: '/_msearch', - method: 'POST', - query, - body: payload.toString('utf8'), - }, - { signal } - ).finally((r) => h.response(r)); - }), - }); - - server.route({ - method: 'POST', - path: '/elasticsearch/{index}/_search', - config: { - validate: { - params: Joi.object().keys({ - index: Joi.string().required(), - }), - }, - }, - handler: abortableRequestHandler(async (signal, req) => { - const { query, payload: body } = req; - try { - return await callWithRequest( - req, - 'transport.request', - { - path: `/${encodeURIComponent(req.params.index)}/_search`, - method: 'POST', - query, - body, - }, - { signal } - ); - } catch (error) { - return JSON.parse(error.response); - } - }), - }); -} diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 8827dc53c5275..3cfda0e0696bb 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -33,7 +33,6 @@ import { import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; // lot of legacy code was assuming this type only had these two methods export type KibanaConfig = Pick; @@ -41,10 +40,7 @@ export type KibanaConfig = Pick; // Extend the defaults with the plugins and server methods we need. declare module 'hapi' { interface PluginProperties { - elasticsearch: ElasticsearchPlugin; - kibana: any; spaces: any; - // add new plugin types here } interface Server { diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts index fd788d3339295..d3a95b32cd425 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.test.ts @@ -139,6 +139,19 @@ describe('calculateHistogramInterval', () => { }) ).toEqual(0.02); }); + + test('should correctly fallback to the default value for empty string', () => { + expect( + calculateHistogramInterval({ + ...params, + maxBucketsUserInput: '', + values: { + min: 0.1, + max: 0.9, + }, + }) + ).toBe(0.01); + }); }); }); }); diff --git a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts index f4e42fa8881ef..378340e876296 100644 --- a/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts +++ b/src/plugins/data/common/search/aggs/buckets/lib/histogram_calculate_interval.ts @@ -27,7 +27,7 @@ interface IntervalValuesRange { export interface CalculateHistogramIntervalParams { interval: string; maxBucketsUiSettings: number; - maxBucketsUserInput?: number; + maxBucketsUserInput?: number | ''; intervalBase?: number; values?: IntervalValuesRange; } @@ -77,12 +77,7 @@ const calculateForGivenInterval = ( - The lower power of 10, times 2 - The lower power of 10, times 5 **/ -const calculateAutoInterval = ( - diff: number, - maxBucketsUiSettings: CalculateHistogramIntervalParams['maxBucketsUiSettings'], - maxBucketsUserInput: CalculateHistogramIntervalParams['maxBucketsUserInput'] -) => { - const maxBars = Math.min(maxBucketsUiSettings, maxBucketsUserInput ?? maxBucketsUiSettings); +const calculateAutoInterval = (diff: number, maxBars: number) => { const exactInterval = diff / maxBars; const lowerPower = Math.pow(10, Math.floor(Math.log10(exactInterval))); @@ -122,7 +117,11 @@ export const calculateHistogramInterval = ({ if (diff) { calculatedInterval = isAuto - ? calculateAutoInterval(diff, maxBucketsUiSettings, maxBucketsUserInput) + ? calculateAutoInterval( + diff, + // Mind maxBucketsUserInput can be an empty string, hence we need to ensure it here + Math.min(maxBucketsUiSettings, maxBucketsUserInput || maxBucketsUiSettings) + ) : calculateForGivenInterval(diff, calculatedInterval, maxBucketsUiSettings); } } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index f159cac664a9e..8e1151b387fee 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -546,13 +546,16 @@ export class QueryStringInputUI extends Component { this.updateSuggestions.cancel(); this.componentIsUnmounting = true; window.removeEventListener('resize', this.handleAutoHeight); - window.removeEventListener('scroll', this.handleListUpdate); + window.removeEventListener('scroll', this.handleListUpdate, { capture: true }); } - handleListUpdate = () => - this.setState({ + handleListUpdate = () => { + if (this.componentIsUnmounting) return; + + return this.setState({ queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(), }); + }; handleAutoHeight = () => { if (this.inputRef !== null && document.activeElement === this.inputRef) { diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx index 7a42ed7fad427..b175066b81c8e 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { act } from 'react-dom/test-utils'; import { mount, ReactWrapper } from 'enzyme'; import sinon from 'sinon'; @@ -111,6 +111,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { requestConfig ); + // Force a re-render of the component to stress-test the useRequest hook and verify its + // state remains unaffected. + const [, setState] = useState(false); + useEffect(() => { + setState(true); + }, []); + hookResult.isInitialRequest = isInitialRequest; hookResult.isLoading = isLoading; hookResult.error = error; diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index e04f84a67b8a3..9d40291423cac 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -49,7 +49,7 @@ export const useRequest = ( // Consumers can use isInitialRequest to implement a polling UX. const requestCountRef = useRef(0); - const isInitialRequest = requestCountRef.current === 0; + const isInitialRequestRef = useRef(true); const pollIntervalIdRef = useRef(null); const clearPollInterval = useCallback(() => { @@ -98,6 +98,9 @@ export const useRequest = ( return; } + // Surface to consumers that at least one request has resolved. + isInitialRequestRef.current = false; + setError(responseError); // If there's an error, keep the data from the last request in case it's still useful to the user. if (!responseError) { @@ -146,7 +149,7 @@ export const useRequest = ( }, [clearPollInterval]); return { - isInitialRequest, + isInitialRequest: isInitialRequestRef.current, isLoading, error, data, diff --git a/test/api_integration/apis/elasticsearch/index.js b/test/api_integration/apis/elasticsearch/index.js deleted file mode 100644 index 5a3dc47aab9bb..0000000000000 --- a/test/api_integration/apis/elasticsearch/index.js +++ /dev/null @@ -1,40 +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. - */ - -export default function ({ getService }) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('elasticsearch', () => { - before(() => esArchiver.load('elasticsearch')); - after(() => esArchiver.unload('elasticsearch')); - - it('allows search to specific index', async () => - await supertest.post('/elasticsearch/elasticsearch/_search').expect(200)); - - it('allows msearch', async () => - await supertest - .post('/elasticsearch/_msearch') - .set('content-type', 'application/x-ndjson') - .send( - '{"index":"logstash-2015.04.21","ignore_unavailable":true}\n{"size":500,"sort":{"@timestamp":"desc"},"query":{"bool":{"must":[{"query_string":{"analyze_wildcard":true,"query":"*"}},{"bool":{"must":[{"range":{"@timestamp":{"gte":1429577068175,"lte":1429577968175}}}],"must_not":[]}}],"must_not":[]}},"highlight":{"pre_tags":["@kibana-highlighted-field@"],"post_tags":["@/kibana-highlighted-field@"],"fields":{"*":{}}},"aggs":{"2":{"date_histogram":{"field":"@timestamp","interval":"30s","min_doc_count":0,"extended_bounds":{"min":1429577068175,"max":1429577968175}}}},"stored_fields":["*"],"_source": true,"script_fields":{},"docvalue_fields":["timestamp_offset","@timestamp","utc_time"]}\n' - ) - .expect(200)); - }); -} diff --git a/test/api_integration/apis/index.js b/test/api_integration/apis/index.js index bfbf873cf0616..d07c099634005 100644 --- a/test/api_integration/apis/index.js +++ b/test/api_integration/apis/index.js @@ -20,7 +20,6 @@ export default function ({ loadTestFile }) { describe('apis', () => { loadTestFile(require.resolve('./core')); - loadTestFile(require.resolve('./elasticsearch')); loadTestFile(require.resolve('./general')); loadTestFile(require.resolve('./home')); loadTestFile(require.resolve('./index_patterns')); diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_baseline.sh similarity index 63% rename from test/scripts/jenkins_visual_regression.sh rename to test/scripts/jenkins_baseline.sh index 17345d4301882..e679ac7f31bd1 100755 --- a/test/scripts/jenkins_visual_regression.sh +++ b/test/scripts/jenkins_baseline.sh @@ -9,10 +9,3 @@ linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" installDir="$PARENT_DIR/install/kibana" mkdir -p "$installDir" tar -xzf "$linuxBuild" -C "$installDir" --strip=1 - -echo " -> running visual regression tests from kibana directory" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_baseline.sh similarity index 64% rename from test/scripts/jenkins_xpack_visual_regression.sh rename to test/scripts/jenkins_xpack_baseline.sh index 55d4a524820c5..7577b6927d166 100755 --- a/test/scripts/jenkins_xpack_visual_regression.sh +++ b/test/scripts/jenkins_xpack_baseline.sh @@ -14,16 +14,5 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1 mkdir -p "$WORKSPACE/kibana-build-xpack" cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/ -# cd "$KIBANA_DIR" -# source "test/scripts/jenkins_xpack_page_load_metrics.sh" - cd "$KIBANA_DIR" source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh" - -echo " -> running visual regression tests from x-pack directory" -cd "$XPACK_DIR" -yarn percy exec -t 10000 -- -- \ - node scripts/functional_tests \ - --debug --bail \ - --kibana-install-dir "$installDir" \ - --config test/visual_regression/config.ts; diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index e5b39584a519b..28eb94405abbb 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -1,4 +1,4 @@ -def withPostBuildReporting(Closure closure) { +def withPostBuildReporting(Map params, Closure closure) { try { closure() } finally { @@ -9,8 +9,10 @@ def withPostBuildReporting(Closure closure) { print ex } - catchErrors { - runErrorReporter([pwd()] + parallelWorkspaces) + if (params.runErrorReporter) { + catchErrors { + runErrorReporter([pwd()] + parallelWorkspaces) + } } catchErrors { diff --git a/vars/workers.groovy b/vars/workers.groovy index e582e996a78b5..b6ff5b27667dd 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -118,11 +118,11 @@ def base(Map params, Closure closure) { // Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing def ci(Map params, Closure closure) { - def config = [ramDisk: true, bootstrapped: true] + params + def config = [ramDisk: true, bootstrapped: true, runErrorReporter: true] + params return base(config) { kibanaPipeline.withGcsArtifactUpload(config.name) { - kibanaPipeline.withPostBuildReporting { + kibanaPipeline.withPostBuildReporting(config) { closure() } } diff --git a/x-pack/dev-tools/jest/create_jest_config.js b/x-pack/dev-tools/jest/create_jest_config.js index a693e008db6ea..e6f160ce8c654 100644 --- a/x-pack/dev-tools/jest/create_jest_config.js +++ b/x-pack/dev-tools/jest/create_jest_config.js @@ -18,7 +18,6 @@ export function createJestConfig({ kibanaDirectory, rootDir, xPackKibanaDirector '^src/core/(.*)': `${kibanaDirectory}/src/core/$1`, '^src/legacy/(.*)': `${kibanaDirectory}/src/legacy/$1`, '^src/plugins/(.*)': `${kibanaDirectory}/src/plugins/$1`, - '^plugins/([^/.]*)(.*)': `${kibanaDirectory}/src/legacy/core_plugins/$1/public$2`, '^legacy/plugins/xpack_main/(.*);': `${xPackKibanaDirectory}/legacy/plugins/xpack_main/public/$1`, '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': fileMockPath, '\\.module.(css|scss)$': `${kibanaDirectory}/src/dev/jest/mocks/css_module_mock.js`, diff --git a/x-pack/index.js b/x-pack/index.js index 074b8e6859dc2..745b4bd72dde8 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -5,9 +5,8 @@ */ import { xpackMain } from './legacy/plugins/xpack_main'; -import { security } from './legacy/plugins/security'; import { spaces } from './legacy/plugins/spaces'; module.exports = function (kibana) { - return [xpackMain(kibana), spaces(kibana), security(kibana)]; + return [xpackMain(kibana), spaces(kibana)]; }; diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts deleted file mode 100644 index c3596d3745e57..0000000000000 --- a/x-pack/legacy/plugins/security/index.ts +++ /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 { Root } from 'joi'; -import { resolve } from 'path'; - -export const security = (kibana: Record) => - new kibana.Plugin({ - id: 'security', - publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], - configPrefix: 'xpack.security', - config: (Joi: Root) => - Joi.object({ enabled: Joi.boolean().default(true) }) - .unknown() - .default(), - init() {}, - }); diff --git a/x-pack/legacy/plugins/spaces/index.ts b/x-pack/legacy/plugins/spaces/index.ts index 725d022879e0d..aec06a4596203 100644 --- a/x-pack/legacy/plugins/spaces/index.ts +++ b/x-pack/legacy/plugins/spaces/index.ts @@ -16,7 +16,7 @@ export const spaces = (kibana: Record) => id: 'spaces', configPrefix: 'xpack.spaces', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch', 'xpack_main'], + require: ['xpack_main'], config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 679c75a0d5942..a3bd66e744fda 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -13,7 +13,7 @@ export const xpackMain = (kibana) => { id: 'xpack_main', configPrefix: 'xpack.xpack_main', publicDir: resolve(__dirname, 'public'), - require: ['elasticsearch'], + require: [], config(Joi) { return Joi.object({ diff --git a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts index 4ec5bc13eea81..3537d1bf42079 100644 --- a/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts +++ b/x-pack/legacy/server/lib/create_router/call_with_request_factory/index.d.ts @@ -5,9 +5,9 @@ */ import { Legacy } from 'kibana'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from '../../../../../../src/core/server'; -export type CallWithRequest = (...args: any[]) => CallCluster; +export type CallWithRequest = (...args: any[]) => LegacyAPICaller; export declare function callWithRequestFactory( server: Legacy.Server, diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index 3524d41646d50..8c233d3691c7f 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -52,6 +52,10 @@ exports[`Error LABEL_NAME 1`] = `undefined`; exports[`Error LCP_FIELD 1`] = `undefined`; +exports[`Error METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Error METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -220,6 +224,10 @@ exports[`Span LABEL_NAME 1`] = `undefined`; exports[`Span LCP_FIELD 1`] = `undefined`; +exports[`Span METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Span METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`; @@ -388,6 +396,10 @@ exports[`Transaction LABEL_NAME 1`] = `undefined`; exports[`Transaction LCP_FIELD 1`] = `undefined`; +exports[`Transaction METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`; + +exports[`Transaction METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`; + exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`; exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts index a1161354e04f4..15a3c642faf32 100644 --- a/x-pack/plugins/apm/common/alert_types.ts +++ b/x-pack/plugins/apm/common/alert_types.ts @@ -7,42 +7,33 @@ import { i18n } from '@kbn/i18n'; export enum AlertType { - ErrorRate = 'apm.error_rate', + ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat. + TransactionErrorRate = 'apm.transaction_error_rate', TransactionDuration = 'apm.transaction_duration', TransactionDurationAnomaly = 'apm.transaction_duration_anomaly', } +const THRESHOLD_MET_GROUP = { + id: 'threshold_met', + name: i18n.translate('xpack.apm.a.thresholdMet', { + defaultMessage: 'Threshold met', + }), +}; + export const ALERT_TYPES_CONFIG = { - [AlertType.ErrorRate]: { - name: i18n.translate('xpack.apm.errorRateAlert.name', { - defaultMessage: 'Error rate', + [AlertType.ErrorCount]: { + name: i18n.translate('xpack.apm.errorCountAlert.name', { + defaultMessage: 'Error count threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate('xpack.apm.errorRateAlert.thresholdMet', { - defaultMessage: 'Threshold met', - }), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, [AlertType.TransactionDuration]: { name: i18n.translate('xpack.apm.transactionDurationAlert.name', { - defaultMessage: 'Transaction duration', + defaultMessage: 'Transaction duration threshold', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, @@ -50,39 +41,24 @@ export const ALERT_TYPES_CONFIG = { name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', { defaultMessage: 'Transaction duration anomaly', }), - actionGroups: [ - { - id: 'threshold_met', - name: i18n.translate( - 'xpack.apm.transactionDurationAlert.thresholdMet', - { - defaultMessage: 'Threshold met', - } - ), - }, - ], + actionGroups: [THRESHOLD_MET_GROUP], + defaultActionGroupId: 'threshold_met', + producer: 'apm', + }, + [AlertType.TransactionErrorRate]: { + name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', { + defaultMessage: 'Transaction error rate threshold', + }), + actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: 'threshold_met', producer: 'apm', }, }; -export const TRANSACTION_ALERT_AGGREGATION_TYPES = { - avg: i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.avg', - { - defaultMessage: 'Average', - } - ), - '95th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.95th', - { - defaultMessage: '95th percentile', - } - ), - '99th': i18n.translate( - 'xpack.apm.transactionDurationAlert.aggregationType.99th', - { - defaultMessage: '99th percentile', - } - ), -}; +// Server side registrations +// x-pack/plugins/apm/server/lib/alerts/.ts +// x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts + +// Client side registrations: +// x-pack/plugins/apm/public/components/alerting//index.tsx +// x-pack/plugins/apm/public/components/alerting/register_apm_alerts diff --git a/x-pack/plugins/apm/common/custom_link/index.ts b/x-pack/plugins/apm/common/custom_link/index.ts new file mode 100644 index 0000000000000..bc0ffefd79c4d --- /dev/null +++ b/x-pack/plugins/apm/common/custom_link/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 { i18n } from '@kbn/i18n'; + +export const INVALID_LICENSE = i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.license.text', + { + defaultMessage: + "To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services.", + } +); diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index 612cb18bbe190..cc6a1fffb2288 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -79,6 +79,10 @@ export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free'; export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total'; export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct'; export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct'; +export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = + 'system.process.cgroup.memory.mem.limit.bytes'; +export const METRIC_CGROUP_MEMORY_USAGE_BYTES = + 'system.process.cgroup.memory.mem.usage.bytes'; export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max'; export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed'; diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx similarity index 83% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx index 632d53a9c63b6..c30cef7210a43 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx @@ -6,14 +6,14 @@ import { storiesOf } from '@storybook/react'; import React from 'react'; -import { ErrorRateAlertTrigger } from '.'; +import { ErrorCountAlertTrigger } from '.'; import { ApmPluginContextValue } from '../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../context/ApmPluginContext/MockApmPluginContext'; -storiesOf('app/ErrorRateAlertTrigger', module).add( +storiesOf('app/ErrorCountAlertTrigger', module).add( 'example', () => { const params = { @@ -26,7 +26,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( value={(mockApmPluginContextValue as unknown) as ApmPluginContextValue} >
- undefined} setAlertProperty={() => undefined} @@ -37,7 +37,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add( }, { info: { - propTablesExclude: [ErrorRateAlertTrigger, MockApmPluginContextWrapper], + propTablesExclude: [ErrorCountAlertTrigger, MockApmPluginContextWrapper], source: false, }, } diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx similarity index 54% rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx index 7b284696477f3..a465b90e7bf05 100644 --- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx @@ -3,36 +3,33 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiFieldNumber, EuiSelect } from '@elastic/eui'; + import { i18n } from '@kbn/i18n'; -import { isFinite } from 'lodash'; import React from 'react'; import { useParams } from 'react-router-dom'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; -import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { useEnvironments } from '../../../hooks/useEnvironments'; import { useUrlParams } from '../../../hooks/useUrlParams'; +import { EnvironmentField, ServiceField, IsAboveField } from '../fields'; import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; -import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression'; -export interface ErrorRateAlertTriggerParams { +export interface AlertParams { windowSize: number; windowUnit: string; threshold: number; + serviceName: string; environment: string; } interface Props { - alertParams: ErrorRateAlertTriggerParams; + alertParams: AlertParams; setAlertParams: (key: string, value: any) => void; setAlertProperty: (key: string, value: any) => void; } -export function ErrorRateAlertTrigger(props: Props) { +export function ErrorCountAlertTrigger(props: Props) { const { setAlertParams, setAlertProperty, alertParams } = props; const { serviceName } = useParams<{ serviceName?: string }>(); const { urlParams } = useUrlParams(); @@ -51,45 +48,20 @@ export function ErrorRateAlertTrigger(props: Props) { ...alertParams, }; - const threshold = isFinite(params.threshold) ? params.threshold : ''; - const fields = [ - - - setAlertParams( - 'environment', - e.target.value as ErrorRateAlertTriggerParams['environment'] - ) - } - compressed - /> - , - , + setAlertParams('environment', e.target.value)} + />, + - - setAlertParams('threshold', parseInt(e.target.value, 10)) - } - compressed - append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', { - defaultMessage: 'errors', - })} - /> - , + onChange={(value) => setAlertParams('threshold', value)} + />, setAlertParams('windowSize', windowSize || '') @@ -108,7 +80,7 @@ export function ErrorRateAlertTrigger(props: Props) { return ( void; setAlertProperty: (key: string, value: any) => void; } export function TransactionDurationAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); - const transactionTypes = useServiceTransactionTypes(urlParams); - - const { start, end } = urlParams; + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); - if (!transactionTypes.length) { + if (!transactionTypes.length || !serviceName) { return null; } @@ -57,7 +77,9 @@ export function TransactionDurationAlertTrigger(props: Props) { aggregationType: 'avg', windowSize: 5, windowUnit: 'm', - transactionType: transactionTypes[0], + + // use the current transaction type or default to the first in the list + transactionType: transactionType || transactionTypes[0], environment: urlParams.environment || ENVIRONMENT_ALL.value, }; @@ -67,47 +89,17 @@ export function TransactionDurationAlertTrigger(props: Props) { }; const fields = [ - - - setAlertParams('environment', e.target.value as Params['environment']) - } - compressed - /> - , - - { - return { - text: key, - value: key, - }; - })} - onChange={(e) => - setAlertParams( - 'transactionType', - e.target.value as Params['transactionType'] - ) - } - compressed - /> - , + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, - setAlertParams( - 'aggregationType', - e.target.value as Params['aggregationType'] - ) - } - compressed - /> - , - - setAlertParams('threshold', e.target.value)} - append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', { - defaultMessage: 'ms', - })} + onChange={(e) => setAlertParams('aggregationType', e.target.value)} compressed /> , + setAlertParams('threshold', value)} + />, setAlertParams('windowSize', timeWindowSize || '') diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx similarity index 100% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx similarity index 75% rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx index 20e0a3f27c4a4..fb4cda56fce04 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { EuiExpression, EuiSelect } from '@elastic/eui'; + +import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types'; @@ -16,14 +17,16 @@ import { AnomalySeverity, SelectAnomalySeverity, } from './SelectAnomalySeverity'; -import { - ENVIRONMENT_ALL, - getEnvironmentLabel, -} from '../../../../common/environment_filter_values'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { TRANSACTION_PAGE_LOAD, TRANSACTION_REQUEST, } from '../../../../common/transaction_types'; +import { + EnvironmentField, + ServiceField, + TransactionTypeField, +} from '../fields'; interface Params { windowSize: number; @@ -42,9 +45,9 @@ interface Props { export function TransactionDurationAnomalyAlertTrigger(props: Props) { const { setAlertParams, alertParams, setAlertProperty } = props; - const { serviceName } = alertParams; const { urlParams } = useUrlParams(); const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); const { start, end } = urlParams; const { environmentOptions } = useEnvironments({ serviceName, start, end }); const supportedTransactionTypes = transactionTypes.filter((transactionType) => @@ -55,10 +58,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { return null; } + // 'page-load' for RUM, 'request' otherwise + const transactionType = supportedTransactionTypes[0]; + const defaults: Params = { windowSize: 15, windowUnit: 'm', - transactionType: supportedTransactionTypes[0], // 'page-load' for RUM, 'request' otherwise + transactionType, serviceName, environment: urlParams.environment || ENVIRONMENT_ALL.value, anomalyScore: 75, @@ -70,31 +76,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { }; const fields = [ - , + , + setAlertParams('environment', e.target.value)} />, - - setAlertParams('environment', e.target.value)} - compressed - /> - , } title={i18n.translate( diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx new file mode 100644 index 0000000000000..4dbf4dc10a907 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx @@ -0,0 +1,110 @@ +/* + * 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 { useParams } from 'react-router-dom'; +import React from 'react'; +import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; +import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types'; +import { useEnvironments } from '../../../hooks/useEnvironments'; +import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes'; +import { useUrlParams } from '../../../hooks/useUrlParams'; +import { ServiceAlertTrigger } from '../ServiceAlertTrigger'; + +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { + ServiceField, + TransactionTypeField, + EnvironmentField, + IsAboveField, +} from '../fields'; + +interface AlertParams { + windowSize: number; + windowUnit: string; + threshold: number; + serviceName: string; + transactionType: string; + environment: string; +} + +interface Props { + alertParams: AlertParams; + setAlertParams: (key: string, value: any) => void; + setAlertProperty: (key: string, value: any) => void; +} + +export function TransactionErrorRateAlertTrigger(props: Props) { + const { setAlertParams, alertParams, setAlertProperty } = props; + const { urlParams } = useUrlParams(); + const transactionTypes = useServiceTransactionTypes(urlParams); + const { serviceName } = useParams<{ serviceName?: string }>(); + const { start, end, transactionType } = urlParams; + const { environmentOptions } = useEnvironments({ serviceName, start, end }); + + if (!transactionTypes.length || !serviceName) { + return null; + } + + const defaultParams = { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: transactionType || transactionTypes[0], + environment: urlParams.environment || ENVIRONMENT_ALL.value, + }; + + const params = { + ...defaultParams, + ...alertParams, + }; + + const fields = [ + , + setAlertParams('environment', e.target.value)} + />, + ({ text: key, value: key }))} + onChange={(e) => setAlertParams('transactionType', e.target.value)} + />, + setAlertParams('threshold', value)} + />, + + setAlertParams('windowSize', timeWindowSize || '') + } + onChangeWindowUnit={(timeWindowUnit) => + setAlertParams('windowUnit', timeWindowUnit) + } + timeWindowSize={params.windowSize} + timeWindowUnit={params.windowUnit} + errors={{ + timeWindowSize: [], + timeWindowUnit: [], + }} + />, + ]; + + return ( + + ); +} + +// Default export is required for React.lazy loading +// +// eslint-disable-next-line import/no-default-export +export default TransactionErrorRateAlertTrigger; diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx new file mode 100644 index 0000000000000..e145d03671a18 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -0,0 +1,108 @@ +/* + * 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 { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSelectOption } from '@elastic/eui'; +import { getEnvironmentLabel } from '../../../common/environment_filter_values'; +import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression'; + +export function ServiceField({ value }: { value?: string }) { + return ( + + ); +} + +export function EnvironmentField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options: EuiSelectOption[]; + onChange: (event: React.ChangeEvent) => void; +}) { + return ( + + + + ); +} + +export function TransactionTypeField({ + currentValue, + options, + onChange, +}: { + currentValue: string; + options?: EuiSelectOption[]; + onChange?: (event: React.ChangeEvent) => void; +}) { + const label = i18n.translate('xpack.apm.alerting.fields.type', { + defaultMessage: 'Type', + }); + + if (!options || options.length === 1) { + return ; + } + + return ( + + + + ); +} + +export function IsAboveField({ + value, + unit, + onChange, + step, +}: { + value: number; + unit: string; + onChange: (value: number) => void; + step?: number; +}) { + return ( + + onChange(parseInt(e.target.value, 10))} + append={unit} + compressed + step={step} + /> + + ); +} diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts new file mode 100644 index 0000000000000..c0a1955e2cc8a --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -0,0 +1,72 @@ +/* + * 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 { lazy } from 'react'; +import { AlertType } from '../../../common/alert_types'; +import { ApmPluginStartDeps } from '../../plugin'; + +export function registerApmAlerts( + alertTypeRegistry: ApmPluginStartDeps['triggers_actions_ui']['alertTypeRegistry'] +) { + alertTypeRegistry.register({ + id: AlertType.ErrorCount, + name: i18n.translate('xpack.apm.alertTypes.errorCount', { + defaultMessage: 'Error count threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDuration, + name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { + defaultMessage: 'Transaction duration threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionErrorRate, + name: i18n.translate('xpack.apm.alertTypes.transactionErrorRate', { + defaultMessage: 'Transaction error rate threshold', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionErrorRateAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); + + alertTypeRegistry.register({ + id: AlertType.TransactionDurationAnomaly, + name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { + defaultMessage: 'Transaction duration anomaly', + }), + iconClass: 'bell', + alertParamsExpression: lazy( + () => import('./TransactionDurationAnomalyAlertTrigger') + ), + validate: () => ({ + errors: [], + }), + requiresAppContext: true, + }); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx index 27c4a37e09c00..c11bfdeae945b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx @@ -24,9 +24,13 @@ const transactionDurationLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.transactionDuration', { defaultMessage: 'Transaction duration' } ); -const errorRateLabel = i18n.translate( - 'xpack.apm.serviceDetails.alertsMenu.errorRate', - { defaultMessage: 'Error rate' } +const transactionErrorRateLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate', + { defaultMessage: 'Transaction error rate' } +); +const errorCountLabel = i18n.translate( + 'xpack.apm.serviceDetails.alertsMenu.errorCount', + { defaultMessage: 'Error count' } ); const createThresholdAlertLabel = i18n.translate( 'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert', @@ -38,8 +42,10 @@ const createAnomalyAlertAlertLabel = i18n.translate( ); const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID = - 'create_transaction_duration'; -const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate'; + 'create_transaction_duration_panel'; +const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID = + 'create_transaction_error_rate_panel'; +const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel'; interface Props { canReadAlerts: boolean; @@ -77,7 +83,14 @@ export function AlertIntegrations(props: Props) { name: transactionDurationLabel, panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, }, - { name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID }, + { + name: transactionErrorRateLabel, + panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + }, + { + name: errorCountLabel, + panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + }, ] : []), ...(canReadAlerts @@ -96,10 +109,13 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction duration panel { id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID, title: transactionDurationLabel, items: [ + // threshold alerts { name: createThresholdAlertLabel, onClick: () => { @@ -107,6 +123,8 @@ export function AlertIntegrations(props: Props) { setPopoverOpen(false); }, }, + + // anomaly alerts ...(canReadAnomalies ? [ { @@ -120,14 +138,32 @@ export function AlertIntegrations(props: Props) { : []), ], }, + + // transaction error rate panel + { + id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID, + title: transactionErrorRateLabel, + items: [ + // threshold alerts + { + name: createThresholdAlertLabel, + onClick: () => { + setAlertType(AlertType.TransactionErrorRate); + setPopoverOpen(false); + }, + }, + ], + }, + + // error alerts panel { - id: CREATE_ERROR_RATE_ALERT_PANEL_ID, - title: errorRateLabel, + id: CREATE_ERROR_COUNT_ALERT_PANEL_ID, + title: errorCountLabel, items: [ { name: createThresholdAlertLabel, onClick: () => { - setAlertType(AlertType.ErrorRate); + setAlertType(AlertType.ErrorCount); setPopoverOpen(false); }, }, diff --git a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx index aa34515ea460a..45a7fa2a118f2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx @@ -7,7 +7,7 @@ import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import React, { useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; +import { INVALID_LICENSE } from '../../../../../../common/custom_link'; import { CustomLink } from '../../../../../../common/custom_link/custom_link_types'; import { useLicense } from '../../../../../hooks/useLicense'; import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher'; @@ -94,15 +94,7 @@ export function CustomLinkOverview() { /> ) ) : ( - + )} diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index 51ac6673251fb..ab3f1026a92dd 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { i18n } from '@kbn/i18n'; -import { lazy } from 'react'; import { ConfigSchema } from '.'; import { FetchDataParams, @@ -34,10 +32,10 @@ import { TriggersAndActionsUIPublicPluginSetup, TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; -import { AlertType } from '../common/alert_types'; import { featureCatalogueEntry } from './featureCatalogueEntry'; import { toggleAppLinkInNav } from './toggleAppLinkInNav'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { registerApmAlerts } from './components/alerting/register_apm_alerts'; export type ApmPluginSetup = void; export type ApmPluginStart = void; @@ -147,51 +145,6 @@ export class ApmPlugin implements Plugin { } public start(core: CoreStart, plugins: ApmPluginStartDeps) { toggleAppLinkInNav(core, this.initializerContext.config.get()); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.ErrorRate, - name: i18n.translate('xpack.apm.alertTypes.errorRate', { - defaultMessage: 'Error rate', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/ErrorRateAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDuration, - name: i18n.translate('xpack.apm.alertTypes.transactionDuration', { - defaultMessage: 'Transaction duration', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => import('./components/shared/TransactionDurationAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); - - plugins.triggers_actions_ui.alertTypeRegistry.register({ - id: AlertType.TransactionDurationAnomaly, - name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', { - defaultMessage: 'Transaction duration anomaly', - }), - iconClass: 'bell', - alertParamsExpression: lazy( - () => - import('./components/shared/TransactionDurationAnomalyAlertTrigger') - ), - validate: () => ({ - errors: [], - }), - requiresAppContext: true, - }); + registerApmAlerts(plugins.triggers_actions_ui.alertTypeRegistry); } } diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index 0f6061653f352..1cda70a140c67 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -5,7 +5,12 @@ */ import { i18n } from '@kbn/i18n'; +import { LicenseType } from '../../licensing/common/types'; import { AlertType } from '../common/alert_types'; +import { + LicensingPluginSetup, + LicensingRequestHandlerContext, +} from '../../licensing/server'; export const APM_FEATURE = { id: 'apm', @@ -58,5 +63,43 @@ export const APM_FEATURE = { }, }; -export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps'; -export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum'; +interface Feature { + name: string; + license: LicenseType; +} +type FeatureName = 'serviceMaps' | 'ml' | 'customLinks'; +export const features: Record = { + serviceMaps: { + name: 'APM service maps', + license: 'platinum', + }, + ml: { + name: 'APM machine learning', + license: 'platinum', + }, + customLinks: { + name: 'APM custom links', + license: 'gold', + }, +}; + +export function registerFeaturesUsage({ + licensingPlugin, +}: { + licensingPlugin: LicensingPluginSetup; +}) { + Object.values(features).forEach(({ name, license }) => { + licensingPlugin.featureUsage.register(name, license); + }); +} + +export function notifyFeatureUsage({ + licensingPlugin, + featureName, +}: { + licensingPlugin: LicensingRequestHandlerContext; + featureName: FeatureName; +}) { + const feature = features[featureName]; + licensingPlugin.featureUsage.notifyUsage(feature.name); +} diff --git a/x-pack/plugins/apm/server/lib/alerts/action_variables.ts b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts new file mode 100644 index 0000000000000..f2558da3a30e4 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts @@ -0,0 +1,48 @@ +/* + * 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 const apmActionVariables = { + serviceName: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.serviceName', + { defaultMessage: 'The service the alert is created for' } + ), + name: 'serviceName', + }, + transactionType: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.transactionType', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'transactionType', + }, + environment: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.environment', + { defaultMessage: 'The transaction type the alert is created for' } + ), + name: 'environment', + }, + threshold: { + description: i18n.translate('xpack.apm.alerts.action_variables.threshold', { + defaultMessage: + 'Any trigger value above this value will cause the alert to fire', + }), + name: 'threshold', + }, + triggerValue: { + description: i18n.translate( + 'xpack.apm.alerts.action_variables.triggerValue', + { + defaultMessage: + 'The value that breached the threshold and triggered the alert', + } + ), + name: 'triggerValue', + }, +}; diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts index 44ca80143bcd9..fcbb4cc5950e0 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts @@ -9,9 +9,10 @@ import { AlertingPlugin } from '../../../../alerts/server'; import { ActionsPlugin } from '../../../../actions/server'; import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type'; import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; -import { registerErrorRateAlertType } from './register_error_rate_alert_type'; +import { registerErrorCountAlertType } from './register_error_count_alert_type'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; +import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type'; interface Params { alerts: AlertingPlugin['setup']; @@ -30,7 +31,11 @@ export function registerApmAlerts(params: Params) { ml: params.ml, config$: params.config$, }); - registerErrorRateAlertType({ + registerErrorCountAlertType({ + alerts: params.alerts, + config$: params.config$, + }); + registerTransactionErrorRateAlertType({ alerts: params.alerts, config$: params.config$, }); diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts similarity index 66% rename from x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts rename to x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 61e3dfee420a5..5455cd9f6a495 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse, @@ -17,11 +17,11 @@ import { import { PROCESSOR_EVENT, SERVICE_NAME, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -29,21 +29,21 @@ interface RegisterAlertParams { } const paramsSchema = schema.object({ - serviceName: schema.string(), windowSize: schema.number(), windowUnit: schema.string(), threshold: schema.number(), + serviceName: schema.string(), environment: schema.string(), }); -const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate]; +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount]; -export function registerErrorRateAlertType({ +export function registerErrorCountAlertType({ alerts, config$, }: RegisterAlertParams) { alerts.registerType({ - id: AlertType.ErrorRate, + id: AlertType.ErrorCount, name: alertTypeConfig.name, actionGroups: alertTypeConfig.actionGroups, defaultActionGroupId: alertTypeConfig.defaultActionGroupId, @@ -52,37 +52,26 @@ export function registerErrorRateAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerErrorRateAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.errorIndices'], size: 0, body: { + track_total_hits: true, query: { bool: { filter: [ @@ -93,21 +82,12 @@ export function registerErrorRateAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'error', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, - track_total_hits: true, }, }; @@ -116,18 +96,19 @@ export function registerErrorRateAlertType({ ESSearchRequest > = await services.callCluster('search', searchParams); - const value = response.hits.total.value; + const errorCount = response.hits.total.value; - if (value && value > alertParams.threshold) { + if (errorCount > alertParams.threshold) { const alertInstance = services.alertInstanceFactory( - AlertType.ErrorRate + AlertType.ErrorCount ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: errorCount, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts index ead28c325692d..373d4bd4da832 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { take } from 'rxjs/operators'; -import { i18n } from '@kbn/i18n'; -import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { ProcessorEvent } from '../../../common/processor_event'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { ESSearchResponse } from '../../../typings/elasticsearch'; import { @@ -16,11 +15,12 @@ import { SERVICE_NAME, TRANSACTION_TYPE, TRANSACTION_DURATION, - SERVICE_ENVIRONMENT, } from '../../../common/elasticsearch_fieldnames'; import { AlertingPlugin } from '../../../../alerts/server'; import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -57,42 +57,22 @@ export function registerTransactionDurationAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, ], }, producer: 'apm', executor: async ({ services, params }) => { const config = await config$.pipe(take(1)).toPromise(); - - const alertParams = params as TypeOf; - + const alertParams = params; const indices = await getApmIndices({ config, savedObjectsClient: services.savedObjectsClient, }); - const environmentTerm = - alertParams.environment === ENVIRONMENT_ALL.value - ? [] - : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }]; - const searchParams = { index: indices['apm_oss.transactionIndices'], size: 0, @@ -107,33 +87,17 @@ export function registerTransactionDurationAlertType({ }, }, }, - { - term: { - [PROCESSOR_EVENT]: 'transaction', - }, - }, - { - term: { - [SERVICE_NAME]: alertParams.serviceName, - }, - }, - { - term: { - [TRANSACTION_TYPE]: alertParams.transactionType, - }, - }, - ...environmentTerm, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), ], }, }, aggs: { agg: alertParams.aggregationType === 'avg' - ? { - avg: { - field: TRANSACTION_DURATION, - }, - } + ? { avg: { field: TRANSACTION_DURATION } } : { percentiles: { field: TRANSACTION_DURATION, @@ -157,19 +121,23 @@ export function registerTransactionDurationAlertType({ const { agg } = response.aggregations; - const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value; + const transactionDuration = + 'values' in agg ? Object.values(agg.values)[0] : agg?.value; - if (value && value > alertParams.threshold * 1000) { + const threshold = alertParams.threshold * 1000; + + if (transactionDuration && transactionDuration > threshold) { const alertInstance = services.alertInstanceFactory( AlertType.TransactionDuration ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { transactionType: alertParams.transactionType, serviceName: alertParams.serviceName, + environment: alertParams.environment, + threshold, + triggerValue: transactionDuration, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 93af51b572aa5..b3526b6a97ad9 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { schema } from '@kbn/config-schema'; import { Observable } from 'rxjs'; -import { i18n } from '@kbn/i18n'; import { KibanaRequest } from '../../../../../../src/core/server'; import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; import { AlertingPlugin } from '../../../../alerts/server'; import { APMConfig } from '../..'; import { MlPluginSetup } from '../../../../ml/server'; import { getMLJobIds } from '../service_map/get_service_anomalies'; +import { apmActionVariables } from './action_variables'; interface RegisterAlertParams { alerts: AlertingPlugin['setup']; @@ -47,24 +47,9 @@ export function registerTransactionDurationAnomalyAlertType({ }, actionVariables: { context: [ - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.serviceName', - { - defaultMessage: 'Service name', - } - ), - name: 'serviceName', - }, - { - description: i18n.translate( - 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.transactionType', - { - defaultMessage: 'Transaction type', - } - ), - name: 'transactionType', - }, + apmActionVariables.serviceName, + apmActionVariables.transactionType, + apmActionVariables.environment, ], }, producer: 'apm', @@ -72,7 +57,7 @@ export function registerTransactionDurationAnomalyAlertType({ if (!ml) { return; } - const alertParams = params as TypeOf; + const alertParams = params; const request = {} as KibanaRequest; const { mlAnomalySearch } = ml.mlSystemProvider(request); const anomalyDetectors = ml.anomalyDetectorsProvider(request); @@ -88,6 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({ const anomalySearchParams = { body: { + terminateAfter: 1, size: 0, query: { bool: { @@ -131,10 +117,10 @@ export function registerTransactionDurationAnomalyAlertType({ ); alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, }); } - - return {}; }, }); } diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts new file mode 100644 index 0000000000000..a6ed40fc15ec6 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -0,0 +1,131 @@ +/* + * 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 { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { EventOutcome } from '../../../common/event_outcome'; +import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types'; +import { ESSearchResponse } from '../../../typings/elasticsearch'; +import { + PROCESSOR_EVENT, + SERVICE_NAME, + TRANSACTION_TYPE, + EVENT_OUTCOME, +} from '../../../common/elasticsearch_fieldnames'; +import { AlertingPlugin } from '../../../../alerts/server'; +import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; +import { APMConfig } from '../..'; +import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { apmActionVariables } from './action_variables'; + +interface RegisterAlertParams { + alerts: AlertingPlugin['setup']; + config$: Observable; +} + +const paramsSchema = schema.object({ + windowSize: schema.number(), + windowUnit: schema.string(), + threshold: schema.number(), + transactionType: schema.string(), + serviceName: schema.string(), + environment: schema.string(), +}); + +const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate]; + +export function registerTransactionErrorRateAlertType({ + alerts, + config$, +}: RegisterAlertParams) { + alerts.registerType({ + id: AlertType.TransactionErrorRate, + name: alertTypeConfig.name, + actionGroups: alertTypeConfig.actionGroups, + defaultActionGroupId: alertTypeConfig.defaultActionGroupId, + validate: { + params: paramsSchema, + }, + actionVariables: { + context: [ + apmActionVariables.transactionType, + apmActionVariables.serviceName, + apmActionVariables.environment, + apmActionVariables.threshold, + apmActionVariables.triggerValue, + ], + }, + producer: 'apm', + executor: async ({ services, params: alertParams }) => { + const config = await config$.pipe(take(1)).toPromise(); + const indices = await getApmIndices({ + config, + savedObjectsClient: services.savedObjectsClient, + }); + + const searchParams = { + index: indices['apm_oss.transactionIndices'], + size: 0, + body: { + track_total_hits: true, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`, + }, + }, + }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { term: { [SERVICE_NAME]: alertParams.serviceName } }, + { term: { [TRANSACTION_TYPE]: alertParams.transactionType } }, + ...getEnvironmentUiFilterES(alertParams.environment), + ], + }, + }, + aggs: { + erroneous_transactions: { + filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + }, + }, + }, + }; + + const response: ESSearchResponse< + unknown, + typeof searchParams + > = await services.callCluster('search', searchParams); + + if (!response.aggregations) { + return; + } + + const errornousTransactionsCount = + response.aggregations.erroneous_transactions.doc_count; + const totalTransactionCount = response.hits.total.value; + const transactionErrorRate = + (errornousTransactionsCount / totalTransactionCount) * 100; + + if (transactionErrorRate > alertParams.threshold) { + const alertInstance = services.alertInstanceFactory( + AlertType.TransactionErrorRate + ); + + alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, { + serviceName: alertParams.serviceName, + transactionType: alertParams.transactionType, + environment: alertParams.environment, + threshold: alertParams.threshold, + triggerValue: transactionErrorRate, + }); + } + }, + }); +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 7bcd945d890ad..d0673335387c6 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -8,6 +8,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; import Boom from 'boom'; +import { ProcessorEvent } from '../../../common/processor_event'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; @@ -79,7 +80,7 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { exists: { field: TRANSACTION_DURATION } }, ...getEnvironmentUiFilterES(environment), ], diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts index a53068d152d03..fcd4f468d4367 100644 --- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts +++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts @@ -85,7 +85,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, { range: { '@timestamp': { gte: start, lt: end } } }, ], }, @@ -606,7 +606,10 @@ export const tasks: TelemetryTask[] = [ timeout, query: { bool: { - filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, range1d], + filter: [ + { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, + range1d, + ], }, }, aggs: { @@ -640,7 +643,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], }, @@ -674,7 +677,7 @@ export const tasks: TelemetryTask[] = [ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, range1d, ], must_not: { diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts index 6ff98a9be75f9..ea8d02eb833cf 100644 --- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts +++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts @@ -5,11 +5,14 @@ */ import { ESFilter } from '../../../../typings/elasticsearch'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values'; +import { + ENVIRONMENT_NOT_DEFINED, + ENVIRONMENT_ALL, +} from '../../../../common/environment_filter_values'; import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames'; export function getEnvironmentUiFilterES(environment?: string): ESFilter[] { - if (!environment) { + if (!environment || environment === ENVIRONMENT_ALL.value) { return []; } if (environment === ENVIRONMENT_NOT_DEFINED.value) { diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap index b88c90a213c67..2868dcfda97b6 100644 --- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap @@ -203,16 +203,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -221,16 +255,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -275,12 +343,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -682,16 +745,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -700,16 +797,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -760,12 +891,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], @@ -1157,16 +1283,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1175,16 +1335,50 @@ Object { "memoryUsedAvg": Object { "avg": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, "memoryUsedMax": Object { "max": Object { "script": Object { - "lang": "expression", - "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']", + "lang": "painless", + "source": " + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = 'system.process.cgroup.memory.mem.limit.bytes'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value; + + double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value; + + return used / total; + ", }, }, }, @@ -1224,12 +1418,7 @@ Object { }, Object { "exists": Object { - "field": "system.memory.actual.free", - }, - }, - Object { - "exists": Object { - "field": "system.memory.total", + "field": "system.process.cgroup.memory.mem.usage.bytes", }, }, ], diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts index 316b0d59d2c5b..a60576ca0c175 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts @@ -6,6 +6,8 @@ import { i18n } from '@kbn/i18n'; import { + METRIC_CGROUP_MEMORY_LIMIT_BYTES, + METRIC_CGROUP_MEMORY_USAGE_BYTES, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, } from '../../../../../../common/elasticsearch_fieldnames'; @@ -14,8 +16,8 @@ import { SetupTimeRange, SetupUIFilters, } from '../../../../helpers/setup_request'; -import { ChartBase } from '../../../types'; import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics'; +import { ChartBase } from '../../../types'; const series = { memoryUsedMax: { @@ -43,36 +45,68 @@ const chartBase: ChartBase = { series, }; -export const percentMemoryUsedScript = { +export const percentSystemMemoryUsedScript = { lang: 'expression', source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`, }; +export const percentCgroupMemoryUsedScript = { + lang: 'painless', + source: ` + /* + When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants. + This number represents the max possible value for the limit field. + */ + double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L; + + String limitKey = '${METRIC_CGROUP_MEMORY_LIMIT_BYTES}'; + + //Should use cgropLimit when value is not empty and not equals to the max limit value. + boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE; + + double total = useCgroupLimit ? doc[limitKey].value : doc['${METRIC_SYSTEM_TOTAL_MEMORY}'].value; + + double used = doc['${METRIC_CGROUP_MEMORY_USAGE_BYTES}'].value; + + return used / total; + `, +}; + export async function getMemoryChartData( setup: Setup & SetupTimeRange & SetupUIFilters, serviceName: string, serviceNodeName?: string ) { - return fetchAndTransformMetrics({ + const cgroupResponse = await fetchAndTransformMetrics({ setup, serviceName, serviceNodeName, chartBase, aggs: { - memoryUsedAvg: { avg: { script: percentMemoryUsedScript } }, - memoryUsedMax: { max: { script: percentMemoryUsedScript } }, + memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } }, }, additionalFilters: [ - { - exists: { - field: METRIC_SYSTEM_FREE_MEMORY, - }, - }, - { - exists: { - field: METRIC_SYSTEM_TOTAL_MEMORY, - }, - }, + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, ], }); + + if (cgroupResponse.noHits) { + return await fetchAndTransformMetrics({ + setup, + serviceName, + serviceNodeName, + chartBase, + aggs: { + memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } }, + memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } }, + }, + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + }); + } + + return cgroupResponse; } diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 1a7d602882395..f25062c67f87a 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -67,7 +67,7 @@ export async function getPageViewTrends({ x: xVal, y: bCount, }; - if (breakdownItem) { + if ('breakdown' in bucket) { const categoryBuckets = bucket.breakdown.buckets; categoryBuckets.forEach(({ key, doc_count: docCount }) => { if (key === 'Other') { diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 88cc26608b850..5c183fd9150dd 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -14,13 +14,17 @@ import { METRIC_SYSTEM_CPU_PERCENT, METRIC_SYSTEM_FREE_MEMORY, METRIC_SYSTEM_TOTAL_MEMORY, + METRIC_CGROUP_MEMORY_USAGE_BYTES, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { ESFilter } from '../../../typings/elasticsearch'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; -import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory'; +import { + percentCgroupMemoryUsedScript, + percentSystemMemoryUsedScript, +} from '../metrics/by_agent/shared/memory'; import { getProcessorEventForAggregatedTransactions, getTransactionDurationFieldForAggregatedTransactions, @@ -205,26 +209,50 @@ async function getMemoryStats({ filter, }: TaskParameters): Promise<{ avgMemoryUsage: number | null }> { const { apmEventClient } = setup; - const response = await apmEventClient.search({ - apm: { - events: [ProcessorEvent.metric], - }, - body: { - size: 0, - query: { - bool: { - filter: [ - ...filter, - { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, - { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, - ], + + const getAvgMemoryUsage = async ({ + additionalFilters, + script, + }: { + additionalFilters: ESFilter[]; + script: typeof percentCgroupMemoryUsedScript; + }) => { + const response = await apmEventClient.search({ + apm: { + events: [ProcessorEvent.metric], + }, + body: { + size: 0, + query: { + bool: { + filter: [...filter, ...additionalFilters], + }, + }, + aggs: { + avgMemoryUsage: { avg: { script } }, }, }, - aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } }, - }, - }); + }); - return { - avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null, + return response.aggregations?.avgMemoryUsage.value ?? null; }; + + let avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } }, + ], + script: percentCgroupMemoryUsedScript, + }); + + if (!avgMemoryUsage) { + avgMemoryUsage = await getAvgMemoryUsage({ + additionalFilters: [ + { exists: { field: METRIC_SYSTEM_FREE_MEMORY } }, + { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } }, + ], + script: percentSystemMemoryUsedScript, + }); + } + + return { avgMemoryUsage }; } diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f25e37927f094..b417f8689b229 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -26,11 +26,7 @@ import { MlPluginSetup } from '../../ml/server'; import { ObservabilityPluginSetup } from '../../observability/server'; import { SecurityPluginSetup } from '../../security/server'; import { TaskManagerSetupContract } from '../../task_manager/server'; -import { - APM_FEATURE, - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE, -} from './feature'; +import { APM_FEATURE, registerFeaturesUsage } from './feature'; import { registerApmAlerts } from './lib/alerts/register_apm_alerts'; import { createApmTelemetry } from './lib/apm_telemetry'; import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client'; @@ -128,10 +124,8 @@ export class APMPlugin implements Plugin { }); plugins.features.registerKibanaFeature(APM_FEATURE); - plugins.licensing.featureUsage.register( - APM_SERVICE_MAPS_FEATURE_NAME, - APM_SERVICE_MAPS_LICENSE_TYPE - ); + + registerFeaturesUsage({ licensingPlugin: plugins.licensing }); createApmApi().init(core, { config$: mergedConfig$, diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 04807cfac1cea..1996d4d4a262d 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -15,7 +15,7 @@ import { getServiceMap } from '../lib/service_map/get_service_map'; import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info'; import { createRoute } from './create_route'; import { rangeRt, uiFiltersRt } from './default_api_types'; -import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature'; +import { notifyFeatureUsage } from '../feature'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; @@ -37,7 +37,11 @@ export const serviceMapRoute = createRoute(() => ({ if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } - context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'serviceMaps', + }); const logger = context.logger; const setup = await setupRequest(context, request); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 2cc0cdb1c2b91..f0a22356d074b 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -15,6 +15,7 @@ import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions'; +import { notifyFeatureUsage } from '../../feature'; // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -62,6 +63,10 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ } await createAnomalyDetectionJobs(setup, environments, context.logger); + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'ml', + }); }, })); diff --git a/x-pack/plugins/apm/server/routes/settings/custom_link.ts b/x-pack/plugins/apm/server/routes/settings/custom_link.ts index 83c23a75e999d..7882383d78ab0 100644 --- a/x-pack/plugins/apm/server/routes/settings/custom_link.ts +++ b/x-pack/plugins/apm/server/routes/settings/custom_link.ts @@ -3,9 +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. */ + +import Boom from 'boom'; import * as t from 'io-ts'; import { pick } from 'lodash'; +import { INVALID_LICENSE } from '../../../common/custom_link'; +import { ILicense } from '../../../../licensing/common/types'; import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options'; +import { notifyFeatureUsage } from '../../feature'; import { setupRequest } from '../../lib/helpers/setup_request'; import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link'; import { @@ -17,6 +22,10 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction'; import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links'; import { createRoute } from '../create_route'; +function isActiveGoldLicense(license: ILicense) { + return license.isActive && license.hasAtLeast('gold'); +} + export const customLinkTransactionRoute = createRoute(() => ({ path: '/api/apm/settings/custom_links/transaction', params: { @@ -37,6 +46,9 @@ export const listCustomLinksRoute = createRoute(() => ({ query: filterOptionsRt, }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { query } = context.params; // picks only the items listed in FILTER_OPTIONS @@ -55,9 +67,17 @@ export const createCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const customLink = context.params.body; const res = await createOrUpdateCustomLink({ customLink, setup }); + + notifyFeatureUsage({ + licensingPlugin: context.licensing, + featureName: 'customLinks', + }); return res; }, })); @@ -75,6 +95,9 @@ export const updateCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const customLink = context.params.body; @@ -99,6 +122,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({ tags: ['access:apm', 'access:apm_write'], }, handler: async ({ context, request }) => { + if (!isActiveGoldLicense(context.licensing.license)) { + throw Boom.forbidden(INVALID_LICENSE); + } const setup = await setupRequest(context, request); const { id } = context.params.path; const res = await deleteCustomLink({ diff --git a/x-pack/plugins/canvas/server/collectors/collector.ts b/x-pack/plugins/canvas/server/collectors/collector.ts index 48396d93d13e6..eb650ca5ad152 100644 --- a/x-pack/plugins/canvas/server/collectors/collector.ts +++ b/x-pack/plugins/canvas/server/collectors/collector.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { LegacyAPICaller } from 'kibana/server'; import { TelemetryCollector } from '../../types'; import { workpadCollector } from './workpad_collector'; @@ -32,7 +32,7 @@ export function registerCanvasUsageCollector( const canvasCollector = usageCollection.makeUsageCollector({ type: 'canvas', isReady: () => true, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const collectorResults = await Promise.all( collectors.map((collector) => collector(kibanaIndex, callCluster)) ); diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index 3517c958b27b8..466a7cc20a497 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -38,10 +38,6 @@ module.exports = { 'src/plugins/data/public/expressions/interpreter' ), 'kbn/interpreter': path.resolve(KIBANA_ROOT, 'packages/kbn-interpreter/target/common'), - 'types/interpreter': path.resolve( - KIBANA_ROOT, - 'src/legacy/core_plugins/interpreter/public/types' - ), tinymath: path.resolve(KIBANA_ROOT, 'node_modules/tinymath/lib/tinymath.es5.js'), core_app_image_assets: path.resolve(KIBANA_ROOT, 'src/core/public/core_app/images'), }, diff --git a/x-pack/plugins/canvas/types/telemetry.ts b/x-pack/plugins/canvas/types/telemetry.ts index 0b354d7677f6e..3b635b2e57926 100644 --- a/x-pack/plugins/canvas/types/telemetry.ts +++ b/x-pack/plugins/canvas/types/telemetry.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; /** Function for collecting information about canvas usage @@ -13,7 +13,7 @@ export type TelemetryCollector = ( /** The server instance */ kibanaIndex: string, /** Function for calling elasticsearch */ - callCluster: CallCluster + callCluster: LegacyAPICaller ) => Record; export interface TelemetryCustomElementDocument { diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md index 31ee304fe2247..ba14be5564be1 100644 --- a/x-pack/plugins/enterprise_search/README.md +++ b/x-pack/plugins/enterprise_search/README.md @@ -13,6 +13,14 @@ This plugin's goal is to provide a Kibana user interface to the Enterprise Searc 2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'` 3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana. +### Kea + +Enterprise Search uses [Kea.js](https://github.com/keajs/kea) to manage our React/Redux state for us. Kea state is handled in our `*Logic` files and exposes [values](https://kea.js.org/docs/guide/concepts#values) and [actions](https://kea.js.org/docs/guide/concepts#actions) for our components to get and set state with. + +#### Debugging Kea + +To debug Kea state in-browser, Kea recommends [Redux Devtools](https://kea.js.org/docs/guide/debugging). To facilitate debugging, we use the [path](https://kea.js.org/docs/guide/debugging/#setting-the-path-manually) key with `snake_case`d paths. The path key should always end with the logic filename (e.g. `['enterprise_search', 'some_logic']`) to make it easy for devs to quickly find/jump to files via IDE tooling. + ## Testing ### Unit tests diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index d6a51e8b482d0..5df25f11e5070 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -76,4 +76,6 @@ export const JSON_HEADER = { Accept: 'application/json', // Required for Enterprise Search APIs }; +export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode'; + export const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 3f71759390879..9388d61041b13 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -16,6 +16,7 @@ export interface IAppActions { } export const AppLogic = kea>({ + path: ['enterprise_search', 'app_search', 'app_logic'], actions: { initializeAppData: (props) => props, }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index 94e9127bbed74..31c7680fd2f1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -54,6 +54,7 @@ describe('AppSearchConfigured', () => { const wrapper = shallow(); expect(wrapper.find(Layout)).toHaveLength(1); + expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(EngineOverview)).toHaveLength(1); }); @@ -61,9 +62,9 @@ describe('AppSearchConfigured', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - shallow(); + shallow(); - expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + expect(initializeAppData).toHaveBeenCalledWith({ ilmEnabled: true }); }); it('does not re-initialize app data', () => { @@ -83,6 +84,14 @@ describe('AppSearchConfigured', () => { expect(wrapper.find(ErrorConnecting)).toHaveLength(1); }); + + it('passes readOnlyMode state', () => { + (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); + }); }); describe('AppSearchNav', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index c4a366930d22a..643c4b5ccc873 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -51,7 +51,7 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData } = useActions(AppLogic); - const { errorConnecting } = useValues(HttpLogic); + const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -63,7 +63,7 @@ export const AppSearchConfigured: React.FC = (props) => { - }> + } readOnlyMode={readOnlyMode}> {errorConnecting ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index a54295548004a..82f884644be4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -69,7 +69,11 @@ export const renderApp = ( > - + diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 3ae48f352b2c1..37a8f16acad6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -32,6 +32,7 @@ const convertToArray = (messages: IFlashMessage | IFlashMessage[]) => !Array.isArray(messages) ? [messages] : messages; export const FlashMessagesLogic = kea>({ + path: ['enterprise_search', 'flash_messages_logic'], actions: { setFlashMessages: (messages) => ({ messages: convertToArray(messages) }), clearFlashMessages: () => null, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts index c032e3b04ebe6..b65499be2f7c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts @@ -16,6 +16,7 @@ describe('HttpLogic', () => { http: null, httpInterceptors: [], errorConnecting: false, + readOnlyMode: false, }; beforeEach(() => { @@ -31,12 +32,17 @@ describe('HttpLogic', () => { describe('initializeHttp()', () => { it('sets values based on passed props', () => { HttpLogic.mount(); - HttpLogic.actions.initializeHttp({ http: mockHttp, errorConnecting: true }); + HttpLogic.actions.initializeHttp({ + http: mockHttp, + errorConnecting: true, + readOnlyMode: true, + }); expect(HttpLogic.values).toEqual({ http: mockHttp, httpInterceptors: [], errorConnecting: true, + readOnlyMode: true, }); }); }); @@ -52,50 +58,110 @@ describe('HttpLogic', () => { }); }); + describe('setReadOnlyMode()', () => { + it('sets readOnlyMode value', () => { + HttpLogic.mount(); + HttpLogic.actions.setReadOnlyMode(true); + expect(HttpLogic.values.readOnlyMode).toEqual(true); + + HttpLogic.actions.setReadOnlyMode(false); + expect(HttpLogic.values.readOnlyMode).toEqual(false); + }); + }); + describe('http interceptors', () => { describe('initializeHttpInterceptors()', () => { beforeEach(() => { HttpLogic.mount(); jest.spyOn(HttpLogic.actions, 'setHttpInterceptors'); - jest.spyOn(HttpLogic.actions, 'setErrorConnecting'); HttpLogic.actions.initializeHttp({ http: mockHttp }); - HttpLogic.actions.initializeHttpInterceptors(); }); it('calls http.intercept and sets an array of interceptors', () => { - mockHttp.intercept.mockImplementationOnce(() => 'removeInterceptorFn' as any); + mockHttp.intercept + .mockImplementationOnce(() => 'removeErrorInterceptorFn' as any) + .mockImplementationOnce(() => 'removeReadOnlyInterceptorFn' as any); HttpLogic.actions.initializeHttpInterceptors(); expect(mockHttp.intercept).toHaveBeenCalled(); - expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith(['removeInterceptorFn']); + expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith([ + 'removeErrorInterceptorFn', + 'removeReadOnlyInterceptorFn', + ]); }); describe('errorConnectingInterceptor', () => { + let interceptedResponse: any; + + beforeEach(() => { + interceptedResponse = mockHttp.intercept.mock.calls[0][0].responseError; + jest.spyOn(HttpLogic.actions, 'setErrorConnecting'); + }); + it('handles errors connecting to Enterprise Search', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/app_search/engines', status: 502 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + const httpResponse = { + response: { url: '/api/app_search/engines', status: 502 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled(); }); it('does not handle non-502 Enterprise Search errors', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/workplace_search/overview', status: 404 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + const httpResponse = { + response: { url: '/api/workplace_search/overview', status: 404 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); - it('does not handle errors for unrelated calls', async () => { - const { responseError } = mockHttp.intercept.mock.calls[0][0] as any; - const httpResponse = { response: { url: '/api/some_other_plugin/', status: 502 } }; - await expect(responseError(httpResponse)).rejects.toEqual(httpResponse); + it('does not handle errors for non-Enterprise Search API calls', async () => { + const httpResponse = { + response: { url: '/api/some_other_plugin/', status: 502 }, + }; + await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse); expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled(); }); }); + + describe('readOnlyModeInterceptor', () => { + let interceptedResponse: any; + + beforeEach(() => { + interceptedResponse = mockHttp.intercept.mock.calls[1][0].response; + jest.spyOn(HttpLogic.actions, 'setReadOnlyMode'); + }); + + it('sets readOnlyMode to true if the response header is true', async () => { + const httpResponse = { + response: { url: '/api/app_search/engines', headers: { get: () => 'true' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(true); + }); + + it('sets readOnlyMode to false if the response header is false', async () => { + const httpResponse = { + response: { url: '/api/workplace_search/overview', headers: { get: () => 'false' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(false); + }); + + it('does not handle headers for non-Enterprise Search API calls', async () => { + const httpResponse = { + response: { url: '/api/some_other_plugin/', headers: { get: () => 'true' } }, + }; + await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse); + + expect(HttpLogic.actions.setReadOnlyMode).not.toHaveBeenCalled(); + }); + }); }); it('sets httpInterceptors and calls all valid remove functions on unmount', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts index ec9db30ddef3b..72380142fe399 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts @@ -6,32 +6,33 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpSetup, HttpInterceptorResponseError } from 'src/core/public'; +import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/public'; +import { IHttpProviderProps } from './http_provider'; + +import { READ_ONLY_MODE_HEADER } from '../../../../common/constants'; export interface IHttpValues { http: HttpSetup; httpInterceptors: Function[]; errorConnecting: boolean; + readOnlyMode: boolean; } export interface IHttpActions { - initializeHttp({ - http, - errorConnecting, - }: { - http: HttpSetup; - errorConnecting?: boolean; - }): { http: HttpSetup; errorConnecting?: boolean }; + initializeHttp({ http, errorConnecting, readOnlyMode }: IHttpProviderProps): IHttpProviderProps; initializeHttpInterceptors(): void; setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] }; setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean }; + setReadOnlyMode(readOnlyMode: boolean): { readOnlyMode: boolean }; } export const HttpLogic = kea>({ + path: ['enterprise_search', 'http_logic'], actions: { - initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }), + initializeHttp: (props) => props, initializeHttpInterceptors: () => null, setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }), setErrorConnecting: (errorConnecting) => ({ errorConnecting }), + setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }), }, reducers: { http: [ @@ -53,6 +54,13 @@ export const HttpLogic = kea>({ setErrorConnecting: (_, { errorConnecting }) => errorConnecting, }, ], + readOnlyMode: [ + false, + { + initializeHttp: (_, { readOnlyMode }) => !!readOnlyMode, + setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode, + }, + ], }, listeners: ({ values, actions }) => ({ initializeHttpInterceptors: () => { @@ -60,13 +68,13 @@ export const HttpLogic = kea>({ const errorConnectingInterceptor = values.http.intercept({ responseError: async (httpResponse) => { - const { url, status } = httpResponse.response!; - const hasErrorConnecting = status === 502; - const isApiResponse = - url.includes('/api/app_search/') || url.includes('/api/workplace_search/'); + if (isEnterpriseSearchApi(httpResponse)) { + const { status } = httpResponse.response!; + const hasErrorConnecting = status === 502; - if (isApiResponse && hasErrorConnecting) { - actions.setErrorConnecting(true); + if (hasErrorConnecting) { + actions.setErrorConnecting(true); + } } // Re-throw error so that downstream catches work as expected @@ -75,7 +83,23 @@ export const HttpLogic = kea>({ }); httpInterceptors.push(errorConnectingInterceptor); - // TODO: Read only mode interceptor + const readOnlyModeInterceptor = values.http.intercept({ + response: async (httpResponse) => { + if (isEnterpriseSearchApi(httpResponse)) { + const readOnlyMode = httpResponse.response!.headers.get(READ_ONLY_MODE_HEADER); + + if (readOnlyMode === 'true') { + actions.setReadOnlyMode(true); + } else { + actions.setReadOnlyMode(false); + } + } + + return Promise.resolve(httpResponse); + }, + }); + httpInterceptors.push(readOnlyModeInterceptor); + actions.setHttpInterceptors(httpInterceptors); }, }), @@ -87,3 +111,11 @@ export const HttpLogic = kea>({ }, }), }); + +/** + * Small helper that checks whether or not an http call is for an Enterprise Search API + */ +const isEnterpriseSearchApi = (httpResponse: HttpResponse) => { + const { url } = httpResponse.response!; + return url.includes('/api/app_search/') || url.includes('/api/workplace_search/'); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx index 81106235780d6..902c910f10d7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx @@ -17,6 +17,7 @@ describe('HttpProvider', () => { const props = { http: {} as any, errorConnecting: false, + readOnlyMode: false, }; const initializeHttp = jest.fn(); const initializeHttpInterceptors = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx index 4c2160195a1af..db1b0d611079a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx @@ -11,9 +11,10 @@ import { HttpSetup } from 'src/core/public'; import { HttpLogic } from './http_logic'; -interface IHttpProviderProps { +export interface IHttpProviderProps { http: HttpSetup; errorConnecting?: boolean; + readOnlyMode?: boolean; } export const HttpProvider: React.FC = (props) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss index f6c83888413d3..e867e9cf5a445 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss @@ -81,4 +81,15 @@ padding: $euiSize; } } + + &__readOnlyMode { + margin: -$euiSizeM 0 $euiSizeL; + + @include euiBreakpoint('m') { + margin: 0 0 $euiSizeL; + } + @include euiBreakpoint('xs', 's') { + margin: 0; + } + } } diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index 623e6e47167d2..7b876d81527fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiPageSideBar, EuiButton, EuiPageBody } from '@elastic/eui'; +import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { Layout, INavContext } from './layout'; @@ -55,6 +55,12 @@ describe('Layout', () => { expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen'); }); + it('renders a read-only mode callout', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiCallOut)).toHaveLength(1); + }); + it('renders children', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index e122c4d5cfdfa..ef8216e8b6711 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import classNames from 'classnames'; -import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton } from '@elastic/eui'; +import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import './layout.scss'; @@ -15,6 +15,7 @@ import './layout.scss'; interface ILayoutProps { navigation: React.ReactNode; restrictWidth?: boolean; + readOnlyMode?: boolean; } export interface INavContext { @@ -22,7 +23,12 @@ export interface INavContext { } export const NavContext = React.createContext({}); -export const Layout: React.FC = ({ children, navigation, restrictWidth }) => { +export const Layout: React.FC = ({ + children, + navigation, + restrictWidth, + readOnlyMode, +}) => { const [isNavOpen, setIsNavOpen] = useState(false); const toggleNavigation = () => setIsNavOpen(!isNavOpen); const closeNavigation = () => setIsNavOpen(false); @@ -56,6 +62,17 @@ export const Layout: React.FC = ({ children, navigation, restrictW {navigation} + {readOnlyMode && ( + + )} {children} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index f88a00f63f487..94bd1d529b65f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -22,6 +22,7 @@ export interface IAppActions { } export const AppLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({ workplaceSearch, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 39280ad6f4be4..fc1943264d72b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -12,6 +12,7 @@ import { Redirect } from 'react-router-dom'; import { shallow } from 'enzyme'; import { useValues, useActions } from 'kea'; +import { Layout } from '../shared/layout'; import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; import { Overview } from './views/overview'; @@ -53,6 +54,7 @@ describe('WorkplaceSearchConfigured', () => { it('renders with layout', () => { const wrapper = shallow(); + expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy(); expect(wrapper.find(Overview)).toHaveLength(1); }); @@ -60,9 +62,9 @@ describe('WorkplaceSearchConfigured', () => { const initializeAppData = jest.fn(); (useActions as jest.Mock).mockImplementation(() => ({ initializeAppData })); - shallow(); + shallow(); - expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true }); + expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true }); }); it('does not re-initialize app data', () => { @@ -82,4 +84,12 @@ describe('WorkplaceSearchConfigured', () => { expect(wrapper.find(ErrorState)).toHaveLength(2); }); + + it('passes readOnlyMode state', () => { + (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true })); + + const wrapper = shallow(); + + expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 6a51b49869eaf..a68dfaf8ea471 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -31,7 +31,7 @@ export const WorkplaceSearch: React.FC = (props) => { export const WorkplaceSearchConfigured: React.FC = (props) => { const { hasInitialized } = useValues(AppLogic); const { initializeAppData } = useActions(AppLogic); - const { errorConnecting } = useValues(HttpLogic); + const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { if (!hasInitialized) initializeAppData(props); @@ -46,7 +46,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { {errorConnecting ? : } - }> + } readOnlyMode={readOnlyMode}> {errorConnecting ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 787d5295db1cf..a156b8a8009f9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -31,6 +31,7 @@ export interface IOverviewValues extends IOverviewServerData { } export const OverviewLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'overview_logic'], actions: { setServerData: (serverData) => serverData, initializeOverview: () => null, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 0c1e81e3aba46..3d0a3181f8ab8 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -5,7 +5,7 @@ */ import { mockConfig, mockLogger } from '../__mocks__'; -import { JSON_HEADER } from '../../common/constants'; +import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; @@ -18,6 +18,9 @@ const responseMock = { custom: jest.fn(), customError: jest.fn(), }; +const mockExpectedResponseHeaders = { + [READ_ONLY_MODE_HEADER]: 'false', +}; describe('EnterpriseSearchRequestHandler', () => { const enterpriseSearchRequestHandler = new EnterpriseSearchRequestHandler({ @@ -58,6 +61,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.custom).toHaveBeenCalledWith({ body: responseBody, statusCode: 200, + headers: mockExpectedResponseHeaders, }); }); @@ -112,11 +116,12 @@ describe('EnterpriseSearchRequestHandler', () => { await makeAPICall(requestHandler); EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example'); - expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 201 }); + expect(responseMock.custom).toHaveBeenCalledWith({ + body: {}, + statusCode: 201, + headers: mockExpectedResponseHeaders, + }); }); - - // TODO: It's possible we may also pass back headers at some point - // from Enterprise Search, e.g. the x-read-only mode header }); }); @@ -140,6 +145,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'some error message', attributes: { errors: ['some error message'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -156,6 +162,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'one,two,three', attributes: { errors: ['one', 'two', 'three'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -171,6 +178,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Bad Request', attributes: { errors: ['Bad Request'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -186,6 +194,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Bad Request', attributes: { errors: ['Bad Request'] }, }, + headers: mockExpectedResponseHeaders, }); }); @@ -201,6 +210,7 @@ describe('EnterpriseSearchRequestHandler', () => { message: 'Not Found', attributes: { errors: ['Not Found'] }, }, + headers: mockExpectedResponseHeaders, }); }); }); @@ -215,12 +225,33 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: expect.stringContaining('Enterprise Search encountered an internal server error'), + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalledWith( 'Enterprise Search Server Error 500 at : "something crashed!"' ); }); + it('handleReadOnlyModeError()', async () => { + EnterpriseSearchAPI.mockReturn( + { errors: ['Read only mode'] }, + { status: 503, headers: { ...JSON_HEADER, [READ_ONLY_MODE_HEADER]: 'true' } } + ); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/503' }); + + await makeAPICall(requestHandler); + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/503'); + + expect(responseMock.customError).toHaveBeenCalledWith({ + statusCode: 503, + body: expect.stringContaining('Enterprise Search is in read-only mode'), + headers: { [READ_ONLY_MODE_HEADER]: 'true' }, + }); + expect(mockLogger.error).toHaveBeenCalledWith( + 'Cannot perform action: Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.' + ); + }); + it('handleInvalidDataError()', async () => { EnterpriseSearchAPI.mockReturn({ results: false }); const requestHandler = enterpriseSearchRequestHandler.createRequest({ @@ -234,6 +265,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Invalid data received from Enterprise Search', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalledWith( 'Invalid data received from : {"results":false}' @@ -250,6 +282,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Error connecting to Enterprise Search: Failed', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalled(); }); @@ -265,6 +298,7 @@ describe('EnterpriseSearchRequestHandler', () => { expect(responseMock.customError).toHaveBeenCalledWith({ statusCode: 502, body: 'Cannot authenticate Enterprise Search user', + headers: mockExpectedResponseHeaders, }); expect(mockLogger.error).toHaveBeenCalled(); }); @@ -279,6 +313,18 @@ describe('EnterpriseSearchRequestHandler', () => { }); }); + it('setResponseHeaders', async () => { + EnterpriseSearchAPI.mockReturn('anything' as any, { + headers: { [READ_ONLY_MODE_HEADER]: 'true' }, + }); + const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/' }); + await makeAPICall(requestHandler); + + expect(enterpriseSearchRequestHandler.headers).toEqual({ + [READ_ONLY_MODE_HEADER]: 'true', + }); + }); + it('isEmptyObj', async () => { expect(enterpriseSearchRequestHandler.isEmptyObj({})).toEqual(true); expect(enterpriseSearchRequestHandler.isEmptyObj({ empty: false })).toEqual(false); @@ -304,9 +350,10 @@ const EnterpriseSearchAPI = { ...expectedParams, }); }, - mockReturn(response: object, options?: object) { + mockReturn(response: object, options?: any) { fetchMock.mockImplementation(() => { - return Promise.resolve(new Response(JSON.stringify(response), options)); + const headers = Object.assign({}, mockExpectedResponseHeaders, options?.headers); + return Promise.resolve(new Response(JSON.stringify(response), { ...options, headers })); }); }, mockReturnError() { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 00d5eaf5d6a83..6b65c16c832fd 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -14,7 +14,7 @@ import { Logger, } from 'src/core/server'; import { ConfigType } from '../index'; -import { JSON_HEADER } from '../../common/constants'; +import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; interface IConstructorDependencies { config: ConfigType; @@ -46,6 +46,7 @@ export interface IEnterpriseSearchRequestHandler { export class EnterpriseSearchRequestHandler { private enterpriseSearchUrl: string; private log: Logger; + private headers: Record = {}; constructor({ config, log }: IConstructorDependencies) { this.log = log; @@ -80,6 +81,9 @@ export class EnterpriseSearchRequestHandler { // Call the Enterprise Search API const apiResponse = await fetch(url, { method, headers, body }); + // Handle response headers + this.setResponseHeaders(apiResponse); + // Handle authentication redirects if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { return this.handleAuthenticationError(response); @@ -88,7 +92,13 @@ export class EnterpriseSearchRequestHandler { // Handle 400-500+ responses from the Enterprise Search server const { status } = apiResponse; if (status >= 500) { - return this.handleServerError(response, apiResponse, url); + if (this.headers[READ_ONLY_MODE_HEADER] === 'true') { + // Handle 503 read-only mode errors + return this.handleReadOnlyModeError(response); + } else { + // Handle unexpected server errors + return this.handleServerError(response, apiResponse, url); + } } else if (status >= 400) { return this.handleClientError(response, apiResponse); } @@ -100,7 +110,11 @@ export class EnterpriseSearchRequestHandler { } // Pass successful responses back to the front-end - return response.custom({ statusCode: status, body: json }); + return response.custom({ + statusCode: status, + headers: this.headers, + body: json, + }); } catch (e) { // Catch connection/auth errors return this.handleConnectionError(response, e); @@ -160,7 +174,7 @@ export class EnterpriseSearchRequestHandler { const { status } = apiResponse; const body = await this.getErrorResponseBody(apiResponse); - return response.customError({ statusCode: status, body }); + return response.customError({ statusCode: status, headers: this.headers, body }); } async handleServerError(response: KibanaResponseFactory, apiResponse: Response, url: string) { @@ -172,14 +186,22 @@ export class EnterpriseSearchRequestHandler { 'Enterprise Search encountered an internal server error. Please contact your system administrator if the problem persists.'; this.log.error(`Enterprise Search Server Error ${status} at <${url}>: ${message}`); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + } + + handleReadOnlyModeError(response: KibanaResponseFactory) { + const errorMessage = + 'Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.'; + + this.log.error(`Cannot perform action: ${errorMessage}`); + return response.customError({ statusCode: 503, headers: this.headers, body: errorMessage }); } handleInvalidDataError(response: KibanaResponseFactory, url: string, json: object) { const errorMessage = 'Invalid data received from Enterprise Search'; this.log.error(`Invalid data received from <${url}>: ${JSON.stringify(json)}`); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } handleConnectionError(response: KibanaResponseFactory, e: Error) { @@ -188,14 +210,26 @@ export class EnterpriseSearchRequestHandler { this.log.error(errorMessage); if (e instanceof Error) this.log.debug(e.stack as string); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); } handleAuthenticationError(response: KibanaResponseFactory) { const errorMessage = 'Cannot authenticate Enterprise Search user'; this.log.error(errorMessage); - return response.customError({ statusCode: 502, body: errorMessage }); + return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage }); + } + + /** + * Set response headers + * + * Currently just forwards the read-only mode header, but we can expand this + * in the future to pass more headers from Enterprise Search as we need them + */ + + setResponseHeaders(apiResponse: Response) { + const readOnlyMode = apiResponse.headers.get(READ_ONLY_MODE_HEADER); + this.headers[READ_ONLY_MODE_HEADER] = readOnlyMode as 'true' | 'false'; } /** diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap index 0217f039e08ba..7bb9954fa3048 100644 --- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap +++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap @@ -7,8 +7,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Canvas", "label": "Canvas", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Canvasundefinedundefined", + "title": "Canvas • Kibana", "url": "/app/test/Canvas", }, Object { @@ -16,8 +21,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Discover", "label": "Discover", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Discoverundefinedundefined", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { @@ -25,8 +35,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Graph", "label": "Graph", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Graphundefinedundefined", + "title": "Graph • Kibana", "url": "/app/test/Graph", }, ] @@ -39,8 +54,13 @@ Array [ "className": "euiSelectableTemplateSitewide__listItem", "key": "Discover", "label": "Discover", + "meta": Array [ + Object { + "text": "Kibana", + }, + ], "prepend": undefined, - "title": "Discoverundefinedundefined", + "title": "Discover • Kibana", "url": "/app/test/Discover", }, Object { diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx index 0d1e8725b4911..11fbc7931e620 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx @@ -21,6 +21,7 @@ type Result = { id: string; type: string } | string; const createResult = (result: Result): GlobalSearchResult => { const id = typeof result === 'string' ? result : result.id; const type = typeof result === 'string' ? 'application' : result.type; + const meta = type === 'application' ? { categoryLabel: 'Kibana' } : { categoryLabel: null }; return { id, @@ -28,6 +29,7 @@ const createResult = (result: Result): GlobalSearchResult => { title: id, url: `/app/test/${id}`, score: 42, + meta, }; }; @@ -74,7 +76,7 @@ describe('SearchBar', () => { expect(findSpy).toHaveBeenCalledTimes(1); expect(findSpy).toHaveBeenCalledWith('', {}); expect(getSelectableProps(component).options).toMatchSnapshot(); - await wait(() => getSearchProps(component).onSearch('d')); + await wait(() => getSearchProps(component).onKeyUpCapture({ currentTarget: { value: 'd' } })); jest.runAllTimers(); component.update(); expect(getSelectableProps(component).options).toMatchSnapshot(); diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx index d00349e21a7e4..e41f9243198ad 100644 --- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx +++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx @@ -52,14 +52,20 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { if (!isMounted()) return; _setOptions([ - ..._options.map((option) => ({ - key: option.id, - label: option.title, - url: option.url, - ...(option.icon && { icon: { type: option.icon } }), - ...(option.type && - option.type !== 'application' && { meta: [{ text: cleanMeta(option.type) }] }), - })), + ..._options.map(({ id, title, url, icon, type, meta }) => { + const option: EuiSelectableTemplateSitewideOption = { + key: id, + label: title, + url, + }; + + if (icon) option.icon = { type: icon }; + + if (type === 'application') option.meta = [{ text: meta?.categoryLabel as string }]; + else option.meta = [{ text: cleanMeta(type) }]; + + return option; + }), ]); }, [isMounted, _setOptions] @@ -133,7 +139,8 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) { onChange={onChange} options={options} searchProps={{ - onSearch: setSearchValue, + onKeyUpCapture: (e: React.KeyboardEvent) => + setSearchValue(e.currentTarget.value), 'data-test-subj': 'header-search', inputRef: setSearchRef, compressed: true, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index a2d5c7c8d5308..b3bf071948956 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -69,6 +69,8 @@ export * from './other_type_name_parameter'; export * from './other_type_json_parameter'; +export * from './meta_parameter'; + export * from './ignore_above_parameter'; export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx new file mode 100644 index 0000000000000..c8af296318b61 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { documentationService } from '../../../../../services/documentation'; +import { UseField, JsonEditorField } from '../../../shared_imports'; +import { getFieldConfig } from '../../../lib'; +import { EditFieldFormRow } from '../fields/edit_field'; + +interface Props { + defaultToggleValue: boolean; +} + +export const MetaParameter: FunctionComponent = ({ defaultToggleValue }) => ( + + + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx index ba9c75baa1987..1550485ebad93 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx @@ -5,14 +5,25 @@ */ import React from 'react'; -import { StoreParameter, DocValuesParameter } from '../../field_parameters'; +import { NormalizedField, ParameterName, Field as FieldType } from '../../../../types'; +import { getFieldConfig } from '../../../../lib'; +import { StoreParameter, DocValuesParameter, MetaParameter } from '../../field_parameters'; import { AdvancedParametersSection } from '../edit_field'; -export const BinaryType = () => { +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +interface Props { + field: NormalizedField; +} + +export const BinaryType = ({ field }: Props) => { return ( + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx index 962606b2f4ffd..1ee2bf22edb44 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx @@ -16,11 +16,13 @@ import { DocValuesParameter, BoostParameter, NullValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -90,6 +92,8 @@ export const BooleanType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx index 74331cb1b6b22..748dc54838270 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx @@ -10,11 +10,12 @@ import { i18n } from '@kbn/i18n'; import { NormalizedField, Field as FieldType } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { UseField, Field } from '../../../../shared_imports'; -import { AnalyzersParameter } from '../../field_parameters'; +import { AnalyzersParameter, MetaParameter } from '../../field_parameters'; import { EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': case 'max_input_length': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -88,6 +89,8 @@ export const CompletionType = ({ field }: Props) => { )} formFieldPath="preserve_position_increments" /> + + ); }; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx index 4c02171d49eec..aa8aefba921e7 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx @@ -6,16 +6,20 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { documentationService } from '../../../../../../services/documentation'; -import { UseField, Field, JsonEditorField } from '../../../../shared_imports'; +import { UseField, Field } from '../../../../shared_imports'; import { getFieldConfig } from '../../../../lib'; -import { NormalizedField } from '../../../../types'; +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { MetaParameter } from '../../field_parameters'; import { AdvancedParametersSection, EditFieldFormRow, BasicParametersSection } from '../edit_field'; interface Props { field: NormalizedField; } +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + export const ConstantKeywordType: FunctionComponent = ({ field }) => { return ( <> @@ -32,50 +36,14 @@ export const ConstantKeywordType: FunctionComponent = ({ field }) => { 'The value of this field for all documents in the index. If not specified, defaults to the value specified in the first document indexed.', } )} - defaultToggleValue={field.source?.value !== undefined} + defaultToggleValue={getDefaultToggleValue('value', field.source)} > - {/* Meta field */} - - - + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx index 0c067d09046d7..35382506a3cd9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx @@ -19,6 +19,7 @@ import { IgnoreMalformedParameter, FormatParameter, LocaleParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'locale': case 'format': + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -73,6 +75,8 @@ export const DateType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx index e96426ece27e8..b1545d44885c8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx @@ -18,6 +18,7 @@ import { NullValueParameter, SimilarityParameter, SplitQueriesOnWhitespaceParameter, + MetaParameter, IgnoreAboveParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -30,6 +31,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'boost': case 'ignore_above': + case 'meta': case 'similarity': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -83,6 +85,8 @@ export const FlattenedType = React.memo(({ field }: Props) => { defaultToggleValue={getDefaultToggleValue('null_value', field.source)} /> + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx index 997e866da35f0..0f28c5080d26d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx @@ -14,11 +14,14 @@ import { IgnoreMalformedParameter, NullValueParameter, IgnoreZValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { + case 'meta': + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; case 'null_value': { return field.null_value !== undefined; } @@ -65,6 +68,8 @@ export const GeoPointType = ({ field }: Props) => { config={getFieldConfig('null_value_geo_point')} /> + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx new file mode 100644 index 0000000000000..1ff97a8d72a21 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx @@ -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 React from 'react'; + +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { getFieldConfig } from '../../../../lib'; +import { IgnoreMalformedParameter, MetaParameter } from '../../field_parameters'; +import { AdvancedParametersSection } from '../edit_field'; + +interface Props { + field: NormalizedField; +} + +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +export const HistogramType = ({ field }: Props) => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 0cf921f66451b..8fcd02e4a362e 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -28,6 +28,7 @@ import { ObjectType } from './object_type'; import { OtherType } from './other_type'; import { NestedType } from './nested_type'; import { JoinType } from './join_type'; +import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; import { WildcardType } from './wildcard_type'; @@ -55,6 +56,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { other: OtherType, nested: NestedType, join: JoinType, + histogram: HistogramType, constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, wildcard: WildcardType, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx index 3d78205934eea..6ad3c9c5d0bd4 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx @@ -18,6 +18,7 @@ import { CoerceNumberParameter, IgnoreMalformedParameter, CopyToParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; import { PARAMETERS_DEFINITION } from '../../../../constants'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'copy_to': case 'boost': + case 'meta': case 'ignore_malformed': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -95,6 +97,8 @@ export const NumericType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx index f87d1f9400101..9a37f55ac8e9d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { NormalizedField, Field as FieldType } from '../../../../types'; +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; import { getFieldConfig } from '../../../../lib'; import { StoreParameter, @@ -14,11 +14,12 @@ import { CoerceNumberParameter, FormatParameter, LocaleParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; import { FormDataProvider } from '../../../../shared_imports'; -const getDefaultToggleValue = (param: 'locale' | 'format' | 'boost', field: FieldType) => { +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; }; @@ -57,6 +58,8 @@ export const RangeType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx index dafbebd24b3fa..3fa456c33f5e9 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx @@ -15,6 +15,7 @@ import { SimilarityParameter, TermVectorParameter, MaxShingleSizeParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, AdvancedParametersSection } from '../edit_field'; @@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'similarity': case 'term_vector': + case 'meta': case 'max_shingle_size': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -65,6 +67,8 @@ export const SearchAsYouType = React.memo(({ field }: Props) => { /> + + ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx index c4ed11097b609..07def791096e7 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx @@ -28,6 +28,7 @@ import { CopyToParameter, TermVectorParameter, FieldDataParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -40,6 +41,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { case 'boost': case 'position_increment_gap': case 'similarity': + case 'meta': case 'term_vector': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -47,7 +49,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => { return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer; } case 'copy_to': { - return field.null_value !== undefined && field.null_value !== ''; + return field[param] !== undefined && field[param] !== ''; } case 'indexPrefixes': { if (field.index_prefixes === undefined) { @@ -241,6 +243,8 @@ export const TextType = React.memo(({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx index 42854673269ae..5cc2addba53b8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx @@ -20,12 +20,14 @@ import { BoostParameter, AnalyzerParameter, NullValueParameter, + MetaParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'analyzer': + case 'meta': case 'boost': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -107,6 +109,8 @@ export const TokenCountType = ({ field }: Props) => { + + diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index 18d9c637bd45b..a4d3bf3832d5c 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -719,6 +719,23 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + histogram: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.histogramDescription', { + defaultMessage: 'Histogram', + }), + value: 'histogram', + documentation: { + main: '/histogram.html', + }, + description: () => ( +

+ +

+ ), + }, join: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.joinDescription', { defaultMessage: 'Join', @@ -863,6 +880,7 @@ export const MAIN_TYPES: MainType[] = [ 'shape', 'text', 'token_count', + 'histogram', 'wildcard', 'other', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index c2a44152ae1ee..97dca49fc93ed 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -59,6 +59,7 @@ export type MainType = | 'geo_point' | 'geo_shape' | 'token_count' + | 'histogram' | 'constant_keyword' | 'wildcard' /** diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts index 605e4db230ce5..b9789b770eb2e 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts @@ -30,6 +30,7 @@ import { import * as AgentService from '../../services/agents'; import * as APIKeyService from '../../services/api_keys'; import { appContextService } from '../../services/app_context'; +import { defaultIngestErrorHandler } from '../../errors'; export const getAgentHandler: RequestHandler { columnOrder: ['col1', 'col2'], columns: { col1: { - label: 'Custom query', + label: 'filters', dataType: 'document', operationType: 'filters', scale: 'ordinal', @@ -209,7 +209,7 @@ describe('filters', () => { }); }); - describe('Modify custom query', () => { + describe('Modify filters', () => { it('should correctly show existing filters ', () => { const setStateSpy = jest.fn(); const instance = mount( @@ -236,7 +236,7 @@ describe('filters', () => { ).toEqual('src : 2'); }); - it('should remove custom query', () => { + it('should remove filter', () => { const setStateSpy = jest.fn(); const instance = mount( = { type: 'filters', - displayName: customQueryLabel, + displayName: filtersLabel, priority: 3, // Higher than any metric getPossibleOperationForField: ({ type }) => { if (type === 'document') { @@ -102,7 +102,7 @@ export const filtersOperation: OperationDefinition = } return { - label: customQueryLabel, + label: filtersLabel, dataType: 'string', operationType: 'filters', scale: 'ordinal', @@ -223,8 +223,8 @@ export const FilterList = ({ defaultMessage: 'This query is invalid', })} onRemoveClick={() => onRemoveFilter(filter.id)} - removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeCustomQuery', { - defaultMessage: 'Remove custom query', + removeTitle={i18n.translate('xpack.lens.indexPattern.filters.removeFilter', { + defaultMessage: 'Remove a filter', })} > diff --git a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx index 98f5878ec927e..07baf29fdd32a 100644 --- a/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/toolbar_popover.tsx @@ -56,6 +56,7 @@ export const ToolbarPopover: React.FunctionComponent = ({ onClick={() => { setOpen(!open); }} + title={title} hasArrow={false} isDisabled={isDisabled} groupPosition={groupPosition} diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx index 7e2e8f0453588..2114d63fcfacd 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers'; import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui'; -import { LayerContextMenu, XyToolbar } from './xy_config_panel'; +import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel'; import { ToolbarPopover } from '../shared_components'; import { AxisSettingsPopover } from './axis_settings_popover'; import { FramePublicAPI } from '../types'; @@ -171,4 +171,48 @@ describe('XY Config panels', () => { expect(component.find(AxisSettingsPopover).length).toEqual(3); }); }); + + describe('Dimension Editor', () => { + test('shows the correct axis side options when in horizontal mode', () => { + const state = testState(); + const component = mount( + + ); + + const options = component + .find(EuiButtonGroup) + .first() + .prop('options') as EuiButtonGroupProps['options']; + + expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Bottom', 'Top']); + }); + + test('shows the default axis side options when not in horizontal mode', () => { + const state = testState(); + const component = mount( + + ); + + const options = component + .find(EuiButtonGroup) + .first() + .prop('options') as EuiButtonGroupProps['options']; + + expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']); + }); + }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx index bc98bf53d9f12..4aa5bd62c05a5 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx @@ -274,9 +274,15 @@ export function XyToolbar(props: VisualizationToolbarProps) { group.groupId === 'left') || {}).length === 0 } @@ -310,9 +316,15 @@ export function XyToolbar(props: VisualizationToolbarProps) { toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange} /> group.groupId === 'right') || {}).length === 0 } @@ -345,6 +357,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) const { state, setState, layerId, accessor } = props; const index = state.layers.findIndex((l) => l.layerId === layerId); const layer = state.layers[index]; + const isHorizontal = isHorizontalChart(state.layers); const axisMode = (layer.yConfig && layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) || @@ -377,15 +390,23 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps) }, { id: `${idPrefix}left`, - label: i18n.translate('xpack.lens.xyChart.axisSide.left', { - defaultMessage: 'Left', - }), + label: isHorizontal + ? i18n.translate('xpack.lens.xyChart.axisSide.bottom', { + defaultMessage: 'Bottom', + }) + : i18n.translate('xpack.lens.xyChart.axisSide.left', { + defaultMessage: 'Left', + }), }, { id: `${idPrefix}right`, - label: i18n.translate('xpack.lens.xyChart.axisSide.right', { - defaultMessage: 'Right', - }), + label: isHorizontal + ? i18n.translate('xpack.lens.xyChart.axisSide.top', { + defaultMessage: 'Top', + }) + : i18n.translate('xpack.lens.xyChart.axisSide.right', { + defaultMessage: 'Right', + }), }, ]} idSelected={`${idPrefix}${axisMode}`} diff --git a/x-pack/plugins/ml/common/constants/app.ts b/x-pack/plugins/ml/common/constants/app.ts index 97dd7a7b0fef5..3d54e9e150fef 100644 --- a/x-pack/plugins/ml/common/constants/app.ts +++ b/x-pack/plugins/ml/common/constants/app.ts @@ -4,6 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export const PLUGIN_ID = 'ml'; export const PLUGIN_ICON = 'machineLearningApp'; export const PLUGIN_ICON_SOLUTION = 'logoKibana'; +export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', { + defaultMessage: 'Machine Learning', +}); diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts index 830537cbadbc8..9a7af2496c03f 100644 --- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -4,4 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +export const ANALYSIS_CONFIG_TYPE = { + OUTLIER_DETECTION: 'outlier_detection', + REGRESSION: 'regression', + CLASSIFICATION: 'classification', +} as const; export const DEFAULT_RESULTS_FIELD = 'ml'; diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts index 44f33aa329e7a..541b8af6fc0fc 100644 --- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts @@ -31,8 +31,16 @@ export const ML_PAGES = { * Open index data visualizer viewer page */ DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', + ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`, ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, + ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`, SETTINGS: 'settings', CALENDARS_MANAGE: 'settings/calendars_list', + CALENDARS_NEW: 'settings/calendars_list/new_calendar', + CALENDARS_EDIT: 'settings/calendars_list/edit_calendar', FILTER_LISTS_MANAGE: 'settings/filter_lists', + FILTER_LISTS_NEW: 'settings/filter_lists/new_filter_list', + FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list', + ACCESS_DENIED: 'access-denied', + OVERVIEW: 'overview', } as const; diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index 96d6c81a3d309..5d0ecf96fb6b5 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -6,6 +6,7 @@ import Boom from 'boom'; import { EsErrorBody } from '../util/errors'; +import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics'; export interface DeleteDataFrameAnalyticsWithIndexStatus { success: boolean; @@ -81,8 +82,4 @@ export interface DataFrameAnalyticsConfig { allow_lazy_start?: boolean; } -export enum ANALYSIS_CONFIG_TYPE { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', -} +export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts index 234be8b6faf90..d176c22bdbb62 100644 --- a/x-pack/plugins/ml/common/types/ml_url_generator.ts +++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts @@ -5,27 +5,21 @@ */ import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query'; -import { JobId } from '../../../reporting/common/types'; +import { JobId } from './anomaly_detection_jobs/job'; import { ML_PAGES } from '../constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from './data_frame_analytics'; type OptionalPageState = object | undefined; export type MLPageState = PageState extends OptionalPageState - ? { page: PageType; pageState?: PageState } + ? { page: PageType; pageState?: PageState; excludeBasePath?: boolean } : PageState extends object - ? { page: PageType; pageState: PageState } - : { page: PageType }; - -export const ANALYSIS_CONFIG_TYPE = { - OUTLIER_DETECTION: 'outlier_detection', - REGRESSION: 'regression', - CLASSIFICATION: 'classification', -} as const; - -type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE]; + ? { page: PageType; pageState: PageState; excludeBasePath?: boolean } + : { page: PageType; excludeBasePath?: boolean }; export interface MlCommonGlobalState { time?: TimeRange; + refreshInterval?: RefreshInterval; } export interface MlCommonAppState { [key: string]: any; @@ -42,16 +36,28 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState { [key: string]: any; } -export interface MlGenericUrlState { - page: - | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER - | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE; - pageState: MlGenericUrlPageState; -} +export type MlGenericUrlState = MLPageState< + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE + | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX + | typeof ML_PAGES.OVERVIEW + | typeof ML_PAGES.CALENDARS_MANAGE + | typeof ML_PAGES.CALENDARS_NEW + | typeof ML_PAGES.FILTER_LISTS_MANAGE + | typeof ML_PAGES.FILTER_LISTS_NEW + | typeof ML_PAGES.SETTINGS + | typeof ML_PAGES.ACCESS_DENIED + | typeof ML_PAGES.DATA_VISUALIZER + | typeof ML_PAGES.DATA_VISUALIZER_FILE + | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT, + MlGenericUrlPageState | undefined +>; export interface AnomalyDetectionQueryState { jobId?: JobId; groupIds?: string[]; + globalState?: MlCommonGlobalState; } export type AnomalyDetectionUrlState = MLPageState< @@ -86,7 +92,7 @@ export interface ExplorerUrlPageState { /** * Job IDs */ - jobIds: JobId[]; + jobIds?: JobId[]; /** * Optionally set the time range in the time picker. */ @@ -104,6 +110,7 @@ export interface ExplorerUrlPageState { */ mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane']; mlExplorerFilter?: ExplorerAppState['mlExplorerFilter']; + globalState?: MlCommonGlobalState; } export type ExplorerUrlState = MLPageState; @@ -122,6 +129,7 @@ export interface TimeSeriesExplorerAppState { to?: string; }; mlTimeSeriesExplorer?: { + forecastId?: string; detectorIndex?: number; entities?: Record; }; @@ -131,10 +139,12 @@ export interface TimeSeriesExplorerAppState { export interface TimeSeriesExplorerPageState extends Pick, Pick { - jobIds: JobId[]; + jobIds?: JobId[]; timeRange?: TimeRange; detectorIndex?: number; entities?: Record; + forecastId?: string; + globalState?: MlCommonGlobalState; } export type TimeSeriesExplorerUrlState = MLPageState< @@ -145,6 +155,7 @@ export type TimeSeriesExplorerUrlState = MLPageState< export interface DataFrameAnalyticsQueryState { jobId?: JobId | JobId[]; groupIds?: string[]; + globalState?: MlCommonGlobalState; } export type DataFrameAnalyticsUrlState = MLPageState< @@ -152,17 +163,10 @@ export type DataFrameAnalyticsUrlState = MLPageState< DataFrameAnalyticsQueryState | undefined >; -export interface DataVisualizerUrlState { - page: - | typeof ML_PAGES.DATA_VISUALIZER - | typeof ML_PAGES.DATA_VISUALIZER_FILE - | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT; -} - export interface DataFrameAnalyticsExplorationQueryState { ml: { jobId: JobId; - analysisType: DataFrameAnalyticsType; + analysisType: DataFrameAnalysisConfigType; }; } @@ -170,7 +174,24 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState< typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, { jobId: JobId; - analysisType: DataFrameAnalyticsType; + analysisType: DataFrameAnalysisConfigType; + globalState?: MlCommonGlobalState; + } +>; + +export type CalendarEditUrlState = MLPageState< + typeof ML_PAGES.CALENDARS_EDIT, + { + calendarId: string; + globalState?: MlCommonGlobalState; + } +>; + +export type FilterEditUrlState = MLPageState< + typeof ML_PAGES.FILTER_LISTS_EDIT, + { + filterId: string; + globalState?: MlCommonGlobalState; } >; @@ -183,5 +204,6 @@ export type MlUrlGeneratorState = | TimeSeriesExplorerUrlState | DataFrameAnalyticsUrlState | DataFrameAnalyticsExplorationUrlState - | DataVisualizerUrlState + | CalendarEditUrlState + | FilterEditUrlState | MlGenericUrlState; diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts index d725984a47d66..d231ed4344389 100644 --- a/x-pack/plugins/ml/common/util/analytics_utils.ts +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -9,8 +9,8 @@ import { ClassificationAnalysis, OutlierAnalysis, RegressionAnalysis, - ANALYSIS_CONFIG_TYPE, } from '../types/data_frame_analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { const keys = Object.keys(arg); diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index fc673397ef177..2c5dbe108ab1e 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -16,7 +16,8 @@ "embeddable", "uiActions", "kibanaLegacy", - "indexPatternManagement" + "indexPatternManagement", + "discover" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx index c281dc4e9ae05..e3bcc53fe697f 100644 --- a/x-pack/plugins/ml/public/application/app.tsx +++ b/x-pack/plugins/ml/public/application/app.tsx @@ -20,6 +20,7 @@ import { MlSetupDependencies, MlStartDependencies } from '../plugin'; import { MlRouter } from './routing'; import { mlApiServicesProvider } from './services/ml_api_service'; import { HttpService } from './services/http_service'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../common/constants/ml_url_generator'; export type MlDependencies = Omit & MlStartDependencies; @@ -50,11 +51,21 @@ export interface MlServicesContext { export type MlGlobalServices = ReturnType; const App: FC = ({ coreStart, deps, appMountParams }) => { + const redirectToMlAccessDeniedPage = async () => { + const accessDeniedPageUrl = await deps.share.urlGenerators + .getUrlGenerator(ML_APP_URL_GENERATOR) + .createUrl({ + page: ML_PAGES.ACCESS_DENIED, + }); + await coreStart.application.navigateToUrl(accessDeniedPageUrl); + }; + const pageDeps = { history: appMountParams.history, indexPatterns: deps.data.indexPatterns, config: coreStart.uiSettings!, setBreadcrumbs: coreStart.chrome!.setBreadcrumbs, + redirectToMlAccessDeniedPage, }; const services = { appName: 'ML', diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts index 653eca126006d..cdd25821ea5ca 100644 --- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts +++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts @@ -33,10 +33,12 @@ export function checkGetManagementMlJobsResolver() { }); } -export function checkGetJobsCapabilitiesResolver(): Promise { +export function checkGetJobsCapabilitiesResolver( + redirectToMlAccessDeniedPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities, isPlatinumOrTrialLicense }) => { + .then(async ({ capabilities, isPlatinumOrTrialLicense }) => { _capabilities = capabilities; // the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list. // all other functionality is controlled by the return capabilities object. @@ -46,21 +48,23 @@ export function checkGetJobsCapabilitiesResolver(): Promise { if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) { return resolve(_capabilities); } else { - window.location.href = '#/access-denied'; + await redirectToMlAccessDeniedPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/access-denied'; + .catch(async (e) => { + await redirectToMlAccessDeniedPage(); return reject(); }); }); } -export function checkCreateJobsCapabilitiesResolver(): Promise { +export function checkCreateJobsCapabilitiesResolver( + redirectToJobsManagementPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities, isPlatinumOrTrialLicense }) => { + .then(async ({ capabilities, isPlatinumOrTrialLicense }) => { _capabilities = capabilities; // if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect, // allow the promise to resolve as the separate license check will redirect then user to @@ -69,34 +73,36 @@ export function checkCreateJobsCapabilitiesResolver(): Promise { return resolve(_capabilities); } else { // if the user has no permission to create a job, - // redirect them back to the Transforms Management page - window.location.href = '#/jobs'; + // redirect them back to the Anomaly Detection Management page + await redirectToJobsManagementPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/jobs'; + .catch(async (e) => { + await redirectToJobsManagementPage(); return reject(); }); }); } -export function checkFindFileStructurePrivilegeResolver(): Promise { +export function checkFindFileStructurePrivilegeResolver( + redirectToMlAccessDeniedPage: () => Promise +): Promise { return new Promise((resolve, reject) => { getCapabilities() - .then(({ capabilities }) => { + .then(async ({ capabilities }) => { _capabilities = capabilities; // the minimum privilege for using ML with a basic license is being able to use the datavisualizer. // all other functionality is controlled by the return _capabilities object if (_capabilities.canFindFileStructure) { return resolve(_capabilities); } else { - window.location.href = '#/access-denied'; + await redirectToMlAccessDeniedPage(); return reject(); } }) - .catch((e) => { - window.location.href = '#/access-denied'; + .catch(async (e) => { + await redirectToMlAccessDeniedPage(); return reject(); }); }); diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap index 9eb44c71aa799..114a6b235d1ad 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap @@ -1,170 +1,527 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`AnnotationsTable Initialization with annotations prop. 1`] = ` - - ", + "end_timestamp": 1455041968976, + "job_id": "farequote", + "modified_time": 1546417097181, + "modified_username": "", + "timestamp": 1455026177994, + "type": "annotation", + }, + ] + } + intl={ + Object { + "defaultFormats": Object {}, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "formatPlural": [Function], + "formatRelative": [Function], + "formatTime": [Function], + "formats": Object { + "date": Object { + "full": Object { + "day": "numeric", + "month": "long", + "weekday": "long", + "year": "numeric", + }, + "long": Object { + "day": "numeric", + "month": "long", + "year": "numeric", + }, + "medium": Object { + "day": "numeric", + "month": "short", + "year": "numeric", + }, + "short": Object { + "day": "numeric", + "month": "numeric", + "year": "2-digit", + }, + }, + "number": Object { + "currency": Object { + "style": "currency", + }, + "percent": Object { + "style": "percent", + }, + }, + "relative": Object { + "days": Object { + "units": "day", + }, + "hours": Object { + "units": "hour", + }, + "minutes": Object { + "units": "minute", + }, + "months": Object { + "units": "month", + }, + "seconds": Object { + "units": "second", + }, + "years": Object { + "units": "year", + }, + }, + "time": Object { + "full": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "long": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short", + }, + "medium": Object { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + }, + "short": Object { + "hour": "numeric", + "minute": "numeric", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "onError": [Function], + "textComponent": Symbol(react.fragment), + "timeZone": null, + } + } + kibana={ + Object { + "notifications": Object { + "toasts": Object { + "danger": [Function], + "show": [Function], + "success": [Function], + "warning": [Function], + }, + }, + "overlays": Object { + "openFlyout": [Function], + "openModal": [Function], + }, + "services": Object {}, + } + } +/> +`; + +exports[`AnnotationsTable Initialization with job config prop. 1`] = ` +", - "end_timestamp": 1455041968976, "job_id": "farequote", - "modified_time": 1546417097181, - "modified_username": "", - "timestamp": 1455026177994, - "type": "annotation", + "query": Object { + "bool": Object { + "adjust_pure_negative": true, + "boost": 1, + "must": Array [ + Object { + "query_string": Object { + "analyze_wildcard": true, + "auto_generate_synonyms_phrase_query": true, + "boost": 1, + "default_operator": "or", + "enable_position_increments": true, + "escape": false, + "fields": Array [], + "fuzziness": "AUTO", + "fuzzy_max_expansions": 50, + "fuzzy_prefix_length": 0, + "fuzzy_transpositions": true, + "max_determinized_states": 10000, + "phrase_slop": 0, + "query": "*", + "type": "best_fields", + }, + }, + ], + }, + }, + "query_delay": "115823ms", + "scroll_size": 1000, + "state": "stopped", }, - ] - } - pagination={ - Object { - "pageSizeOptions": Array [ - 5, - 10, - 25, - ], - } - } - responsive={true} - rowProps={[Function]} - search={ - Object { - "box": Object { - "incremental": true, - "schema": true, - }, - "defaultQuery": "event:(user or delayed_data)", - "filters": Array [ - Object { - "field": "event", - "multiSelect": "or", - "name": "Event", - "options": Array [], - "type": "field_value_selection", - }, - ], - } - } - sorting={ - Object { - "sort": Object { - "direction": "asc", - "field": "timestamp", + "description": "", + "established_model_memory": 42102, + "finished_time": 1546418359427, + "job_id": "farequote", + "job_type": "anomaly_detector", + "job_version": "7.0.0", + "model_plot_config": Object { + "enabled": true, }, - } + "model_size_stats": Object { + "bucket_allocation_failures_count": 0, + "job_id": "farequote", + "log_time": 1546418359000, + "memory_status": "ok", + "model_bytes": 42102, + "result_type": "model_size_stats", + "timestamp": 1455232500000, + "total_by_field_count": 3, + "total_over_field_count": 0, + "total_partition_field_count": 2, + }, + "model_snapshot_id": "1546418359", + "model_snapshot_min_version": "6.4.0", + "model_snapshot_retention_days": 1, + "results_index_name": "shared", + "state": "closed", + }, + ] + } + kibana={ + Object { + "notifications": Object { + "toasts": Object { + "danger": [Function], + "show": [Function], + "success": [Function], + "warning": [Function], + }, + }, + "overlays": Object { + "openFlyout": [Function], + "openModal": [Function], + }, + "services": Object {}, } - tableLayout="fixed" - /> - -`; - -exports[`AnnotationsTable Initialization with job config prop. 1`] = ` - - - - - + } +/> `; exports[`AnnotationsTable Minimal initialization without props. 1`] = ` - + `; diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 9dabfce163dbb..d5025fd3c3649 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -13,7 +13,6 @@ import uniq from 'lodash/uniq'; import PropTypes from 'prop-types'; -import rison from 'rison-node'; import React, { Component, Fragment } from 'react'; import memoizeOne from 'memoize-one'; import { @@ -54,12 +53,15 @@ import { ANNOTATION_EVENT_USER, ANNOTATION_EVENT_DELAYED_DATA, } from '../../../../../common/constants/annotations'; +import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../../common/constants/app'; const CURRENT_SERIES = 'current_series'; /** * Table component for rendering the lists of annotations for an ML job. */ -export class AnnotationsTable extends Component { +class AnnotationsTableUI extends Component { static propTypes = { annotations: PropTypes.array, jobs: PropTypes.array, @@ -199,7 +201,17 @@ export class AnnotationsTable extends Component { } } - openSingleMetricView = (annotation = {}) => { + openSingleMetricView = async (annotation = {}) => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + // Creates the link to the Single Metric Viewer. // Set the total time range from the start to the end of the annotation. const job = this.getJob(annotation.job_id); @@ -210,30 +222,10 @@ export class AnnotationsTable extends Component { ); const from = new Date(dataCounts.earliest_record_timestamp).toISOString(); const to = new Date(resultLatest).toISOString(); - - const globalSettings = { - ml: { - jobIds: [job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from, - to, - mode: 'absolute', - }, - }; - - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, - }, + const timeRange = { + from, + to, + mode: 'absolute', }; let mlTimeSeriesExplorer = {}; const entityCondition = {}; @@ -247,11 +239,11 @@ export class AnnotationsTable extends Component { }; if (annotation.timestamp < dataCounts.earliest_record_timestamp) { - globalSettings.time.from = new Date(annotation.timestamp).toISOString(); + timeRange.from = new Date(annotation.timestamp).toISOString(); } if (annotation.end_timestamp > dataCounts.latest_record_timestamp) { - globalSettings.time.to = new Date(annotation.end_timestamp).toISOString(); + timeRange.to = new Date(annotation.end_timestamp).toISOString(); } } @@ -274,14 +266,34 @@ export class AnnotationsTable extends Component { entityCondition[annotation.by_field_name] = annotation.by_field_value; } mlTimeSeriesExplorer.entities = entityCondition; - appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; - - const _g = rison.encode(globalSettings); - const _a = rison.encode(appState); + // appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer; + + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const singleMetricViewerLink = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + timeRange, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + jobIds: [job.job_id], + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, + }, + ...mlTimeSeriesExplorer, + }, + excludeBasePath: true, + }); - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); + addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, singleMetricViewerLink); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); }; onMouseOverRow = (record) => { @@ -686,3 +698,5 @@ export class AnnotationsTable extends Component { ); } } + +export const AnnotationsTable = withKibana(AnnotationsTableUI); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js index fdeab0c49e32b..6025dd1c7433e 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js @@ -29,6 +29,8 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils'; import { getIndexPatternIdFromName } from '../../util/index_utils'; import { replaceStringTokens } from '../../util/string_utils'; +import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../common/constants/app'; /* * Component for rendering the links menu inside a cell in the anomalies table. */ @@ -142,7 +144,18 @@ class LinksMenuUI extends Component { } }; - viewSeries = () => { + viewSeries = async () => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const record = this.props.anomaly.source; const bounds = this.props.bounds; const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z @@ -171,44 +184,36 @@ class LinksMenuUI extends Component { entityCondition[record.by_field_name] = record.by_field_value; } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { + const singleMetricViewerLink = await mlUrlGenerator.createUrl({ + excludeBasePath: true, + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { jobIds: [record.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeRange: { + from: from, + to: to, + mode: 'absolute', + }, zoom: { from: zoomFrom, to: zoomTo, }, detectorIndex: record.detector_index, entities: entityCondition, - }, - query: { query_string: { analyze_wildcard: true, query: '*', }, }, }); - - // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'. - let path = '#/timeseriesexplorer'; - path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`; - window.open(path, '_blank'); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); }; viewExamples = () => { diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx index 4a63a8cd7e716..d54a7fe81e858 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx @@ -6,13 +6,22 @@ import React from 'react'; import { Router } from 'react-router-dom'; -import { render, fireEvent } from '@testing-library/react'; +import { render } from '@testing-library/react'; import { createBrowserHistory } from 'history'; import { I18nProvider } from '@kbn/i18n/react'; import { AnomalyResultsViewSelector } from './index'; +jest.mock('../../contexts/kibana', () => { + return { + useMlUrlGenerator: () => ({ + createUrl: jest.fn(), + }), + useNavigateToPath: () => jest.fn(), + }; +}); + describe('AnomalyResultsViewSelector', () => { test('should create selector with correctly selected value', () => { const history = createBrowserHistory(); @@ -31,27 +40,4 @@ describe('AnomalyResultsViewSelector', () => { getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer').hasAttribute('checked') ).toBe(true); }); - - test('should open window to other results view when clicking on non-checked input', () => { - // Create mock for window.open - const mockedOpen = jest.fn(); - const originalOpen = window.open; - window.open = mockedOpen; - - const history = createBrowserHistory(); - - const { getByTestId } = render( - - - - - - ); - - fireEvent.click(getByTestId('mlAnomalyResultsViewSelectorExplorer')); - expect(mockedOpen).toHaveBeenCalledWith('#/explorer', '_self'); - - // Clean-up window.open. - window.open = originalOpen; - }); }); diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx index 78acb422851e3..c4c8f06bbbc3a 100644 --- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx +++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx @@ -5,21 +5,25 @@ */ import React, { FC, useMemo } from 'react'; -import { encode } from 'rison-node'; import { EuiButtonGroup } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useUrlState } from '../../util/url_state'; +import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; interface Props { - viewId: 'timeseriesexplorer' | 'explorer'; + viewId: typeof ML_PAGES.SINGLE_METRIC_VIEWER | typeof ML_PAGES.ANOMALY_EXPLORER; } // Component for rendering a set of buttons for switching between the Anomaly Detection results views. export const AnomalyResultsViewSelector: FC = ({ viewId }) => { + const urlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const toggleButtonsIcons = useMemo( () => [ { @@ -28,7 +32,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { defaultMessage: 'View results in the Single Metric Viewer', }), iconType: 'visLine', - value: 'timeseriesexplorer', + value: ML_PAGES.SINGLE_METRIC_VIEWER, 'data-test-subj': 'mlAnomalyResultsViewSelectorSingleMetricViewer', }, { @@ -37,7 +41,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { defaultMessage: 'View results in the Anomaly Explorer', }), iconType: 'visTable', - value: 'explorer', + value: ML_PAGES.ANOMALY_EXPLORER, 'data-test-subj': 'mlAnomalyResultsViewSelectorExplorer', }, ], @@ -46,9 +50,14 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { const [globalState] = useUrlState('_g'); - const onChangeView = (newViewId: string) => { - const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; - window.open(`#/${newViewId}${fullGlobalStateString}`, '_self'); + const onChangeView = async (newViewId: Props['viewId']) => { + const url = await urlGenerator.createUrl({ + page: newViewId, + pageState: { + globalState, + }, + }); + await navigateToPath(url); }; return ( @@ -60,7 +69,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => { data-test-subj="mlAnomalyResultsViewSelector" options={toggleButtonsIcons} idSelected={viewId} - onChange={onChangeView} + onChange={(newViewId: string) => onChangeView(newViewId as Props['viewId'])} isIconOnly /> ); diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts index 368e758a027c4..b4668810b9421 100644 --- a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts +++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts @@ -22,16 +22,19 @@ export const useCreateADLinks = () => { const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE); const createLinkWithUserDefaults = useCallback( (location, jobList) => { - const resultsPageUrl = mlJobService.createResultsUrlForJobs( + return mlJobService.createResultsUrlForJobs( jobList, location, useUserTimeSettings === true && userTimeSettings !== undefined ? userTimeSettings : undefined ); - return `${basePath.get()}/app/ml${resultsPageUrl}`; }, [basePath] ); return { createLinkWithUserDefaults }; }; + +export type CreateLinkWithUserDefaults = ReturnType< + typeof useCreateADLinks +>['createLinkWithUserDefaults']; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index 22815fe593d57..6aad5d53c3a3c 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -32,6 +32,7 @@ import { UseIndexDataReturnType } from './types'; import { DecisionPathPopover } from './feature_importance/decision_path_popover'; import { TopClasses } from '../../../../common/types/feature_importance'; import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics'; // TODO Fix row hovering + bar highlighting // import { hoveredRow$ } from './column_chart'; @@ -44,7 +45,7 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( interface PropsWithoutHeader extends UseIndexDataReturnType { baseline?: number; - analysisType?: ANALYSIS_CONFIG_TYPE; + analysisType?: DataFrameAnalysisConfigType; resultsField?: string; dataTestSubj: string; toastNotifications: CoreSetup['notifications']['toasts']; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx index 263337f93e9a8..7c4428db71b3b 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx @@ -13,10 +13,11 @@ import { FeatureImportance, TopClasses } from '../../../../../common/types/featu import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common'; import { ClassificationDecisionPath } from './decision_path_classification'; import { useMlKibana } from '../../../contexts/kibana'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; interface DecisionPathPopoverProps { featureImportance: FeatureImportance[]; - analysisType: ANALYSIS_CONFIG_TYPE; + analysisType: DataFrameAnalysisConfigType; predictionFieldName?: string; baseline?: number; predictedValue?: number | string | undefined; diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js index 1f03dbe134756..279afc8c50339 100644 --- a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js +++ b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js @@ -9,11 +9,16 @@ import PropTypes from 'prop-types'; import { EuiIcon, EuiFlexItem } from '@elastic/eui'; import { CreateJobLinkCard } from '../create_job_link_card'; +import { useMlKibana } from '../../contexts/kibana'; export const RecognizedResult = ({ config, indexPattern, savedSearch }) => { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); const id = savedSearch === null ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`; - - const href = `#/jobs/new_job/recognize?id=${config.id}&${id}`; + const href = `${basePath.get()}/app/ml/jobs/new_job/recognize?id=${config.id}&${id}`; let logo = null; // if a logo is available, use that, otherwise display the id diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 3a4875fa243fd..671f0b196ce35 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -4,15 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState } from 'react'; -import { encode } from 'rison-node'; +import React, { FC, useState, useEffect } from 'react'; -import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui'; +import { EuiTabs, EuiTab } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - -import { useUrlState } from '../../util/url_state'; - import { TabId } from './navigation_menu'; +import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; +import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator'; +import { useUrlState } from '../../util/url_state'; +import { ML_APP_NAME } from '../../../../common/constants/app'; export interface Tab { id: TabId; @@ -66,20 +66,57 @@ function getTabs(disableLinks: boolean): Tab[] { } interface TabData { testSubject: string; - pathId?: string; + pathId?: MlUrlGeneratorState['page']; + name: string; } const TAB_DATA: Record = { - overview: { testSubject: 'mlMainTab overview' }, + overview: { + testSubject: 'mlMainTab overview', + name: i18n.translate('xpack.ml.overviewTabLabel', { + defaultMessage: 'Overview', + }), + }, // Note that anomaly detection jobs list is mapped to ml#/jobs. - anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' }, - data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' }, - datavisualizer: { testSubject: 'mlMainTab dataVisualizer' }, - settings: { testSubject: 'mlMainTab settings' }, - 'access-denied': { testSubject: 'mlMainTab overview' }, + anomaly_detection: { + testSubject: 'mlMainTab anomalyDetection', + name: i18n.translate('xpack.ml.anomalyDetectionTabLabel', { + defaultMessage: 'Anomaly Detection', + }), + pathId: 'jobs', + }, + data_frame_analytics: { + testSubject: 'mlMainTab dataFrameAnalytics', + name: i18n.translate('xpack.ml.dataFrameAnalyticsTabLabel', { + defaultMessage: 'Data Frame Analytics', + }), + }, + datavisualizer: { + testSubject: 'mlMainTab dataVisualizer', + name: i18n.translate('xpack.ml.dataVisualizerTabLabel', { + defaultMessage: 'Data Visualizer', + }), + }, + settings: { + testSubject: 'mlMainTab settings', + name: i18n.translate('xpack.ml.settingsTabLabel', { + defaultMessage: 'Settings', + }), + }, + 'access-denied': { + testSubject: 'mlMainTab overview', + name: i18n.translate('xpack.ml.accessDeniedTabLabel', { + defaultMessage: 'Access Denied', + }), + }, }; export const MainTabs: FC = ({ tabId, disableLinks }) => { + const { + services: { + chrome: { docTitle }, + }, + } = useMlKibana(); const [globalState] = useUrlState('_g'); const [selectedTabId, setSelectedTabId] = useState(tabId); function onSelectedTabChanged(id: TabId) { @@ -87,16 +124,40 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { } const tabs = getTabs(disableLinks); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToTab = async (defaultPathId: MlUrlGeneratorState['page']) => { + const pageState = + globalState?.refreshInterval !== undefined + ? { + globalState: { + refreshInterval: globalState.refreshInterval, + }, + } + : undefined; + // TODO - Fix ts so passing pageState won't default to MlGenericUrlState when pageState is passed in + // @ts-ignore + const path = await mlUrlGenerator.createUrl({ + page: defaultPathId, + // only retain the refreshInterval part of globalState + // appState will not be considered. + pageState, + }); + + await navigateToPath(path, false); + }; + + useEffect(() => { + docTitle.change([TAB_DATA[selectedTabId].name, ML_APP_NAME]); + }, [selectedTabId]); return ( {tabs.map((tab: Tab) => { const { id, disabled } = tab; const testSubject = TAB_DATA[id].testSubject; - const defaultPathId = TAB_DATA[id].pathId || id; - // globalState (e.g. selected jobs and time range) should be retained when changing pages. - // appState will not be considered. - const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : ''; + const defaultPathId = (TAB_DATA[id].pathId || id) as MlUrlGeneratorState['page']; return disabled ? ( @@ -104,21 +165,18 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { ) : (
- { + onSelectedTabChanged(id); + redirectToTab(defaultPathId); + }} + isSelected={id === selectedTabId} + key={`tab-${id}-key`} > - onSelectedTabChanged(id)} - isSelected={id === selectedTabId} - key={`tab-${id}-key`} - > - {tab.name} - - + {tab.name} +
); })} diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js index 48e0da72f067c..eb12cb7679674 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js @@ -17,8 +17,19 @@ import { ScopeExpression } from './scope_expression'; import { checkPermission } from '../../capabilities/check_capabilities'; import { getScopeFieldDefaults } from './utils'; import { FormattedMessage } from '@kbn/i18n/react'; +import { ML_PAGES } from '../../../../common/constants/ml_url_generator'; +import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana'; function NoFilterListsCallOut() { + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + const redirectToFilterManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.FILTER_LISTS_MANAGE, + }); + await navigateToPath(path, true); + }; + return ( + useKibana(); export type MlKibanaReactContextValue = KibanaReactContextValue; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts index 48385ad3ae6a8..d448185c914b8 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ +import { useCallback, useEffect, useState } from 'react'; import { useMlKibana } from './kibana_context'; import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator'; +import { useUrlState } from '../../util/url_state'; export const useMlUrlGenerator = () => { const { @@ -18,3 +21,59 @@ export const useMlUrlGenerator = () => { return getUrlGenerator(ML_APP_URL_GENERATOR); }; + +export const useMlLink = (params: MlUrlGeneratorState): string => { + const [href, setHref] = useState(params.page); + const mlUrlGenerator = useMlUrlGenerator(); + + useEffect(() => { + let isCancelled = false; + const generateUrl = async (_params: MlUrlGeneratorState) => { + const url = await mlUrlGenerator.createUrl(_params); + if (!isCancelled) { + setHref(url); + } + }; + generateUrl(params); + return () => { + isCancelled = true; + }; + }, [params]); + + return href; +}; + +export const useCreateAndNavigateToMlLink = ( + page: MlUrlGeneratorState['page'] +): (() => Promise) => { + const mlUrlGenerator = useMlUrlGenerator(); + const [globalState] = useUrlState('_g'); + + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToMlPage = useCallback( + async (_page: MlUrlGeneratorState['page']) => { + const pageState = + globalState?.refreshInterval !== undefined + ? { + globalState: { + refreshInterval: globalState.refreshInterval, + }, + } + : undefined; + + // TODO: fix ts only interpreting it as MlUrlGenericState if pageState is passed + // @ts-ignore + const url = await mlUrlGenerator.createUrl({ page: _page, pageState }); + await navigateToUrl(url); + }, + [mlUrlGenerator, navigateToUrl] + ); + + // returns the onClick callback + return useCallback(() => redirectToMlPage(page), [redirectToMlPage, page]); +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 60681fb6e7bbe..d22bba7738db4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -15,8 +15,8 @@ import { SavedSearchQuery } from '../../contexts/ml'; import { AnalysisConfig, ClassificationAnalysis, + DataFrameAnalysisConfigType, RegressionAnalysis, - ANALYSIS_CONFIG_TYPE, } from '../../../../common/types/data_frame_analytics'; import { isOutlierAnalysis, @@ -26,6 +26,7 @@ import { getDependentVar, getPredictedFieldName, } from '../../../../common/util/analytics_utils'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics'; export type IndexPattern = string; export enum ANALYSIS_ADVANCED_FIELDS { @@ -429,7 +430,7 @@ interface LoadEvalDataConfig { predictionFieldName?: string; searchQuery?: ResultsSearchQuery; ignoreDefaultQuery?: boolean; - jobType: ANALYSIS_CONFIG_TYPE; + jobType: DataFrameAnalysisConfigType; requiresKeyword?: boolean; } @@ -550,7 +551,7 @@ export { isRegressionAnalysis, isClassificationAnalysis, getPredictionFieldName, - ANALYSIS_CONFIG_TYPE, getDependentVar, getPredictedFieldName, + ANALYSIS_CONFIG_TYPE, }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts index 00d735d9a866e..83eebccd310e3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts @@ -14,7 +14,6 @@ export { UpdateDataFrameAnalyticsConfig, IndexPattern, REFRESH_ANALYTICS_LIST_STATE, - ANALYSIS_CONFIG_TYPE, OUTLIER_ANALYSIS_METHOD, RegressionEvaluateResponse, getValuesFromResponse, @@ -26,6 +25,7 @@ export { SEARCH_SIZE, defaultSearchQuery, SearchQuery, + ANALYSIS_CONFIG_TYPE, } from './analytics'; export { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx index 1e5dbee3499bd..1e6a616fedd64 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; -import { ANALYSIS_CONFIG_TYPE } from '../../../../common'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx index 88c89df86b29a..310cd4e3b3a79 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx @@ -16,6 +16,7 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com import { CATEGORICAL_TYPES } from './form_options_validation'; import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../../../../services/new_job_capabilities_service'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; const containsClassificationFieldsCb = ({ name, type }: Field) => !OMIT_FIELDS.includes(name) && @@ -32,13 +33,13 @@ const containsRegressionFieldsCb = ({ name, type }: Field) => const containsOutlierFieldsCb = ({ name, type }: Field) => !OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type); -const callbacks: Record boolean> = { +const callbacks: Record boolean> = { [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb, [ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb, [ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb, }; -const messages: Record = { +const messages: Record = { [ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: ( = ({ jobId, analysisType }) => { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index eea579ef1d064..84b1c4241aaf2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -29,7 +29,6 @@ import { SEARCH_SIZE, defaultSearchQuery, getAnalysisType, - ANALYSIS_CONFIG_TYPE, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; @@ -39,6 +38,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; import { useMlKibana } from '../../../../../contexts/kibana'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', @@ -195,7 +195,7 @@ export const ExplorationResultsTable: FC = React.memo( {...classificationData} dataTestSubj="mlExplorationDataGrid" toastNotifications={getToastNotifications()} - analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE} + analysisType={(analysisType as unknown) as DataFrameAnalysisConfigType} />
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index c8349084dbda8..f4f01330271fc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -26,11 +26,12 @@ import { OutlierExploration } from './components/outlier_exploration'; import { RegressionExploration } from './components/regression_exploration'; import { ClassificationExploration } from './components/classification_exploration'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; export const Page: FC<{ jobId: string; - analysisType: ANALYSIS_CONFIG_TYPE; + analysisType: DataFrameAnalysisConfigType; }> = ({ jobId, analysisType }) => ( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx index a3595b51d0a59..2363e6fbecc9d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx @@ -7,24 +7,32 @@ import React, { useCallback, useMemo } from 'react'; import { getAnalysisType } from '../../../../common/analytics'; -import { useNavigateToPath } from '../../../../../contexts/kibana'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana'; -import { - getResultsUrl, - DataFrameAnalyticsListAction, - DataFrameAnalyticsListRow, -} from '../analytics_list/common'; +import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common'; import { getViewLinkStatus } from './get_view_link_status'; import { viewActionButtonText, ViewButton } from './view_button'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; export type ViewAction = ReturnType; export const useViewAction = () => { + const mlUrlGenerator = useMlUrlGenerator(); const navigateToPath = useNavigateToPath(); + const redirectToTab = async (jobId: string, analysisType: DataFrameAnalysisConfigType) => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { jobId, analysisType }, + }); + + await navigateToPath(path, false); + }; + const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => { - const analysisType = getAnalysisType(item.config.analysis); - navigateToPath(getResultsUrl(item.id, analysisType)); + const analysisType = getAnalysisType(item.config.analysis) as DataFrameAnalysisConfigType; + redirectToTab(item.id, analysisType); }, []); const action: DataFrameAnalyticsListAction = useMemo( diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 0c3bff58c25cd..2f8e087a6a3f0 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -15,12 +15,8 @@ import { EuiSearchBarProps, EuiSpacer, } from '@elastic/eui'; - -import { - DataFrameAnalyticsId, - useRefreshAnalyticsList, - ANALYSIS_CONFIG_TYPE, -} from '../../../../common'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; +import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts index 994357412510d..37076d400f021 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts @@ -9,11 +9,8 @@ import { EuiTableActionsColumnType, Query, Ast } from '@elastic/eui'; import { DATA_FRAME_TASK_STATE } from './data_frame_task_state'; export { DATA_FRAME_TASK_STATE }; -import { - DataFrameAnalyticsId, - DataFrameAnalyticsConfig, - ANALYSIS_CONFIG_TYPE, -} from '../../../../common'; +import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; export enum DATA_FRAME_MODE { BATCH = 'batch', @@ -111,10 +108,7 @@ export interface DataFrameAnalyticsListRow { checkpointing: object; config: DataFrameAnalyticsConfig; id: DataFrameAnalyticsId; - job_type: - | ANALYSIS_CONFIG_TYPE.CLASSIFICATION - | ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION - | ANALYSIS_CONFIG_TYPE.REGRESSION; + job_type: DataFrameAnalysisConfigType; mode: string; state: DataFrameAnalyticsStats['state']; stats: DataFrameAnalyticsStats; @@ -137,10 +131,6 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) { return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100; } -export function getResultsUrl(jobId: string, analysisType: ANALYSIS_CONFIG_TYPE | string) { - return `#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`; -} - // The single Action type is not exported as is // from EUI so we use that code to get the single // Action type from the array of actions. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx index ef1d373a55a12..1af99d2a1ed00 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx @@ -19,8 +19,6 @@ import { EuiLink, RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url'; - import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common'; import { getDataFrameAnalyticsProgressPhase, @@ -32,6 +30,8 @@ import { DataFrameAnalyticsStats, } from './common'; import { useActions } from './use_actions'; +import { useMlLink } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; enum TASK_STATE_COLOR { analyzing = 'primary', @@ -134,9 +134,14 @@ export const progressColumn = { 'data-test-subj': 'mlAnalyticsTableColumnProgress', }; -export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => ( - {item.id} -); +export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => { + const href = useMlLink({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { jobId: item.id }, + }); + + return {item.id}; +}; export const useColumns = ( expandedRowItemIds: DataFrameAnalyticsId[], @@ -145,7 +150,6 @@ export const useColumns = ( isMlEnabledInSpace: boolean = true ) => { const { actions, modals } = useActions(isManagementTable); - function toggleDetails(item: DataFrameAnalyticsListRow) { const index = expandedRowItemIds.indexOf(item.config.id); if (index !== -1) { @@ -200,7 +204,7 @@ export const useColumns = ( 'data-test-subj': 'mlAnalyticsTableColumnId', scope: 'row', render: (item: DataFrameAnalyticsListRow) => - isManagementTable ? getDFAnalyticsJobIdLink(item) : item.id, + isManagementTable ? : item.id, }, { field: DataFrameAnalyticsListColumn.description, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 338b6444671a6..dbc7a23f2258b 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -29,21 +29,23 @@ import { useInferenceApiService } from '../../../../../services/ml_api_service/i import { ModelsTableToConfigMapping } from './index'; import { TIME_FORMAT } from '../../../../../../../common/constants/time_format'; import { DeleteModelsModal } from './delete_models_modal'; -import { useMlKibana, useNotifications } from '../../../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana'; import { ExpandedRow } from './expanded_row'; -import { getResultsUrl } from '../analytics_list/common'; import { ModelConfigResponse, ModelPipelines, TrainedModelStat, } from '../../../../../../../common/types/inference'; import { + getAnalysisType, REFRESH_ANALYTICS_LIST_STATE, refreshAnalyticsList$, useRefreshAnalyticsList, } from '../../../../common'; import { useTableSettings } from '../analytics_list/use_table_settings'; import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar'; +import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; type Stats = Omit; @@ -61,6 +63,7 @@ export const ModelsList: FC = () => { application: { navigateToUrl, capabilities }, }, } = useMlKibana(); + const urlGenerator = useMlUrlGenerator(); const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; @@ -278,12 +281,19 @@ export const ModelsList: FC = () => { type: 'icon', available: (item) => item.metadata?.analytics_config?.id, onClick: async (item) => { - await navigateToUrl( - getResultsUrl( - item.metadata?.analytics_config.id, - Object.keys(item.metadata?.analytics_config.analysis)[0] - ) - ); + if (item.metadata?.analytics_config === undefined) return; + + const url = await urlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: item.metadata?.analytics_config.id as string, + analysisType: getAnalysisType( + item.metadata?.analytics_config.analysis + ) as DataFrameAnalysisConfigType, + }, + }); + + await navigateToUrl(url); }, isPrimary: true, }, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts index 7cd9fcc052f1a..178638322bacd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts @@ -33,13 +33,13 @@ import { JOB_ID_MAX_LENGTH, ALLOWED_DATA_UNITS, } from '../../../../../../../common/constants/validation'; +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; import { getDependentVar, getNumTopFeatureImportanceValues, getTrainingPercent, isRegressionAnalysis, isClassificationAnalysis, - ANALYSIS_CONFIG_TYPE, NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN, TRAINING_PERCENT_MIN, TRAINING_PERCENT_MAX, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts index 4926decaa7f9c..2a89c5a5fd686 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts @@ -8,13 +8,14 @@ import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/com import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { mlNodesAvailable } from '../../../../../ml_nodes_check'; -import { ANALYSIS_CONFIG_TYPE, defaultSearchQuery } from '../../../../common/analytics'; +import { defaultSearchQuery, getAnalysisType } from '../../../../common/analytics'; import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone'; import { DataFrameAnalyticsConfig, DataFrameAnalyticsId, + DataFrameAnalysisConfigType, } from '../../../../../../../common/types/data_frame_analytics'; - +import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics'; export enum DEFAULT_MODEL_MEMORY_LIMIT { regression = '100mb', outlier_detection = '50mb', @@ -28,7 +29,7 @@ export const UNSET_CONFIG_ITEM = '--'; export type EsIndexName = string; export type DependentVariable = string; export type IndexPatternTitle = string; -export type AnalyticsJobType = ANALYSIS_CONFIG_TYPE | undefined; +export type AnalyticsJobType = DataFrameAnalysisConfigType | undefined; type IndexPatternId = string; export type SourceIndexMap = Record< IndexPatternTitle, @@ -290,7 +291,7 @@ export function getFormStateFromJobConfig( analyticsJobConfig: Readonly, isClone: boolean = true ): Partial { - const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE; + const jobType = getAnalysisType(analyticsJobConfig.analysis) as DataFrameAnalysisConfigType; const resultState: Partial = { jobType, diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 41f3bab8113f0..14427dd5c6ef2 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -11,7 +11,7 @@ import { GetDataFrameAnalyticsStatsResponseOk, } from '../../../../../services/ml_api_service/data_frame_analytics'; import { - ANALYSIS_CONFIG_TYPE, + getAnalysisType, REFRESH_ANALYTICS_LIST_STATE, refreshAnalyticsList$, } from '../../../../common'; @@ -25,6 +25,7 @@ import { isDataFrameAnalyticsStopped, } from '../../components/analytics_list/common'; import { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; +import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics'; export const isGetDataFrameAnalyticsStatsResponseOk = ( arg: any @@ -143,7 +144,7 @@ export const getAnalyticsFactory = ( checkpointing: {}, config, id: config.id, - job_type: Object.keys(config.analysis)[0] as ANALYSIS_CONFIG_TYPE, + job_type: getAnalysisType(config.analysis) as DataFrameAnalysisConfigType, mode: DATA_FRAME_MODE.BATCH, state: stats.state, stats, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx index 769b83c03110b..7c30dc0cac690 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx @@ -52,7 +52,10 @@ function startTrialDescription() { export const DatavisualizerSelector: FC = () => { useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); const { - services: { licenseManagement }, + services: { + licenseManagement, + http: { basePath }, + }, } = useMlKibana(); const navigateToPath = useNavigateToPath(); @@ -183,7 +186,10 @@ export const DatavisualizerSelector: FC = () => { } description={startTrialDescription()} footer={ - + = ({ to: 'now', }); const [showCreateJobLink, setShowCreateJobLink] = useState(false); - const [globalStateString, setGlobalStateString] = useState(''); + const [globalState, setGlobalState] = useState(); + + const [discoverLink, setDiscoverLink] = useState(''); const { services: { http: { basePath }, }, } = useMlKibana(); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const { + services: { + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = useMlKibana(); + + useEffect(() => { + let unmounted = false; + + const getDiscoverUrl = async (): Promise => { + const state: DiscoverUrlGeneratorState = { + indexPatternId, + }; + + if (globalState?.time) { + state.timeRange = globalState.time; + } + if (!unmounted) { + const discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR); + const discoverUrl = await discoverUrlGenerator.createUrl(state); + setDiscoverLink(discoverUrl); + } + }; + getDiscoverUrl(); + + return () => { + unmounted = true; + }; + }, [indexPatternId, getUrlGenerator]); + + const openInDataVisualizer = useCallback(async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, + pageState: { + index: indexPatternId, + globalState, + }, + }); + await navigateToPath(path); + }, [indexPatternId, globalState]); + + const redirectToADCreateJobsSelectTypePage = useCallback(async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, + pageState: { + index: indexPatternId, + globalState, + }, + }); + await navigateToPath(path); + }, [indexPatternId, globalState]); useEffect(() => { setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable()); @@ -49,11 +113,13 @@ export const ResultsLinks: FC = ({ }, []); useEffect(() => { - const _g = - timeFieldName !== undefined - ? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))` - : ''; - setGlobalStateString(_g); + const _globalState: MlCommonGlobalState = { + time: { + from: duration.from, + to: duration.to, + }, + }; + setGlobalState(_globalState); }, [duration]); async function updateTimeValues(recheck = true) { @@ -89,7 +155,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`${basePath.get()}/app/discover#/?&_a=(index:'${indexPatternId}')${globalStateString}`} + href={discoverLink} /> )} @@ -108,7 +174,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`} + onClick={redirectToADCreateJobsSelectTypePage} /> )} @@ -124,7 +190,7 @@ export const ResultsLinks: FC = ({ /> } description="" - href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`} + onClick={openInDataVisualizer} /> )} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx index 1f2c97b128e3f..ab738ca0f1545 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx @@ -9,11 +9,11 @@ import React, { FC, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui'; - +import { Link } from 'react-router-dom'; import { IndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { DataRecognizer } from '../../../../components/data_recognizer'; -import { getBasePath } from '../../../../util/dependency_cache'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; interface Props { indexPattern: IndexPattern; @@ -21,7 +21,6 @@ interface Props { export const ActionsPanel: FC = ({ indexPattern }) => { const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); - const basePath = getBasePath(); const recognizerResults = { count: 0, @@ -29,12 +28,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => { setRecognizerResultsCount(recognizerResults.count); }, }; - - function openAdvancedJobWizard() { - // TODO - pass the search string to the advanced job page as well as the index pattern - // (add in with new advanced job wizard?) - window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self'); - } + const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`; // Note we use display:none for the DataRecognizer section as it needs to be // passed the recognizerResults object, and then run the recognizer check which @@ -78,19 +72,19 @@ export const ActionsPanel: FC = ({ indexPattern }) => {

- + + +
); }; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap index c6503a639997d..826f7b707cfdf 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap @@ -3,17 +3,20 @@ exports[`ExplorerNoInfluencersFound snapshot 1`] = ` - - + + + + } data-test-subj="mlNoJobsFound" iconType="alert" diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js index 6f391f9746f23..029ca0475015f 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js @@ -7,25 +7,40 @@ /* * React component for rendering EuiEmptyPrompt when no jobs were found. */ - +import { Link } from 'react-router-dom'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useMlLink } from '../../../contexts/kibana/use_create_url'; -export const ExplorerNoJobsFound = () => ( - - - - } - actions={ - - - - } - data-test-subj="mlNoJobsFound" - /> -); +export const ExplorerNoJobsFound = () => { + const ADJobsManagementUrl = useMlLink({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + excludeBasePath: true, + }); + return ( + + + + } + actions={ + + + + + + } + data-test-subj="mlNoJobsFound" + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js index bcb11cad9674c..c9645b787a8e0 100644 --- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js +++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js @@ -8,6 +8,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ExplorerNoJobsFound } from './explorer_no_jobs_found'; +jest.mock('../../../contexts/kibana/use_create_url', () => ({ + useMlLink: jest.fn().mockReturnValue('/jobs'), +})); describe('ExplorerNoInfluencersFound', () => { test('snapshot', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js index 4fb783bfb6006..8f03b1903800a 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { EuiButtonEmpty, @@ -28,6 +28,10 @@ import { CHART_TYPE } from '../explorer_constants'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MlTooltipComponent } from '../../components/chart_tooltip'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../common/constants/app'; +import { addItemToRecentlyAccessed } from '../../util/recently_accessed'; const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', { defaultMessage: @@ -51,7 +55,23 @@ function getChartId(series) { } // Wrapper for a single explorer chart -function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) { +function ExplorerChartContainer({ + series, + severity, + tooManyBuckets, + wrapLabel, + navigateToApp, + mlUrlGenerator, +}) { + const redirectToSingleMetricViewer = useCallback(async () => { + const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series); + addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink); + + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerLink, + }); + }, [mlUrlGenerator]); + const { detectorLabel, entityFields } = series; const chartType = getChartType(series); @@ -106,7 +126,7 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) iconSide="right" iconType="visLine" size="xs" - onClick={() => window.open(getExploreSeriesLink(series), '_blank')} + onClick={redirectToSingleMetricViewer} > @@ -150,12 +170,24 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) } // Flex layout wrapper for all explorer charts -export const ExplorerChartsContainer = ({ +export const ExplorerChartsContainerUI = ({ chartsPerRow, seriesToPlot, severity, tooManyBuckets, + kibana, }) => { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = kibana; + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + // doesn't allow a setting of `columns={1}` when chartsPerRow would be 1. // If that's the case we trick it doing that with the following settings: const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto'; @@ -177,9 +209,13 @@ export const ExplorerChartsContainer = ({ severity={severity} tooManyBuckets={tooManyBuckets} wrapLabel={wrapLabel} + navigateToApp={navigateToApp} + mlUrlGenerator={mlUrlGenerator} /> ))} ); }; + +export const ExplorerChartsContainer = withKibana(ExplorerChartsContainerUI); diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js index 8257ac2b3a703..2da212c8f2f29 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js @@ -40,6 +40,12 @@ jest.mock('../../services/job_service', () => ({ }, })); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: (comp) => { + return comp; + }, +})); + describe('ExplorerChartsContainer', () => { const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 }; const originalGetBBox = SVGElement.prototype.getBBox; @@ -47,10 +53,22 @@ describe('ExplorerChartsContainer', () => { beforeEach(() => (SVGElement.prototype.getBBox = () => mockedGetBBox)); afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox)); + const kibanaContextMock = { + services: { + application: { navigateToApp: jest.fn() }, + share: { + urlGenerators: { getUrlGenerator: jest.fn() }, + }, + }, + }; test('Minimal Initialization', () => { const wrapper = shallow( - + ); @@ -71,10 +89,11 @@ describe('ExplorerChartsContainer', () => { ], chartsPerRow: 1, tooManyBuckets: false, + severity: 10, }; const wrapper = mount( - + ); @@ -98,10 +117,11 @@ describe('ExplorerChartsContainer', () => { ], chartsPerRow: 1, tooManyBuckets: false, + severity: 10, }; const wrapper = mount( - + ); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js index d0d0442dd4aee..85a342838a506 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js @@ -5,13 +5,20 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links'; +import { Link } from 'react-router-dom'; +import { useMlKibana } from '../../../../contexts/kibana'; -export function ResultLinks({ jobs }) { +export function ResultLinks({ jobs, isManagementTable }) { + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); const openJobsInSingleMetricViewerText = i18n.translate( 'xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText', { @@ -37,29 +44,59 @@ export function ResultLinks({ jobs }) { const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob; const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true; const { createLinkWithUserDefaults } = useCreateADLinks(); + const timeSeriesExplorerLink = useMemo( + () => createLinkWithUserDefaults('timeseriesexplorer', jobs), + [jobs] + ); + const anomalyExplorerLink = useMemo(() => createLinkWithUserDefaults('explorer', jobs), [jobs]); + return ( {singleMetricVisible && ( + {isManagementTable ? ( + + ) : ( + + + + )} + + )} + + {isManagementTable ? ( - - )} - - + ) : ( + + + + )}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js index 8f89c4a049189..73b212b97b4cc 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js @@ -5,10 +5,10 @@ */ import React from 'react'; -import { EuiLink } from '@elastic/eui'; import { detectorToString } from '../../../../util/string_utils'; import { formatValues, filterObjects } from './format_values'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; export function extractJobDetails(job) { if (Object.keys(job).length === 0) { @@ -61,7 +61,7 @@ export function extractJobDetails(job) { if (job.calendars) { calendars.items = job.calendars.map((c) => [ '', - {c}, + {c}, ]); // remove the calendars list from the general section // so not to show it twice. diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js index b6157c8694a18..b32070fff73aa 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js @@ -5,8 +5,6 @@ */ import PropTypes from 'prop-types'; -import rison from 'rison-node'; - import React, { Component } from 'react'; import { @@ -30,13 +28,19 @@ import { getLatestDataOrBucketTimestamp, isTimeSeriesViewJob, } from '../../../../../../../common/util/job_utils'; +import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public'; +import { + ML_APP_URL_GENERATOR, + ML_PAGES, +} from '../../../../../../../common/constants/ml_url_generator'; +import { PLUGIN_ID } from '../../../../../../../common/constants/app'; const MAX_FORECASTS = 500; /** * Table component for rendering the lists of forecasts run on an ML job. */ -export class ForecastsTable extends Component { +export class ForecastsTableUI extends Component { constructor(props) { super(props); this.state = { @@ -78,7 +82,17 @@ export class ForecastsTable extends Component { } } - openSingleMetricView(forecast) { + async openSingleMetricView(forecast) { + const { + services: { + application: { navigateToApp }, + + share: { + urlGenerators: { getUrlGenerator }, + }, + }, + } = this.props.kibana; + // Creates the link to the Single Metric Viewer. // Set the total time range from the start of the job data to the end of the forecast, const dataCounts = this.props.job.data_counts; @@ -93,31 +107,7 @@ export class ForecastsTable extends Component { ? new Date(forecast.forecast_end_timestamp).toISOString() : new Date(resultLatest).toISOString(); - const _g = rison.encode({ - ml: { - jobIds: [this.props.job.job_id], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from, - to, - mode: 'absolute', - }, - }); - - const appState = { - query: { - query_string: { - analyze_wildcard: true, - query: '*', - }, - }, - }; - + let mlTimeSeriesExplorer = {}; if (forecast !== undefined) { // Set the zoom to show duration before the forecast equal to the length of the forecast. const forecastDurationMs = @@ -126,8 +116,7 @@ export class ForecastsTable extends Component { forecast.forecast_start_timestamp - forecastDurationMs, jobEarliest ); - - appState.mlTimeSeriesExplorer = { + mlTimeSeriesExplorer = { forecastId: forecast.forecast_id, zoom: { from: new Date(zoomFrom).toISOString(), @@ -136,11 +125,39 @@ export class ForecastsTable extends Component { }; } - const _a = rison.encode(appState); - - const url = `?_g=${_g}&_a=${_a}`; - addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url); - window.open(`#/timeseriesexplorer${url}`, '_self'); + const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR); + const singleMetricViewerForecastLink = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + timeRange: { + from, + to, + mode: 'absolute', + }, + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + jobIds: [this.props.job.job_id], + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, + }, + ...mlTimeSeriesExplorer, + }, + excludeBasePath: true, + }); + addItemToRecentlyAccessed( + 'timeseriesexplorer', + this.props.job.job_id, + singleMetricViewerForecastLink + ); + await navigateToApp(PLUGIN_ID, { + path: singleMetricViewerForecastLink, + }); } render() { @@ -322,6 +339,8 @@ export class ForecastsTable extends Component { ); } } -ForecastsTable.propTypes = { +ForecastsTableUI.propTypes = { job: PropTypes.object.isRequired, }; + +export const ForecastsTable = withKibana(ForecastsTableUI); diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js index a5469357ba1a1..8b5d6009cc61e 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { JobGroup } from '../job_group'; -import { getGroupIdsUrl, TAB_IDS } from '../../../../util/get_selected_ids_url'; +import { AnomalyDetectionJobIdLink } from './job_id_link'; export function JobDescription({ job, isManagementTable }) { return ( @@ -17,11 +17,7 @@ export function JobDescription({ job, isManagementTable }) { {job.description}   {job.groups.map((group) => { if (isManagementTable === true) { - return ( - - - - ); + return ; } return ; })} diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx new file mode 100644 index 0000000000000..0e84619899d71 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { AnomalyDetectionQueryState } from '../../../../../../common/types/ml_url_generator'; +// @ts-ignore +import { JobGroup } from '../job_group'; + +interface JobIdLink { + id: string; +} + +interface GroupIdLink { + groupId: string; + children: string; +} + +type AnomalyDetectionJobIdLinkProps = JobIdLink | GroupIdLink; + +function isGroupIdLink(props: JobIdLink | GroupIdLink): props is GroupIdLink { + return (props as GroupIdLink).groupId !== undefined; +} +export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) => { + const mlUrlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToJobsManagementPage = async () => { + const pageState: AnomalyDetectionQueryState = {}; + if (isGroupIdLink(props)) { + pageState.groupIds = [props.groupId]; + } else { + pageState.jobId = props.id; + } + const url = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState, + }); + await navigateToUrl(url); + }; + if (isGroupIdLink(props)) { + return ( + redirectToJobsManagementPage()}> + + + ); + } else { + return ( + redirectToJobsManagementPage()}> + {props.id} + + ); + } +}; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js index fa4ea09b89ff9..8bc0057b27d6d 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js @@ -14,12 +14,12 @@ import { toLocaleString } from '../../../../util/string_utils'; import { ResultLinks, actionsMenuContent } from '../job_actions'; import { JobDescription } from './job_description'; import { JobIcon } from '../../../../components/job_message_icon'; -import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url'; import { TIME_FORMAT } from '../../../../../../common/constants/time_format'; -import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui'; +import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiScreenReaderOnly } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { AnomalyDetectionJobIdLink } from './job_id_link'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -71,7 +71,7 @@ export class JobsList extends Component { return id; } - return {id}; + return ; } getPageOfJobs(index, size, sortField, sortDirection) { @@ -241,7 +241,7 @@ export class JobsList extends Component { name: i18n.translate('xpack.ml.jobsList.actionsLabel', { defaultMessage: 'Actions', }), - render: (item) => , + render: (item) => , }, ]; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js index fdffa8b38ae04..81effe8d3ebeb 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js @@ -11,13 +11,13 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; - -function newJob() { - window.location.href = `#/jobs/new_job`; -} +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; export function NewJobButton() { const buttonEnabled = checkPermission('canCreateJob') && mlNodesAvailable(); + const newJob = useCreateAndNavigateToMlLink(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX); + return ( { const { @@ -73,7 +74,7 @@ export const CalendarsSelection: FC = () => { }; const manageCalendarsHref = getUrlForApp(PLUGIN_ID, { - path: '/settings/calendars_list', + path: ML_PAGES.CALENDARS_MANAGE, }); return ( diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 669b8837e74b5..021039c06e320 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -39,7 +39,10 @@ import { JobSectionTitle, DatafeedSectionTitle } from './components/common'; export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => { const { - services: { notifications }, + services: { + notifications, + http: { basePath }, + }, } = useMlKibana(); const navigateToPath = useNavigateToPath(); @@ -108,7 +111,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => jobCreator.end, isSingleMetricJobCreator(jobCreator) === true ? 'timeseriesexplorer' : 'explorer' ); - window.open(url, '_blank'); + navigateToPath(`${basePath.get()}/app/ml/${url}`); } function clickResetJob() { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts index 69df2773f9f8d..cedaaa3b5dfaa 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts @@ -4,19 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ApplicationStart } from 'kibana/public'; import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public'; import { mlJobService } from '../../../../services/job_service'; import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils'; import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs'; import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job'; -export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) { +export async function preConfiguredJobRedirect( + indexPatterns: IndexPatternsContract, + basePath: string, + navigateToUrl: ApplicationStart['navigateToUrl'] +) { const { job } = mlJobService.tempJobCloningObjects; if (job) { try { await loadIndexPatterns(indexPatterns); const redirectUrl = getWizardUrlFromCloningJob(job); - window.location.href = `#/${redirectUrl}`; + await navigateToUrl(`${basePath}/app/ml/${redirectUrl}`); return Promise.reject(); } catch (error) { return Promise.resolve(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx index be0135ec3f1e0..1a91f6d51ed4d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx @@ -19,6 +19,7 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useNavigateToPath } from '../../../../contexts/kibana'; + import { useMlContext } from '../../../../contexts/ml'; import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana'; import { DataRecognizer } from '../../../../components/data_recognizer'; @@ -26,10 +27,15 @@ import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed'; import { timeBasedIndexCheck } from '../../../../util/index_utils'; import { CreateJobLinkCard } from '../../../../components/create_job_link_card'; import { CategorizationIcon } from './categorization_job_icon'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; export const Page: FC = () => { const mlContext = useMlContext(); const navigateToPath = useNavigateToPath(); + const onSelectDifferentIndex = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX + ); const [recognizerResultsCount, setRecognizerResultsCount] = useState(0); @@ -193,7 +199,7 @@ export const Page: FC = () => { defaultMessage="Anomaly detection can only be run over indices which are time based." />
- + = ({ moduleId, existingGroupIds }) => { const { services: { notifications }, } = useMlKibana(); + const urlGenerator = useMlUrlGenerator(); + // #region State const [jobPrefix, setJobPrefix] = useState(''); const [jobs, setJobs] = useState([]); @@ -185,14 +189,20 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { }) ); setKibanaObjects(merge(kibanaObjects, kibanaResponse)); - setResultsUrl( - mlJobService.createResultsUrl( - jobsResponse.filter(({ success }) => success).map(({ id }) => id), - resultTimeRange.start, - resultTimeRange.end, - 'explorer' - ) - ); + + const url = await urlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_EXPLORER, + pageState: { + jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id), + timeRange: { + from: moment(resultTimeRange.start).format(TIME_FORMAT), + to: moment(resultTimeRange.end).format(TIME_FORMAT), + mode: 'absolute', + }, + }, + }); + + setResultsUrl(url); const failedJobsCount = jobsResponse.reduce((count, { success }) => { return success ? count : count + 1; }, 0); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts index e3b0fd4cefe0c..97a03fa21035f 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts @@ -6,33 +6,40 @@ import { i18n } from '@kbn/i18n'; import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache'; -import { mlJobService } from '../../../services/job_service'; import { ml } from '../../../services/ml_api_service'; import { KibanaObjects } from './page'; +import { NavigateToPath } from '../../../contexts/kibana'; +import { CreateLinkWithUserDefaults } from '../../../components/custom_hooks/use_create_ad_links'; /** * Checks whether the jobs in a data recognizer module have been created. * Redirects to the Anomaly Explorer to view the jobs if they have been created, * or the recognizer job wizard for the module if not. */ -export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise { +export function checkViewOrCreateJobs( + moduleId: string, + indexPatternId: string, + createLinkWithUserDefaults: CreateLinkWithUserDefaults, + navigateToPath: NavigateToPath +): Promise { return new Promise((resolve, reject) => { // Load the module, and check if the job(s) in the module have been created. // If so, load the jobs in the Anomaly Explorer. // Otherwise open the data recognizer wizard for the module. // Always want to call reject() so as not to load original page. ml.dataRecognizerModuleJobsExist({ moduleId }) - .then((resp: any) => { + .then(async (resp: any) => { if (resp.jobsExist === true) { - const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer'); - window.location.href = resultsPageUrl; + // also honor user's time filter setting in Advanced Settings + const url = createLinkWithUserDefaults('explorer', resp.jobs); + await navigateToPath(url); reject(); } else { - window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`; + await navigateToPath(`/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`); reject(); } }) - .catch((err: Error) => { + .catch(async (err: Error) => { // eslint-disable-next-line no-console console.error(`Error checking whether jobs in module ${moduleId} exists`, err); const toastNotifications = getToastNotifications(); @@ -46,8 +53,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): 'An error occurred trying to check whether the jobs in the module have been created.', }), }); - - window.location.href = '#/jobs'; + await navigateToPath(`/jobs`); reject(); }); }); diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index 0af6030df28b1..9c9096dfdfc21 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -31,6 +31,7 @@ import { getDocLinks } from '../../../../util/dependency_cache'; import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index'; import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list'; import { AccessDeniedPage } from '../access_denied_page'; +import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; interface Tab { 'data-test-subj': string; @@ -75,8 +76,9 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] { export const JobsListPage: FC<{ coreStart: CoreStart; + share: SharePluginStart; history: ManagementAppMountParams['history']; -}> = ({ coreStart, history }) => { +}> = ({ coreStart, share, history }) => { const [initialized, setInitialized] = useState(false); const [accessDenied, setAccessDenied] = useState(false); const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false); @@ -136,7 +138,7 @@ export const JobsListPage: FC<{ return ( - + { - ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element); + ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element); return () => { unmountComponentAtNode(element); clearCache(); @@ -30,7 +32,7 @@ export async function mountApp( core: CoreSetup, params: ManagementAppMountParams ) { - const [coreStart] = await core.getStartServices(); + const [coreStart, pluginsStart] = await core.getStartServices(); setDependencyCache({ docLinks: coreStart.docLinks!, @@ -41,5 +43,5 @@ export async function mountApp( params.setBreadcrumbs(getJobsListBreadcrumbs()); - return renderApp(params.element, params.history, coreStart); + return renderApp(params.element, params.history, coreStart, pluginsStart.share); } diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts index 1792999eee4c2..d0cfd16d7562f 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts @@ -9,7 +9,7 @@ import { ml } from '../services/ml_api_service'; let mlNodeCount: number = 0; let userHasPermissionToViewMlNodeCount: boolean = false; -export async function checkMlNodesAvailable() { +export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise) { try { const nodes = await getMlNodeCount(); if (nodes.count !== undefined && nodes.count > 0) { @@ -20,7 +20,7 @@ export async function checkMlNodesAvailable() { } catch (error) { // eslint-disable-next-line no-console console.error(error); - window.location.href = '#/jobs'; + await redirectToJobsManagementPage(); Promise.reject(); } } diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx index 395a570083c0d..4f0cbc0adddf2 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx @@ -4,30 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, FC } from 'react'; +import React, { FC, useMemo } from 'react'; import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useNavigateToPath } from '../../../contexts/kibana'; +import { Link } from 'react-router-dom'; +import { useMlLink } from '../../../contexts/kibana'; import { getAnalysisType } from '../../../data_frame_analytics/common/analytics'; -import { - getResultsUrl, - DataFrameAnalyticsListRow, -} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; +import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; interface Props { item: DataFrameAnalyticsListRow; } export const ViewLink: FC = ({ item }) => { - const navigateToPath = useNavigateToPath(); - - const clickHandler = useCallback(() => { - const analysisType = getAnalysisType(item.config.analysis); - navigateToPath(getResultsUrl(item.id, analysisType)); - }, []); - const { disabled, tooltipContent } = getViewLinkStatus(item); const viewJobResultsButtonText = i18n.translate( @@ -38,23 +31,34 @@ export const ViewLink: FC = ({ item }) => { ); const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent; + const analysisType = useMemo(() => getAnalysisType(item.config.analysis), [item]); + + const viewAnalyticsResultsLink = useMlLink({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION, + pageState: { + jobId: item.id, + analysisType: analysisType as DataFrameAnalysisConfigType, + }, + excludeBasePath: true, + }); return ( - - {i18n.translate('xpack.ml.overview.analytics.viewActionName', { - defaultMessage: 'View', - })} - + + + {i18n.translate('xpack.ml.overview.analytics.viewActionName', { + defaultMessage: 'View', + })} + + ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx index be8038cc5049d..4d810c47415a7 100644 --- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx @@ -23,6 +23,8 @@ import { AnalyticsTable } from './table'; import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service'; import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common'; import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; interface Props { jobCreationDisabled: boolean; @@ -35,6 +37,16 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { const [errorMessage, setErrorMessage] = useState(undefined); const [isInitialized, setIsInitialized] = useState(false); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToDataFrameAnalyticsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + const getAnalytics = getAnalyticsFactory( setAnalytics, setAnalyticsStats, @@ -75,7 +87,6 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { {isInitialized === false && ( )} -      {errorMessage === undefined && isInitialized === true && analytics.length === 0 && ( = ({ jobCreationDisabled }) => { } actions={ = ({ jobCreationDisabled }) => { )} {isInitialized === true && analytics.length > 0 && ( <> + @@ -136,7 +148,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => { defaultMessage: 'Refresh', })} - + {i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', { defaultMessage: 'Manage jobs', })} diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx index a71141d0356d0..dfba7c9651266 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx @@ -7,6 +7,7 @@ import React, { FC } from 'react'; import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Link } from 'react-router-dom'; import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs'; import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; @@ -26,19 +27,20 @@ export const ExplorerLink: FC = ({ jobsList }) => { return ( - - {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', { - defaultMessage: 'View', - })} - + + + {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', { + defaultMessage: 'View', + })} + + ); }; diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx index 0bfd2c2e49232..1cb6bab7fd768 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx @@ -16,12 +16,13 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import moment from 'moment'; -import { useMlKibana } from '../../../contexts/kibana'; +import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; import { AnomalyDetectionTable } from './table'; import { ml } from '../../../services/ml_api_service'; import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils'; import { Dictionary } from '../../../../../common/types/common'; import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; export type GroupsDictionary = Dictionary; @@ -39,8 +40,6 @@ type MaxScoresByGroup = Dictionary<{ index?: number; }>; -const createJobLink = '#/jobs/new_job/step/index_or_search'; - function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup { const anomalyScores: MaxScoresByGroup = {}; groups.forEach((group) => { @@ -58,6 +57,23 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { const { services: { notifications }, } = useMlKibana(); + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToJobsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + + const redirectToCreateJobSelectIndexPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX, + }); + await navigateToPath(path, true); + }; + const [isLoading, setIsLoading] = useState(false); const [groups, setGroups] = useState({}); const [groupsCount, setGroupsCount] = useState(0); @@ -157,7 +173,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => { return ( {typeof errorMessage !== 'undefined' && errorDisplay} - {isLoading && }    + {isLoading && } {isLoading === false && typeof errorMessage === 'undefined' && groupsCount === 0 && ( = ({ jobCreationDisabled }) => { actions={ = ({ jobCreationDisabled }) => { defaultMessage: 'Refresh', })} - + {i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', { defaultMessage: 'Manage jobs', })} diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx index 945116b0534bb..8515431d49b17 100644 --- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx +++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx @@ -88,7 +88,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData {i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', { defaultMessage: 'Max anomaly score', - })}{' '} + })} @@ -203,6 +203,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData return ( + diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts index d0a4f999af758..398ec5b4759d2 100644 --- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts +++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts @@ -54,6 +54,20 @@ export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ href: '/jobs/new_job', }); +export const CALENDAR_MANAGEMENT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { + defaultMessage: 'Calendar management', + }), + href: '/settings/calendars_list', +}); + +export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({ + text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { + defaultMessage: 'Filter lists', + }), + href: '/settings/filter_lists', +}); + const breadcrumbs = { ML_BREADCRUMB, SETTINGS_BREADCRUMB, @@ -61,6 +75,8 @@ const breadcrumbs = { DATA_FRAME_ANALYTICS_BREADCRUMB, DATA_VISUALIZER_BREADCRUMB, CREATE_JOB_BREADCRUMB, + CALENDAR_MANAGEMENT_BREADCRUMB, + FILTER_LISTS_BREADCRUMB, }; type Breadcrumb = keyof typeof breadcrumbs; @@ -76,10 +92,12 @@ export const breadcrumbOnClickFactory = ( export const getBreadcrumbWithUrlForApp = ( breadcrumbName: Breadcrumb, - navigateToPath: NavigateToPath + navigateToPath: NavigateToPath, + basePath: string ): EuiBreadcrumb => { return { - ...breadcrumbs[breadcrumbName], + text: breadcrumbs[breadcrumbName].text, + href: `${basePath}/app/ml${breadcrumbs[breadcrumbName].href}`, onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath), }; }; diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts index 958221df8a636..9cebb67166a66 100644 --- a/x-pack/plugins/ml/public/application/routing/resolvers.ts +++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts @@ -21,13 +21,17 @@ export interface ResolverResults { interface BasicResolverDependencies { indexPatterns: IndexPatternsContract; + redirectToMlAccessDeniedPage: () => Promise; } -export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({ +export const basicResolvers = ({ + indexPatterns, + redirectToMlAccessDeniedPage, +}: BasicResolverDependencies): Resolvers => ({ checkFullLicense, getMlNodeCount, loadMlServerInfo, loadIndexPatterns: () => loadIndexPatterns(indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), loadSavedSearches, }); diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx index 22a17c4ea089a..7cb3a2f07c2ee 100644 --- a/x-pack/plugins/ml/public/application/routing/router.tsx +++ b/x-pack/plugins/ml/public/application/routing/router.tsx @@ -12,7 +12,7 @@ import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/publi import { ChromeBreadcrumb } from 'kibana/public'; import { IndexPatternsContract } from 'src/plugins/data/public'; -import { useNavigateToPath } from '../contexts/kibana'; +import { useMlKibana, useNavigateToPath } from '../contexts/kibana'; import { MlContext, MlContextValue } from '../contexts/ml'; import { UrlStateProvider } from '../util/url_state'; @@ -39,6 +39,7 @@ interface PageDependencies { history: AppMountParameters['history']; indexPatterns: IndexPatternsContract; setBreadcrumbs: ChromeStart['setBreadcrumbs']; + redirectToMlAccessDeniedPage: () => Promise; } export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => { @@ -75,10 +76,16 @@ const MlRoutes: FC<{ pageDeps: PageDependencies; }> = ({ pageDeps }) => { const navigateToPath = useNavigateToPath(); + const { + services: { + http: { basePath }, + }, + } = useMlKibana(); + return ( <> {Object.entries(routes).map(([name, routeFactory]) => { - const route = routeFactory(navigateToPath); + const route = routeFactory(navigateToPath, basePath.get()); return ( ({ +export const analyticsJobsCreationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/new_job', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', { defaultMessage: 'Data Frame Analytics', diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx index 47cc002ab4d83..f9f2ebe48f4aa 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx @@ -10,21 +10,25 @@ import { decode } from 'rison-node'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_exploration'; -import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics'; -export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const analyticsJobExplorationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/exploration', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', { defaultMessage: 'Exploration', @@ -38,16 +42,31 @@ const PageWrapper: FC = ({ location, deps }) => { const { context } = useResolver('', undefined, deps.config, basicResolvers(deps)); const { _g }: Record = parse(location.search, { sort: false }); + const urlGenerator = useMlUrlGenerator(); + const { + services: { + application: { navigateToUrl }, + }, + } = useMlKibana(); + + const redirectToAnalyticsManagementPage = async () => { + const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE }); + await navigateToUrl(url); + }; + let globalState: any = null; try { globalState = decode(_g); } catch (error) { // eslint-disable-next-line no-console - console.error('Could not parse global state'); - window.location.href = '#data_frame_analytics'; + console.error( + 'Could not parse global state. Redirecting to Data Frame Analytics Management Page.' + ); + redirectToAnalyticsManagementPage(); + return <>; } const jobId: string = globalState.ml.jobId; - const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType; + const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType; return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx index b6ef9ea81b4ba..80706a82121d5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx @@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const analyticsJobsListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', { defaultMessage: 'Job Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx index 7bf7784d1b559..b1fd6e93a744c 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx @@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../data_frame_analytics/pages/analytics_management'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const modelsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const modelsListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/data_frame_analytics/models', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel', { defaultMessage: 'Model Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx index efe5c3cba04a5..f40b754a23ccb 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx @@ -21,19 +21,25 @@ import { checkBasicLicense } from '../../../license'; import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const selectorRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/datavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkBasicLicense, - checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, + checkFindFileStructurePrivilege: () => + checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx index 485af52c45a55..837616a8a76d2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx @@ -24,12 +24,15 @@ import { loadIndexPatterns } from '../../../util/index_utils'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const fileBasedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/filedatavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', { defaultMessage: 'File', @@ -40,10 +43,13 @@ export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute = }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver('', undefined, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver, + checkFindFileStructurePrivilege: () => + checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx index 358b8773e3460..e3d0e5050fca5 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx @@ -20,13 +20,18 @@ import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_ca import { loadIndexPatterns } from '../../../util/index_utils'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; -export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const indexBasedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/datavisualizer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', { defaultMessage: 'Index', @@ -37,12 +42,17 @@ export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute }); const PageWrapper: FC = ({ location, deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context } = useResolver(index, savedSearchId, deps.config, { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); return ( diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 30b9bc2af219f..00d64a2f1bd1d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -35,12 +35,15 @@ import { useTimefilter } from '../../contexts/kibana'; import { isViewBySwimLaneData } from '../../explorer/swimlane_container'; import { JOB_ID } from '../../../../common/constants/anomalies'; -export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const explorerRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/explorer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', { defaultMessage: 'Anomaly Explorer', diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx index 38a7900916ba8..2863e59508e35 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx @@ -20,12 +20,12 @@ import { JobsPage } from '../../jobs/jobs_list'; import { useTimefilter } from '../../contexts/kibana'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/jobs', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', { defaultMessage: 'Job Management', diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx index d8605c4cc9115..0ef3b384dcf5d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx @@ -8,7 +8,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useMlKibana } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -19,6 +19,8 @@ import { checkBasicLicense } from '../../../license'; import { loadIndexPatterns } from '../../../util/index_utils'; import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { checkMlNodesAvailable } from '../../../ml_nodes_check'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; enum MODE { NEW_JOB, @@ -30,9 +32,9 @@ interface IndexOrSearchPageProps extends PageProps { mode: MODE; } -const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), +const getBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', { defaultMessage: 'Create job', @@ -41,7 +43,10 @@ const getBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const indexOrSearchRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/step/index_or_search', render: (props, deps) => ( ), - breadcrumbs: getBreadcrumbs(navigateToPath), + breadcrumbs: getBreadcrumbs(navigateToPath, basePath), }); -export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const dataVizIndexOrSearchRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/datavisualizer_index_select', render: (props, deps) => ( ), - breadcrumbs: getBreadcrumbs(navigateToPath), + breadcrumbs: getBreadcrumbs(navigateToPath, basePath), }); const PageWrapper: FC = ({ nextStepPath, deps, mode }) => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = useMlKibana(); + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const newJobResolvers = { ...basicResolvers(deps), - preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns), + preConfiguredJobRedirect: () => + preConfiguredJobRedirect(deps.indexPatterns, basePath.get(), navigateToUrl), }; const dataVizResolvers = { checkBasicLicense, loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns), - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }; const { context } = useResolver( diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx index b8ab29d40fa1f..543e01fbd326d 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx @@ -17,12 +17,12 @@ import { basicResolvers } from '../../resolvers'; import { Page } from '../../../jobs/new_job/pages/job_type'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const jobTypeRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/jobs/new_job/step/job_type', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', { defaultMessage: 'Create job', diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx index 6be58828ee1a5..654d7184cfcf2 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx @@ -9,7 +9,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { NavigateToPath } from '../../../contexts/kibana'; +import { NavigateToPath, useNavigateToPath } from '../../../contexts/kibana'; import { MlRoute, PageLoader, PageProps } from '../../router'; import { useResolver } from '../../use_resolver'; @@ -18,14 +18,18 @@ import { Page } from '../../../jobs/new_job/recognize'; import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers'; import { mlJobService } from '../../../services/job_service'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links'; -export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const recognizeRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/recognize', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', { defaultMessage: 'Recognized index', @@ -60,10 +64,14 @@ const CheckViewOrCreateWrapper: FC = ({ location, deps }) => { const { id: moduleId, index: indexPatternId }: Record = parse(location.search, { sort: false, }); + const { createLinkWithUserDefaults } = useCreateADLinks(); + + const navigateToPath = useNavigateToPath(); // the single resolver checkViewOrCreateJobs redirects only. so will always reject useResolver(undefined, undefined, deps.config, { - checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId), + checkViewOrCreateJobs: () => + checkViewOrCreateJobs(moduleId, indexPatternId, createLinkWithUserDefaults, navigateToPath), }); return null; }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx index 35085fd557577..8a82a9a8dbc49 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx @@ -19,19 +19,21 @@ import { mlJobService } from '../../../services/job_service'; import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service'; import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; interface WizardPageProps extends PageProps { jobType: JOB_TYPE; } -const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath), +const getBaseBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath), ]; -const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', { defaultMessage: 'Single metric', @@ -40,8 +42,8 @@ const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', { defaultMessage: 'Multi-metric', @@ -50,8 +52,8 @@ const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', { defaultMessage: 'Population', @@ -60,8 +62,8 @@ const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', { defaultMessage: 'Advanced configuration', @@ -70,8 +72,8 @@ const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ - ...getBaseBreadcrumbs(navigateToPath), +const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [ + ...getBaseBreadcrumbs(navigateToPath, basePath), { text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', { defaultMessage: 'Categorization', @@ -80,41 +82,60 @@ const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [ }, ]; -export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const singleMetricRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/single_metric', render: (props, deps) => , - breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath), + breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath, basePath), }); -export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const multiMetricRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/multi_metric', render: (props, deps) => , - breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath), + breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath, basePath), }); -export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const populationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/population', render: (props, deps) => , - breadcrumbs: getPopulationBreadcrumbs(navigateToPath), + breadcrumbs: getPopulationBreadcrumbs(navigateToPath, basePath), }); -export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const advancedRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/advanced', render: (props, deps) => , - breadcrumbs: getAdvancedBreadcrumbs(navigateToPath), + breadcrumbs: getAdvancedBreadcrumbs(navigateToPath, basePath), }); -export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const categorizationRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/jobs/new_job/categorization', render: (props, deps) => , - breadcrumbs: getCategorizationBreadcrumbs(navigateToPath), + breadcrumbs: getCategorizationBreadcrumbs(navigateToPath, basePath), }); const PageWrapper: FC = ({ location, jobType, deps }) => { + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); + const { index, savedSearchId }: Record = parse(location.search, { sort: false }); const { context, results } = useResolver(index, savedSearchId, deps.config, { ...basicResolvers(deps), - privileges: checkCreateJobsCapabilitiesResolver, + privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage), jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns), existingJobsAndGroups: mlJobService.getJobAndGroupIds, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx index 174e9804b9689..0e07b0edfbe56 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx @@ -22,11 +22,14 @@ import { loadMlServerInfo } from '../../services/ml_server_info'; import { useTimefilter } from '../../contexts/kibana'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs'; -export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const overviewRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/overview', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.overview.overviewLabel', { defaultMessage: 'Overview', @@ -37,9 +40,11 @@ export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, loadMlServerInfo, }); @@ -52,7 +57,7 @@ const PageWrapper: FC = ({ deps }) => { ); }; -export const appRootRouteFactory = (): MlRoute => ({ +export const appRootRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({ path: '/', render: () => , breadcrumbs: [], diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx index f2ae57f1ec961..2460971239618 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx @@ -10,7 +10,6 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { NavigateToPath } from '../../../contexts/kibana'; @@ -25,27 +24,27 @@ import { } from '../../../capabilities/check_capabilities'; import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { CalendarsList } from '../../../settings/calendars'; -import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const calendarListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', { - defaultMessage: 'Calendar management', - }), - onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath), - }, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx index a5c30e1eaaacc..4e0a8340590a4 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx @@ -26,6 +26,8 @@ import { import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { NewCalendar } from '../../../settings/calendars'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; enum MODE { NEW, @@ -36,12 +38,16 @@ interface NewCalendarPageProps extends PageProps { mode: MODE; } -export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const newCalendarRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list/new_calendar', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', { defaultMessage: 'Create', @@ -51,12 +57,16 @@ export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute ], }); -export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const editCalendarRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/calendars_list/edit_calendar/:calendarId', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', { defaultMessage: 'Edit', @@ -72,11 +82,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx index d734e18d72bab..4e39cfce82e36 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx @@ -10,7 +10,6 @@ */ import React, { FC } from 'react'; -import { i18n } from '@kbn/i18n'; import { NavigateToPath } from '../../../contexts/kibana'; @@ -26,27 +25,27 @@ import { import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { FilterLists } from '../../../settings/filter_lists'; -import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const filterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), - { - text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', { - defaultMessage: 'Filter lists', - }), - onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath), - }, + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx index c6f17bc7f6f68..5fe56b024e413 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx @@ -27,6 +27,8 @@ import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { EditFilterList } from '../../../settings/filter_lists'; import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; +import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; enum MODE { NEW, @@ -37,12 +39,17 @@ interface NewFilterPageProps extends PageProps { mode: MODE; } -export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const newFilterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists/new_filter_list', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), + { text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', { defaultMessage: 'Create', @@ -52,12 +59,16 @@ export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRou ], }); -export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const editFilterListRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings/filter_lists/edit_filter_list/:filterId', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', { defaultMessage: 'Edit', @@ -73,11 +84,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => { const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/); filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined; } + const { redirectToMlAccessDeniedPage } = deps; + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, - checkMlNodesAvailable, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), + checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage), }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx index 3f4b269851469..3159c2ae88166 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx @@ -26,19 +26,24 @@ import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes'; import { AnomalyDetectionSettingsContext, Settings } from '../../../settings'; import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs'; -export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const settingsRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/settings', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath), ], }); const PageWrapper: FC = ({ deps }) => { + const { redirectToMlAccessDeniedPage } = deps; + const { context } = useResolver(undefined, undefined, deps.config, { checkFullLicense, - checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver, + checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage), getMlNodeCount, }); diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx index 11ec074bac1db..b60a265560455 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx @@ -19,6 +19,11 @@ jest.mock('../../contexts/kibana/kibana_context', () => { useMlKibana: () => { return { services: { + chrome: { docTitle: { change: jest.fn() } }, + application: { getUrlForApp: jest.fn(), navigateToUrl: jest.fn() }, + share: { + urlGenerators: { getUrlGenerator: jest.fn() }, + }, uiSettings: { get: jest.fn() }, data: { query: { diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx index 817c975415997..03588872d6be0 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx @@ -39,12 +39,15 @@ import { basicResolvers } from '../resolvers'; import { getBreadcrumbWithUrlForApp } from '../breadcrumbs'; import { useTimefilter } from '../../contexts/kibana'; -export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({ +export const timeSeriesExplorerRouteFactory = ( + navigateToPath: NavigateToPath, + basePath: string +): MlRoute => ({ path: '/timeseriesexplorer', render: (props, deps) => , breadcrumbs: [ - getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath), - getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath), + getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath), + getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath), { text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer', diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts index 4967e3a684a6b..e4cd90145bee4 100644 --- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts +++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts @@ -16,6 +16,8 @@ import { createSearchItems } from '../jobs/new_job/utils/new_job_utils'; import { ResolverResults, Resolvers } from './resolvers'; import { MlContextValue } from '../contexts/ml'; import { useNotifications } from '../contexts/kibana'; +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; export const useResolver = ( indexPatternId: string | undefined, @@ -34,6 +36,9 @@ export const useResolver = ( const [context, setContext] = useState(null); const [results, setResults] = useState(tempResults); + const redirectToJobsManagementPage = useCreateAndNavigateToMlLink( + ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE + ); useEffect(() => { (async () => { @@ -73,7 +78,7 @@ export const useResolver = ( defaultMessage: 'An error has occurred', }), }); - window.location.href = '#/'; + await redirectToJobsManagementPage(); } } else { setContext({}); diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index dfa1b5f4e68cd..ea97492ae0f5a 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -797,7 +797,6 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') { let path = ''; if (resultsPage !== undefined) { - path += '#/'; path += resultsPage; } diff --git a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx index 16d7e1605263c..57caa56b2f10e 100644 --- a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx +++ b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx @@ -25,6 +25,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AnomalyDetectionSettingsContext } from './anomaly_detection_settings_context'; import { useNotifications } from '../contexts/kibana'; import { ml } from '../services/ml_api_service'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; +import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url'; export const AnomalyDetectionSettings: FC = () => { const [calendarsCount, setCalendarsCount] = useState(0); @@ -35,6 +37,10 @@ export const AnomalyDetectionSettings: FC = () => { ); const { toasts } = useNotifications(); + const redirectToCalendarList = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE); + const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW); + const redirectToFilterLists = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_MANAGE); + const redirectToNewFilterListPage = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_NEW); useEffect(() => { loadSummaryStats(); @@ -126,7 +132,7 @@ export const AnomalyDetectionSettings: FC = () => { flush="left" size="l" color="primary" - href="#/settings/calendars_list" + onClick={redirectToCalendarList} isDisabled={canGetCalendars === false} > { flush="left" size="l" color="primary" - href="#/settings/calendars_list/new_calendar" + onClick={redirectToNewCalendarPage} isDisabled={canCreateCalendar === false} > {

@@ -194,7 +200,7 @@ export const AnomalyDetectionSettings: FC = () => { flush="left" size="l" color="primary" - href="#/settings/filter_lists" + onClick={redirectToFilterLists} isDisabled={canGetFilters === false} > { data-test-subj="mlFilterListsCreateButton" size="l" color="primary" - href="#/settings/filter_lists/new_filter_list" + onClick={redirectToNewFilterListPage} isDisabled={canCreateFilter === false} > + + + } + labelType="label" + > + + + + } + labelType="label" + > + + @@ -137,7 +200,6 @@ exports[`CalendarForm Renders calendar form 1`] = ` grow={false} > @@ -215,7 +218,7 @@ export const CalendarForm = ({ - + ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); const testProps = { calendarId: '', canCreateCalendar: true, @@ -31,6 +34,7 @@ const testProps = { selectedGroupOptions: [], selectedJobOptions: [], showNewEventModal: jest.fn(), + isGlobalCalendar: false, }; describe('CalendarForm', () => { diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js index 1fe16e4588bd7..a5eb212ba127e 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js @@ -20,6 +20,7 @@ import { ImportModal } from './import_modal'; import { ml } from '../../../services/ml_api_service'; import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; class NewCalendarUI extends Component { static propTypes = { @@ -55,6 +56,16 @@ class NewCalendarUI extends Component { this.formSetup(); } + returnToCalendarsManagementPage = async () => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = this.props.kibana; + await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.CALENDARS_MANAGE}`, true); + }; + async formSetup() { try { const { jobIds, groupIds, calendars } = await getCalendarSettingsData(); @@ -146,7 +157,7 @@ class NewCalendarUI extends Component { try { await ml.addCalendar(calendar); - window.location = '#/settings/calendars_list'; + await this.returnToCalendarsManagementPage(); } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); @@ -167,7 +178,7 @@ class NewCalendarUI extends Component { try { await ml.updateCalendar(calendar); - window.location = '#/settings/calendars_list'; + await this.returnToCalendarsManagementPage(); } catch (error) { console.log('Error saving calendar', error); this.setState({ saving: false }); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js index 2cff255bd1ce3..068d443300088 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js @@ -4,6 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +jest.mock('../../../contexts/kibana/use_create_url', () => ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); + jest.mock('../../../components/navigation_menu', () => ({ NavigationMenu: () =>
, })); diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap index cc1c524c19b57..50cacd7b3545a 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap @@ -77,7 +77,6 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = ` "toolsRight": Array [ diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js index 77331c4a987dc..6b4403aef7c7b 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js @@ -7,12 +7,14 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui'; - +import { EuiButton, EuiInMemoryTable } from '@elastic/eui'; +import { Link } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { GLOBAL_CALENDAR } from '../../../../../../common/constants/calendars'; +import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url'; +import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator'; export const CalendarsListTable = ({ calendarsList, @@ -24,6 +26,8 @@ export const CalendarsListTable = ({ mlNodesAvailable, itemsSelected, }) => { + const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW); + const sorting = { sort: { field: 'calendar_id', @@ -46,12 +50,9 @@ export const CalendarsListTable = ({ truncateText: true, scope: 'row', render: (id) => ( - + {id} - + ), 'data-test-subj': 'mlCalendarListColumnId', }, @@ -101,7 +102,7 @@ export const CalendarsListTable = ({ size="s" data-test-subj="mlCalendarButtonCreate" key="new_calendar_button" - href="#/settings/calendars_list/new_calendar" + onClick={redirectToNewCalendarPage} isDisabled={canCreateCalendar === false || mlNodesAvailable === false} > @@ -115,6 +116,7 @@ export const CalendarsListTable = ({ canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false } data-test-subj="mlCalendarButtonDelete" + key="delete_calendar_button" > ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); const calendars = [ { @@ -42,7 +47,11 @@ describe('CalendarsListTable', () => { }); test('New button enabled if permission available', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -56,7 +65,11 @@ describe('CalendarsListTable', () => { canCreateCalendar: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); @@ -70,7 +83,11 @@ describe('CalendarsListTable', () => { mlNodesAvailable: false, }; - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + + + ); const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]'); const button = buttons.find('EuiButton'); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index 41b7aa63f55ef..681c54ca9eee0 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -34,6 +34,7 @@ import { ItemsGrid } from '../../../components/items_grid'; import { NavigationMenu } from '../../../components/navigation_menu'; import { isValidFilterListId, saveFilterList } from './utils'; import { ml } from '../../../services/ml_api_service'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; const DEFAULT_ITEMS_PER_PAGE = 50; @@ -67,10 +68,6 @@ function getActivePage(activePageState, itemsPerPage, numMatchingItems) { return activePage; } -function returnToFiltersList() { - window.location.href = `#/settings/filter_lists`; -} - export class EditFilterListUI extends Component { static displayName = 'EditFilterList'; static propTypes = { @@ -105,6 +102,16 @@ export class EditFilterListUI extends Component { } } + returnToFiltersList = async () => { + const { + services: { + http: { basePath }, + application: { navigateToUrl }, + }, + } = this.props.kibana; + await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.FILTER_LISTS_MANAGE}`, true); + }; + loadFilterList = (filterId) => { ml.filters .filters({ filterId }) @@ -279,7 +286,7 @@ export class EditFilterListUI extends Component { saveFilterList(filterId, description, items, loadedFilter) .then((savedFilter) => { this.setLoadedFilterState(savedFilter); - returnToFiltersList(); + this.returnToFiltersList(); }) .catch((resp) => { console.log(`Error saving filter ${filterId}:`, resp); @@ -355,7 +362,7 @@ export class EditFilterListUI extends Component { /> - + this.returnToFiltersList()}> @@ -84,12 +88,9 @@ function getColumns() { defaultMessage: 'ID', }), render: (id) => ( - + {id} - + ), sortable: true, scope: 'row', @@ -213,7 +214,7 @@ export function FilterListsTable({ isSelectable={true} data-test-subj="mlFilterListsTable" rowProps={(item) => ({ - 'data-test-subj': `mlCalendarListRow row-${item.filter_id}`, + 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`, })} />
diff --git a/x-pack/plugins/ml/public/application/settings/settings.test.tsx b/x-pack/plugins/ml/public/application/settings/settings.test.tsx index f16bf62632152..a5e69f233e2df 100644 --- a/x-pack/plugins/ml/public/application/settings/settings.test.tsx +++ b/x-pack/plugins/ml/public/application/settings/settings.test.tsx @@ -22,6 +22,10 @@ jest.mock('../contexts/kibana', () => ({ }, })); +jest.mock('../contexts/kibana/use_create_url', () => ({ + useCreateAndNavigateToMlLink: jest.fn(), +})); + describe('Settings', () => { function runCheckButtonsDisabledTest( canGetFilters: boolean, diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx index deecb9fb45b51..88bf769aa2936 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx @@ -12,26 +12,40 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; +import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; -export const TimeseriesexplorerNoJobsFound = () => ( - - - - } - actions={ - - - - } - /> -); +export const TimeseriesexplorerNoJobsFound = () => { + const mlUrlGenerator = useMlUrlGenerator(); + const navigateToPath = useNavigateToPath(); + + const redirectToJobsManagementPage = async () => { + const path = await mlUrlGenerator.createUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + }); + await navigateToPath(path, true); + }; + + return ( + + + + } + actions={ + + + + } + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js index 4ec7c5cb6d819..ca55bb10b13d5 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.js @@ -8,11 +8,9 @@ import d3 from 'd3'; import { calculateTextWidth } from './string_utils'; import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact'; import moment from 'moment'; -import rison from 'rison-node'; - import { getTimefilter } from './dependency_cache'; - import { CHART_TYPE } from '../explorer/explorer_constants'; +import { ML_PAGES } from '../../../common/constants/ml_url_generator'; export const LINE_CHART_ANOMALY_RADIUS = 7; export const MULTI_BUCKET_SYMBOL_SIZE = 100; // In square pixels for use with d3 symbol.size @@ -212,7 +210,7 @@ export function getChartType(config) { return chartType; } -export function getExploreSeriesLink(series) { +export async function getExploreSeriesLink(mlUrlGenerator, series) { // Open the Single Metric dashboard over the same overall bounds and // zoomed in to the same time as the current chart. const timefilter = getTimefilter(); @@ -227,46 +225,44 @@ export function getExploreSeriesLink(series) { // to identify the particular series to view. // Initially pass them in the mlTimeSeriesExplorer part of the AppState. // TODO - do we want to pass the entities via the filter? - const entityCondition = {}; - series.entityFields.forEach((entity) => { - entityCondition[entity.fieldName] = entity.fieldValue; - }); + let entityCondition; + if (series.entityFields.length > 0) { + entityCondition = {}; + series.entityFields.forEach((entity) => { + entityCondition[entity.fieldName] = entity.fieldValue; + }); + } - // Use rison to build the URL . - const _g = rison.encode({ - ml: { + const url = await mlUrlGenerator.createUrl({ + page: ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { jobIds: [series.jobId], - }, - refreshInterval: { - display: 'Off', - pause: false, - value: 0, - }, - time: { - from: from, - to: to, - mode: 'absolute', - }, - }); - - const _a = rison.encode({ - mlTimeSeriesExplorer: { + refreshInterval: { + display: 'Off', + pause: false, + value: 0, + }, + timeRange: { + from: from, + to: to, + mode: 'absolute', + }, zoom: { from: zoomFrom, to: zoomTo, }, detectorIndex: series.detectorIndex, entities: entityCondition, - }, - query: { - query_string: { - analyze_wildcard: true, - query: '*', + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, }, }, + excludeBasePath: true, }); - - return `#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`; + return url; } export function showMultiBucketAnomalyMarker(point) { diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js index b7cf11c088a1e..955dd7cbea0a1 100644 --- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js +++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js @@ -35,7 +35,6 @@ import { render } from '@testing-library/react'; import { chartLimits, getChartType, - getExploreSeriesLink, getTickValues, getXTransform, isLabelLengthAboveThreshold, @@ -238,20 +237,6 @@ describe('ML - chart utils', () => { }); }); - describe('getExploreSeriesLink', () => { - test('get timeseriesexplorer link', () => { - const link = getExploreSeriesLink(seriesConfig); - const expectedLink = - `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` + - `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` + - `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` + - `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` + - `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`; - - expect(link).toBe(expectedLink); - }); - }); - describe('numTicks', () => { test('returns 10 for 1000', () => { expect(numTicks(1000)).toBe(10); diff --git a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts b/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts deleted file mode 100644 index 806626577008e..0000000000000 --- a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts +++ /dev/null @@ -1,39 +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 rison from 'rison-node'; -import { getBasePath } from './dependency_cache'; - -export enum TAB_IDS { - DATA_FRAME_ANALYTICS = 'data_frame_analytics', - ANOMALY_DETECTION = 'jobs', -} - -function getSelectedIdsUrl(tabId: TAB_IDS, settings: { [key: string]: string | string[] }): string { - // Create url for filtering by job id or group ids for kibana management table - const encoded = rison.encode(settings); - const url = `?mlManagement=${encoded}`; - const basePath = getBasePath(); - - return `${basePath.get()}/app/ml#/${tabId}${url}`; -} - -// Create url for filtering by group ids for kibana management table -export function getGroupIdsUrl(tabId: TAB_IDS, ids: string[]): string { - const settings = { - groupIds: ids, - }; - - return getSelectedIdsUrl(tabId, settings); -} - -// Create url for filtering by job id for kibana management table -export function getJobIdUrl(tabId: TAB_IDS, id: string): string { - const settings = { - jobId: id, - }; - - return getSelectedIdsUrl(tabId, settings); -} diff --git a/x-pack/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/plugins/ml/public/application/util/recently_accessed.ts index ab879e421cb09..04ccd84c561bb 100644 --- a/x-pack/plugins/ml/public/application/util/recently_accessed.ts +++ b/x-pack/plugins/ml/public/application/util/recently_accessed.ts @@ -37,7 +37,7 @@ export function addItemToRecentlyAccessed(page: string, itemId: string, url: str return; } - url = `ml#/${page}/${url}`; + url = url.startsWith('/') ? `/app/ml${url}` : `/app/ml/${page}/${url}`; const recentlyAccessed = getRecentlyAccessed(); recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id); } diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts index c4aebb108e7b9..6a44756412fe3 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts @@ -11,13 +11,14 @@ import { ExplorerAppState, ExplorerGlobalState, ExplorerUrlState, + MlCommonGlobalState, MlGenericUrlState, TimeSeriesExplorerAppState, TimeSeriesExplorerGlobalState, TimeSeriesExplorerUrlState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; -import { createIndexBasedMlUrl } from './common'; +import { createGenericMlUrl } from './common'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; /** * Creates URL to the Anomaly Detection Job management page @@ -30,18 +31,29 @@ export function createAnomalyDetectionJobManagementUrl( if (!params || isEmpty(params)) { return url; } - const { jobId, groupIds } = params; - const queryState: AnomalyDetectionQueryState = { - jobId, - groupIds, - }; + const { jobId, groupIds, globalState } = params; + if (jobId || groupIds) { + const queryState: AnomalyDetectionQueryState = { + jobId, + groupIds, + }; - url = setStateToKbnUrl( - 'mlManagement', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); + url = setStateToKbnUrl( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + + if (globalState) { + url = setStateToKbnUrl>( + '_g', + globalState, + { useHash: false, storeInHashQuery: false }, + url + ); + } return url; } @@ -49,13 +61,24 @@ export function createAnomalyDetectionCreateJobSelectType( appBasePath: string, pageState: MlGenericUrlState['pageState'] ): string { - return createIndexBasedMlUrl( + return createGenericMlUrl( appBasePath, ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, pageState ); } +export function createAnomalyDetectionCreateJobSelectIndex( + appBasePath: string, + pageState: MlGenericUrlState['pageState'] +): string { + return createGenericMlUrl( + appBasePath, + ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX, + pageState + ); +} + /** * Creates URL to the Anomaly Explorer page */ @@ -75,36 +98,35 @@ export function createExplorerUrl( query, mlExplorerSwimlane = {}, mlExplorerFilter = {}, + globalState, } = params; const appState: Partial = { mlExplorerSwimlane, mlExplorerFilter, }; + let queryState: Partial = {}; + if (globalState) queryState = globalState; if (query) appState.query = query; - if (jobIds) { - const queryState: Partial = { - ml: { - jobIds, - }, + queryState.ml = { + jobIds, }; - - if (timeRange) queryState.time = timeRange; - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - url = setStateToKbnUrl>( - '_g', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); - url = setStateToKbnUrl>( - '_a', - appState, - { useHash: false, storeInHashQuery: false }, - url - ); } + if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (timeRange) queryState.time = timeRange; + + url = setStateToKbnUrl>( + '_g', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + url = setStateToKbnUrl>( + '_a', + appState, + { useHash: false, storeInHashQuery: false }, + url + ); return url; } @@ -120,19 +142,36 @@ export function createSingleMetricViewerUrl( if (!params) { return url; } - const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params; + const { + timeRange, + jobIds, + refreshInterval, + zoom, + query, + detectorIndex, + forecastId, + entities, + globalState, + } = params; + + let queryState: Partial = {}; + if (globalState) queryState = globalState; - const queryState: TimeSeriesExplorerGlobalState = { - ml: { + if (jobIds) { + queryState.ml = { jobIds, - }, - refreshInterval, - time: timeRange, - }; + }; + } + if (refreshInterval) queryState.refreshInterval = refreshInterval; + if (timeRange) queryState.time = timeRange; const appState: Partial = {}; const mlTimeSeriesExplorer: Partial = {}; + if (forecastId !== undefined) { + mlTimeSeriesExplorer.forecastId = forecastId; + } + if (detectorIndex !== undefined) { mlTimeSeriesExplorer.detectorIndex = detectorIndex; } @@ -146,7 +185,7 @@ export function createSingleMetricViewerUrl( appState.query = { query_string: query, }; - url = setStateToKbnUrl( + url = setStateToKbnUrl>( '_g', queryState, { useHash: false, storeInHashQuery: false }, diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts index 57cfc52045282..f929e513e618a 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/common.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts @@ -19,37 +19,40 @@ export function extractParams(urlState: UrlState) { * Creates generic index based search ML url * e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a` */ -export function createIndexBasedMlUrl( +export function createGenericMlUrl( appBasePath: string, page: MlGenericUrlState['page'], pageState: MlGenericUrlState['pageState'] ): string { - const { globalState, appState, index, savedSearchId, ...restParams } = pageState; let url = `${appBasePath}/${page}`; - if (index !== undefined && savedSearchId === undefined) { - url = `${url}?index=${index}`; - } - if (index === undefined && savedSearchId !== undefined) { - url = `${url}?savedSearchId=${savedSearchId}`; - } + if (pageState) { + const { globalState, appState, index, savedSearchId, ...restParams } = pageState; + if (index !== undefined && savedSearchId === undefined) { + url = `${url}?index=${index}`; + } + if (index === undefined && savedSearchId !== undefined) { + url = `${url}?savedSearchId=${savedSearchId}`; + } - if (!isEmpty(restParams)) { - Object.keys(restParams).forEach((key) => { - url = setStateToKbnUrl( - key, - restParams[key], - { useHash: false, storeInHashQuery: false }, - url - ); - }); - } + if (!isEmpty(restParams)) { + Object.keys(restParams).forEach((key) => { + url = setStateToKbnUrl( + key, + restParams[key], + { useHash: false, storeInHashQuery: false }, + url + ); + }); + } - if (globalState) { - url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); - } - if (appState) { - url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + if (globalState) { + url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url); + } + if (appState) { + url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url); + } } + return url; } diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts index 8cf10a2acb64f..88761edf241a9 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts @@ -12,6 +12,7 @@ import { DataFrameAnalyticsExplorationUrlState, DataFrameAnalyticsQueryState, DataFrameAnalyticsUrlState, + MlCommonGlobalState, } from '../../common/types/ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public'; @@ -23,18 +24,28 @@ export function createDataFrameAnalyticsJobManagementUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`; if (mlUrlGeneratorState) { - const { jobId, groupIds } = mlUrlGeneratorState; - const queryState: Partial = { - jobId, - groupIds, - }; + const { jobId, groupIds, globalState } = mlUrlGeneratorState; + if (jobId || groupIds) { + const queryState: Partial = { + jobId, + groupIds, + }; - url = setStateToKbnUrl>( - 'mlManagement', - queryState, - { useHash: false, storeInHashQuery: false }, - url - ); + url = setStateToKbnUrl>( + 'mlManagement', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + if (globalState) { + url = setStateToKbnUrl>( + '_g', + globalState, + { useHash: false, storeInHashQuery: false }, + url + ); + } } return url; @@ -50,12 +61,14 @@ export function createDataFrameAnalyticsExplorationUrl( let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`; if (mlUrlGeneratorState) { - const { jobId, analysisType } = mlUrlGeneratorState; + const { jobId, analysisType, globalState } = mlUrlGeneratorState; + const queryState: DataFrameAnalyticsExplorationQueryState = { ml: { jobId, analysisType, }, + ...globalState, }; url = setStateToKbnUrl( diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts deleted file mode 100644 index 24693df5025d9..0000000000000 --- a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts +++ /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. - */ - -/** - * Creates URL to the Data Visualizer page - */ -import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator'; -import { createIndexBasedMlUrl } from './common'; -import { ML_PAGES } from '../../common/constants/ml_url_generator'; - -export function createDataVisualizerUrl( - appBasePath: string, - { page }: DataVisualizerUrlState -): string { - return `${appBasePath}/${page}`; -} - -/** - * Creates URL to the Index Data Visualizer - */ -export function createIndexDataVisualizerUrl( - appBasePath: string, - pageState: MlGenericUrlState['pageState'] -): string { - return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState); -} diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts index 55bc6d3668de7..754f5bec57a07 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts @@ -6,7 +6,7 @@ import { MlUrlGenerator } from './ml_url_generator'; import { ML_PAGES } from '../../common/constants/ml_url_generator'; -import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator'; +import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics'; describe('MlUrlGenerator', () => { const urlGenerator = new MlUrlGenerator({ diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts index b69260d8d4157..abec5cc2b7d1e 100644 --- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts +++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts @@ -16,6 +16,7 @@ import { MlUrlGeneratorState } from '../../common/types/ml_url_generator'; import { createAnomalyDetectionJobManagementUrl, createAnomalyDetectionCreateJobSelectType, + createAnomalyDetectionCreateJobSelectIndex, createExplorerUrl, createSingleMetricViewerUrl, } from './anomaly_detection_urls_generator'; @@ -23,10 +24,8 @@ import { createDataFrameAnalyticsJobManagementUrl, createDataFrameAnalyticsExplorationUrl, } from './data_frame_analytics_urls_generator'; -import { - createIndexDataVisualizerUrl, - createDataVisualizerUrl, -} from './data_visualizer_urls_generator'; +import { createGenericMlUrl } from './common'; +import { createEditCalendarUrl, createEditFilterUrl } from './settings_urls_generator'; declare module '../../../../../src/plugins/share/public' { export interface UrlGeneratorStateMapping { @@ -44,8 +43,12 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition => { - const appBasePath = this.params.appBasePath; + public readonly createUrl = async ( + mlUrlGeneratorParams: MlUrlGeneratorState + ): Promise => { + const { excludeBasePath, ...mlUrlGeneratorState } = mlUrlGeneratorParams; + const appBasePath = excludeBasePath === true ? '' : this.params.appBasePath; + switch (mlUrlGeneratorState.page) { case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE: return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState); @@ -56,18 +59,39 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition { defaultMessage: 'Import your own CSV, NDJSON, or log file.', }), icon: 'document', - path: '/app/ml#/filedatavisualizer', + path: '/app/ml/filedatavisualizer', showOnHomePage: true, category: FeatureCatalogueCategory.DATA, order: 520, diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json index 245b7e0819c7d..bb0323ed9ae78 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json @@ -1,6 +1,6 @@ { "job_type": "anomaly_detector", - "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privliege elevation via locally run exploits or malware activity.", + "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.", "groups": [ "security", "auditbeat", diff --git a/x-pack/plugins/reporting/common/schema_utils.ts b/x-pack/plugins/reporting/common/schema_utils.ts new file mode 100644 index 0000000000000..f9b5c90e3c366 --- /dev/null +++ b/x-pack/plugins/reporting/common/schema_utils.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 { ByteSizeValue } from '@kbn/config-schema'; +import moment from 'moment'; + +/* + * For cleaner code: use these functions when a config schema value could be + * one type or another. This allows you to treat the value as one type. + */ + +export const durationToNumber = (value: number | moment.Duration): number => { + if (typeof value === 'number') { + return value; + } + return value.asMilliseconds(); +}; + +export const byteSizeValueToNumber = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return value; + } + + return value.getValueInBytes(); +}; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index 65db13f22788b..f326d365351f2 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,8 +6,8 @@ import { EuiBasicTable, - EuiFlexItem, EuiFlexGroup, + EuiFlexItem, EuiPageContent, EuiSpacer, EuiText, @@ -23,6 +23,7 @@ import { Subscription } from 'rxjs'; import { ApplicationStart, ToastsSetup } from 'src/core/public'; import { ILicense, LicensingPluginSetup } from '../../../licensing/public'; import { Poller } from '../../common/poller'; +import { durationToNumber } from '../../common/schema_utils'; import { JobStatuses } from '../../constants'; import { checkLicense } from '../lib/license_check'; import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client'; @@ -183,17 +184,19 @@ class ReportListingUi extends Component { public componentDidMount() { this.mounted = true; + const { pollConfig, license$ } = this.props; + const pollFrequencyInMillis = durationToNumber(pollConfig.jobsRefresh.interval); this.poller = new Poller({ functionToPoll: () => { return this.fetchJobs(); }, - pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval, + pollFrequencyInMillis, trailing: false, continuePollingOnError: true, - pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier, + pollFrequencyErrorMultiplier: pollConfig.jobsRefresh.intervalErrorMultiplier, }); this.poller.start(); - this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler); + this.licenseSubscription = license$.subscribe(this.licenseHandler); } private licenseHandler = (license: ILicense) => { diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx index d003d4c581699..a134377e194b8 100644 --- a/x-pack/plugins/reporting/public/plugin.tsx +++ b/x-pack/plugins/reporting/public/plugin.tsx @@ -26,6 +26,7 @@ import { import { ManagementSetup } from '../../../../src/plugins/management/public'; import { SharePluginSetup } from '../../../../src/plugins/share/public'; import { LicensingPluginSetup } from '../../licensing/public'; +import { durationToNumber } from '../common/schema_utils'; import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types'; import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants'; import { getGeneralErrorToast } from './components'; @@ -158,8 +159,7 @@ 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; - + const interval = durationToNumber(this.config.poll.jobsRefresh.interval); Rx.timer(0, interval) .pipe( takeUntil(this.stop$), // stop the interval when stop method is called diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 88be86d1ecc30..6897f07c45e2b 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -21,6 +21,7 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; import { getChromiumDisconnectedError } from '../'; import { BROWSER_TYPE } from '../../../../common/constants'; +import { durationToNumber } from '../../../../common/schema_utils'; import { CaptureConfig } from '../../../../server/types'; import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; @@ -90,7 +91,7 @@ export class HeadlessChromiumDriverFactory { // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(this.captureConfig.timeouts.openUrl); + page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); logger.debug(`Browser page driver created`); } catch (err) { diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index a89b952702e1b..b9c6f8e7591e3 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -17,6 +17,8 @@ export const config: PluginConfigDescriptor = { unused('capture.concurrency'), unused('capture.settleTime'), unused('capture.timeout'), + unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), + unused('poll.jobsRefresh.intervalErrorMultiplier'), unused('kibanaApp'), ], }; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 69e4d443cf040..9fc3d4329879e 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -8,101 +8,242 @@ import { ConfigSchema } from './schema'; describe('Reporting Config Schema', () => { it(`context {"dev":false,"dist":false} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({ - capture: { - browser: { - autoDownload: true, - chromium: { proxy: { enabled: false } }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": true, + "chromium": Object { + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 1, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 1, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); }); it(`context {"dev":false,"dist":true} produces correct config`, () => { - expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({ - capture: { - browser: { - autoDownload: false, - chromium: { - inspect: false, - proxy: { enabled: false }, - }, - type: 'chromium', + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchInlineSnapshot(` + Object { + "capture": Object { + "browser": Object { + "autoDownload": false, + "chromium": Object { + "inspect": false, + "proxy": Object { + "enabled": false, + }, + }, + "type": "chromium", + }, + "loadDelay": "PT3S", + "maxAttempts": 3, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + "timeouts": Object { + "openUrl": "PT1M", + "renderComplete": "PT30S", + "waitForElements": "PT30S", + }, + "viewport": Object { + "height": 1200, + "width": 1950, + }, + "zoom": 2, + }, + "csv": Object { + "checkForFormulas": true, + "enablePanelActionDownload": true, + "escapeFormulaValues": false, + "maxSizeBytes": ByteSizeValue { + "valueInBytes": 10485760, + }, + "scroll": Object { + "duration": "30s", + "size": 500, + }, + "useByteOrderMarkEncoding": false, }, - loadDelay: 3000, - maxAttempts: 3, - networkPolicy: { - enabled: true, - rules: [ - { allow: true, host: undefined, protocol: 'http:' }, - { allow: true, host: undefined, protocol: 'https:' }, - { allow: true, host: undefined, protocol: 'ws:' }, - { allow: true, host: undefined, protocol: 'wss:' }, - { allow: true, host: undefined, protocol: 'data:' }, - { allow: false, host: undefined, protocol: undefined }, + "enabled": true, + "index": ".reporting", + "kibanaServer": Object {}, + "poll": Object { + "jobCompletionNotifier": Object { + "interval": 10000, + "intervalErrorMultiplier": 5, + }, + "jobsRefresh": Object { + "interval": 5000, + "intervalErrorMultiplier": 5, + }, + }, + "queue": Object { + "indexInterval": "week", + "pollEnabled": true, + "pollInterval": "PT3S", + "pollIntervalErrorMultiplier": 10, + "timeout": "PT2M", + }, + "roles": Object { + "allow": Array [ + "reporting_user", ], }, - viewport: { height: 1200, width: 1950 }, - zoom: 2, - }, - csv: { - checkForFormulas: true, - enablePanelActionDownload: true, - maxSizeBytes: 10485760, - scroll: { duration: '30s', size: 500 }, - }, - index: '.reporting', - kibanaServer: {}, - poll: { - jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 }, - jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 }, - }, - queue: { - indexInterval: 'week', - pollEnabled: true, - pollInterval: 3000, - pollIntervalErrorMultiplier: 10, - timeout: 120000, - }, - roles: { allow: ['reporting_user'] }, - }); + } + `); + }); + + it('allows Duration values for certain keys', () => { + expect(ConfigSchema.validate({ queue: { timeout: '2m' } }).queue.timeout).toMatchInlineSnapshot( + `"PT2M"` + ); + + expect( + ConfigSchema.validate({ capture: { loadDelay: '3s' } }).capture.loadDelay + ).toMatchInlineSnapshot(`"PT3S"`); + + expect( + ConfigSchema.validate({ + capture: { timeouts: { openUrl: '1m', waitForElements: '30s', renderComplete: '10s' } }, + }).capture.timeouts + ).toMatchInlineSnapshot(` + Object { + "openUrl": "PT1M", + "renderComplete": "PT10S", + "waitForElements": "PT30S", + } + `); + }); + + it('allows ByteSizeValue values for certain keys', () => { + expect(ConfigSchema.validate({ csv: { maxSizeBytes: '12mb' } }).csv.maxSizeBytes) + .toMatchInlineSnapshot(` + ByteSizeValue { + "valueInBytes": 12582912, + } + `); }); it(`allows optional settings`, () => { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index a81ffd754946b..8276e8b49d348 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { schema, TypeOf } from '@kbn/config-schema'; +import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema'; import moment from 'moment'; const KibanaServerSchema = schema.object({ @@ -33,9 +33,13 @@ const KibanaServerSchema = schema.object({ const QueueSchema = schema.object({ indexInterval: schema.string({ defaultValue: 'week' }), pollEnabled: schema.boolean({ defaultValue: true }), - pollInterval: schema.number({ defaultValue: 3000 }), + pollInterval: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }), - timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }), + timeout: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 2 }), + }), }); const RulesSchema = schema.object({ @@ -46,9 +50,15 @@ const RulesSchema = schema.object({ const CaptureSchema = schema.object({ timeouts: schema.object({ - openUrl: schema.number({ defaultValue: 60000 }), - waitForElements: schema.number({ defaultValue: 30000 }), - renderComplete: schema.number({ defaultValue: 30000 }), + openUrl: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ minutes: 1 }), + }), + waitForElements: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), + renderComplete: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 30 }), + }), }), networkPolicy: schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -68,9 +78,9 @@ const CaptureSchema = schema.object({ width: schema.number({ defaultValue: 1950 }), height: schema.number({ defaultValue: 1200 }), }), - loadDelay: schema.number({ - defaultValue: moment.duration(3, 's').asMilliseconds(), - }), // TODO: use schema.duration + loadDelay: schema.oneOf([schema.number(), schema.duration()], { + defaultValue: moment.duration({ seconds: 3 }), + }), browser: schema.object({ autoDownload: schema.conditional( schema.contextRef('dist'), @@ -116,13 +126,13 @@ const CsvSchema = schema.object({ checkForFormulas: schema.boolean({ defaultValue: true }), escapeFormulaValues: schema.boolean({ defaultValue: false }), enablePanelActionDownload: schema.boolean({ defaultValue: true }), - maxSizeBytes: schema.number({ - defaultValue: 1024 * 1024 * 10, // 10MB - }), // TODO: use schema.byteSize + maxSizeBytes: schema.oneOf([schema.number(), schema.byteSize()], { + defaultValue: ByteSizeValue.parse('10mb'), + }), useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }), scroll: schema.object({ duration: schema.string({ - defaultValue: '30s', + defaultValue: '30s', // this value is passed directly to ES, so string only format is preferred validate(value) { if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) { return 'must be a duration string'; @@ -146,18 +156,16 @@ const RolesSchema = schema.object({ const IndexSchema = schema.string({ defaultValue: '.reporting' }); +// Browser side polling: job completion notifier, management table auto-refresh +// NOTE: can not use schema.duration, a bug prevents it being passed to the browser correctly const PollSchema = schema.object({ jobCompletionNotifier: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(10, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 10000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), jobsRefresh: schema.object({ - interval: schema.number({ - defaultValue: moment.duration(5, 's').asMilliseconds(), - }), // TODO: use schema.duration - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), + interval: schema.number({ defaultValue: 5000 }), + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused }), }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts index 754bc7bc75cb5..a0d8ff0852544 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import sinon from 'sinon'; import { ReportingConfig } from '../../'; import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { BasePayload } from '../../types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; @@ -15,17 +18,10 @@ import { getConditionalHeaders, getCustomLogo } from './'; let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - beforeEach(async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('custom-hostname'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReportingPlugin = await createMockReportingCore(mockConfig); }); @@ -84,10 +80,9 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav const mockGetSavedObjectsClient = jest.fn(); mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient; - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('kibanaServer', 'hostname').returns('localhost'); - mockConfigGet.withArgs('server', 'basePath').returns('/sbp'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); const permittedHeaders = { foo: 'bar', @@ -134,25 +129,12 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav }); describe('config formatting', () => { - test(`lowercases server.host`, async () => { - const mockConfigGet = sinon.stub().withArgs('server', 'host').returns('COOL-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - - const conditionalHeaders = await getConditionalHeaders({ - job: {} as BasePayload, - filteredHeaders: {}, - config: mockConfig, - }); - expect(conditionalHeaders.conditions.hostname).toEqual('cool-hostname'); - }); - test(`lowercases kibanaServer.hostname`, async () => { - const mockConfigGet = sinon - .stub() - .withArgs('kibanaServer', 'hostname') - .returns('GREAT-HOSTNAME'); - mockConfig = getMockConfig(mockConfigGet); - const conditionalHeaders = await getConditionalHeaders({ + const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); + + const conditionalHeaders = getConditionalHeaders({ job: { title: 'cool-job-bro', type: 'csv', diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts index 8c02fdd69de8b..ec4e54632eef5 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ReportingCore } from '../../core'; -import { createMockReportingCore } from '../../test_helpers'; +import { ReportingConfig, ReportingCore } from '../../'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getConditionalHeaders, getCustomLogo } from './'; -const mockConfigGet = jest.fn().mockImplementation((key: string) => { - return 'localhost'; -}); -const mockConfig = { get: mockConfigGet, kbnConfig: { get: mockConfigGet } }; - +let mockConfig: ReportingConfig; let mockReportingPlugin: ReportingCore; + beforeEach(async () => { + mockConfig = createMockConfig(createMockConfigSchema()); mockReportingPlugin = await createMockReportingCore(mockConfig); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts index 355536000326e..fae66b26a83e0 100644 --- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts @@ -5,6 +5,7 @@ */ import { ReportingConfig } from '../../'; +import { createMockConfig } from '../../test_helpers'; import { TaskPayloadPNG } from '../png/types'; import { TaskPayloadPDF } from '../printable_pdf/types'; import { getFullUrls } from './get_full_urls'; @@ -15,12 +16,6 @@ interface FullUrlsOpts { } let mockConfig: ReportingConfig; -const getMockConfig = (mockConfigGet: jest.Mock) => { - return { - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, - }; -}; beforeEach(() => { const reportingConfig: Record = { @@ -29,10 +24,7 @@ beforeEach(() => { 'kibanaServer.protocol': 'http', 'server.basePath': '/sbp', }; - const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { - return reportingConfig[keys.join('.') as string]; - }); - mockConfig = getMockConfig(mockConfigGet); + mockConfig = createMockConfig(reportingConfig); }); const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF; diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 15432d0cbd147..72b42143a24f7 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -6,6 +6,7 @@ import nodeCrypto from '@elastic/node-crypto'; import { ElasticsearchServiceSetup, IUiSettingsClient } from 'kibana/server'; +import moment from 'moment'; // @ts-ignore import Puid from 'puid'; import sinon from 'sinon'; @@ -73,6 +74,7 @@ describe('CSV Execute Job', function () { beforeEach(async function () { configGetStub = sinon.stub(); + configGetStub.withArgs('queue', 'timeout').returns(moment.duration('2m')); configGetStub.withArgs('index').returns('.reporting-foo-test'); configGetStub.withArgs('encryptionKey').returns(encryptionKey); configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts index 06aa2434afc3f..e383f21143149 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts @@ -6,11 +6,12 @@ import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from 'src/core/server'; -import { getFieldFormats } from '../../../services'; import { ReportingConfig } from '../../../'; import { CancellationToken } from '../../../../../../plugins/reporting/common'; import { CSV_BOM_CHARS } from '../../../../common/constants'; +import { byteSizeValueToNumber } from '../../../../common/schema_utils'; import { LevelLogger } from '../../../lib'; +import { getFieldFormats } from '../../../services'; import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types'; import { checkIfRowsHaveFormulas } from './check_cells_for_formulas'; import { createEscapeValue } from './escape_value'; @@ -64,7 +65,7 @@ export function createGenerateCsv(logger: LevelLogger) { ); const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues); const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : ''; - const builder = new MaxSizeStringBuilder(settings.maxSizeBytes, bom); + const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom); const { fields, metaFields, conflictedTypesFields } = job; const header = `${fields.map(escapeValue).join(settings.separator)}\n`; diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index fdc51dc1c9c87..e7322bdc0d408 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -10,7 +10,11 @@ import * as Rx from 'rxjs'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; -import { createMockReportingCore } from '../../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../../test_helpers'; import { generatePdfObservableFactory } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; @@ -39,20 +43,16 @@ const encryptHeaders = async (headers: Record) => { const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF; beforeEach(async () => { - const kbnConfig = { - 'server.basePath': '/sbp', - }; const reportingConfig = { + 'server.basePath': '/sbp', index: '.reports-test', encryptionKey: mockEncryptionKey, 'kibanaServer.hostname': 'localhost', 'kibanaServer.port': 5601, 'kibanaServer.protocol': 'http', }; - const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')], - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, - }; + const mockSchema = createMockConfigSchema(reportingConfig); + const mockReportingConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockReportingConfig); @@ -79,7 +79,7 @@ test(`passes browserTimezone to generatePdf`, async () => { const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; await runTask( 'pdfJobId', @@ -98,7 +98,7 @@ test(`passes browserTimezone to generatePdf`, async () => { test(`returns content_type of application/pdf`, async () => { const logger = getMockLogger(); - const runTask = await runTaskFnFactory(mockReporting, logger); + const runTask = runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); const generatePdfObservable = await generatePdfObservableFactory(mockReporting); @@ -117,7 +117,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); - const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); + const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); const { content } = await runTask( 'pdfJobId', diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts index 85188c07eeb20..1fcd750849331 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts @@ -6,7 +6,12 @@ import * as sinon from 'sinon'; import { ReportingConfig, ReportingCore } from '../../server'; -import { createMockReportingCore } from '../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../test_helpers'; import { createWorkerFactory } from './create_worker'; // @ts-ignore import { Esqueue } from './esqueue'; @@ -14,16 +19,13 @@ import { Esqueue } from './esqueue'; import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch'; import { ExportTypesRegistry } from './export_types_registry'; -const configGetStub = sinon.stub(); -configGetStub.withArgs('queue').returns({ - pollInterval: 3300, - pollIntervalErrorMultiplier: 10, -}); -configGetStub.withArgs('server', 'name').returns('test-server-123'); -configGetStub.withArgs('server', 'uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr'); +const logger = createMockLevelLogger(); +const reportingConfig = { + queue: { pollInterval: 3300, pollIntervalErrorMultiplier: 10 }, + server: { name: 'test-server-123', uuid: 'g9ymiujthvy6v8yrh7567g6fwzgzftzfr' }, +}; const executeJobFactoryStub = sinon.stub(); -const getMockLogger = sinon.stub(); const getMockExportTypesRegistry = ( exportTypes: any[] = [{ runTaskFnFactory: executeJobFactoryStub }] @@ -39,18 +41,18 @@ describe('Create Worker', () => { let client: ClientMock; beforeEach(async () => { - mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockReporting = await createMockReportingCore(mockConfig); mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry(); - // @ts-ignore over-riding config manually - mockReporting.config = mockConfig; + client = new ClientMock(); queue = new Esqueue('reporting-queue', { client }); executeJobFactoryStub.reset(); }); test('Creates a single Esqueue worker for Reporting', async () => { - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); @@ -82,7 +84,7 @@ Object { { runTaskFnFactory: executeJobFactoryStub }, ]); mockReporting.getExportTypesRegistry = () => exportTypesRegistry; - const createWorker = createWorkerFactory(mockReporting, getMockLogger()); + const createWorker = createWorkerFactory(mockReporting, logger); const registerWorkerSpy = sinon.spy(queue, 'registerWorker'); await createWorker(queue); diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts index dd5c560455274..c1c88dd8a54ba 100644 --- a/x-pack/plugins/reporting/server/lib/create_worker.ts +++ b/x-pack/plugins/reporting/server/lib/create_worker.ts @@ -6,6 +6,7 @@ import { CancellationToken } from '../../common'; import { PLUGIN_ID } from '../../common/constants'; +import { durationToNumber } from '../../common/schema_utils'; import { ReportingCore } from '../../server'; import { LevelLogger } from '../../server/lib'; import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types'; @@ -57,7 +58,7 @@ export function createWorkerFactory(reporting: ReportingCore, log const workerOptions = { kibanaName, kibanaId, - interval: queueConfig.pollInterval, + interval: durationToNumber(queueConfig.pollInterval), intervalErrorMultiplier: queueConfig.pollIntervalErrorMultiplier, }; const worker = queue.registerWorker(PLUGIN_ID, workerFn, workerOptions); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts index 49c690e8c024d..89cb4221c96b2 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; -import { LevelLogger, startTrace } from '../'; import { LayoutInstance } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; @@ -31,9 +32,10 @@ export const getNumberOfItems = async ( // the dashboard is using the `itemsCountAttribute` attribute to let us // know how many items to expect since gridster incrementally adds panels // we have to use this hint to wait for all of them + const timeout = durationToNumber(captureConfig.timeouts.waitForElements); await browser.waitForSelector( `${renderCompleteSelector},[${itemsCountAttribute}]`, - { timeout: captureConfig.timeouts.waitForElements }, + { timeout }, { context: CONTEXT_READMETADATA }, logger ); @@ -59,6 +61,7 @@ export const getNumberOfItems = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts index f893951815e9e..2fc711d4d6f07 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts @@ -43,6 +43,7 @@ export const injectCustomCss = async ( logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.injectCss', { defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 3749e4372bdab..5b671e9f5b47e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -15,12 +15,17 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({ }), })); +import moment from 'moment'; import * as Rx from 'rxjs'; -import { LevelLogger } from '../'; -import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { HeadlessChromiumDriver } from '../../browsers'; -import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers'; -import { CaptureConfig, ConditionalHeaders } from '../../types'; +import { + createMockBrowserDriverFactory, + createMockConfig, + createMockConfigSchema, + createMockLayoutInstance, + createMockLevelLogger, +} from '../../test_helpers'; +import { ConditionalHeaders } from '../../types'; import { ElementsPositionAndAttribute } from './'; import * as contexts from './constants'; import { screenshotsObservableFactory } from './observable'; @@ -28,11 +33,22 @@ import { screenshotsObservableFactory } from './observable'; /* * Mocks */ -const mockLogger = jest.fn(loggingSystemMock.create); -const logger = new LevelLogger(mockLogger()); +const logger = createMockLevelLogger(); -const mockConfig = { timeouts: { openUrl: 13 } } as CaptureConfig; -const mockLayout = createMockLayoutInstance(mockConfig); +const reportingConfig = { + capture: { + loadDelay: moment.duration(2, 's'), + timeouts: { + openUrl: moment.duration(2, 'm'), + waitForElements: moment.duration(20, 's'), + renderComplete: moment.duration(10, 's'), + }, + }, +}; +const mockSchema = createMockConfigSchema(reportingConfig); +const mockConfig = createMockConfig(mockSchema); +const captureConfig = mockConfig.get('capture'); +const mockLayout = createMockLayoutInstance(captureConfig); /* * Tests @@ -45,7 +61,7 @@ describe('Screenshot Observable Pipeline', () => { }); it('pipelines a single url into screenshot and timeRange', async () => { - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index.htm'], @@ -106,7 +122,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const result = await getScreenshots$({ logger, urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], @@ -205,7 +221,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -300,7 +316,7 @@ describe('Screenshot Observable Pipeline', () => { }); // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, @@ -333,7 +349,7 @@ describe('Screenshot Observable Pipeline', () => { mockLayout.getViewport = () => null; // test - const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory); + const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory); const getScreenshot = async () => { return await getScreenshots$({ logger, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts index c21ef3b91fab3..e28f50851f4d9 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts @@ -5,9 +5,10 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; +import { LevelLogger, startTrace } from '../'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig, ConditionalHeaders } from '../../types'; -import { LevelLogger, startTrace } from '../'; export const openUrl = async ( captureConfig: CaptureConfig, @@ -19,16 +20,14 @@ export const openUrl = async ( ): Promise => { const endTrace = startTrace('open_url', 'wait'); try { + const timeout = durationToNumber(captureConfig.timeouts.openUrl); await browser.open( url, - { - conditionalHeaders, - waitForSelector: pageLoadSelector, - timeout: captureConfig.timeouts.openUrl, - }, + { conditionalHeaders, waitForSelector: pageLoadSelector, timeout }, logger ); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`, diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts index f36a7b6f73664..edd4f71b2adac 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { durationToNumber } from '../../../common/schema_utils'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LevelLogger, startTrace } from '../'; @@ -67,7 +68,7 @@ export const waitForRenderComplete = async ( return Promise.all(renderedTasks).then(hackyWaitForVisualizations); }, - args: [layout.selectors.renderComplete, captureConfig.loadDelay], + args: [layout.selectors.renderComplete, durationToNumber(captureConfig.loadDelay)], }, { context: CONTEXT_WAITFORRENDER }, logger diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index 779d00442522d..5f86a2b3bf00b 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -5,8 +5,9 @@ */ import { i18n } from '@kbn/i18n'; -import { HeadlessChromiumDriver } from '../../browsers'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, startTrace } from '../'; +import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; @@ -25,7 +26,7 @@ const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { export const waitForVisualizations = async ( captureConfig: CaptureConfig, browser: HeadlessChromiumDriver, - itemsCount: number, + toEqual: number, layout: LayoutInstance, logger: LevelLogger ): Promise => { @@ -35,29 +36,26 @@ export const waitForVisualizations = async ( logger.debug( i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', { defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`, - values: { itemsCount }, + values: { itemsCount: toEqual }, }) ); try { + const timeout = durationToNumber(captureConfig.timeouts.renderComplete); await browser.waitFor( - { - fn: getCompletedItemsCount, - args: [{ renderCompleteSelector }], - toEqual: itemsCount, - timeout: captureConfig.timeouts.renderComplete, - }, + { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, logger ); - logger.debug(`found ${itemsCount} rendered elements in the DOM`); + logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { + logger.error(err); throw new Error( i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`, values: { - count: itemsCount, + count: toEqual, configKey: 'xpack.reporting.capture.timeouts.renderComplete', error: err, }, diff --git a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts index 71ce0b1e572f8..7b8b851f5bd72 100644 --- a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts +++ b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts @@ -8,7 +8,6 @@ import moment, { unitOfTime } from 'moment'; export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute']; -// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema export function indexTimestamp(intervalStr: string, separator = '-') { const startOf = intervalStr as unitOfTime.StartOf; if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter'); diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index b87466ca289cf..8dc4edd200052 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,15 +7,15 @@ import sinon from 'sinon'; import { ElasticsearchServiceSetup } from 'src/core/server'; import { ReportingConfig, ReportingCore } from '../..'; -import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; +import { + createMockConfig, + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../test_helpers'; import { Report } from './report'; import { ReportingStore } from './store'; -const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({ - get: mockConfigGet, - kbnConfig: { get: mockConfigGet }, -}); - describe('ReportingStore', () => { const mockLogger = createMockLevelLogger(); let mockConfig: ReportingConfig; @@ -25,10 +25,12 @@ describe('ReportingStore', () => { const mockElasticsearch = { legacy: { client: { callAsInternalUser: callClusterStub } } }; beforeEach(async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('week'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'week' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); callClusterStub.reset(); @@ -67,15 +69,17 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); it('throws if options has invalid indexInterval', async () => { - const mockConfigGet = sinon.stub(); - mockConfigGet.withArgs('index').returns('.reporting-test'); - mockConfigGet.withArgs('queue', 'indexInterval').returns('centurially'); - mockConfig = getMockConfig(mockConfigGet); + const reportingConfig = { + index: '.reporting-test', + queue: { indexInterval: 'centurially' }, + }; + const mockSchema = createMockConfigSchema(reportingConfig); + mockConfig = createMockConfig(mockSchema); mockCore = await createMockReportingCore(mockConfig); const store = new ReportingStore(mockCore, mockLogger); @@ -159,7 +163,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); @@ -190,7 +194,7 @@ describe('ReportingStore', () => { priority: 10, started_at: undefined, status: 'pending', - timeout: undefined, + timeout: 120000, }); }); }); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index b1309cbdeb94d..0aae8b567bcdb 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -5,6 +5,7 @@ */ import { ElasticsearchServiceSetup } from 'src/core/server'; +import { durationToNumber } from '../../../common/schema_utils'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types'; @@ -45,7 +46,7 @@ export class ReportingStore { this.indexPrefix = config.get('index'); this.indexInterval = config.get('queue', 'indexInterval'); this.jobSettings = { - timeout: config.get('queue', 'timeout'), + timeout: durationToNumber(config.get('queue', 'timeout')), browser_type: config.get('capture', 'browser', 'type'), max_attempts: config.get('capture', 'maxAttempts'), priority: 10, // unused diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index d323a281c06ff..3f2f472ab0623 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -32,8 +32,8 @@ describe('Reporting Plugin', () => { beforeEach(async () => { configSchema = createMockConfigSchema(); initContext = coreMock.createPluginInitializerContext(configSchema); - coreSetup = await coreMock.createSetup(configSchema); - coreStart = await coreMock.createStart(); + coreSetup = coreMock.createSetup(configSchema); + coreStart = coreMock.createStart(); pluginSetup = ({ licensing: {}, features: featuresPluginMock.createSetup(), diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index f92fbfc7013cf..71ca0661a42a9 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -33,7 +33,15 @@ describe('POST /diagnose/browser', () => { const mockedCreateInterface: any = createInterface; const config = { - get: jest.fn().mockImplementation(() => ({})), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'capture.browser.chromium.proxy': + return { enabled: false }; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts index 24b85220defb4..33620bc9a0038 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -54,25 +54,30 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger validate: {}, }, userHandler(async (user, context, req, res) => { - const logs = await browserStartLogs(reporting, logger).toPromise(); - const knownIssues = Object.keys(logsToHelpMap) as Array; + try { + const logs = await browserStartLogs(reporting, logger).toPromise(); + const knownIssues = Object.keys(logsToHelpMap) as Array; - const boundSuccessfully = logs.includes(`DevTools listening on`); - const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { - const helpText = logsToHelpMap[knownIssue]; - if (logs.includes(knownIssue)) { - helpTexts.push(helpText); - } - return helpTexts; - }, []); + const boundSuccessfully = logs.includes(`DevTools listening on`); + const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { + const helpText = logsToHelpMap[knownIssue]; + if (logs.includes(knownIssue)) { + helpTexts.push(helpText); + } + return helpTexts; + }, []); - const response: DiagnosticResponse = { - success: boundSuccessfully && !help.length, - help, - logs, - }; + const response: DiagnosticResponse = { + success: boundSuccessfully && !help.length, + help, + logs, + }; - return res.ok({ body: response }); + return res.ok({ body: response }); + } catch (err) { + logger.error(err); + return res.custom({ statusCode: 500 }); + } }) ); }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts index 624397246656d..a112d04f38c7b 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts @@ -35,7 +35,15 @@ describe('POST /diagnose/config', () => { } as unknown) as any; config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + const key = keys.join('.'); + switch (key) { + case 'queue.timeout': + return 120000; + case 'csv.maxSizeBytes': + return 1024; + } + }), kbnConfig: { get: jest.fn() }, }; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts index 198ba63e2614d..95c3a05bbf680 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { ByteSizeValue } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; -import numeral from '@elastic/numeral'; import { defaults, get } from 'lodash'; import { ReportingCore } from '../..'; import { API_DIAGNOSE_URL } from '../../../common/constants'; @@ -16,6 +16,14 @@ import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_rout const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; +const numberToByteSizeValue = (value: number | ByteSizeValue) => { + if (typeof value === 'number') { + return new ByteSizeValue(value); + } + + return value; +}; + export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { const setupDeps = reporting.getPluginSetupDeps(); const userHandler = authorizedUserPreRoutingFactory(reporting); @@ -42,12 +50,10 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) 'http.max_content_length', '100mb' ); - const elasticSearchMaxContentBytes = numeral().unformat( - elasticSearchMaxContent.toUpperCase() - ); - const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); + const elasticSearchMaxContentBytes = ByteSizeValue.parse(elasticSearchMaxContent); + const kibanaMaxContentBytes = numberToByteSizeValue(config.get('csv', 'maxSizeBytes')); - if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { + if (kibanaMaxContentBytes.isGreaterThan(elasticSearchMaxContentBytes)) { const maxContentSizeWarning = i18n.translate( 'xpack.reporting.diagnostic.configSizeMismatch', { @@ -55,8 +61,8 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) `xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` + `Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`, values: { - kibanaMaxContentBytes, - elasticSearchMaxContentBytes, + kibanaMaxContentBytes: kibanaMaxContentBytes.getValueInBytes(), + elasticSearchMaxContentBytes: elasticSearchMaxContentBytes.getValueInBytes(), KIBANA_MAX_SIZE_BYTES_PATH, ES_MAX_SIZE_BYTES_PATH, }, diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index ec4ab0446ae5f..287da0d2ed5ec 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -33,7 +33,11 @@ describe('POST /diagnose/screenshot', () => { }; const config = { - get: jest.fn(), + get: jest.fn().mockImplementation((...keys) => { + if (keys.join('.') === 'queue.timeout') { + return 120000; + } + }), kbnConfig: { get: jest.fn() }, }; const mockLogger = createMockLevelLogger(); diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts index 2957bc76f4682..187c69f4a72ef 100644 --- a/x-pack/plugins/reporting/server/routes/jobs.test.ts +++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts @@ -10,9 +10,8 @@ import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '..'; import { ReportingInternalSetup } from '../core'; -import { LevelLogger } from '../lib'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockReportingCore } from '../test_helpers'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ExportTypeDefinition } from '../types'; import { registerJobInfoRoutes } from './jobs'; @@ -25,11 +24,7 @@ describe('GET /api/reporting/jobs/download', () => { let exportTypesRegistry: ExportTypesRegistry; let core: ReportingCore; - const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } }; - const mockLogger = ({ - error: jest.fn(), - debug: jest.fn(), - } as unknown) as jest.Mocked; + const config = createMockConfig(createMockConfigSchema()); const getHits = (...sources: any) => { return { @@ -86,8 +81,6 @@ describe('GET /api/reporting/jobs/download', () => { }); afterEach(async () => { - mockLogger.debug.mockReset(); - mockLogger.error.mockReset(); await server.stop(); }); diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts index 50780a577af02..932ebfdd22bbc 100644 --- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts @@ -4,24 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'kibana/server'; import { coreMock, httpServerMock } from 'src/core/server/mocks'; import { ReportingCore } from '../../'; -import { createMockReportingCore } from '../../test_helpers'; -import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; import { ReportingInternalSetup } from '../../core'; +import { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from '../../test_helpers'; +import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing'; let mockCore: ReportingCore; -const kbnConfig = { - 'server.basePath': '/sbp', -}; -const reportingConfig = { - 'roles.allow': ['reporting_user'], -}; -const mockReportingConfig = { - get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')] || 'whoah!', - kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] }, -}; +const mockConfig: any = { 'server.basePath': '/sbp', 'roles.allow': ['reporting_user'] }; +const mockReportingConfigSchema = createMockConfigSchema(mockConfig); +const mockReportingConfig = createMockConfig(mockReportingConfigSchema); const getMockContext = () => (({ diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index f2785bce10964..d6996d2caf1bc 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import moment from 'moment'; import { Page } from 'puppeteer'; import * as Rx from 'rxjs'; import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; @@ -15,6 +16,7 @@ import { CaptureConfig } from '../types'; interface CreateMockBrowserDriverFactoryOpts { evaluate: jest.Mock, any[]>; waitForSelector: jest.Mock, any[]>; + waitFor: jest.Mock, any[]>; screenshot: jest.Mock, any[]>; open: jest.Mock, any[]>; getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; @@ -86,6 +88,7 @@ const getCreatePage = (driver: HeadlessChromiumDriver) => const defaultOpts: CreateMockBrowserDriverFactoryOpts = { evaluate: mockBrowserEvaluate, waitForSelector: mockWaitForSelector, + waitFor: jest.fn(), screenshot: mockScreenshot, open: jest.fn(), getCreatePage, @@ -96,7 +99,11 @@ export const createMockBrowserDriverFactory = async ( opts: Partial = {} ): Promise => { const captureConfig: CaptureConfig = { - timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 }, + timeouts: { + openUrl: moment.duration(60, 's'), + waitForElements: moment.duration(30, 's'), + renderComplete: moment.duration(30, 's'), + }, browser: { type: 'chromium', chromium: { @@ -108,18 +115,14 @@ export const createMockBrowserDriverFactory = async ( }, networkPolicy: { enabled: true, rules: [] }, viewport: { width: 800, height: 600 }, - loadDelay: 2000, + loadDelay: moment.duration(2, 's'), zoom: 2, maxAttempts: 1, }; const binaryPath = '/usr/local/share/common/secure/super_awesome_binary'; - const mockBrowserDriverFactory = await chromium.createDriverFactory( - binaryPath, - captureConfig, - logger - ); - const mockPage = {} as Page; + const mockBrowserDriverFactory = chromium.createDriverFactory(binaryPath, captureConfig, logger); + const mockPage = ({ setViewport: () => {} } as unknown) as Page; const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, { inspect: true, networkPolicy: captureConfig.networkPolicy, @@ -127,6 +130,7 @@ export const createMockBrowserDriverFactory = async ( // mock the driver methods as either default mocks or passed-in mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore + mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor; mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index 559726e0b8a99..6ec35db5caec6 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -9,14 +9,16 @@ jest.mock('../usage'); jest.mock('../browsers'); jest.mock('../lib/create_queue'); +import _ from 'lodash'; import * as Rx from 'rxjs'; -import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { chromium, HeadlessChromiumDriverFactory, initializeBrowserDriverFactory, } from '../browsers'; +import { ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { ReportingStartDeps } from '../types'; @@ -57,12 +59,58 @@ const createMockPluginStart = ( }; }; -export const createMockConfigSchema = (overrides?: any) => ({ - index: '.reporting', - kibanaServer: { hostname: 'localhost', port: '80' }, - capture: { browser: { chromium: { disableSandbox: true } } }, - ...overrides, -}); +interface ReportingConfigTestType { + index: string; + encryptionKey: string; + queue: Partial; + kibanaServer: Partial; + csv: Partial; + capture: any; + server?: any; +} + +export const createMockConfigSchema = ( + overrides: Partial = {} +): ReportingConfigTestType => { + // deeply merge the defaults and the provided partial schema + return { + index: '.reporting', + encryptionKey: 'cool-encryption-key-where-did-you-find-it', + ...overrides, + kibanaServer: { + hostname: 'localhost', + port: 80, + ...overrides.kibanaServer, + }, + capture: { + browser: { + chromium: { + disableSandbox: true, + }, + }, + ...overrides.capture, + }, + queue: { + timeout: 120000, + ...overrides.queue, + }, + csv: { + ...overrides.csv, + }, + }; +}; + +export const createMockConfig = ( + reportingConfig: Partial +): ReportingConfig => { + const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => { + return _.get(reportingConfig, keys.join('.')); + }); + return { + get: mockConfigGet, + kbnConfig: { get: mockConfigGet }, + }; +}; export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({ data: startMock.data, diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index 2d5ef9fdd768d..96357dc915eef 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -4,8 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export { createMockServer } from './create_mock_server'; -export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin'; export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; export { createMockLayoutInstance } from './create_mock_layoutinstance'; export { createMockLevelLogger } from './create_mock_levellogger'; +export { + createMockConfig, + createMockConfigSchema, + createMockReportingCore, +} from './create_mock_reportingplugin'; +export { createMockServer } from './create_mock_server'; diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 213bea3bc3eec..1211d4c2cf1c3 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -5,7 +5,7 @@ */ import { get } from 'lodash'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { ReportingConfig } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; import { GetLicense } from './'; @@ -118,7 +118,7 @@ async function handleResponse(response: SearchResponse): Promise { const reportingIndex = config.get('index'); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts index ed2abef2542de..fc2dce441c621 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts @@ -8,8 +8,8 @@ import * as Rx from 'rxjs'; import sinon from 'sinon'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingConfig, ReportingCore } from '../'; -import { createMockReportingCore } from '../test_helpers'; import { getExportTypesRegistry } from '../lib/export_types_registry'; +import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers'; import { ReportingSetupDeps } from '../types'; import { FeaturesAvailability } from './'; import { @@ -54,17 +54,13 @@ function getPluginsMock( } as unknown) as ReportingSetupDeps & { usageCollection: UsageCollectionSetup }; } -const getMockReportingConfig = () => ({ - get: () => {}, - kbnConfig: { get: () => '' }, -}); const getResponseMock = (base = {}) => base; describe('license checks', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); @@ -189,7 +185,7 @@ describe('data modeling', () => { let mockConfig: ReportingConfig; let mockCore: ReportingCore; beforeAll(async () => { - mockConfig = getMockReportingConfig(); + mockConfig = createMockConfig(createMockConfigSchema()); mockCore = await createMockReportingCore(mockConfig); }); test('with normal looking usage data', async () => { @@ -455,7 +451,7 @@ describe('data modeling', () => { describe('Ready for collection observable', () => { test('converts observable to promise', async () => { - const mockConfig = getMockReportingConfig(); + const mockConfig = createMockConfig(createMockConfigSchema()); const mockReporting = await createMockReportingCore(mockConfig); const usageCollection = getMockUsageCollection(); diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts index 100d09a2da7e4..8f26579726ff1 100644 --- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts +++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.ts @@ -5,7 +5,7 @@ */ import { first, map } from 'rxjs/operators'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { ReportingCore } from '../'; import { ExportTypesRegistry } from '../lib/export_types_registry'; @@ -36,7 +36,7 @@ export function getReportingUsageCollector( ) { return usageCollection.makeUsageCollector({ type: 'reporting', - fetch: (callCluster: CallCluster) => { + fetch: (callCluster: LegacyAPICaller) => { const config = reporting.getConfig(); return getReportingUsage(config, getLicense, callCluster, exportTypesRegistry); }, diff --git a/x-pack/plugins/rollup/server/collectors/register.ts b/x-pack/plugins/rollup/server/collectors/register.ts index aa06d3f696d00..daacc065629a4 100644 --- a/x-pack/plugins/rollup/server/collectors/register.ts +++ b/x-pack/plugins/rollup/server/collectors/register.ts @@ -6,7 +6,7 @@ import { get } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; +import { LegacyAPICaller } from 'kibana/server'; interface IdToFlagMap { [key: string]: boolean; @@ -27,7 +27,7 @@ function createIdToFlagMap(ids: string[]) { }, {} as any); } -async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCluster) { +async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: LegacyAPICaller) { const searchParams = { size: ES_MAX_RESULT_WINDOW_DEFAULT_VALUE, index: kibanaIndex, @@ -56,7 +56,7 @@ async function fetchRollupIndexPatterns(kibanaIndex: string, callCluster: CallCl async function fetchRollupSavedSearches( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap ) { const searchParams = { @@ -104,7 +104,7 @@ async function fetchRollupSavedSearches( async function fetchRollupVisualizations( kibanaIndex: string, - callCluster: CallCluster, + callCluster: LegacyAPICaller, rollupIndexPatternToFlagMap: IdToFlagMap, rollupSavedSearchesToFlagMap: IdToFlagMap ) { @@ -211,7 +211,7 @@ export function registerRollupUsageCollector( total: { type: 'long' }, }, }, - fetch: async (callCluster: CallCluster) => { + fetch: async (callCluster: LegacyAPICaller) => { const rollupIndexPatterns = await fetchRollupIndexPatterns(kibanaIndex, callCluster); const rollupIndexPatternToFlagMap = createIdToFlagMap(rollupIndexPatterns); diff --git a/x-pack/plugins/searchprofiler/server/plugin.ts b/x-pack/plugins/searchprofiler/server/plugin.ts index 0dfb65aa6f857..032593d5e3b31 100644 --- a/x-pack/plugins/searchprofiler/server/plugin.ts +++ b/x-pack/plugins/searchprofiler/server/plugin.ts @@ -20,10 +20,9 @@ export class SearchProfilerServerPlugin implements Plugin { this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing, elasticsearch }: AppServerPluginDependencies) { + async setup({ http }: CoreSetup, { licensing }: AppServerPluginDependencies) { const router = http.createRouter(); profileRoute.register({ - elasticsearch, router, getLicenseStatus: () => this.licenseStatus, log: this.log, diff --git a/x-pack/plugins/searchprofiler/server/types.ts b/x-pack/plugins/searchprofiler/server/types.ts index 7aa0032afba13..84733b0ccfd95 100644 --- a/x-pack/plugins/searchprofiler/server/types.ts +++ b/x-pack/plugins/searchprofiler/server/types.ts @@ -5,18 +5,15 @@ */ import { IRouter, Logger } from 'kibana/server'; -import { ElasticsearchPlugin } from '../../../../src/legacy/core_plugins/elasticsearch'; import { LicensingPluginSetup } from '../../licensing/server'; import { LicenseStatus } from '../common'; export interface AppServerPluginDependencies { licensing: LicensingPluginSetup; - elasticsearch: ElasticsearchPlugin; } export interface RouteDependencies { getLicenseStatus: () => LicenseStatus; - elasticsearch: ElasticsearchPlugin; router: IRouter; log: Logger; } diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index 642e86059ed6e..c8d9b46d5a0d2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -297,13 +297,13 @@ Object { class="sc-fzoyAV jWxvlI siemWrapperPage" >

Beta @@ -330,7 +330,7 @@ Object {