diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 9a49c19b94df22..33ecfcd84fd3e6 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 d81f6af4cec28f..7daa42af7024d5 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 e00a67f6c78a48..b4c9c6a4ec39e4 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 9c8d753a2d6681..3489dcd0182934 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 eadca229bc19c6..7022320328c859 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 517bda374af9d7..b707405ffbeaff 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 b1d1335eb18885..78472bb3f517d0 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 eeb5564667ec4a..d8bd39b9dcdf4a 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 362c34d416743a..19487efe1366c9 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 486c8563c54562..5d31db63773fa8 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 e22dc03cf57aaa..ba58dcdfa4d581 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 83e7bb19e57baa..00000000000000 --- 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 f90f490d680350..00000000000000 --- 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 b5403e1f13de77..00000000000000 --- 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 0b8786f0c28413..00000000000000 --- 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 d79dd4ae4e449a..00000000000000 --- 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 0e7692f6be7555..00000000000000 --- 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 7302241c469393..00000000000000 --- 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 8827dc53c52750..3cfda0e0696bbe 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 fd788d33392951..d3a95b32cd4250 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 f4e42fa8881ef5..378340e876296b 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 f159cac664a9e9..8e1151b387fee7 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 7a42ed7fad4274..b175066b81c8ee 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 e04f84a67b8a3c..9d40291423cac2 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 5a3dc47aab9bbd..00000000000000 --- 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 bfbf873cf06164..d07c099634005d 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 17345d4301882b..e679ac7f31bd15 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 55d4a524820c57..7577b6927d166f 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 e5b39584a519bd..28eb94405abbb6 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 e582e996a78b55..b6ff5b27667dd6 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 a693e008db6ea0..e6f160ce8c6542 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 074b8e6859dc2b..745b4bd72dde8e 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 c3596d3745e57e..00000000000000 --- 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 725d022879e0d7..aec06a4596203a 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 679c75a0d59428..a3bd66e744fdaa 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 4ec5bc13eea816..3537d1bf420798 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 3524d41646d50a..8c233d3691c7fa 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 a1161354e04f45..15a3c642faf324 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 00000000000000..bc0ffefd79c4da --- /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 612cb18bbe190b..cc6a1fffb22885 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 632d53a9c63b65..c30cef7210a435 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 7b284696477f3f..a465b90e7bf05f 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 20e0a3f27c4a45..fb4cda56fce048 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 00000000000000..4dbf4dc10a9076 --- /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 00000000000000..e145d03671a180 --- /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 00000000000000..c0a1955e2cc8a4 --- /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 27c4a37e09c008..c11bfdeae945be 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 aa34515ea460af..45a7fa2a118f2c 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 51ac6673251fb8..ab3f1026a92dd4 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 0f6061653f3523..1cda70a140c673 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 00000000000000..f2558da3a30e48 --- /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 44ca80143bcd9a..fcbb4cc5950e06 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 61e3dfee420a51..5455cd9f6a4951 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 ead28c325692d6..373d4bd4da832f 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 93af51b572aa57..b3526b6a97ad9f 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 00000000000000..a6ed40fc15ec63 --- /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 7bcd945d890ad3..d0673335387c64 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 a53068d152d03a..fcd4f468d4367f 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 6ff98a9be75f90..ea8d02eb833cfd 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 b88c90a213c671..2868dcfda97b6e 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 316b0d59d2c5b6..a60576ca0c175e 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 1a7d6028823953..f25062c67f87ad 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 88cc26608b8508..5c183fd9150dd0 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 f25e37927f0941..b417f8689b2290 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 04807cfac1cea9..1996d4d4a262dc 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 2cc0cdb1c2b91b..f0a22356d074bb 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 83c23a75e999d5..7882383d78ab04 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 48396d93d13e63..eb650ca5ad1524 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 3517c958b27b8a..466a7cc20a497e 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 0b354d7677f6e7..3b635b2e579269 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 31ee304fe22477..ba14be5564be17 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 d6a51e8b482d0a..5df25f11e50705 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 3f71759390879a..9388d61041b13e 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 94e9127bbed74b..31c7680fd2f1c9 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 c4a366930d22aa..643c4b5ccc8731 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 a54295548004a8..82f884644be4ad 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 3ae48f352b2c18..37a8f16acad6df 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 c032e3b04ebe62..b65499be2f7c03 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 ec9db30ddef3ba..72380142fe3998 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 81106235780d6d..902c910f10d7c3 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 4c2160195a1af3..db1b0d611079a4 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 f6c83888413d37..e867e9cf5a445b 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 623e6e47167d26..7b876d81527fac 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 e122c4d5cfdfaf..ef8216e8b6711c 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 f88a00f63f4873..94bd1d529b65ff 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 39280ad6f4be4a..fc1943264d72bb 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 6a51b49869eafb..a68dfaf8ea4711 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 787d5295db1cf2..a156b8a8009f9f 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 0c1e81e3aba462..3d0a3181f8ab87 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 00d5eaf5d6a83b..6b65c16c832fd8 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 0217f039e08bab..7bb9954fa30489 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 0d1e8725b4911d..11fbc7931e6201 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 d00349e21a7e4f..e41f9243198ad9 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 a2d5c7c8d53085..b3bf0719489562 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 00000000000000..c8af296318b61c --- /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 ba9c75baa1987d..1550485ebad934 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 962606b2f4ffd8..1ee2bf22edb444 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 74331cb1b6b221..748dc54838270e 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 4c02171d49eec8..aa8aefba921e77 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 0c067d09046d73..35382506a3cd9f 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 e96426ece27e8e..b1545d44885c8f 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 997e866da35f0e..0f28c5080d26d5 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 00000000000000..1ff97a8d72a217 --- /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 0cf921f66451b8..8fcd02e4a362ea 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 3d78205934eeaa..6ad3c9c5d0bd4c 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 f87d1f94001018..9a37f55ac8e9d2 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 dafbebd24b3fa7..3fa456c33f5e90 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 c4ed11097b6098..07def791096e7c 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 42854673269aee..5cc2addba53b87 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 18d9c637bd45b9..a4d3bf3832d5c5 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 c2a44152ae1ee8..97dca49fc93ed5 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 605e4db230ce59..b9789b770eb2e2 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 98f5878ec927ed..07baf29fdd32aa 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 7e2e8f04535885..2114d63fcfacd4 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 bc98bf53d9f122..4aa5bd62c05a5e 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 97dd7a7b0fef54..3d54e9e150fefa 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 830537cbadbc8d..9a7af2496c03fe 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 44f33aa329e7ae..541b8af6fc0fc7 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 96d6c81a3d309b..5d0ecf96fb6b5f 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 234be8b6faf909..d176c22bdbb62f 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 d725984a47d661..d231ed43443892 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 fc673397ef177d..2c5dbe108ab1e9 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 c281dc4e9ae059..e3bcc53fe697fc 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 653eca126006db..cdd25821ea5ca2 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 9eb44c71aa7996..114a6b235d1adf 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 9dabfce163dbb3..d5025fd3c36492 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 fdeab0c49e32b4..6025dd1c7433e7 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 4a63a8cd7e7163..d54a7fe81e8580 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 78acb422851e36..c4c8f06bbbc3a0 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 368e758a027c49..b4668810b94210 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 22815fe593d57a..6aad5d53c3a3c9 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 263337f93e9a8c..7c4428db71b3b6 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 1f03dbe1347569..279afc8c503399 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 3a4875fa243fda..671f0b196ce35f 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 48e0da72f067cd..eb12cb7679674c 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 48385ad3ae6a82..d448185c914b86 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 60681fb6e7bbe2..d22bba7738db42 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 00d735d9a866e9..83eebccd310e3c 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 1e5dbee3499bda..1e6a616fedd64d 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 88c89df86b29ab..310cd4e3b3a79c 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 eea579ef1d064f..84b1c4241aaf20 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 c8349084dbda88..f4f01330271fce 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 a3595b51d0a596..2363e6fbecc9d8 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 0c3bff58c25cdd..2f8e087a6a3f0e 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 994357412510df..37076d400f0211 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 ef1d373a55a124..1af99d2a1ed00c 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 338b6444671a68..dbc7a23f2258ba 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 7cd9fcc052f1a5..178638322bacdb 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 4926decaa7f9c0..2a89c5a5fd686c 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 41f3bab8113f0e..14427dd5c6ef28 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 769b83c03110b1..7c30dc0cac6901 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 1f2c97b128e3f8..ab738ca0f1545b 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 c6503a639997db..826f7b707cfdf6 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 6f391f9746f232..029ca0475015f8 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 bcb11cad9674ce..c9645b787a8e02 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 4fb783bfb60063..8f03b1903800a4 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 8257ac2b3a7036..2da212c8f2f293 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 d0d0442dd4aeed..85a342838a5062 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 8f89c4a049189e..73b212b97b4ccd 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 b6157c8694a185..b32070fff73aad 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 a5469357ba1a1b..8b5d6009cc61ed 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 00000000000000..0e84619899d71b --- /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 fa4ea09b89ff91..8bc0057b27d6db 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 fdffa8b38ae04c..81effe8d3ebebd 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 669b8837e74b50..021039c06e3209 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 69df2773f9f8d7..cedaaa3b5dfaac 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 be0135ec3f1e09..1a91f6d51ed4d0 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 e3b0fd4cefe0ca..97a03fa21035fe 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 0af6030df28b19..9c9096dfdfc21b 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 1792999eee4c2e..d0cfd16d7562f2 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 395a570083c0de..4f0cbc0adddf2c 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 be8038cc5049d2..4d810c47415a79 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 a71141d0356d07..dfba7c96512667 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 0bfd2c2e492323..1cb6bab7fd7683 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 945116b0534bb8..8515431d49b17c 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 d0a4f999af7582..398ec5b4759d24 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 958221df8a6361..9cebb67166a666 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 22a17c4ea089a9..7cb3a2f07c2ee2 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 47cc002ab4d830..f9f2ebe48f4aa2 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 b6ef9ea81b4bab..80706a82121d51 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 7bf7784d1b5598..b1fd6e93a744c8 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 efe5c3cba04a55..f40b754a23ccb1 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 485af52c45a551..837616a8a76d2a 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 358b8773e3460e..e3d0e5050fca5f 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 30b9bc2af219f7..00d64a2f1bd1df 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 38a7900916ba83..2863e59508e351 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 d8605c4cc91152..0ef3b384dcf5de 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 b8ab29d40fa1f7..543e01fbd326d5 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 6be58828ee1a55..654d7184cfcf27 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 35085fd5575773..8a82a9a8dbc49f 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 174e9804b96893..0e07b0edfbe560 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 f2ae57f1ec961b..24609712396184 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 a5c30e1eaaacc3..4e0a8340590a4d 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 d734e18d72babc..4e39cfce82e36f 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 c6f17bc7f6f683..5fe56b024e413e 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 3f4b2698514691..3159c2ae881663 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 11ec074bac1db4..b60a2655604551 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 817c9754159971..03588872d6be03 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 4967e3a684a6b8..e4cd90145bee40 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 dfa1b5f4e68cd0..ea97492ae0f5ad 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 16d7e1605263c6..57caa56b2f10e3 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 1fe16e4588bd76..a5eb212ba127ee 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 2cff255bd1ce3a..068d4433000883 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 cc1c524c19b57b..50cacd7b3545a5 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 77331c4a987dca..6b4403aef7c7b0 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 41b7aa63f55ef5..681c54ca9eee07 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 f16bf626321524..a5e69f233e2df8 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 deecb9fb45b514..88bf769aa29365 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 4ec7c5cb6d819a..ca55bb10b13d56 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 b7cf11c088a1ec..955dd7cbea0a18 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 806626577008e0..00000000000000 --- 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 ab879e421cb094..04ccd84c561bb3 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 c4aebb108e7b92..6a44756412fe39 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 57cfc52045282b..f929e513e618a0 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 8cf10a2acb64f2..88761edf241a97 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 24693df5025d9d..00000000000000 --- 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 55bc6d3668de78..754f5bec57a07b 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 b69260d8d4157b..abec5cc2b7d1ef 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 245b7e0819c7db..bb0323ed9ae781 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 00000000000000..f9b5c90e3c366b --- /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 65db13f22788b5..f326d365351f20 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 d003d4c581699f..a134377e194b8b 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 88be86d1ecc308..6897f07c45e2bb 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 a89b952702e1b5..b9c6f8e7591e34 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 69e4d443cf0402..9fc3d4329879eb 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 a81ffd754946bf..8276e8b49d3483 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 754bc7bc75cb53..a0d8ff08525447 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 8c02fdd69de8bd..ec4e54632eef55 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 355536000326e7..fae66b26a83e0c 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 15432d0cbd1474..72b42143a24f70 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 06aa2434afc3f4..e383f21143149c 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 fdc51dc1c9c878..e7322bdc0d4084 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 85188c07eeb207..1fcd7508493312 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 dd5c5604552743..c1c88dd8a54bae 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 49c690e8c024d9..89cb4221c96b2c 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 f893951815e9ef..2fc711d4d6f07d 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 3749e4372bdab0..5b671e9f5b47e9 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 c21ef3b91fab3f..e28f50851f4d91 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 f36a7b6f73664a..edd4f71b2adacc 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 779d00442522d0..5f86a2b3bf00b9 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 71ce0b1e572f84..7b8b851f5bd729 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 b87466ca289cfd..8dc4edd2000527 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 b1309cbdeb94db..0aae8b567bcdb1 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 d323a281c06ffb..3f2f472ab06230 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 f92fbfc7013cfe..71ca0661a42a91 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 24b85220defb4c..33620bc9a00383 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 624397246656d4..a112d04f38c7b5 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 198ba63e2614db..95c3a05bbf6809 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 ec4ab0446ae5f0..287da0d2ed5ecd 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 2957bc76f46826..187c69f4a72ef7 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 50780a577af02b..932ebfdd22bbca 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 f2785bce10964a..d6996d2caf1bcf 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 559726e0b8a993..6ec35db5caec66 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 2d5ef9fdd768d0..96357dc915eef5 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 213bea3bc3eec3..1211d4c2cf1c33 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 ed2abef2542deb..fc2dce441c6214 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 100d09a2da7e41..8f26579726ff1c 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 aa06d3f696d000..daacc065629a47 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 0dfb65aa6f8577..032593d5e3b316 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 7aa0032afba138..84733b0ccfd954 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 642e86059ed6e6..c8d9b46d5a0d28 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 {