diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture
index 9a49c19b94df2..33ecfcd84fd3e 100644
--- a/.ci/Jenkinsfile_baseline_capture
+++ b/.ci/Jenkinsfile_baseline_capture
@@ -11,14 +11,14 @@ kibanaPipeline(timeoutMinutes: 120) {
'CI_PARALLEL_PROCESS_NUMBER=1'
]) {
parallel([
- 'oss-visualRegression': {
- workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) {
- kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')()
+ 'oss-baseline': {
+ workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) {
+ kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')()
}
},
- 'xpack-visualRegression': {
- workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) {
- kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')()
+ 'xpack-baseline': {
+ workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) {
+ kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')()
}
},
])
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index c9962d9976e2d..7daa42af7024d 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -163,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
@@ -282,7 +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
-/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
diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc
index e00a67f6c78a4..b4c9c6a4ec39e 100644
--- a/docs/apm/troubleshooting.asciidoc
+++ b/docs/apm/troubleshooting.asciidoc
@@ -49,7 +49,7 @@ GET /_template/apm-{version}
*Using Logstash, Kafka, etc.*
If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka),
then the index template will not be set up automatically. Instead, you'll need to
-{apm-server-ref}/configuration-template.html[load the template manually].
+{apm-server-ref}/apm-server-template.html[load the template manually].
*Using a custom index names*
This problem can also occur if you've customized the index name that you write APM data to.
diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc
index 9c8d753a2d668..3489dcd018293 100644
--- a/docs/settings/reporting-settings.asciidoc
+++ b/docs/settings/reporting-settings.asciidoc
@@ -104,15 +104,14 @@ security is enabled, `xpack.security.encryptionKey`.
[cols="2*<"]
|===
| `xpack.reporting.queue.pollInterval`
- | Specifies the number of milliseconds that the reporting poller waits between polling the
- index for any pending Reporting jobs. Defaults to `3000` (3 seconds).
+ | Specify the {ref}/common-options.html#time-units[time] that the reporting poller waits between polling the index for any
+ pending Reporting jobs. Can be specified as number of milliseconds. Defaults to `3s`.
| [[xpack-reporting-q-timeout]] `xpack.reporting.queue.timeout` {ess-icon}
- | How long each worker has to produce a report. If your machine is slow or under
- heavy load, you might need to increase this timeout. Specified in milliseconds.
- If a Reporting job execution time goes over this time limit, the job will be
- marked as a failure and there will not be a download available.
- Defaults to `120000` (two minutes).
+ | {ref}/common-options.html#time-units[How long] each worker has to produce a report. If your machine is slow or under heavy
+ load, you might need to increase this timeout. If a Reporting job execution goes over this time limit, the job is marked as a
+ failure and no download will be available. Can be specified as number of milliseconds.
+ Defaults to `2m`.
|===
@@ -127,24 +126,24 @@ control the capturing process.
|===
a| `xpack.reporting.capture.timeouts`
`.openUrl` {ess-icon}
- | Specify how long to allow the Reporting browser to wait for the "Loading..." screen
- to dismiss and find the initial data for the Kibana page. If the time is
- exceeded, a page screenshot is captured showing the current state, and the download link shows a warning message.
- Defaults to `60000` (1 minute).
+ | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for the "Loading..." screen
+ to dismiss and find the initial data for the page. If the time is exceeded, a screenshot is captured showing the current
+ page, and the download link shows a warning message. Can be specified as number of milliseconds.
+ Defaults to `1m`.
a| `xpack.reporting.capture.timeouts`
`.waitForElements` {ess-icon}
- | Specify how long to allow the Reporting browser to wait for all visualization
- panels to load on the Kibana page. If the time is exceeded, a page screenshot
- is captured showing the current state, and the download link shows a warning message. Defaults to `30000` (30
- seconds).
+ | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualization panels
+ to load on the page. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows
+ a warning message. Can be specified as number of milliseconds.
+ Defaults to `30s`.
a| `xpack.reporting.capture.timeouts`
`.renderComplete` {ess-icon}
- | Specify how long to allow the Reporting browser to wait for all visualizations to
- fetch and render the data. If the time is exceeded, a
- page screenshot is captured showing the current state, and the download link shows a warning message. Defaults to
- `30000` (30 seconds).
+ | Specify the {ref}/common-options.html#time-units[time] to allow the Reporting browser to wait for all visualizations to
+ fetch and render the data. If the time is exceeded, a screenshot is captured showing the current page, and the download link shows a
+ warning message. Can be specified as number of milliseconds.
+ Defaults to `30s`.
|===
@@ -163,11 +162,10 @@ available, but there will likely be errors in the visualizations in the report.
job, as many times as this setting. Defaults to `3`.
| `xpack.reporting.capture.loadDelay`
- | When visualizations are not evented, this is the amount of time before
- taking a screenshot. All visualizations that ship with {kib} are evented, so this
- setting should not have much effect. If you are seeing empty images instead of
- visualizations, try increasing this value.
- Defaults to `3000` (3 seconds).
+ | Specify the {ref}/common-options.html#time-units[amount of time] before taking a screenshot when visualizations are not evented.
+ All visualizations that ship with {kib} are evented, so this setting should not have much effect. If you are seeing empty images
+ instead of visualizations, try increasing this value.
+ Defaults to `3s`.
| [[xpack-reporting-browser]] `xpack.reporting.capture.browser.type` {ess-icon}
| Specifies the browser to use to capture screenshots. This setting exists for
@@ -213,9 +211,9 @@ a| `xpack.reporting.capture.browser`
[cols="2*<"]
|===
| [[xpack-reporting-csv]] `xpack.reporting.csv.maxSizeBytes` {ess-icon}
- | The maximum size of a CSV file before being truncated. This setting exists to prevent
- large exports from causing performance and storage issues.
- Defaults to `10485760` (10mB).
+ | The maximum {ref}/common-options.html#byte-units[byte size] of a CSV file before being truncated. This setting exists to
+ prevent large exports from causing performance and storage issues. Can be specified as number of bytes.
+ Defaults to `10mb`.
| `xpack.reporting.csv.scroll.size`
| Number of documents retrieved from {es} for each scroll iteration during a CSV
@@ -223,7 +221,7 @@ a| `xpack.reporting.capture.browser`
Defaults to `500`.
| `xpack.reporting.csv.scroll.duration`
- | Amount of time allowed before {kib} cleans the scroll context during a CSV export.
+ | Amount of {ref}/common-options.html#time-units[time] allowed before {kib} cleans the scroll context during a CSV export.
Defaults to `30s`.
| `xpack.reporting.csv.checkForFormulas`
diff --git a/docs/user/alerting/action-types/server-log.asciidoc b/docs/user/alerting/action-types/server-log.asciidoc
index eadca229bc19c..7022320328c85 100644
--- a/docs/user/alerting/action-types/server-log.asciidoc
+++ b/docs/user/alerting/action-types/server-log.asciidoc
@@ -2,7 +2,7 @@
[[server-log-action-type]]
=== Server log action
-This action type writes and entry to the {kib} server log.
+This action type writes an entry to the {kib} server log.
[float]
[[server-log-connector-configuration]]
diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc
index 16f82477756b7..4919625340da2 100644
--- a/docs/user/dashboard/url-drilldown.asciidoc
+++ b/docs/user/dashboard/url-drilldown.asciidoc
@@ -197,6 +197,7 @@ context.panel.timeRange.indexPatternIds
| ID of saved object behind a panel.
| *Single click*
+
| event.value
| Value behind clicked data point.
@@ -208,6 +209,22 @@ context.panel.timeRange.indexPatternIds
| event.negate
| Boolean, indicating whether clicked data point resulted in negative filter.
+|
+| event.points
+| Some visualizations have clickable points that emit more than one data point. Use list of data points in case a single value is insufficient. +
+
+Example:
+
+`{{json event.points}}` +
+`{{event.points.[0].key}}` +
+`{{event.points.[0].value}}`
+`{{#each event.points}}key=value&{{/each}}`
+
+Note:
+
+`{{event.value}}` is a shorthand for `{{event.points.[0].value}}` +
+`{{event.key}}` is a shorthand for `{{event.points.[0].key}}`
+
| *Range selection*
| event.from +
event.to
diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts
index 362c34d416743..19487efe1366c 100644
--- a/src/dev/build/tasks/os_packages/docker_generator/run.ts
+++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts
@@ -40,7 +40,7 @@ export async function runDockerGenerator(
ubi: boolean = false
) {
// UBI var config
- const baseOSImage = ubi ? 'registry.access.redhat.com/ubi8/ubi-minimal:latest' : 'centos:8';
+ const baseOSImage = ubi ? 'docker.elastic.co/ubi8/ubi-minimal:latest' : 'centos:8';
const ubiVersionTag = 'ubi8';
const ubiImageFlavor = ubi ? `-${ubiVersionTag}` : '';
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
index f159cac664a9e..8e1151b387fee 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx
@@ -546,13 +546,16 @@ export class QueryStringInputUI extends Component {
this.updateSuggestions.cancel();
this.componentIsUnmounting = true;
window.removeEventListener('resize', this.handleAutoHeight);
- window.removeEventListener('scroll', this.handleListUpdate);
+ window.removeEventListener('scroll', this.handleListUpdate, { capture: true });
}
- handleListUpdate = () =>
- this.setState({
+ handleListUpdate = () => {
+ if (this.componentIsUnmounting) return;
+
+ return this.setState({
queryBarRect: this.queryBarInputDivRefInstance.current?.getBoundingClientRect(),
});
+ };
handleAutoHeight = () => {
if (this.inputRef !== null && document.activeElement === this.inputRef) {
diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
index 7a42ed7fad427..b175066b81c8e 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
+++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import React from 'react';
+import React, { useState, useEffect } from 'react';
import { act } from 'react-dom/test-utils';
import { mount, ReactWrapper } from 'enzyme';
import sinon from 'sinon';
@@ -111,6 +111,13 @@ export const createUseRequestHelpers = (): UseRequestHelpers => {
requestConfig
);
+ // Force a re-render of the component to stress-test the useRequest hook and verify its
+ // state remains unaffected.
+ const [, setState] = useState(false);
+ useEffect(() => {
+ setState(true);
+ }, []);
+
hookResult.isInitialRequest = isInitialRequest;
hookResult.isLoading = isLoading;
hookResult.error = error;
diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts
index e04f84a67b8a3..9d40291423cac 100644
--- a/src/plugins/es_ui_shared/public/request/use_request.ts
+++ b/src/plugins/es_ui_shared/public/request/use_request.ts
@@ -49,7 +49,7 @@ export const useRequest = (
// Consumers can use isInitialRequest to implement a polling UX.
const requestCountRef = useRef(0);
- const isInitialRequest = requestCountRef.current === 0;
+ const isInitialRequestRef = useRef(true);
const pollIntervalIdRef = useRef(null);
const clearPollInterval = useCallback(() => {
@@ -98,6 +98,9 @@ export const useRequest = (
return;
}
+ // Surface to consumers that at least one request has resolved.
+ isInitialRequestRef.current = false;
+
setError(responseError);
// If there's an error, keep the data from the last request in case it's still useful to the user.
if (!responseError) {
@@ -146,7 +149,7 @@ export const useRequest = (
}, [clearPollInterval]);
return {
- isInitialRequest,
+ isInitialRequest: isInitialRequestRef.current,
isLoading,
error,
data,
diff --git a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
index aa1de4b2443a4..dd6953ebcda99 100644
--- a/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
+++ b/src/plugins/telemetry/server/routes/telemetry_opt_in.ts
@@ -26,6 +26,7 @@ import {
StatsGetterConfig,
TelemetryCollectionManagerPluginSetup,
} from 'src/plugins/telemetry_collection_manager/server';
+import { SavedObjectsErrorHelpers } from '../../../../core/server';
import { getTelemetryAllowChangingOptInStatus } from '../../common/telemetry_config';
import { sendTelemetryOptInStatus } from './telemetry_opt_in_stats';
@@ -109,7 +110,13 @@ export function registerTelemetryOptInRoutes({
});
}
- await updateTelemetrySavedObject(context.core.savedObjects.client, attributes);
+ try {
+ await updateTelemetrySavedObject(context.core.savedObjects.client, attributes);
+ } catch (e) {
+ if (SavedObjectsErrorHelpers.isForbiddenError(e)) {
+ return res.forbidden();
+ }
+ }
return res.ok({ body: optInStatus });
}
);
diff --git a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
index e63ff28f42d96..f1aff6322522a 100644
--- a/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
+++ b/src/plugins/ui_actions/public/triggers/value_click_trigger.ts
@@ -27,6 +27,6 @@ export const valueClickTrigger: Trigger<'VALUE_CLICK_TRIGGER'> = {
defaultMessage: 'Single click',
}),
description: i18n.translate('uiActions.triggers.valueClickDescription', {
- defaultMessage: 'A single point on the visualization',
+ defaultMessage: 'A data point click on the visualization',
}),
};
diff --git a/src/plugins/vis_type_timeseries/server/index.ts b/src/plugins/vis_type_timeseries/server/index.ts
index f460257caf5e3..333ed0ff64fdb 100644
--- a/src/plugins/vis_type_timeseries/server/index.ts
+++ b/src/plugins/vis_type_timeseries/server/index.ts
@@ -21,7 +21,7 @@ import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/serve
import { VisTypeTimeseriesConfig, config as configSchema } from './config';
import { VisTypeTimeseriesPlugin } from './plugin';
-export { VisTypeTimeseriesSetup, Framework } from './plugin';
+export { VisTypeTimeseriesSetup } from './plugin';
export const config: PluginConfigDescriptor = {
deprecations: ({ unused, renameFromRoot }) => [
@@ -39,10 +39,10 @@ export const config: PluginConfigDescriptor = {
export { ValidationTelemetryServiceSetup } from './validation_telemetry';
-// @ts-ignore
-export { AbstractSearchStrategy } from './lib/search_strategies/strategies/abstract_search_strategy';
-// @ts-ignore
-export { AbstractSearchRequest } from './lib/search_strategies/search_requests/abstract_request';
+export {
+ AbstractSearchStrategy,
+ ReqFacade,
+} from './lib/search_strategies/strategies/abstract_search_strategy';
// @ts-ignore
export { DefaultSearchCapabilities } from './lib/search_strategies/default_search_capabilities';
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
index 0f0d99bff6f1c..777de89672bbe 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts
@@ -38,6 +38,7 @@ export async function getFields(
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
+ requestContext,
...request,
framework,
payload: {},
@@ -48,22 +49,6 @@ export async function getFields(
},
getUiSettingsService: () => requestContext.core.uiSettings.client,
getSavedObjectsClient: () => requestContext.core.savedObjects.client,
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- return {
- callWithRequest: async (req: any, endpoint: string, params: any) => {
- return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(
- endpoint,
- params
- );
- },
- };
- },
- },
- },
- },
getEsShardTimeout: async () => {
return await framework.globalConfig$
.pipe(
diff --git a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
index f697e754a2e00..5eef2b53e2431 100644
--- a/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/get_vis_data.ts
@@ -21,7 +21,7 @@ import { FakeRequest, RequestHandlerContext } from 'kibana/server';
import _ from 'lodash';
import { first, map } from 'rxjs/operators';
import { getPanelData } from './vis_data/get_panel_data';
-import { Framework } from '../index';
+import { Framework } from '../plugin';
import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy';
interface GetVisDataResponse {
@@ -65,28 +65,13 @@ export function getVisData(
// level object passed from here. The layers should be refactored fully at some point, but for now
// this works and we are still using the New Platform services for these vis data portions.
const reqFacade: ReqFacade = {
+ requestContext,
...request,
framework,
pre: {},
payload: request.body,
getUiSettingsService: () => requestContext.core.uiSettings.client,
getSavedObjectsClient: () => requestContext.core.savedObjects.client,
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- return {
- callWithRequest: async (req: any, endpoint: string, params: any) => {
- return await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser(
- endpoint,
- params
- );
- },
- };
- },
- },
- },
- },
getEsShardTimeout: async () => {
return await framework.globalConfig$
.pipe(
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js
deleted file mode 100644
index abd2a4c65d35c..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.js
+++ /dev/null
@@ -1,28 +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 class AbstractSearchRequest {
- constructor(req, callWithRequest) {
- this.req = req;
- this.callWithRequest = callWithRequest;
- }
-
- search() {
- throw new Error('AbstractSearchRequest: search method should be defined');
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js
deleted file mode 100644
index 6f71aa63728d5..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/abstract_request.test.js
+++ /dev/null
@@ -1,46 +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 { AbstractSearchRequest } from './abstract_request';
-
-describe('AbstractSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
-
- beforeEach(() => {
- req = {};
- callWithRequest = jest.fn();
- searchRequest = new AbstractSearchRequest(req, callWithRequest);
- });
-
- test('should init an AbstractSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should throw an error trying to search', () => {
- try {
- searchRequest.search();
- } catch (error) {
- expect(error instanceof Error).toBe(true);
- expect(error.message).toEqual('AbstractSearchRequest: search method should be defined');
- }
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js
deleted file mode 100644
index 9ada39e359589..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.js
+++ /dev/null
@@ -1,49 +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 { AbstractSearchRequest } from './abstract_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-const SEARCH_METHOD = 'msearch';
-
-export class MultiSearchRequest extends AbstractSearchRequest {
- async search(searches) {
- const includeFrozen = await this.req
- .getUiSettingsService()
- .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- const multiSearchBody = searches.reduce(
- (acc, { body, index }) => [
- ...acc,
- {
- index,
- ignoreUnavailable: true,
- },
- body,
- ],
- []
- );
-
- const { responses } = await this.callWithRequest(this.req, SEARCH_METHOD, {
- body: multiSearchBody,
- rest_total_hits_as_int: true,
- ignore_throttled: !includeFrozen,
- });
-
- return responses;
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js
deleted file mode 100644
index c113db76332b7..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/multi_search_request.test.js
+++ /dev/null
@@ -1,67 +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 { MultiSearchRequest } from './multi_search_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-describe('MultiSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({ responses: [] });
- searchRequest = new MultiSearchRequest(req, callWithRequest);
- });
-
- test('should init an MultiSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should get the response from elastic msearch', async () => {
- const searches = [
- { body: 'body1', index: 'index' },
- { body: 'body2', index: 'index' },
- ];
-
- const responses = await searchRequest.search(searches);
-
- expect(responses).toEqual([]);
- expect(req.getUiSettingsService).toHaveBeenCalled();
- expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- expect(callWithRequest).toHaveBeenCalledWith(req, 'msearch', {
- body: [
- { ignoreUnavailable: true, index: 'index' },
- 'body1',
- { ignoreUnavailable: true, index: 'index' },
- 'body2',
- ],
- rest_total_hits_as_int: true,
- ignore_throttled: !includeFrozen,
- });
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js
deleted file mode 100644
index e6e3bcb527286..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.js
+++ /dev/null
@@ -1,37 +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 { AbstractSearchRequest } from './abstract_request';
-
-import { MultiSearchRequest } from './multi_search_request';
-import { SingleSearchRequest } from './single_search_request';
-
-export class SearchRequest extends AbstractSearchRequest {
- getSearchRequestType(searches) {
- const isMultiSearch = Array.isArray(searches) && searches.length > 1;
- const SearchRequest = isMultiSearch ? MultiSearchRequest : SingleSearchRequest;
-
- return new SearchRequest(this.req, this.callWithRequest);
- }
-
- async search(options) {
- const concreteSearchRequest = this.getSearchRequestType(options);
-
- return concreteSearchRequest.search(options);
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js
deleted file mode 100644
index 3d35a4aa37c5a..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/search_request.test.js
+++ /dev/null
@@ -1,76 +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 { SearchRequest } from './search_request';
-import { MultiSearchRequest } from './multi_search_request';
-import { SingleSearchRequest } from './single_search_request';
-
-describe('SearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({ responses: [] });
- searchRequest = new SearchRequest(req, callWithRequest);
- });
-
- test('should init an AbstractSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should return search value', async () => {
- const concreteSearchRequest = {
- search: jest.fn().mockReturnValue('concreteSearchRequest'),
- };
- const options = {};
- searchRequest.getSearchRequestType = jest.fn().mockReturnValue(concreteSearchRequest);
-
- const result = await searchRequest.search(options);
-
- expect(result).toBe('concreteSearchRequest');
- });
-
- test('should return a MultiSearchRequest for multi searches', () => {
- const searches = [
- { index: 'index', body: 'body' },
- { index: 'index', body: 'body' },
- ];
-
- const result = searchRequest.getSearchRequestType(searches);
-
- expect(result instanceof MultiSearchRequest).toBe(true);
- });
-
- test('should return a SingleSearchRequest for single search', () => {
- const searches = [{ index: 'index', body: 'body' }];
-
- const result = searchRequest.getSearchRequestType(searches);
-
- expect(result instanceof SingleSearchRequest).toBe(true);
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js
deleted file mode 100644
index 7d8b60a7e4595..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.js
+++ /dev/null
@@ -1,37 +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 { AbstractSearchRequest } from './abstract_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-const SEARCH_METHOD = 'search';
-
-export class SingleSearchRequest extends AbstractSearchRequest {
- async search([{ body, index }]) {
- const includeFrozen = await this.req
- .getUiSettingsService()
- .get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- const resp = await this.callWithRequest(this.req, SEARCH_METHOD, {
- ignore_throttled: !includeFrozen,
- body,
- index,
- });
-
- return [resp];
- }
-}
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js
deleted file mode 100644
index b899814f2fe13..0000000000000
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_requests/single_search_request.test.js
+++ /dev/null
@@ -1,59 +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 { SingleSearchRequest } from './single_search_request';
-import { UI_SETTINGS } from '../../../../../data/server';
-
-describe('SingleSearchRequest', () => {
- let searchRequest;
- let req;
- let callWithRequest;
- let getServiceMock;
- let includeFrozen;
-
- beforeEach(() => {
- includeFrozen = false;
- getServiceMock = jest.fn().mockResolvedValue(includeFrozen);
- req = {
- getUiSettingsService: jest.fn().mockReturnValue({ get: getServiceMock }),
- };
- callWithRequest = jest.fn().mockReturnValue({});
- searchRequest = new SingleSearchRequest(req, callWithRequest);
- });
-
- test('should init an SingleSearchRequest instance', () => {
- expect(searchRequest.req).toBe(req);
- expect(searchRequest.callWithRequest).toBe(callWithRequest);
- expect(searchRequest.search).toBeDefined();
- });
-
- test('should get the response from elastic search', async () => {
- const searches = [{ body: 'body', index: 'index' }];
-
- const responses = await searchRequest.search(searches);
-
- expect(responses).toEqual([{}]);
- expect(req.getUiSettingsService).toHaveBeenCalled();
- expect(getServiceMock).toHaveBeenCalledWith(UI_SETTINGS.SEARCH_INCLUDE_FROZEN);
- expect(callWithRequest).toHaveBeenCalledWith(req, 'search', {
- body: 'body',
- index: 'index',
- ignore_throttled: !includeFrozen,
- });
- });
-});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
index ecd09653b3b48..66ea4f017dd90 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/search_strategies_registry.test.ts
@@ -65,7 +65,7 @@ describe('SearchStrategyRegister', () => {
});
test('should add a strategy if it is an instance of AbstractSearchStrategy', () => {
- const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {});
+ const anotherSearchStrategy = new MockSearchStrategy('es');
const addedStrategies = registry.addStrategy(anotherSearchStrategy);
expect(addedStrategies.length).toEqual(2);
@@ -75,7 +75,7 @@ describe('SearchStrategyRegister', () => {
test('should return a MockSearchStrategy instance', async () => {
const req = {};
const indexPattern = '*';
- const anotherSearchStrategy = new MockSearchStrategy({}, {} as any, {});
+ const anotherSearchStrategy = new MockSearchStrategy('es');
registry.addStrategy(anotherSearchStrategy);
const { searchStrategy, capabilities } = (await registry.getViableStrategy(req, indexPattern))!;
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
index 1fbaffd794c89..6773ee482b098 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js
@@ -18,24 +18,13 @@
*/
import { AbstractSearchStrategy } from './abstract_search_strategy';
-class SearchRequest {
- constructor(req, callWithRequest) {
- this.req = req;
- this.callWithRequest = callWithRequest;
- }
-}
-
describe('AbstractSearchStrategy', () => {
let abstractSearchStrategy;
- let server;
- let callWithRequestFactory;
let req;
let mockedFields;
let indexPattern;
beforeEach(() => {
- server = {};
- callWithRequestFactory = jest.fn().mockReturnValue('callWithRequest');
mockedFields = {};
req = {
pre: {
@@ -45,16 +34,11 @@ describe('AbstractSearchStrategy', () => {
},
};
- abstractSearchStrategy = new AbstractSearchStrategy(
- server,
- callWithRequestFactory,
- SearchRequest
- );
+ abstractSearchStrategy = new AbstractSearchStrategy('es');
});
test('should init an AbstractSearchStrategy instance', () => {
- expect(abstractSearchStrategy.getCallWithRequestInstance).toBeDefined();
- expect(abstractSearchStrategy.getSearchRequest).toBeDefined();
+ expect(abstractSearchStrategy.search).toBeDefined();
expect(abstractSearchStrategy.getFieldsForWildcard).toBeDefined();
expect(abstractSearchStrategy.checkForViability).toBeDefined();
});
@@ -68,17 +52,46 @@ describe('AbstractSearchStrategy', () => {
});
});
- test('should invoke callWithRequestFactory with req param passed', () => {
- abstractSearchStrategy.getCallWithRequestInstance(req);
+ test('should return response', async () => {
+ const searches = [{ body: 'body', index: 'index' }];
+ const searchFn = jest.fn().mockReturnValue(Promise.resolve({}));
- expect(callWithRequestFactory).toHaveBeenCalledWith(server, req);
- });
-
- test('should return a search request', () => {
- const searchRequest = abstractSearchStrategy.getSearchRequest(req);
+ const responses = await abstractSearchStrategy.search(
+ {
+ requestContext: {},
+ framework: {
+ core: {
+ getStartServices: jest.fn().mockReturnValue(
+ Promise.resolve([
+ {},
+ {
+ data: {
+ search: {
+ search: searchFn,
+ },
+ },
+ },
+ ])
+ ),
+ },
+ },
+ },
+ searches
+ );
- expect(searchRequest instanceof SearchRequest).toBe(true);
- expect(searchRequest.callWithRequest).toBe('callWithRequest');
- expect(searchRequest.req).toBe(req);
+ expect(responses).toEqual([{}]);
+ expect(searchFn).toHaveBeenCalledWith(
+ {},
+ {
+ params: {
+ body: 'body',
+ index: 'index',
+ },
+ indexType: undefined,
+ },
+ {
+ strategy: 'es',
+ }
+ );
});
});
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
index 0b1c6e6e20414..92b7e6976962e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts
@@ -18,7 +18,7 @@
*/
import {
- LegacyAPICaller,
+ RequestHandlerContext,
FakeRequest,
IUiSettingsClient,
SavedObjectsClientContract,
@@ -33,6 +33,7 @@ import { IndexPatternsFetcher } from '../../../../../data/server';
* This will be replaced by standard KibanaRequest and RequestContext objects in a later version.
*/
export type ReqFacade = FakeRequest & {
+ requestContext: RequestHandlerContext;
framework: Framework;
payload: unknown;
pre: {
@@ -40,34 +41,42 @@ export type ReqFacade = FakeRequest & {
};
getUiSettingsService: () => IUiSettingsClient;
getSavedObjectsClient: () => SavedObjectsClientContract;
- server: {
- plugins: {
- elasticsearch: {
- getCluster: () => {
- callWithRequest: (req: ReqFacade, endpoint: string, params: any) => Promise;
- };
- };
- };
- };
getEsShardTimeout: () => Promise;
};
export class AbstractSearchStrategy {
- public getCallWithRequestInstance: (req: ReqFacade) => LegacyAPICaller;
- public getSearchRequest: (req: ReqFacade) => any;
-
- constructor(
- server: any,
- callWithRequestFactory: (server: any, req: ReqFacade) => LegacyAPICaller,
- SearchRequest: any
- ) {
- this.getCallWithRequestInstance = (req) => callWithRequestFactory(server, req);
+ public searchStrategyName!: string;
+ public indexType?: string;
+ public additionalParams: any;
- this.getSearchRequest = (req) => {
- const callWithRequest = this.getCallWithRequestInstance(req);
+ constructor(name: string, type?: string, additionalParams: any = {}) {
+ this.searchStrategyName = name;
+ this.indexType = type;
+ this.additionalParams = additionalParams;
+ }
- return new SearchRequest(req, callWithRequest);
- };
+ async search(req: ReqFacade, bodies: any[], options = {}) {
+ const [, deps] = await req.framework.core.getStartServices();
+ const requests: any[] = [];
+ bodies.forEach((body) => {
+ requests.push(
+ deps.data.search.search(
+ req.requestContext,
+ {
+ params: {
+ ...body,
+ ...this.additionalParams,
+ },
+ indexType: this.indexType,
+ },
+ {
+ ...options,
+ strategy: this.searchStrategyName,
+ }
+ )
+ );
+ });
+ return Promise.all(requests);
}
async getFieldsForWildcard(req: ReqFacade, indexPattern: string, capabilities: any) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
index 63f2911ce1118..7c3609ae3c405 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.js
@@ -16,21 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
+
+import { ES_SEARCH_STRATEGY } from '../../../../../data/server';
import { AbstractSearchStrategy } from './abstract_search_strategy';
-import { SearchRequest } from '../search_requests/search_request';
import { DefaultSearchCapabilities } from '../default_search_capabilities';
-const callWithRequestFactory = (server, request) => {
- const { callWithRequest } = request.server.plugins.elasticsearch.getCluster('data');
-
- return callWithRequest;
-};
-
export class DefaultSearchStrategy extends AbstractSearchStrategy {
name = 'default';
- constructor(server) {
- super(server, callWithRequestFactory, SearchRequest);
+ constructor() {
+ super(ES_SEARCH_STRATEGY);
}
checkForViability(req) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
index 2e3a459bf06fd..a9994ba3e1f75 100644
--- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
+++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/default_search_strategy.test.js
@@ -20,42 +20,20 @@ import { DefaultSearchStrategy } from './default_search_strategy';
describe('DefaultSearchStrategy', () => {
let defaultSearchStrategy;
- let server;
- let callWithRequest;
let req;
beforeEach(() => {
- server = {};
- callWithRequest = jest.fn();
- req = {
- server: {
- plugins: {
- elasticsearch: {
- getCluster: jest.fn().mockReturnValue({
- callWithRequest,
- }),
- },
- },
- },
- };
- defaultSearchStrategy = new DefaultSearchStrategy(server);
+ req = {};
+ defaultSearchStrategy = new DefaultSearchStrategy();
});
test('should init an DefaultSearchStrategy instance', () => {
expect(defaultSearchStrategy.name).toBe('default');
expect(defaultSearchStrategy.checkForViability).toBeDefined();
- expect(defaultSearchStrategy.getCallWithRequestInstance).toBeDefined();
- expect(defaultSearchStrategy.getSearchRequest).toBeDefined();
+ expect(defaultSearchStrategy.search).toBeDefined();
expect(defaultSearchStrategy.getFieldsForWildcard).toBeDefined();
});
- test('should invoke callWithRequestFactory with passed params', () => {
- const value = defaultSearchStrategy.getCallWithRequestInstance(req);
-
- expect(value).toBe(callWithRequest);
- expect(req.server.plugins.elasticsearch.getCluster).toHaveBeenCalledWith('data');
- });
-
test('should check a strategy for viability', () => {
const value = defaultSearchStrategy.checkForViability(req);
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
index b015aaf0ef8db..d8a230dfeef4e 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_annotations.js
@@ -39,7 +39,6 @@ export async function getAnnotations({
capabilities,
series,
}) {
- const searchRequest = searchStrategy.getSearchRequest(req);
const annotations = panel.annotations.filter(validAnnotation);
const lastSeriesTimestamp = getLastSeriesTimestamp(series);
const handleAnnotationResponseBy = handleAnnotationResponse(lastSeriesTimestamp);
@@ -47,6 +46,7 @@ export async function getAnnotations({
const bodiesPromises = annotations.map((annotation) =>
getAnnotationRequestParams(req, panel, annotation, esQueryConfig, capabilities)
);
+
const searches = (await Promise.all(bodiesPromises)).reduce(
(acc, items) => acc.concat(items),
[]
@@ -55,10 +55,10 @@ export async function getAnnotations({
if (!searches.length) return { responses: [] };
try {
- const data = await searchRequest.search(searches);
+ const data = await searchStrategy.search(req.framework.core, req.requestContext, searches);
return annotations.reduce((acc, annotation, index) => {
- acc[annotation.id] = handleAnnotationResponseBy(data[index], annotation);
+ acc[annotation.id] = handleAnnotationResponseBy(data[index].rawResponse, annotation);
return acc;
}, {});
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
index ee48816c6a8af..1eace13c2e336 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_series_data.js
@@ -28,7 +28,6 @@ export async function getSeriesData(req, panel) {
searchStrategy,
capabilities,
} = await req.framework.searchStrategyRegistry.getViableStrategyForPanel(req, panel);
- const searchRequest = searchStrategy.getSearchRequest(req);
const esQueryConfig = await getEsQueryConfig(req);
const meta = {
type: panel.type,
@@ -45,8 +44,13 @@ export async function getSeriesData(req, panel) {
[]
);
- const data = await searchRequest.search(searches);
- const series = data.map(handleResponseBody(panel));
+ const data = await searchStrategy.search(req, searches);
+
+ const handleResponseBodyFn = handleResponseBody(panel);
+
+ const series = data.map((resp) =>
+ handleResponseBodyFn(resp.rawResponse ? resp.rawResponse : resp)
+ );
let annotations = null;
if (panel.annotations && panel.annotations.length) {
diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
index 1d1c245907959..3791eb229db5b 100644
--- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
+++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_table_data.js
@@ -30,7 +30,6 @@ export async function getTableData(req, panel) {
searchStrategy,
capabilities,
} = await req.framework.searchStrategyRegistry.getViableStrategy(req, panelIndexPattern);
- const searchRequest = searchStrategy.getSearchRequest(req);
const esQueryConfig = await getEsQueryConfig(req);
const { indexPatternObject } = await getIndexPatternObject(req, panelIndexPattern);
@@ -41,13 +40,18 @@ export async function getTableData(req, panel) {
try {
const body = buildRequestBody(req, panel, esQueryConfig, indexPatternObject, capabilities);
- const [resp] = await searchRequest.search([
+ const [resp] = await searchStrategy.search(req, [
{
body,
index: panelIndexPattern,
},
]);
- const buckets = get(resp, 'aggregations.pivot.buckets', []);
+
+ const buckets = get(
+ resp.rawResponse ? resp.rawResponse : resp,
+ 'aggregations.pivot.buckets',
+ []
+ );
return {
...meta,
diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts
index d863937a4e3dc..678ba2b371978 100644
--- a/src/plugins/vis_type_timeseries/server/plugin.ts
+++ b/src/plugins/vis_type_timeseries/server/plugin.ts
@@ -33,6 +33,7 @@ import { VisTypeTimeseriesConfig } from './config';
import { getVisData, GetVisData, GetVisDataOptions } from './lib/get_vis_data';
import { ValidationTelemetryService } from './validation_telemetry';
import { UsageCollectionSetup } from '../../usage_collection/server';
+import { PluginStart } from '../../data/server';
import { visDataRoutes } from './routes/vis';
// @ts-ignore
import { fieldsRoutes } from './routes/fields';
@@ -47,6 +48,10 @@ interface VisTypeTimeseriesPluginSetupDependencies {
usageCollection?: UsageCollectionSetup;
}
+interface VisTypeTimeseriesPluginStartDependencies {
+ data: PluginStart;
+}
+
export interface VisTypeTimeseriesSetup {
getVisData: (
requestContext: RequestHandlerContext,
@@ -57,7 +62,7 @@ export interface VisTypeTimeseriesSetup {
}
export interface Framework {
- core: CoreSetup;
+ core: CoreSetup;
plugins: any;
config$: Observable;
globalConfig$: PluginInitializerContext['config']['legacy']['globalConfig$'];
@@ -74,7 +79,10 @@ export class VisTypeTimeseriesPlugin implements Plugin {
this.validationTelementryService = new ValidationTelemetryService();
}
- public setup(core: CoreSetup, plugins: VisTypeTimeseriesPluginSetupDependencies) {
+ public setup(
+ core: CoreSetup,
+ plugins: VisTypeTimeseriesPluginSetupDependencies
+ ) {
const logger = this.initializerContext.logger.get('visTypeTimeseries');
core.uiSettings.register(uiSettings);
const config$ = this.initializerContext.config.create();
diff --git a/src/plugins/vis_type_timeseries/server/routes/vis.ts b/src/plugins/vis_type_timeseries/server/routes/vis.ts
index 48efd4398e4d4..1ca8b57ab230f 100644
--- a/src/plugins/vis_type_timeseries/server/routes/vis.ts
+++ b/src/plugins/vis_type_timeseries/server/routes/vis.ts
@@ -21,7 +21,8 @@ import { IRouter, KibanaRequest } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import { getVisData, GetVisDataOptions } from '../lib/get_vis_data';
import { visPayloadSchema } from '../../common/vis_schema';
-import { Framework, ValidationTelemetryServiceSetup } from '../index';
+import { ValidationTelemetryServiceSetup } from '../index';
+import { Framework } from '../plugin';
const escapeHatch = schema.object({}, { unknowns: 'allow' });
diff --git a/test/scripts/jenkins_visual_regression.sh b/test/scripts/jenkins_baseline.sh
similarity index 63%
rename from test/scripts/jenkins_visual_regression.sh
rename to test/scripts/jenkins_baseline.sh
index 17345d4301882..e679ac7f31bd1 100755
--- a/test/scripts/jenkins_visual_regression.sh
+++ b/test/scripts/jenkins_baseline.sh
@@ -9,10 +9,3 @@ linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
-
-echo " -> running visual regression tests from kibana directory"
-yarn percy exec -t 10000 -- -- \
- node scripts/functional_tests \
- --debug --bail \
- --kibana-install-dir "$installDir" \
- --config test/visual_regression/config.ts;
diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_baseline.sh
similarity index 64%
rename from test/scripts/jenkins_xpack_visual_regression.sh
rename to test/scripts/jenkins_xpack_baseline.sh
index 55d4a524820c5..7577b6927d166 100755
--- a/test/scripts/jenkins_xpack_visual_regression.sh
+++ b/test/scripts/jenkins_xpack_baseline.sh
@@ -14,16 +14,5 @@ tar -xzf "$linuxBuild" -C "$installDir" --strip=1
mkdir -p "$WORKSPACE/kibana-build-xpack"
cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/
-# cd "$KIBANA_DIR"
-# source "test/scripts/jenkins_xpack_page_load_metrics.sh"
-
cd "$KIBANA_DIR"
source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
-
-echo " -> running visual regression tests from x-pack directory"
-cd "$XPACK_DIR"
-yarn percy exec -t 10000 -- -- \
- node scripts/functional_tests \
- --debug --bail \
- --kibana-install-dir "$installDir" \
- --config test/visual_regression/config.ts;
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index e5b39584a519b..28eb94405abbb 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -1,4 +1,4 @@
-def withPostBuildReporting(Closure closure) {
+def withPostBuildReporting(Map params, Closure closure) {
try {
closure()
} finally {
@@ -9,8 +9,10 @@ def withPostBuildReporting(Closure closure) {
print ex
}
- catchErrors {
- runErrorReporter([pwd()] + parallelWorkspaces)
+ if (params.runErrorReporter) {
+ catchErrors {
+ runErrorReporter([pwd()] + parallelWorkspaces)
+ }
}
catchErrors {
diff --git a/vars/workers.groovy b/vars/workers.groovy
index e582e996a78b5..b6ff5b27667dd 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -118,11 +118,11 @@ def base(Map params, Closure closure) {
// Worker for ci processes. Extends the base worker and adds GCS artifact upload, error reporting, junit processing
def ci(Map params, Closure closure) {
- def config = [ramDisk: true, bootstrapped: true] + params
+ def config = [ramDisk: true, bootstrapped: true, runErrorReporter: true] + params
return base(config) {
kibanaPipeline.withGcsArtifactUpload(config.name) {
- kibanaPipeline.withPostBuildReporting {
+ kibanaPipeline.withPostBuildReporting(config) {
closure()
}
}
diff --git a/x-pack/index.js b/x-pack/index.js
index 074b8e6859dc2..745b4bd72dde8 100644
--- a/x-pack/index.js
+++ b/x-pack/index.js
@@ -5,9 +5,8 @@
*/
import { xpackMain } from './legacy/plugins/xpack_main';
-import { security } from './legacy/plugins/security';
import { spaces } from './legacy/plugins/spaces';
module.exports = function (kibana) {
- return [xpackMain(kibana), spaces(kibana), security(kibana)];
+ return [xpackMain(kibana), spaces(kibana)];
};
diff --git a/x-pack/legacy/plugins/security/index.ts b/x-pack/legacy/plugins/security/index.ts
deleted file mode 100644
index 24e63f089e702..0000000000000
--- a/x-pack/legacy/plugins/security/index.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { Root } from 'joi';
-import { resolve } from 'path';
-
-export const security = (kibana: Record) =>
- new kibana.Plugin({
- id: 'security',
- publicDir: resolve(__dirname, 'public'),
- require: [],
- configPrefix: 'xpack.security',
- config: (Joi: Root) =>
- Joi.object({ enabled: Joi.boolean().default(true) })
- .unknown()
- .default(),
- init() {},
- });
diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
index 3524d41646d50..8c233d3691c7f 100644
--- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
+++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap
@@ -52,6 +52,10 @@ exports[`Error LABEL_NAME 1`] = `undefined`;
exports[`Error LCP_FIELD 1`] = `undefined`;
+exports[`Error METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
+
+exports[`Error METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`;
+
exports[`Error METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Error METRIC_JAVA_GC_TIME 1`] = `undefined`;
@@ -220,6 +224,10 @@ exports[`Span LABEL_NAME 1`] = `undefined`;
exports[`Span LCP_FIELD 1`] = `undefined`;
+exports[`Span METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
+
+exports[`Span METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`;
+
exports[`Span METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Span METRIC_JAVA_GC_TIME 1`] = `undefined`;
@@ -388,6 +396,10 @@ exports[`Transaction LABEL_NAME 1`] = `undefined`;
exports[`Transaction LCP_FIELD 1`] = `undefined`;
+exports[`Transaction METRIC_CGROUP_MEMORY_LIMIT_BYTES 1`] = `undefined`;
+
+exports[`Transaction METRIC_CGROUP_MEMORY_USAGE_BYTES 1`] = `undefined`;
+
exports[`Transaction METRIC_JAVA_GC_COUNT 1`] = `undefined`;
exports[`Transaction METRIC_JAVA_GC_TIME 1`] = `undefined`;
diff --git a/x-pack/plugins/apm/common/alert_types.ts b/x-pack/plugins/apm/common/alert_types.ts
index a1161354e04f4..15a3c642faf32 100644
--- a/x-pack/plugins/apm/common/alert_types.ts
+++ b/x-pack/plugins/apm/common/alert_types.ts
@@ -7,42 +7,33 @@
import { i18n } from '@kbn/i18n';
export enum AlertType {
- ErrorRate = 'apm.error_rate',
+ ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
+ TransactionErrorRate = 'apm.transaction_error_rate',
TransactionDuration = 'apm.transaction_duration',
TransactionDurationAnomaly = 'apm.transaction_duration_anomaly',
}
+const THRESHOLD_MET_GROUP = {
+ id: 'threshold_met',
+ name: i18n.translate('xpack.apm.a.thresholdMet', {
+ defaultMessage: 'Threshold met',
+ }),
+};
+
export const ALERT_TYPES_CONFIG = {
- [AlertType.ErrorRate]: {
- name: i18n.translate('xpack.apm.errorRateAlert.name', {
- defaultMessage: 'Error rate',
+ [AlertType.ErrorCount]: {
+ name: i18n.translate('xpack.apm.errorCountAlert.name', {
+ defaultMessage: 'Error count threshold',
}),
- actionGroups: [
- {
- id: 'threshold_met',
- name: i18n.translate('xpack.apm.errorRateAlert.thresholdMet', {
- defaultMessage: 'Threshold met',
- }),
- },
- ],
+ actionGroups: [THRESHOLD_MET_GROUP],
defaultActionGroupId: 'threshold_met',
producer: 'apm',
},
[AlertType.TransactionDuration]: {
name: i18n.translate('xpack.apm.transactionDurationAlert.name', {
- defaultMessage: 'Transaction duration',
+ defaultMessage: 'Transaction duration threshold',
}),
- actionGroups: [
- {
- id: 'threshold_met',
- name: i18n.translate(
- 'xpack.apm.transactionDurationAlert.thresholdMet',
- {
- defaultMessage: 'Threshold met',
- }
- ),
- },
- ],
+ actionGroups: [THRESHOLD_MET_GROUP],
defaultActionGroupId: 'threshold_met',
producer: 'apm',
},
@@ -50,39 +41,24 @@ export const ALERT_TYPES_CONFIG = {
name: i18n.translate('xpack.apm.transactionDurationAnomalyAlert.name', {
defaultMessage: 'Transaction duration anomaly',
}),
- actionGroups: [
- {
- id: 'threshold_met',
- name: i18n.translate(
- 'xpack.apm.transactionDurationAlert.thresholdMet',
- {
- defaultMessage: 'Threshold met',
- }
- ),
- },
- ],
+ actionGroups: [THRESHOLD_MET_GROUP],
+ defaultActionGroupId: 'threshold_met',
+ producer: 'apm',
+ },
+ [AlertType.TransactionErrorRate]: {
+ name: i18n.translate('xpack.apm.transactionErrorRateAlert.name', {
+ defaultMessage: 'Transaction error rate threshold',
+ }),
+ actionGroups: [THRESHOLD_MET_GROUP],
defaultActionGroupId: 'threshold_met',
producer: 'apm',
},
};
-export const TRANSACTION_ALERT_AGGREGATION_TYPES = {
- avg: i18n.translate(
- 'xpack.apm.transactionDurationAlert.aggregationType.avg',
- {
- defaultMessage: 'Average',
- }
- ),
- '95th': i18n.translate(
- 'xpack.apm.transactionDurationAlert.aggregationType.95th',
- {
- defaultMessage: '95th percentile',
- }
- ),
- '99th': i18n.translate(
- 'xpack.apm.transactionDurationAlert.aggregationType.99th',
- {
- defaultMessage: '99th percentile',
- }
- ),
-};
+// Server side registrations
+// x-pack/plugins/apm/server/lib/alerts/.ts
+// x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts
+
+// Client side registrations:
+// x-pack/plugins/apm/public/components/alerting//index.tsx
+// x-pack/plugins/apm/public/components/alerting/register_apm_alerts
diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
index 612cb18bbe190..cc6a1fffb2288 100644
--- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
+++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
@@ -79,6 +79,10 @@ export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free';
export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total';
export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct';
export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct';
+export const METRIC_CGROUP_MEMORY_LIMIT_BYTES =
+ 'system.process.cgroup.memory.mem.limit.bytes';
+export const METRIC_CGROUP_MEMORY_USAGE_BYTES =
+ 'system.process.cgroup.memory.mem.usage.bytes';
export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max';
export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed';
diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx
similarity index 83%
rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx
rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx
index 632d53a9c63b6..c30cef7210a43 100644
--- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.stories.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.stories.tsx
@@ -6,14 +6,14 @@
import { storiesOf } from '@storybook/react';
import React from 'react';
-import { ErrorRateAlertTrigger } from '.';
+import { ErrorCountAlertTrigger } from '.';
import { ApmPluginContextValue } from '../../../context/ApmPluginContext';
import {
mockApmPluginContextValue,
MockApmPluginContextWrapper,
} from '../../../context/ApmPluginContext/MockApmPluginContext';
-storiesOf('app/ErrorRateAlertTrigger', module).add(
+storiesOf('app/ErrorCountAlertTrigger', module).add(
'example',
() => {
const params = {
@@ -26,7 +26,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add(
value={(mockApmPluginContextValue as unknown) as ApmPluginContextValue}
>
-
undefined}
setAlertProperty={() => undefined}
@@ -37,7 +37,7 @@ storiesOf('app/ErrorRateAlertTrigger', module).add(
},
{
info: {
- propTablesExclude: [ErrorRateAlertTrigger, MockApmPluginContextWrapper],
+ propTablesExclude: [ErrorCountAlertTrigger, MockApmPluginContextWrapper],
source: false,
},
}
diff --git a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx
similarity index 54%
rename from x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
rename to x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx
index 7b284696477f3..a465b90e7bf05 100644
--- a/x-pack/plugins/apm/public/components/shared/ErrorRateAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/ErrorCountAlertTrigger/index.tsx
@@ -3,36 +3,33 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFieldNumber, EuiSelect } from '@elastic/eui';
+
import { i18n } from '@kbn/i18n';
-import { isFinite } from 'lodash';
import React from 'react';
import { useParams } from 'react-router-dom';
import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
-import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
-import {
- ENVIRONMENT_ALL,
- getEnvironmentLabel,
-} from '../../../../common/environment_filter_values';
+import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types';
+import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import { useEnvironments } from '../../../hooks/useEnvironments';
import { useUrlParams } from '../../../hooks/useUrlParams';
+import { EnvironmentField, ServiceField, IsAboveField } from '../fields';
import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
-import { PopoverExpression } from '../ServiceAlertTrigger/PopoverExpression';
-export interface ErrorRateAlertTriggerParams {
+export interface AlertParams {
windowSize: number;
windowUnit: string;
threshold: number;
+ serviceName: string;
environment: string;
}
interface Props {
- alertParams: ErrorRateAlertTriggerParams;
+ alertParams: AlertParams;
setAlertParams: (key: string, value: any) => void;
setAlertProperty: (key: string, value: any) => void;
}
-export function ErrorRateAlertTrigger(props: Props) {
+export function ErrorCountAlertTrigger(props: Props) {
const { setAlertParams, setAlertProperty, alertParams } = props;
const { serviceName } = useParams<{ serviceName?: string }>();
const { urlParams } = useUrlParams();
@@ -51,45 +48,20 @@ export function ErrorRateAlertTrigger(props: Props) {
...alertParams,
};
- const threshold = isFinite(params.threshold) ? params.threshold : '';
-
const fields = [
-
-
- setAlertParams(
- 'environment',
- e.target.value as ErrorRateAlertTriggerParams['environment']
- )
- }
- compressed
- />
- ,
- ,
+ setAlertParams('environment', e.target.value)}
+ />,
+
-
- setAlertParams('threshold', parseInt(e.target.value, 10))
- }
- compressed
- append={i18n.translate('xpack.apm.errorRateAlertTrigger.errors', {
- defaultMessage: 'errors',
- })}
- />
- ,
+ onChange={(value) => setAlertParams('threshold', value)}
+ />,
setAlertParams('windowSize', windowSize || '')
@@ -108,7 +80,7 @@ export function ErrorRateAlertTrigger(props: Props) {
return (
void;
setAlertProperty: (key: string, value: any) => void;
}
export function TransactionDurationAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
- const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
-
const transactionTypes = useServiceTransactionTypes(urlParams);
-
- const { start, end } = urlParams;
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end, transactionType } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
- if (!transactionTypes.length) {
+ if (!transactionTypes.length || !serviceName) {
return null;
}
@@ -57,7 +77,9 @@ export function TransactionDurationAlertTrigger(props: Props) {
aggregationType: 'avg',
windowSize: 5,
windowUnit: 'm',
- transactionType: transactionTypes[0],
+
+ // use the current transaction type or default to the first in the list
+ transactionType: transactionType || transactionTypes[0],
environment: urlParams.environment || ENVIRONMENT_ALL.value,
};
@@ -67,47 +89,17 @@ export function TransactionDurationAlertTrigger(props: Props) {
};
const fields = [
-
-
- setAlertParams('environment', e.target.value as Params['environment'])
- }
- compressed
- />
- ,
-
- {
- return {
- text: key,
- value: key,
- };
- })}
- onChange={(e) =>
- setAlertParams(
- 'transactionType',
- e.target.value as Params['transactionType']
- )
- }
- compressed
- />
- ,
+ ,
+ setAlertParams('environment', e.target.value)}
+ />,
+ ({ text: key, value: key }))}
+ onChange={(e) => setAlertParams('transactionType', e.target.value)}
+ />,
- setAlertParams(
- 'aggregationType',
- e.target.value as Params['aggregationType']
- )
- }
- compressed
- />
- ,
-
- setAlertParams('threshold', e.target.value)}
- append={i18n.translate('xpack.apm.transactionDurationAlertTrigger.ms', {
- defaultMessage: 'ms',
- })}
+ onChange={(e) => setAlertParams('aggregationType', e.target.value)}
compressed
/>
,
+ setAlertParams('threshold', value)}
+ />,
setAlertParams('windowSize', timeWindowSize || '')
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx
similarity index 100%
rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx
rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx
diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx
similarity index 75%
rename from x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
rename to x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx
index 20e0a3f27c4a4..fb4cda56fce04 100644
--- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/index.tsx
+++ b/x-pack/plugins/apm/public/components/alerting/TransactionDurationAnomalyAlertTrigger/index.tsx
@@ -3,7 +3,8 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiExpression, EuiSelect } from '@elastic/eui';
+
+import { useParams } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { ALERT_TYPES_CONFIG } from '../../../../common/alert_types';
@@ -16,14 +17,16 @@ import {
AnomalySeverity,
SelectAnomalySeverity,
} from './SelectAnomalySeverity';
-import {
- ENVIRONMENT_ALL,
- getEnvironmentLabel,
-} from '../../../../common/environment_filter_values';
+import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
import {
TRANSACTION_PAGE_LOAD,
TRANSACTION_REQUEST,
} from '../../../../common/transaction_types';
+import {
+ EnvironmentField,
+ ServiceField,
+ TransactionTypeField,
+} from '../fields';
interface Params {
windowSize: number;
@@ -42,9 +45,9 @@ interface Props {
export function TransactionDurationAnomalyAlertTrigger(props: Props) {
const { setAlertParams, alertParams, setAlertProperty } = props;
- const { serviceName } = alertParams;
const { urlParams } = useUrlParams();
const transactionTypes = useServiceTransactionTypes(urlParams);
+ const { serviceName } = useParams<{ serviceName?: string }>();
const { start, end } = urlParams;
const { environmentOptions } = useEnvironments({ serviceName, start, end });
const supportedTransactionTypes = transactionTypes.filter((transactionType) =>
@@ -55,10 +58,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
return null;
}
+ // 'page-load' for RUM, 'request' otherwise
+ const transactionType = supportedTransactionTypes[0];
+
const defaults: Params = {
windowSize: 15,
windowUnit: 'm',
- transactionType: supportedTransactionTypes[0], // 'page-load' for RUM, 'request' otherwise
+ transactionType,
serviceName,
environment: urlParams.environment || ENVIRONMENT_ALL.value,
anomalyScore: 75,
@@ -70,31 +76,13 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) {
};
const fields = [
- ,
+ ,
+ setAlertParams('environment', e.target.value)}
/>,
-
- setAlertParams('environment', e.target.value)}
- compressed
- />
- ,
}
title={i18n.translate(
diff --git a/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx
new file mode 100644
index 0000000000000..4dbf4dc10a907
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/TransactionErrorRateAlertTrigger/index.tsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { useParams } from 'react-router-dom';
+import React from 'react';
+import { ForLastExpression } from '../../../../../triggers_actions_ui/public';
+import { ALERT_TYPES_CONFIG, AlertType } from '../../../../common/alert_types';
+import { useEnvironments } from '../../../hooks/useEnvironments';
+import { useServiceTransactionTypes } from '../../../hooks/useServiceTransactionTypes';
+import { useUrlParams } from '../../../hooks/useUrlParams';
+import { ServiceAlertTrigger } from '../ServiceAlertTrigger';
+
+import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values';
+import {
+ ServiceField,
+ TransactionTypeField,
+ EnvironmentField,
+ IsAboveField,
+} from '../fields';
+
+interface AlertParams {
+ windowSize: number;
+ windowUnit: string;
+ threshold: number;
+ serviceName: string;
+ transactionType: string;
+ environment: string;
+}
+
+interface Props {
+ alertParams: AlertParams;
+ setAlertParams: (key: string, value: any) => void;
+ setAlertProperty: (key: string, value: any) => void;
+}
+
+export function TransactionErrorRateAlertTrigger(props: Props) {
+ const { setAlertParams, alertParams, setAlertProperty } = props;
+ const { urlParams } = useUrlParams();
+ const transactionTypes = useServiceTransactionTypes(urlParams);
+ const { serviceName } = useParams<{ serviceName?: string }>();
+ const { start, end, transactionType } = urlParams;
+ const { environmentOptions } = useEnvironments({ serviceName, start, end });
+
+ if (!transactionTypes.length || !serviceName) {
+ return null;
+ }
+
+ const defaultParams = {
+ threshold: 30,
+ windowSize: 5,
+ windowUnit: 'm',
+ transactionType: transactionType || transactionTypes[0],
+ environment: urlParams.environment || ENVIRONMENT_ALL.value,
+ };
+
+ const params = {
+ ...defaultParams,
+ ...alertParams,
+ };
+
+ const fields = [
+ ,
+ setAlertParams('environment', e.target.value)}
+ />,
+ ({ text: key, value: key }))}
+ onChange={(e) => setAlertParams('transactionType', e.target.value)}
+ />,
+ setAlertParams('threshold', value)}
+ />,
+
+ setAlertParams('windowSize', timeWindowSize || '')
+ }
+ onChangeWindowUnit={(timeWindowUnit) =>
+ setAlertParams('windowUnit', timeWindowUnit)
+ }
+ timeWindowSize={params.windowSize}
+ timeWindowUnit={params.windowUnit}
+ errors={{
+ timeWindowSize: [],
+ timeWindowUnit: [],
+ }}
+ />,
+ ];
+
+ return (
+
+ );
+}
+
+// Default export is required for React.lazy loading
+//
+// eslint-disable-next-line import/no-default-export
+export default TransactionErrorRateAlertTrigger;
diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx
new file mode 100644
index 0000000000000..e145d03671a18
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx
@@ -0,0 +1,108 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiSelectOption } from '@elastic/eui';
+import { getEnvironmentLabel } from '../../../common/environment_filter_values';
+import { PopoverExpression } from './ServiceAlertTrigger/PopoverExpression';
+
+export function ServiceField({ value }: { value?: string }) {
+ return (
+
+ );
+}
+
+export function EnvironmentField({
+ currentValue,
+ options,
+ onChange,
+}: {
+ currentValue: string;
+ options: EuiSelectOption[];
+ onChange: (event: React.ChangeEvent) => void;
+}) {
+ return (
+
+
+
+ );
+}
+
+export function TransactionTypeField({
+ currentValue,
+ options,
+ onChange,
+}: {
+ currentValue: string;
+ options?: EuiSelectOption[];
+ onChange?: (event: React.ChangeEvent) => void;
+}) {
+ const label = i18n.translate('xpack.apm.alerting.fields.type', {
+ defaultMessage: 'Type',
+ });
+
+ if (!options || options.length === 1) {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export function IsAboveField({
+ value,
+ unit,
+ onChange,
+ step,
+}: {
+ value: number;
+ unit: string;
+ onChange: (value: number) => void;
+ step?: number;
+}) {
+ return (
+
+ onChange(parseInt(e.target.value, 10))}
+ append={unit}
+ compressed
+ step={step}
+ />
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts
new file mode 100644
index 0000000000000..c0a1955e2cc8a
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import { lazy } from 'react';
+import { AlertType } from '../../../common/alert_types';
+import { ApmPluginStartDeps } from '../../plugin';
+
+export function registerApmAlerts(
+ alertTypeRegistry: ApmPluginStartDeps['triggers_actions_ui']['alertTypeRegistry']
+) {
+ alertTypeRegistry.register({
+ id: AlertType.ErrorCount,
+ name: i18n.translate('xpack.apm.alertTypes.errorCount', {
+ defaultMessage: 'Error count threshold',
+ }),
+ iconClass: 'bell',
+ alertParamsExpression: lazy(() => import('./ErrorCountAlertTrigger')),
+ validate: () => ({
+ errors: [],
+ }),
+ requiresAppContext: true,
+ });
+
+ alertTypeRegistry.register({
+ id: AlertType.TransactionDuration,
+ name: i18n.translate('xpack.apm.alertTypes.transactionDuration', {
+ defaultMessage: 'Transaction duration threshold',
+ }),
+ iconClass: 'bell',
+ alertParamsExpression: lazy(
+ () => import('./TransactionDurationAlertTrigger')
+ ),
+ validate: () => ({
+ errors: [],
+ }),
+ requiresAppContext: true,
+ });
+
+ alertTypeRegistry.register({
+ id: AlertType.TransactionErrorRate,
+ name: i18n.translate('xpack.apm.alertTypes.transactionErrorRate', {
+ defaultMessage: 'Transaction error rate threshold',
+ }),
+ iconClass: 'bell',
+ alertParamsExpression: lazy(
+ () => import('./TransactionErrorRateAlertTrigger')
+ ),
+ validate: () => ({
+ errors: [],
+ }),
+ requiresAppContext: true,
+ });
+
+ alertTypeRegistry.register({
+ id: AlertType.TransactionDurationAnomaly,
+ name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', {
+ defaultMessage: 'Transaction duration anomaly',
+ }),
+ iconClass: 'bell',
+ alertParamsExpression: lazy(
+ () => import('./TransactionDurationAnomalyAlertTrigger')
+ ),
+ validate: () => ({
+ errors: [],
+ }),
+ requiresAppContext: true,
+ });
+}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx
index 27c4a37e09c00..c11bfdeae945b 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceDetails/AlertIntegrations/index.tsx
@@ -24,9 +24,13 @@ const transactionDurationLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.transactionDuration',
{ defaultMessage: 'Transaction duration' }
);
-const errorRateLabel = i18n.translate(
- 'xpack.apm.serviceDetails.alertsMenu.errorRate',
- { defaultMessage: 'Error rate' }
+const transactionErrorRateLabel = i18n.translate(
+ 'xpack.apm.serviceDetails.alertsMenu.transactionErrorRate',
+ { defaultMessage: 'Transaction error rate' }
+);
+const errorCountLabel = i18n.translate(
+ 'xpack.apm.serviceDetails.alertsMenu.errorCount',
+ { defaultMessage: 'Error count' }
);
const createThresholdAlertLabel = i18n.translate(
'xpack.apm.serviceDetails.alertsMenu.createThresholdAlert',
@@ -38,8 +42,10 @@ const createAnomalyAlertAlertLabel = i18n.translate(
);
const CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID =
- 'create_transaction_duration';
-const CREATE_ERROR_RATE_ALERT_PANEL_ID = 'create_error_rate';
+ 'create_transaction_duration_panel';
+const CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID =
+ 'create_transaction_error_rate_panel';
+const CREATE_ERROR_COUNT_ALERT_PANEL_ID = 'create_error_count_panel';
interface Props {
canReadAlerts: boolean;
@@ -77,7 +83,14 @@ export function AlertIntegrations(props: Props) {
name: transactionDurationLabel,
panel: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
},
- { name: errorRateLabel, panel: CREATE_ERROR_RATE_ALERT_PANEL_ID },
+ {
+ name: transactionErrorRateLabel,
+ panel: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
+ },
+ {
+ name: errorCountLabel,
+ panel: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
+ },
]
: []),
...(canReadAlerts
@@ -96,10 +109,13 @@ export function AlertIntegrations(props: Props) {
: []),
],
},
+
+ // transaction duration panel
{
id: CREATE_TRANSACTION_DURATION_ALERT_PANEL_ID,
title: transactionDurationLabel,
items: [
+ // threshold alerts
{
name: createThresholdAlertLabel,
onClick: () => {
@@ -107,6 +123,8 @@ export function AlertIntegrations(props: Props) {
setPopoverOpen(false);
},
},
+
+ // anomaly alerts
...(canReadAnomalies
? [
{
@@ -120,14 +138,32 @@ export function AlertIntegrations(props: Props) {
: []),
],
},
+
+ // transaction error rate panel
+ {
+ id: CREATE_TRANSACTION_ERROR_RATE_ALERT_PANEL_ID,
+ title: transactionErrorRateLabel,
+ items: [
+ // threshold alerts
+ {
+ name: createThresholdAlertLabel,
+ onClick: () => {
+ setAlertType(AlertType.TransactionErrorRate);
+ setPopoverOpen(false);
+ },
+ },
+ ],
+ },
+
+ // error alerts panel
{
- id: CREATE_ERROR_RATE_ALERT_PANEL_ID,
- title: errorRateLabel,
+ id: CREATE_ERROR_COUNT_ALERT_PANEL_ID,
+ title: errorCountLabel,
items: [
{
name: createThresholdAlertLabel,
onClick: () => {
- setAlertType(AlertType.ErrorRate);
+ setAlertType(AlertType.ErrorCount);
setPopoverOpen(false);
},
},
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index 51ac6673251fb..ab3f1026a92dd 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -4,8 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { i18n } from '@kbn/i18n';
-import { lazy } from 'react';
import { ConfigSchema } from '.';
import {
FetchDataParams,
@@ -34,10 +32,10 @@ import {
TriggersAndActionsUIPublicPluginSetup,
TriggersAndActionsUIPublicPluginStart,
} from '../../triggers_actions_ui/public';
-import { AlertType } from '../common/alert_types';
import { featureCatalogueEntry } from './featureCatalogueEntry';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
+import { registerApmAlerts } from './components/alerting/register_apm_alerts';
export type ApmPluginSetup = void;
export type ApmPluginStart = void;
@@ -147,51 +145,6 @@ export class ApmPlugin implements Plugin {
}
public start(core: CoreStart, plugins: ApmPluginStartDeps) {
toggleAppLinkInNav(core, this.initializerContext.config.get());
-
- plugins.triggers_actions_ui.alertTypeRegistry.register({
- id: AlertType.ErrorRate,
- name: i18n.translate('xpack.apm.alertTypes.errorRate', {
- defaultMessage: 'Error rate',
- }),
- iconClass: 'bell',
- alertParamsExpression: lazy(
- () => import('./components/shared/ErrorRateAlertTrigger')
- ),
- validate: () => ({
- errors: [],
- }),
- requiresAppContext: true,
- });
-
- plugins.triggers_actions_ui.alertTypeRegistry.register({
- id: AlertType.TransactionDuration,
- name: i18n.translate('xpack.apm.alertTypes.transactionDuration', {
- defaultMessage: 'Transaction duration',
- }),
- iconClass: 'bell',
- alertParamsExpression: lazy(
- () => import('./components/shared/TransactionDurationAlertTrigger')
- ),
- validate: () => ({
- errors: [],
- }),
- requiresAppContext: true,
- });
-
- plugins.triggers_actions_ui.alertTypeRegistry.register({
- id: AlertType.TransactionDurationAnomaly,
- name: i18n.translate('xpack.apm.alertTypes.transactionDurationAnomaly', {
- defaultMessage: 'Transaction duration anomaly',
- }),
- iconClass: 'bell',
- alertParamsExpression: lazy(
- () =>
- import('./components/shared/TransactionDurationAnomalyAlertTrigger')
- ),
- validate: () => ({
- errors: [],
- }),
- requiresAppContext: true,
- });
+ registerApmAlerts(plugins.triggers_actions_ui.alertTypeRegistry);
}
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/action_variables.ts b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts
new file mode 100644
index 0000000000000..f2558da3a30e4
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/alerts/action_variables.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const apmActionVariables = {
+ serviceName: {
+ description: i18n.translate(
+ 'xpack.apm.alerts.action_variables.serviceName',
+ { defaultMessage: 'The service the alert is created for' }
+ ),
+ name: 'serviceName',
+ },
+ transactionType: {
+ description: i18n.translate(
+ 'xpack.apm.alerts.action_variables.transactionType',
+ { defaultMessage: 'The transaction type the alert is created for' }
+ ),
+ name: 'transactionType',
+ },
+ environment: {
+ description: i18n.translate(
+ 'xpack.apm.alerts.action_variables.environment',
+ { defaultMessage: 'The transaction type the alert is created for' }
+ ),
+ name: 'environment',
+ },
+ threshold: {
+ description: i18n.translate('xpack.apm.alerts.action_variables.threshold', {
+ defaultMessage:
+ 'Any trigger value above this value will cause the alert to fire',
+ }),
+ name: 'threshold',
+ },
+ triggerValue: {
+ description: i18n.translate(
+ 'xpack.apm.alerts.action_variables.triggerValue',
+ {
+ defaultMessage:
+ 'The value that breached the threshold and triggered the alert',
+ }
+ ),
+ name: 'triggerValue',
+ },
+};
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts
index 44ca80143bcd9..fcbb4cc5950e0 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_apm_alerts.ts
@@ -9,9 +9,10 @@ import { AlertingPlugin } from '../../../../alerts/server';
import { ActionsPlugin } from '../../../../actions/server';
import { registerTransactionDurationAlertType } from './register_transaction_duration_alert_type';
import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type';
-import { registerErrorRateAlertType } from './register_error_rate_alert_type';
+import { registerErrorCountAlertType } from './register_error_count_alert_type';
import { APMConfig } from '../..';
import { MlPluginSetup } from '../../../../ml/server';
+import { registerTransactionErrorRateAlertType } from './register_transaction_error_rate_alert_type';
interface Params {
alerts: AlertingPlugin['setup'];
@@ -30,7 +31,11 @@ export function registerApmAlerts(params: Params) {
ml: params.ml,
config$: params.config$,
});
- registerErrorRateAlertType({
+ registerErrorCountAlertType({
+ alerts: params.alerts,
+ config$: params.config$,
+ });
+ registerTransactionErrorRateAlertType({
alerts: params.alerts,
config$: params.config$,
});
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
similarity index 66%
rename from x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts
rename to x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
index 61e3dfee420a5..5455cd9f6a495 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_error_rate_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts
@@ -4,11 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema, TypeOf } from '@kbn/config-schema';
+import { schema } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
-import { i18n } from '@kbn/i18n';
-import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
+import { ProcessorEvent } from '../../../common/processor_event';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import {
ESSearchResponse,
@@ -17,11 +17,11 @@ import {
import {
PROCESSOR_EVENT,
SERVICE_NAME,
- SERVICE_ENVIRONMENT,
} from '../../../common/elasticsearch_fieldnames';
import { AlertingPlugin } from '../../../../alerts/server';
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
import { APMConfig } from '../..';
+import { apmActionVariables } from './action_variables';
interface RegisterAlertParams {
alerts: AlertingPlugin['setup'];
@@ -29,21 +29,21 @@ interface RegisterAlertParams {
}
const paramsSchema = schema.object({
- serviceName: schema.string(),
windowSize: schema.number(),
windowUnit: schema.string(),
threshold: schema.number(),
+ serviceName: schema.string(),
environment: schema.string(),
});
-const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorRate];
+const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.ErrorCount];
-export function registerErrorRateAlertType({
+export function registerErrorCountAlertType({
alerts,
config$,
}: RegisterAlertParams) {
alerts.registerType({
- id: AlertType.ErrorRate,
+ id: AlertType.ErrorCount,
name: alertTypeConfig.name,
actionGroups: alertTypeConfig.actionGroups,
defaultActionGroupId: alertTypeConfig.defaultActionGroupId,
@@ -52,37 +52,26 @@ export function registerErrorRateAlertType({
},
actionVariables: {
context: [
- {
- description: i18n.translate(
- 'xpack.apm.registerErrorRateAlertType.variables.serviceName',
- {
- defaultMessage: 'Service name',
- }
- ),
- name: 'serviceName',
- },
+ apmActionVariables.serviceName,
+ apmActionVariables.environment,
+ apmActionVariables.threshold,
+ apmActionVariables.triggerValue,
],
},
producer: 'apm',
executor: async ({ services, params }) => {
const config = await config$.pipe(take(1)).toPromise();
-
- const alertParams = params as TypeOf;
-
+ const alertParams = params;
const indices = await getApmIndices({
config,
savedObjectsClient: services.savedObjectsClient,
});
- const environmentTerm =
- alertParams.environment === ENVIRONMENT_ALL.value
- ? []
- : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }];
-
const searchParams = {
index: indices['apm_oss.errorIndices'],
size: 0,
body: {
+ track_total_hits: true,
query: {
bool: {
filter: [
@@ -93,21 +82,12 @@ export function registerErrorRateAlertType({
},
},
},
- {
- term: {
- [PROCESSOR_EVENT]: 'error',
- },
- },
- {
- term: {
- [SERVICE_NAME]: alertParams.serviceName,
- },
- },
- ...environmentTerm,
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
+ { term: { [SERVICE_NAME]: alertParams.serviceName } },
+ ...getEnvironmentUiFilterES(alertParams.environment),
],
},
},
- track_total_hits: true,
},
};
@@ -116,18 +96,19 @@ export function registerErrorRateAlertType({
ESSearchRequest
> = await services.callCluster('search', searchParams);
- const value = response.hits.total.value;
+ const errorCount = response.hits.total.value;
- if (value && value > alertParams.threshold) {
+ if (errorCount > alertParams.threshold) {
const alertInstance = services.alertInstanceFactory(
- AlertType.ErrorRate
+ AlertType.ErrorCount
);
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
serviceName: alertParams.serviceName,
+ environment: alertParams.environment,
+ threshold: alertParams.threshold,
+ triggerValue: errorCount,
});
}
-
- return {};
},
});
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
index ead28c325692d..373d4bd4da832 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_alert_type.ts
@@ -4,11 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema, TypeOf } from '@kbn/config-schema';
+import { schema } from '@kbn/config-schema';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
-import { i18n } from '@kbn/i18n';
-import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
+import { ProcessorEvent } from '../../../common/processor_event';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import { ESSearchResponse } from '../../../typings/elasticsearch';
import {
@@ -16,11 +15,12 @@ import {
SERVICE_NAME,
TRANSACTION_TYPE,
TRANSACTION_DURATION,
- SERVICE_ENVIRONMENT,
} from '../../../common/elasticsearch_fieldnames';
import { AlertingPlugin } from '../../../../alerts/server';
import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
import { APMConfig } from '../..';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
+import { apmActionVariables } from './action_variables';
interface RegisterAlertParams {
alerts: AlertingPlugin['setup'];
@@ -57,42 +57,22 @@ export function registerTransactionDurationAlertType({
},
actionVariables: {
context: [
- {
- description: i18n.translate(
- 'xpack.apm.registerTransactionDurationAlertType.variables.serviceName',
- {
- defaultMessage: 'Service name',
- }
- ),
- name: 'serviceName',
- },
- {
- description: i18n.translate(
- 'xpack.apm.registerTransactionDurationAlertType.variables.transactionType',
- {
- defaultMessage: 'Transaction type',
- }
- ),
- name: 'transactionType',
- },
+ apmActionVariables.serviceName,
+ apmActionVariables.transactionType,
+ apmActionVariables.environment,
+ apmActionVariables.threshold,
+ apmActionVariables.triggerValue,
],
},
producer: 'apm',
executor: async ({ services, params }) => {
const config = await config$.pipe(take(1)).toPromise();
-
- const alertParams = params as TypeOf;
-
+ const alertParams = params;
const indices = await getApmIndices({
config,
savedObjectsClient: services.savedObjectsClient,
});
- const environmentTerm =
- alertParams.environment === ENVIRONMENT_ALL.value
- ? []
- : [{ term: { [SERVICE_ENVIRONMENT]: alertParams.environment } }];
-
const searchParams = {
index: indices['apm_oss.transactionIndices'],
size: 0,
@@ -107,33 +87,17 @@ export function registerTransactionDurationAlertType({
},
},
},
- {
- term: {
- [PROCESSOR_EVENT]: 'transaction',
- },
- },
- {
- term: {
- [SERVICE_NAME]: alertParams.serviceName,
- },
- },
- {
- term: {
- [TRANSACTION_TYPE]: alertParams.transactionType,
- },
- },
- ...environmentTerm,
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+ { term: { [SERVICE_NAME]: alertParams.serviceName } },
+ { term: { [TRANSACTION_TYPE]: alertParams.transactionType } },
+ ...getEnvironmentUiFilterES(alertParams.environment),
],
},
},
aggs: {
agg:
alertParams.aggregationType === 'avg'
- ? {
- avg: {
- field: TRANSACTION_DURATION,
- },
- }
+ ? { avg: { field: TRANSACTION_DURATION } }
: {
percentiles: {
field: TRANSACTION_DURATION,
@@ -157,19 +121,23 @@ export function registerTransactionDurationAlertType({
const { agg } = response.aggregations;
- const value = 'values' in agg ? Object.values(agg.values)[0] : agg?.value;
+ const transactionDuration =
+ 'values' in agg ? Object.values(agg.values)[0] : agg?.value;
- if (value && value > alertParams.threshold * 1000) {
+ const threshold = alertParams.threshold * 1000;
+
+ if (transactionDuration && transactionDuration > threshold) {
const alertInstance = services.alertInstanceFactory(
AlertType.TransactionDuration
);
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
transactionType: alertParams.transactionType,
serviceName: alertParams.serviceName,
+ environment: alertParams.environment,
+ threshold,
+ triggerValue: transactionDuration,
});
}
-
- return {};
},
});
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts
index 93af51b572aa5..b3526b6a97ad9 100644
--- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts
@@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema, TypeOf } from '@kbn/config-schema';
+import { schema } from '@kbn/config-schema';
import { Observable } from 'rxjs';
-import { i18n } from '@kbn/i18n';
import { KibanaRequest } from '../../../../../../src/core/server';
import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
import { AlertingPlugin } from '../../../../alerts/server';
import { APMConfig } from '../..';
import { MlPluginSetup } from '../../../../ml/server';
import { getMLJobIds } from '../service_map/get_service_anomalies';
+import { apmActionVariables } from './action_variables';
interface RegisterAlertParams {
alerts: AlertingPlugin['setup'];
@@ -47,24 +47,9 @@ export function registerTransactionDurationAnomalyAlertType({
},
actionVariables: {
context: [
- {
- description: i18n.translate(
- 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.serviceName',
- {
- defaultMessage: 'Service name',
- }
- ),
- name: 'serviceName',
- },
- {
- description: i18n.translate(
- 'xpack.apm.registerTransactionDurationAnomalyAlertType.variables.transactionType',
- {
- defaultMessage: 'Transaction type',
- }
- ),
- name: 'transactionType',
- },
+ apmActionVariables.serviceName,
+ apmActionVariables.transactionType,
+ apmActionVariables.environment,
],
},
producer: 'apm',
@@ -72,7 +57,7 @@ export function registerTransactionDurationAnomalyAlertType({
if (!ml) {
return;
}
- const alertParams = params as TypeOf;
+ const alertParams = params;
const request = {} as KibanaRequest;
const { mlAnomalySearch } = ml.mlSystemProvider(request);
const anomalyDetectors = ml.anomalyDetectorsProvider(request);
@@ -88,6 +73,7 @@ export function registerTransactionDurationAnomalyAlertType({
const anomalySearchParams = {
body: {
+ terminateAfter: 1,
size: 0,
query: {
bool: {
@@ -131,10 +117,10 @@ export function registerTransactionDurationAnomalyAlertType({
);
alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
serviceName: alertParams.serviceName,
+ transactionType: alertParams.transactionType,
+ environment: alertParams.environment,
});
}
-
- return {};
},
});
}
diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
new file mode 100644
index 0000000000000..a6ed40fc15ec6
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { Observable } from 'rxjs';
+import { take } from 'rxjs/operators';
+import { ProcessorEvent } from '../../../common/processor_event';
+import { EventOutcome } from '../../../common/event_outcome';
+import { AlertType, ALERT_TYPES_CONFIG } from '../../../common/alert_types';
+import { ESSearchResponse } from '../../../typings/elasticsearch';
+import {
+ PROCESSOR_EVENT,
+ SERVICE_NAME,
+ TRANSACTION_TYPE,
+ EVENT_OUTCOME,
+} from '../../../common/elasticsearch_fieldnames';
+import { AlertingPlugin } from '../../../../alerts/server';
+import { getApmIndices } from '../settings/apm_indices/get_apm_indices';
+import { APMConfig } from '../..';
+import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es';
+import { apmActionVariables } from './action_variables';
+
+interface RegisterAlertParams {
+ alerts: AlertingPlugin['setup'];
+ config$: Observable;
+}
+
+const paramsSchema = schema.object({
+ windowSize: schema.number(),
+ windowUnit: schema.string(),
+ threshold: schema.number(),
+ transactionType: schema.string(),
+ serviceName: schema.string(),
+ environment: schema.string(),
+});
+
+const alertTypeConfig = ALERT_TYPES_CONFIG[AlertType.TransactionErrorRate];
+
+export function registerTransactionErrorRateAlertType({
+ alerts,
+ config$,
+}: RegisterAlertParams) {
+ alerts.registerType({
+ id: AlertType.TransactionErrorRate,
+ name: alertTypeConfig.name,
+ actionGroups: alertTypeConfig.actionGroups,
+ defaultActionGroupId: alertTypeConfig.defaultActionGroupId,
+ validate: {
+ params: paramsSchema,
+ },
+ actionVariables: {
+ context: [
+ apmActionVariables.transactionType,
+ apmActionVariables.serviceName,
+ apmActionVariables.environment,
+ apmActionVariables.threshold,
+ apmActionVariables.triggerValue,
+ ],
+ },
+ producer: 'apm',
+ executor: async ({ services, params: alertParams }) => {
+ const config = await config$.pipe(take(1)).toPromise();
+ const indices = await getApmIndices({
+ config,
+ savedObjectsClient: services.savedObjectsClient,
+ });
+
+ const searchParams = {
+ index: indices['apm_oss.transactionIndices'],
+ size: 0,
+ body: {
+ track_total_hits: true,
+ query: {
+ bool: {
+ filter: [
+ {
+ range: {
+ '@timestamp': {
+ gte: `now-${alertParams.windowSize}${alertParams.windowUnit}`,
+ },
+ },
+ },
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
+ { term: { [SERVICE_NAME]: alertParams.serviceName } },
+ { term: { [TRANSACTION_TYPE]: alertParams.transactionType } },
+ ...getEnvironmentUiFilterES(alertParams.environment),
+ ],
+ },
+ },
+ aggs: {
+ erroneous_transactions: {
+ filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } },
+ },
+ },
+ },
+ };
+
+ const response: ESSearchResponse<
+ unknown,
+ typeof searchParams
+ > = await services.callCluster('search', searchParams);
+
+ if (!response.aggregations) {
+ return;
+ }
+
+ const errornousTransactionsCount =
+ response.aggregations.erroneous_transactions.doc_count;
+ const totalTransactionCount = response.hits.total.value;
+ const transactionErrorRate =
+ (errornousTransactionsCount / totalTransactionCount) * 100;
+
+ if (transactionErrorRate > alertParams.threshold) {
+ const alertInstance = services.alertInstanceFactory(
+ AlertType.TransactionErrorRate
+ );
+
+ alertInstance.scheduleActions(alertTypeConfig.defaultActionGroupId, {
+ serviceName: alertParams.serviceName,
+ transactionType: alertParams.transactionType,
+ environment: alertParams.environment,
+ threshold: alertParams.threshold,
+ triggerValue: transactionErrorRate,
+ });
+ }
+ },
+ });
+}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index 7bcd945d890ad..d0673335387c6 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -8,6 +8,7 @@ import { Logger } from 'kibana/server';
import uuid from 'uuid/v4';
import { snakeCase } from 'lodash';
import Boom from 'boom';
+import { ProcessorEvent } from '../../../common/processor_event';
import { ML_ERRORS } from '../../../common/anomaly_detection';
import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
@@ -79,7 +80,7 @@ async function createAnomalyDetectionJob({
query: {
bool: {
filter: [
- { term: { [PROCESSOR_EVENT]: 'transaction' } },
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
{ exists: { field: TRANSACTION_DURATION } },
...getEnvironmentUiFilterES(environment),
],
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index a53068d152d03..fcd4f468d4367 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -85,7 +85,7 @@ export const tasks: TelemetryTask[] = [
query: {
bool: {
filter: [
- { term: { [PROCESSOR_EVENT]: 'transaction' } },
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
{ range: { '@timestamp': { gte: start, lt: end } } },
],
},
@@ -606,7 +606,10 @@ export const tasks: TelemetryTask[] = [
timeout,
query: {
bool: {
- filter: [{ term: { [PROCESSOR_EVENT]: 'error' } }, range1d],
+ filter: [
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } },
+ range1d,
+ ],
},
},
aggs: {
@@ -640,7 +643,7 @@ export const tasks: TelemetryTask[] = [
query: {
bool: {
filter: [
- { term: { [PROCESSOR_EVENT]: 'transaction' } },
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
range1d,
],
},
@@ -674,7 +677,7 @@ export const tasks: TelemetryTask[] = [
query: {
bool: {
filter: [
- { term: { [PROCESSOR_EVENT]: 'transaction' } },
+ { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } },
range1d,
],
must_not: {
diff --git a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
index 6ff98a9be75f9..ea8d02eb833cf 100644
--- a/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
+++ b/x-pack/plugins/apm/server/lib/helpers/convert_ui_filters/get_environment_ui_filter_es.ts
@@ -5,11 +5,14 @@
*/
import { ESFilter } from '../../../../typings/elasticsearch';
-import { ENVIRONMENT_NOT_DEFINED } from '../../../../common/environment_filter_values';
+import {
+ ENVIRONMENT_NOT_DEFINED,
+ ENVIRONMENT_ALL,
+} from '../../../../common/environment_filter_values';
import { SERVICE_ENVIRONMENT } from '../../../../common/elasticsearch_fieldnames';
export function getEnvironmentUiFilterES(environment?: string): ESFilter[] {
- if (!environment) {
+ if (!environment || environment === ENVIRONMENT_ALL.value) {
return [];
}
if (environment === ENVIRONMENT_NOT_DEFINED.value) {
diff --git a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
index b88c90a213c67..2868dcfda97b6 100644
--- a/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
+++ b/x-pack/plugins/apm/server/lib/metrics/__snapshots__/queries.test.ts.snap
@@ -203,16 +203,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -221,16 +255,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -275,12 +343,7 @@ Object {
},
Object {
"exists": Object {
- "field": "system.memory.actual.free",
- },
- },
- Object {
- "exists": Object {
- "field": "system.memory.total",
+ "field": "system.process.cgroup.memory.mem.usage.bytes",
},
},
],
@@ -682,16 +745,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -700,16 +797,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -760,12 +891,7 @@ Object {
},
Object {
"exists": Object {
- "field": "system.memory.actual.free",
- },
- },
- Object {
- "exists": Object {
- "field": "system.memory.total",
+ "field": "system.process.cgroup.memory.mem.usage.bytes",
},
},
],
@@ -1157,16 +1283,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -1175,16 +1335,50 @@ Object {
"memoryUsedAvg": Object {
"avg": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
"memoryUsedMax": Object {
"max": Object {
"script": Object {
- "lang": "expression",
- "source": "1 - doc['system.memory.actual.free'] / doc['system.memory.total']",
+ "lang": "painless",
+ "source": "
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = 'system.process.cgroup.memory.mem.limit.bytes';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['system.memory.total'].value;
+
+ double used = doc['system.process.cgroup.memory.mem.usage.bytes'].value;
+
+ return used / total;
+ ",
},
},
},
@@ -1224,12 +1418,7 @@ Object {
},
Object {
"exists": Object {
- "field": "system.memory.actual.free",
- },
- },
- Object {
- "exists": Object {
- "field": "system.memory.total",
+ "field": "system.process.cgroup.memory.mem.usage.bytes",
},
},
],
diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
index 316b0d59d2c5b..a60576ca0c175 100644
--- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
+++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/memory/index.ts
@@ -6,6 +6,8 @@
import { i18n } from '@kbn/i18n';
import {
+ METRIC_CGROUP_MEMORY_LIMIT_BYTES,
+ METRIC_CGROUP_MEMORY_USAGE_BYTES,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
} from '../../../../../../common/elasticsearch_fieldnames';
@@ -14,8 +16,8 @@ import {
SetupTimeRange,
SetupUIFilters,
} from '../../../../helpers/setup_request';
-import { ChartBase } from '../../../types';
import { fetchAndTransformMetrics } from '../../../fetch_and_transform_metrics';
+import { ChartBase } from '../../../types';
const series = {
memoryUsedMax: {
@@ -43,36 +45,68 @@ const chartBase: ChartBase = {
series,
};
-export const percentMemoryUsedScript = {
+export const percentSystemMemoryUsedScript = {
lang: 'expression',
source: `1 - doc['${METRIC_SYSTEM_FREE_MEMORY}'] / doc['${METRIC_SYSTEM_TOTAL_MEMORY}']`,
};
+export const percentCgroupMemoryUsedScript = {
+ lang: 'painless',
+ source: `
+ /*
+ When no limit is specified in the container, docker allows the app as much memory / swap memory as it wants.
+ This number represents the max possible value for the limit field.
+ */
+ double CGROUP_LIMIT_MAX_VALUE = 9223372036854771712L;
+
+ String limitKey = '${METRIC_CGROUP_MEMORY_LIMIT_BYTES}';
+
+ //Should use cgropLimit when value is not empty and not equals to the max limit value.
+ boolean useCgroupLimit = doc.containsKey(limitKey) && !doc[limitKey].empty && doc[limitKey].value != CGROUP_LIMIT_MAX_VALUE;
+
+ double total = useCgroupLimit ? doc[limitKey].value : doc['${METRIC_SYSTEM_TOTAL_MEMORY}'].value;
+
+ double used = doc['${METRIC_CGROUP_MEMORY_USAGE_BYTES}'].value;
+
+ return used / total;
+ `,
+};
+
export async function getMemoryChartData(
setup: Setup & SetupTimeRange & SetupUIFilters,
serviceName: string,
serviceNodeName?: string
) {
- return fetchAndTransformMetrics({
+ const cgroupResponse = await fetchAndTransformMetrics({
setup,
serviceName,
serviceNodeName,
chartBase,
aggs: {
- memoryUsedAvg: { avg: { script: percentMemoryUsedScript } },
- memoryUsedMax: { max: { script: percentMemoryUsedScript } },
+ memoryUsedAvg: { avg: { script: percentCgroupMemoryUsedScript } },
+ memoryUsedMax: { max: { script: percentCgroupMemoryUsedScript } },
},
additionalFilters: [
- {
- exists: {
- field: METRIC_SYSTEM_FREE_MEMORY,
- },
- },
- {
- exists: {
- field: METRIC_SYSTEM_TOTAL_MEMORY,
- },
- },
+ { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } },
],
});
+
+ if (cgroupResponse.noHits) {
+ return await fetchAndTransformMetrics({
+ setup,
+ serviceName,
+ serviceNodeName,
+ chartBase,
+ aggs: {
+ memoryUsedAvg: { avg: { script: percentSystemMemoryUsedScript } },
+ memoryUsedMax: { max: { script: percentSystemMemoryUsedScript } },
+ },
+ additionalFilters: [
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
+ ],
+ });
+ }
+
+ return cgroupResponse;
}
diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
index 1a7d602882395..f25062c67f87a 100644
--- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
+++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts
@@ -67,7 +67,7 @@ export async function getPageViewTrends({
x: xVal,
y: bCount,
};
- if (breakdownItem) {
+ if ('breakdown' in bucket) {
const categoryBuckets = bucket.breakdown.buckets;
categoryBuckets.forEach(({ key, doc_count: docCount }) => {
if (key === 'Other') {
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index 88cc26608b850..5c183fd9150dd 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -14,13 +14,17 @@ import {
METRIC_SYSTEM_CPU_PERCENT,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
+ METRIC_CGROUP_MEMORY_USAGE_BYTES,
TRANSACTION_TYPE,
} from '../../../common/elasticsearch_fieldnames';
import { ProcessorEvent } from '../../../common/processor_event';
import { rangeFilter } from '../../../common/utils/range_filter';
import { ESFilter } from '../../../typings/elasticsearch';
import { Setup, SetupTimeRange } from '../helpers/setup_request';
-import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory';
+import {
+ percentCgroupMemoryUsedScript,
+ percentSystemMemoryUsedScript,
+} from '../metrics/by_agent/shared/memory';
import {
getProcessorEventForAggregatedTransactions,
getTransactionDurationFieldForAggregatedTransactions,
@@ -205,26 +209,50 @@ async function getMemoryStats({
filter,
}: TaskParameters): Promise<{ avgMemoryUsage: number | null }> {
const { apmEventClient } = setup;
- const response = await apmEventClient.search({
- apm: {
- events: [ProcessorEvent.metric],
- },
- body: {
- size: 0,
- query: {
- bool: {
- filter: [
- ...filter,
- { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
- { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
- ],
+
+ const getAvgMemoryUsage = async ({
+ additionalFilters,
+ script,
+ }: {
+ additionalFilters: ESFilter[];
+ script: typeof percentCgroupMemoryUsedScript;
+ }) => {
+ const response = await apmEventClient.search({
+ apm: {
+ events: [ProcessorEvent.metric],
+ },
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [...filter, ...additionalFilters],
+ },
+ },
+ aggs: {
+ avgMemoryUsage: { avg: { script } },
},
},
- aggs: { avgMemoryUsage: { avg: { script: percentMemoryUsedScript } } },
- },
- });
+ });
- return {
- avgMemoryUsage: response.aggregations?.avgMemoryUsage.value ?? null,
+ return response.aggregations?.avgMemoryUsage.value ?? null;
};
+
+ let avgMemoryUsage = await getAvgMemoryUsage({
+ additionalFilters: [
+ { exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } },
+ ],
+ script: percentCgroupMemoryUsedScript,
+ });
+
+ if (!avgMemoryUsage) {
+ avgMemoryUsage = await getAvgMemoryUsage({
+ additionalFilters: [
+ { exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
+ { exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
+ ],
+ script: percentSystemMemoryUsedScript,
+ });
+ }
+
+ return { avgMemoryUsage };
}
diff --git a/x-pack/plugins/data_enhanced/server/index.ts b/x-pack/plugins/data_enhanced/server/index.ts
index fbe1ecc10d632..3c5d5d1e99d13 100644
--- a/x-pack/plugins/data_enhanced/server/index.ts
+++ b/x-pack/plugins/data_enhanced/server/index.ts
@@ -11,4 +11,6 @@ export function plugin(initializerContext: PluginInitializerContext) {
return new EnhancedDataServerPlugin(initializerContext);
}
+export { ENHANCED_ES_SEARCH_STRATEGY } from '../common';
+
export { EnhancedDataServerPlugin as Plugin };
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts
index 4906d0342be84..64af67aefa4be 100644
--- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts
@@ -5,7 +5,6 @@
*/
import { UrlDrilldown, ActionContext, Config } from './url_drilldown';
-import { coreMock } from '../../../../../../src/core/public/mocks';
import { IEmbeddable } from '../../../../../../src/plugins/embeddable/public/lib/embeddables';
const mockDataPoints = [
@@ -52,7 +51,6 @@ const mockNavigateToUrl = jest.fn(() => Promise.resolve());
describe('UrlDrilldown', () => {
const urlDrilldown = new UrlDrilldown({
getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }),
- getOpenModal: () => Promise.resolve(coreMock.createStart().overlays.openModal),
getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs',
getVariablesHelpDocsLink: () => 'http://localhost:5601/docs',
navigateToUrl: mockNavigateToUrl,
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx
index 80478e6490b8f..04f60662d88a3 100644
--- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx
@@ -5,7 +5,6 @@
*/
import React from 'react';
-import { OverlayStart } from 'kibana/public';
import { reactToUiComponent } from '../../../../../../src/plugins/kibana_react/public';
import { ChartActionContext, IEmbeddable } from '../../../../../../src/plugins/embeddable/public';
import { CollectConfigProps as CollectConfigPropsBase } from '../../../../../../src/plugins/kibana_utils/public';
@@ -29,7 +28,6 @@ import { txtUrlDrilldownDisplayName } from './i18n';
interface UrlDrilldownDeps {
getGlobalScope: () => UrlDrilldownGlobalScope;
navigateToUrl: (url: string) => Promise;
- getOpenModal: () => Promise;
getSyntaxHelpDocsLink: () => string;
getVariablesHelpDocsLink: () => string;
}
@@ -112,13 +110,10 @@ export class UrlDrilldown implements Drilldown
- urlDrilldownCompileUrl(config.url.template, await this.buildRuntimeScope(context));
+ urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
public readonly execute = async (config: Config, context: ActionContext) => {
- const url = await urlDrilldownCompileUrl(
- config.url.template,
- await this.buildRuntimeScope(context, { allowPrompts: true })
- );
+ const url = urlDrilldownCompileUrl(config.url.template, this.buildRuntimeScope(context));
if (config.openInNewTab) {
window.open(url, '_blank', 'noopener');
} else {
@@ -134,14 +129,11 @@ export class UrlDrilldown implements Drilldown {
+ private buildRuntimeScope = (context: ActionContext) => {
return urlDrilldownBuildScope({
globalScope: this.deps.getGlobalScope(),
contextScope: getContextScope(context),
- eventScope: await getEventScope(context, this.deps, opts),
+ eventScope: getEventScope(context),
});
};
}
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts
new file mode 100644
index 0000000000000..bb1baf5b96428
--- /dev/null
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.test.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 {
+ getEventScope,
+ getMockEventScope,
+ ValueClickTriggerEventScope,
+} from './url_drilldown_scope';
+
+const createPoint = ({
+ field,
+ value,
+}: {
+ field: string;
+ value: string | null | number | boolean;
+}) => ({
+ table: {
+ columns: [
+ {
+ name: field,
+ id: '1-1',
+ meta: {
+ type: 'histogram',
+ indexPatternId: 'logstash-*',
+ aggConfigParams: {
+ field,
+ interval: 30,
+ otherBucket: true,
+ },
+ },
+ },
+ ],
+ rows: [
+ {
+ '1-1': '2048',
+ },
+ ],
+ },
+ column: 0,
+ row: 0,
+ value,
+});
+
+describe('VALUE_CLICK_TRIGGER', () => {
+ describe('supports `points[]`', () => {
+ test('getEventScope()', () => {
+ const mockDataPoints = [
+ createPoint({ field: 'field0', value: 'value0' }),
+ createPoint({ field: 'field1', value: 'value1' }),
+ createPoint({ field: 'field2', value: 'value2' }),
+ ];
+
+ const eventScope = getEventScope({
+ data: { data: mockDataPoints },
+ }) as ValueClickTriggerEventScope;
+
+ expect(eventScope.key).toBe('field0');
+ expect(eventScope.value).toBe('value0');
+ expect(eventScope.points).toHaveLength(mockDataPoints.length);
+ expect(eventScope.points).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "key": "field0",
+ "value": "value0",
+ },
+ Object {
+ "key": "field1",
+ "value": "value1",
+ },
+ Object {
+ "key": "field2",
+ "value": "value2",
+ },
+ ]
+ `);
+ });
+
+ test('getMockEventScope()', () => {
+ const mockEventScope = getMockEventScope([
+ 'VALUE_CLICK_TRIGGER',
+ ]) as ValueClickTriggerEventScope;
+ expect(mockEventScope.points.length).toBeGreaterThan(3);
+ expect(mockEventScope.points).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "key": "event.points.0.key",
+ "value": "event.points.0.value",
+ },
+ Object {
+ "key": "event.points.1.key",
+ "value": "event.points.1.value",
+ },
+ Object {
+ "key": "event.points.2.key",
+ "value": "event.points.2.value",
+ },
+ Object {
+ "key": "event.points.3.key",
+ "value": "event.points.3.value",
+ },
+ ]
+ `);
+ });
+ });
+
+ describe('handles undefined, null or missing values', () => {
+ test('undefined or missing values are removed from the result scope', () => {
+ const point = createPoint({ field: undefined } as any);
+ const eventScope = getEventScope({
+ data: { data: [point] },
+ }) as ValueClickTriggerEventScope;
+
+ expect('key' in eventScope).toBeFalsy();
+ expect('value' in eventScope).toBeFalsy();
+ });
+
+ test('null value stays in the result scope', () => {
+ const point = createPoint({ field: 'field', value: null });
+ const eventScope = getEventScope({
+ data: { data: [point] },
+ }) as ValueClickTriggerEventScope;
+
+ expect(eventScope.value).toBeNull();
+ });
+ });
+});
diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
similarity index 51%
rename from x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx
rename to x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
index d3e3510f1b24e..15a9a3ba77d88 100644
--- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.tsx
+++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown_scope.ts
@@ -9,19 +9,7 @@
* Please refer to ./README.md for explanation of different scope sources
*/
-import React from 'react';
-import {
- EuiButton,
- EuiButtonEmpty,
- EuiModalBody,
- EuiModalFooter,
- EuiModalHeader,
- EuiModalHeaderTitle,
- EuiRadioGroup,
-} from '@elastic/eui';
-import uniqBy from 'lodash/uniqBy';
-import { FormattedMessage } from '@kbn/i18n/react';
-import type { Query, Filter, TimeRange } from '../../../../../../src/plugins/data/public';
+import type { Filter, Query, TimeRange } from '../../../../../../src/plugins/data/public';
import {
IEmbeddable,
isRangeSelectTriggerContext,
@@ -31,8 +19,6 @@ import {
} from '../../../../../../src/plugins/embeddable/public';
import type { ActionContext, ActionFactoryContext, UrlTrigger } from './url_drilldown';
import { SELECT_RANGE_TRIGGER } from '../../../../../../src/plugins/ui_actions/public';
-import { OverlayStart } from '../../../../../../src/core/public';
-import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public';
type ContextScopeInput = ActionContext | ActionFactoryContext;
@@ -113,38 +99,35 @@ export function getContextScope(contextScopeInput: ContextScopeInput): UrlDrilld
/**
* URL drilldown event scope,
- * available as: {{event.key}}, {{event.from}}
+ * available as {{event.$}}
*/
-type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope;
-type EventScopeInput = ActionContext;
-interface ValueClickTriggerEventScope {
+export type UrlDrilldownEventScope = ValueClickTriggerEventScope | RangeSelectTriggerEventScope;
+export type EventScopeInput = ActionContext;
+export interface ValueClickTriggerEventScope {
key?: string;
- value?: string | number | boolean;
+ value: Primitive;
negate: boolean;
+ points: Array<{ key?: string; value: Primitive }>;
}
-interface RangeSelectTriggerEventScope {
+export interface RangeSelectTriggerEventScope {
key: string;
from?: string | number;
to?: string | number;
}
-export async function getEventScope(
- eventScopeInput: EventScopeInput,
- deps: { getOpenModal: () => Promise },
- opts: { allowPrompts: boolean } = { allowPrompts: false }
-): Promise {
+export function getEventScope(eventScopeInput: EventScopeInput): UrlDrilldownEventScope {
if (isRangeSelectTriggerContext(eventScopeInput)) {
return getEventScopeFromRangeSelectTriggerContext(eventScopeInput);
} else if (isValueClickTriggerContext(eventScopeInput)) {
- return getEventScopeFromValueClickTriggerContext(eventScopeInput, deps, opts);
+ return getEventScopeFromValueClickTriggerContext(eventScopeInput);
} else {
throw new Error("UrlDrilldown [getEventScope] can't build scope from not supported trigger");
}
}
-async function getEventScopeFromRangeSelectTriggerContext(
+function getEventScopeFromRangeSelectTriggerContext(
eventScopeInput: RangeSelectContext
-): Promise {
+): RangeSelectTriggerEventScope {
const { table, column: columnIndex, range } = eventScopeInput.data;
const column = table.columns[columnIndex];
return cleanEmptyKeys({
@@ -154,18 +137,23 @@ async function getEventScopeFromRangeSelectTriggerContext(
});
}
-async function getEventScopeFromValueClickTriggerContext(
- eventScopeInput: ValueClickContext,
- deps: { getOpenModal: () => Promise },
- opts: { allowPrompts: boolean } = { allowPrompts: false }
-): Promise {
+function getEventScopeFromValueClickTriggerContext(
+ eventScopeInput: ValueClickContext
+): ValueClickTriggerEventScope {
const negate = eventScopeInput.data.negate ?? false;
- const point = await getSingleValue(eventScopeInput.data.data, deps, opts);
- const { key, value } = getKeyValueFromPoint(point);
+ const points = eventScopeInput.data.data.map(({ table, value, column: columnIndex }) => {
+ const column = table.columns[columnIndex];
+ return {
+ value: toPrimitiveOrUndefined(value) as Primitive,
+ key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined,
+ };
+ });
+
return cleanEmptyKeys({
- key,
- value,
+ key: points[0]?.key,
+ value: points[0]?.value,
negate,
+ points,
});
}
@@ -182,29 +170,28 @@ export function getMockEventScope([trigger]: UrlTrigger[]): UrlDrilldownEventSco
to: new Date().toISOString(),
};
} else {
+ // number of mock points to generate
+ // should be larger or equal of any possible data points length emitted by VALUE_CLICK_TRIGGER
+ const nPoints = 4;
+ const points = new Array(nPoints).fill(0).map((_, index) => ({
+ key: `event.points.${index}.key`,
+ value: `event.points.${index}.value`,
+ }));
return {
- key: 'event.key',
- value: 'event.value',
+ key: `event.key`,
+ value: `event.value`,
negate: false,
+ points,
};
}
}
-function getKeyValueFromPoint(
- point: ValueClickContext['data']['data'][0]
-): Pick {
- const { table, column: columnIndex, value } = point;
- const column = table.columns[columnIndex];
- return {
- key: toPrimitiveOrUndefined(column?.meta?.aggConfigParams?.field) as string | undefined,
- value: toPrimitiveOrUndefined(value),
- };
-}
-
-function toPrimitiveOrUndefined(v: unknown): string | number | boolean | undefined {
- if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string') return v;
+type Primitive = string | number | boolean | null;
+function toPrimitiveOrUndefined(v: unknown): Primitive | undefined {
+ if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'string' || v === null)
+ return v;
if (typeof v === 'object' && v instanceof Date) return v.toISOString();
- if (typeof v === 'undefined' || v === null) return undefined;
+ if (typeof v === 'undefined') return undefined;
return String(v);
}
@@ -216,104 +203,3 @@ function cleanEmptyKeys>(obj: T): T {
});
return obj;
}
-
-/**
- * VALUE_CLICK_TRIGGER could have multiple data points
- * Prompt user which data point to use in a drilldown
- */
-async function getSingleValue(
- data: ValueClickContext['data']['data'],
- deps: { getOpenModal: () => Promise },
- opts: { allowPrompts: boolean } = { allowPrompts: false }
-): Promise {
- data = uniqBy(data.filter(Boolean), (point) => {
- const { key, value } = getKeyValueFromPoint(point);
- return `${key}:${value}`;
- });
- if (data.length === 0)
- throw new Error(`[trigger = "VALUE_CLICK_TRIGGER"][getSingleValue] no value to pick from`);
- if (data.length === 1) return Promise.resolve(data[0]);
- if (!opts.allowPrompts) return Promise.resolve(data[0]);
- return new Promise(async (resolve, reject) => {
- const openModal = await deps.getOpenModal();
- const overlay = openModal(
- toMountPoint(
- overlay.close()}
- onSubmit={(point) => {
- if (point) {
- resolve(point);
- }
- overlay.close();
- }}
- data={data}
- />
- )
- );
- overlay.onClose.then(() => reject());
- });
-}
-
-function GetSingleValuePopup({
- data,
- onCancel,
- onSubmit,
-}: {
- data: ValueClickContext['data']['data'];
- onCancel: () => void;
- onSubmit: (value: ValueClickContext['data']['data'][0]) => void;
-}) {
- const values = data
- .map((point) => {
- const { key, value } = getKeyValueFromPoint(point);
- return {
- point,
- id: key ?? '',
- label: `${key}:${value}`,
- };
- })
- .filter((value) => Boolean(value.id));
-
- const [selectedValueId, setSelectedValueId] = React.useState(values[0].id);
-
- return (
-
-
-
-
-
-
-
-
- setSelectedValueId(id)}
- name="drilldownValues"
- />
-
-
-
-
-
-
- onSubmit(values.find((v) => v.id === selectedValueId)?.point!)}
- data-test-subj="applySingleValuePopoverButton"
- fill
- >
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
index 187db998e06ea..2138a372523b7 100644
--- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts
+++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts
@@ -74,7 +74,6 @@ export class EmbeddableEnhancedPlugin
getGlobalScope: urlDrilldownGlobalScopeProvider({ core }),
navigateToUrl: (url: string) =>
core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)),
- getOpenModal: () => core.getStartServices().then(([{ overlays }]) => overlays.openModal),
getSyntaxHelpDocsLink: () =>
startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax,
getVariablesHelpDocsLink: () =>
diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md
index 31ee304fe2247..ba14be5564be1 100644
--- a/x-pack/plugins/enterprise_search/README.md
+++ b/x-pack/plugins/enterprise_search/README.md
@@ -13,6 +13,14 @@ This plugin's goal is to provide a Kibana user interface to the Enterprise Searc
2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'`
3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana.
+### Kea
+
+Enterprise Search uses [Kea.js](https://github.com/keajs/kea) to manage our React/Redux state for us. Kea state is handled in our `*Logic` files and exposes [values](https://kea.js.org/docs/guide/concepts#values) and [actions](https://kea.js.org/docs/guide/concepts#actions) for our components to get and set state with.
+
+#### Debugging Kea
+
+To debug Kea state in-browser, Kea recommends [Redux Devtools](https://kea.js.org/docs/guide/debugging). To facilitate debugging, we use the [path](https://kea.js.org/docs/guide/debugging/#setting-the-path-manually) key with `snake_case`d paths. The path key should always end with the logic filename (e.g. `['enterprise_search', 'some_logic']`) to make it easy for devs to quickly find/jump to files via IDE tooling.
+
## Testing
### Unit tests
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
index d6a51e8b482d0..5df25f11e5070 100644
--- a/x-pack/plugins/enterprise_search/common/constants.ts
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -76,4 +76,6 @@ export const JSON_HEADER = {
Accept: 'application/json', // Required for Enterprise Search APIs
};
+export const READ_ONLY_MODE_HEADER = 'x-ent-search-read-only-mode';
+
export const ENGINES_PAGE_SIZE = 10;
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
index 3f71759390879..9388d61041b13 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts
@@ -16,6 +16,7 @@ export interface IAppActions {
}
export const AppLogic = kea>({
+ path: ['enterprise_search', 'app_search', 'app_logic'],
actions: {
initializeAppData: (props) => props,
},
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
index 94e9127bbed74..31c7680fd2f1c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
@@ -54,6 +54,7 @@ describe('AppSearchConfigured', () => {
const wrapper = shallow();
expect(wrapper.find(Layout)).toHaveLength(1);
+ expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy();
expect(wrapper.find(EngineOverview)).toHaveLength(1);
});
@@ -61,9 +62,9 @@ describe('AppSearchConfigured', () => {
const initializeAppData = jest.fn();
(useActions as jest.Mock).mockImplementation(() => ({ initializeAppData }));
- shallow();
+ shallow();
- expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true });
+ expect(initializeAppData).toHaveBeenCalledWith({ ilmEnabled: true });
});
it('does not re-initialize app data', () => {
@@ -83,6 +84,14 @@ describe('AppSearchConfigured', () => {
expect(wrapper.find(ErrorConnecting)).toHaveLength(1);
});
+
+ it('passes readOnlyMode state', () => {
+ (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true }));
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true);
+ });
});
describe('AppSearchNav', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
index c4a366930d22a..643c4b5ccc873 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
@@ -51,7 +51,7 @@ export const AppSearchUnconfigured: React.FC = () => (
export const AppSearchConfigured: React.FC = (props) => {
const { hasInitialized } = useValues(AppLogic);
const { initializeAppData } = useActions(AppLogic);
- const { errorConnecting } = useValues(HttpLogic);
+ const { errorConnecting, readOnlyMode } = useValues(HttpLogic);
useEffect(() => {
if (!hasInitialized) initializeAppData(props);
@@ -63,7 +63,7 @@ export const AppSearchConfigured: React.FC = (props) => {
- }>
+ } readOnlyMode={readOnlyMode}>
{errorConnecting ? (
) : (
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx
index a54295548004a..82f884644be4a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx
@@ -69,7 +69,11 @@ export const renderApp = (
>
-
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
index 3ae48f352b2c1..37a8f16acad6d 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts
@@ -32,6 +32,7 @@ const convertToArray = (messages: IFlashMessage | IFlashMessage[]) =>
!Array.isArray(messages) ? [messages] : messages;
export const FlashMessagesLogic = kea>({
+ path: ['enterprise_search', 'flash_messages_logic'],
actions: {
setFlashMessages: (messages) => ({ messages: convertToArray(messages) }),
clearFlashMessages: () => null,
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts
index c032e3b04ebe6..b65499be2f7c0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.test.ts
@@ -16,6 +16,7 @@ describe('HttpLogic', () => {
http: null,
httpInterceptors: [],
errorConnecting: false,
+ readOnlyMode: false,
};
beforeEach(() => {
@@ -31,12 +32,17 @@ describe('HttpLogic', () => {
describe('initializeHttp()', () => {
it('sets values based on passed props', () => {
HttpLogic.mount();
- HttpLogic.actions.initializeHttp({ http: mockHttp, errorConnecting: true });
+ HttpLogic.actions.initializeHttp({
+ http: mockHttp,
+ errorConnecting: true,
+ readOnlyMode: true,
+ });
expect(HttpLogic.values).toEqual({
http: mockHttp,
httpInterceptors: [],
errorConnecting: true,
+ readOnlyMode: true,
});
});
});
@@ -52,50 +58,110 @@ describe('HttpLogic', () => {
});
});
+ describe('setReadOnlyMode()', () => {
+ it('sets readOnlyMode value', () => {
+ HttpLogic.mount();
+ HttpLogic.actions.setReadOnlyMode(true);
+ expect(HttpLogic.values.readOnlyMode).toEqual(true);
+
+ HttpLogic.actions.setReadOnlyMode(false);
+ expect(HttpLogic.values.readOnlyMode).toEqual(false);
+ });
+ });
+
describe('http interceptors', () => {
describe('initializeHttpInterceptors()', () => {
beforeEach(() => {
HttpLogic.mount();
jest.spyOn(HttpLogic.actions, 'setHttpInterceptors');
- jest.spyOn(HttpLogic.actions, 'setErrorConnecting');
HttpLogic.actions.initializeHttp({ http: mockHttp });
-
HttpLogic.actions.initializeHttpInterceptors();
});
it('calls http.intercept and sets an array of interceptors', () => {
- mockHttp.intercept.mockImplementationOnce(() => 'removeInterceptorFn' as any);
+ mockHttp.intercept
+ .mockImplementationOnce(() => 'removeErrorInterceptorFn' as any)
+ .mockImplementationOnce(() => 'removeReadOnlyInterceptorFn' as any);
HttpLogic.actions.initializeHttpInterceptors();
expect(mockHttp.intercept).toHaveBeenCalled();
- expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith(['removeInterceptorFn']);
+ expect(HttpLogic.actions.setHttpInterceptors).toHaveBeenCalledWith([
+ 'removeErrorInterceptorFn',
+ 'removeReadOnlyInterceptorFn',
+ ]);
});
describe('errorConnectingInterceptor', () => {
+ let interceptedResponse: any;
+
+ beforeEach(() => {
+ interceptedResponse = mockHttp.intercept.mock.calls[0][0].responseError;
+ jest.spyOn(HttpLogic.actions, 'setErrorConnecting');
+ });
+
it('handles errors connecting to Enterprise Search', async () => {
- const { responseError } = mockHttp.intercept.mock.calls[0][0] as any;
- const httpResponse = { response: { url: '/api/app_search/engines', status: 502 } };
- await expect(responseError(httpResponse)).rejects.toEqual(httpResponse);
+ const httpResponse = {
+ response: { url: '/api/app_search/engines', status: 502 },
+ };
+ await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse);
expect(HttpLogic.actions.setErrorConnecting).toHaveBeenCalled();
});
it('does not handle non-502 Enterprise Search errors', async () => {
- const { responseError } = mockHttp.intercept.mock.calls[0][0] as any;
- const httpResponse = { response: { url: '/api/workplace_search/overview', status: 404 } };
- await expect(responseError(httpResponse)).rejects.toEqual(httpResponse);
+ const httpResponse = {
+ response: { url: '/api/workplace_search/overview', status: 404 },
+ };
+ await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse);
expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled();
});
- it('does not handle errors for unrelated calls', async () => {
- const { responseError } = mockHttp.intercept.mock.calls[0][0] as any;
- const httpResponse = { response: { url: '/api/some_other_plugin/', status: 502 } };
- await expect(responseError(httpResponse)).rejects.toEqual(httpResponse);
+ it('does not handle errors for non-Enterprise Search API calls', async () => {
+ const httpResponse = {
+ response: { url: '/api/some_other_plugin/', status: 502 },
+ };
+ await expect(interceptedResponse(httpResponse)).rejects.toEqual(httpResponse);
expect(HttpLogic.actions.setErrorConnecting).not.toHaveBeenCalled();
});
});
+
+ describe('readOnlyModeInterceptor', () => {
+ let interceptedResponse: any;
+
+ beforeEach(() => {
+ interceptedResponse = mockHttp.intercept.mock.calls[1][0].response;
+ jest.spyOn(HttpLogic.actions, 'setReadOnlyMode');
+ });
+
+ it('sets readOnlyMode to true if the response header is true', async () => {
+ const httpResponse = {
+ response: { url: '/api/app_search/engines', headers: { get: () => 'true' } },
+ };
+ await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse);
+
+ expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(true);
+ });
+
+ it('sets readOnlyMode to false if the response header is false', async () => {
+ const httpResponse = {
+ response: { url: '/api/workplace_search/overview', headers: { get: () => 'false' } },
+ };
+ await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse);
+
+ expect(HttpLogic.actions.setReadOnlyMode).toHaveBeenCalledWith(false);
+ });
+
+ it('does not handle headers for non-Enterprise Search API calls', async () => {
+ const httpResponse = {
+ response: { url: '/api/some_other_plugin/', headers: { get: () => 'true' } },
+ };
+ await expect(interceptedResponse(httpResponse)).resolves.toEqual(httpResponse);
+
+ expect(HttpLogic.actions.setReadOnlyMode).not.toHaveBeenCalled();
+ });
+ });
});
it('sets httpInterceptors and calls all valid remove functions on unmount', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
index ec9db30ddef3b..72380142fe399 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_logic.ts
@@ -6,32 +6,33 @@
import { kea, MakeLogicType } from 'kea';
-import { HttpSetup, HttpInterceptorResponseError } from 'src/core/public';
+import { HttpSetup, HttpInterceptorResponseError, HttpResponse } from 'src/core/public';
+import { IHttpProviderProps } from './http_provider';
+
+import { READ_ONLY_MODE_HEADER } from '../../../../common/constants';
export interface IHttpValues {
http: HttpSetup;
httpInterceptors: Function[];
errorConnecting: boolean;
+ readOnlyMode: boolean;
}
export interface IHttpActions {
- initializeHttp({
- http,
- errorConnecting,
- }: {
- http: HttpSetup;
- errorConnecting?: boolean;
- }): { http: HttpSetup; errorConnecting?: boolean };
+ initializeHttp({ http, errorConnecting, readOnlyMode }: IHttpProviderProps): IHttpProviderProps;
initializeHttpInterceptors(): void;
setHttpInterceptors(httpInterceptors: Function[]): { httpInterceptors: Function[] };
setErrorConnecting(errorConnecting: boolean): { errorConnecting: boolean };
+ setReadOnlyMode(readOnlyMode: boolean): { readOnlyMode: boolean };
}
export const HttpLogic = kea>({
+ path: ['enterprise_search', 'http_logic'],
actions: {
- initializeHttp: ({ http, errorConnecting }) => ({ http, errorConnecting }),
+ initializeHttp: (props) => props,
initializeHttpInterceptors: () => null,
setHttpInterceptors: (httpInterceptors) => ({ httpInterceptors }),
setErrorConnecting: (errorConnecting) => ({ errorConnecting }),
+ setReadOnlyMode: (readOnlyMode) => ({ readOnlyMode }),
},
reducers: {
http: [
@@ -53,6 +54,13 @@ export const HttpLogic = kea>({
setErrorConnecting: (_, { errorConnecting }) => errorConnecting,
},
],
+ readOnlyMode: [
+ false,
+ {
+ initializeHttp: (_, { readOnlyMode }) => !!readOnlyMode,
+ setReadOnlyMode: (_, { readOnlyMode }) => readOnlyMode,
+ },
+ ],
},
listeners: ({ values, actions }) => ({
initializeHttpInterceptors: () => {
@@ -60,13 +68,13 @@ export const HttpLogic = kea>({
const errorConnectingInterceptor = values.http.intercept({
responseError: async (httpResponse) => {
- const { url, status } = httpResponse.response!;
- const hasErrorConnecting = status === 502;
- const isApiResponse =
- url.includes('/api/app_search/') || url.includes('/api/workplace_search/');
+ if (isEnterpriseSearchApi(httpResponse)) {
+ const { status } = httpResponse.response!;
+ const hasErrorConnecting = status === 502;
- if (isApiResponse && hasErrorConnecting) {
- actions.setErrorConnecting(true);
+ if (hasErrorConnecting) {
+ actions.setErrorConnecting(true);
+ }
}
// Re-throw error so that downstream catches work as expected
@@ -75,7 +83,23 @@ export const HttpLogic = kea>({
});
httpInterceptors.push(errorConnectingInterceptor);
- // TODO: Read only mode interceptor
+ const readOnlyModeInterceptor = values.http.intercept({
+ response: async (httpResponse) => {
+ if (isEnterpriseSearchApi(httpResponse)) {
+ const readOnlyMode = httpResponse.response!.headers.get(READ_ONLY_MODE_HEADER);
+
+ if (readOnlyMode === 'true') {
+ actions.setReadOnlyMode(true);
+ } else {
+ actions.setReadOnlyMode(false);
+ }
+ }
+
+ return Promise.resolve(httpResponse);
+ },
+ });
+ httpInterceptors.push(readOnlyModeInterceptor);
+
actions.setHttpInterceptors(httpInterceptors);
},
}),
@@ -87,3 +111,11 @@ export const HttpLogic = kea>({
},
}),
});
+
+/**
+ * Small helper that checks whether or not an http call is for an Enterprise Search API
+ */
+const isEnterpriseSearchApi = (httpResponse: HttpResponse) => {
+ const { url } = httpResponse.response!;
+ return url.includes('/api/app_search/') || url.includes('/api/workplace_search/');
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx
index 81106235780d6..902c910f10d7c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.test.tsx
@@ -17,6 +17,7 @@ describe('HttpProvider', () => {
const props = {
http: {} as any,
errorConnecting: false,
+ readOnlyMode: false,
};
const initializeHttp = jest.fn();
const initializeHttpInterceptors = jest.fn();
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx
index 4c2160195a1af..db1b0d611079a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/http/http_provider.tsx
@@ -11,9 +11,10 @@ import { HttpSetup } from 'src/core/public';
import { HttpLogic } from './http_logic';
-interface IHttpProviderProps {
+export interface IHttpProviderProps {
http: HttpSetup;
errorConnecting?: boolean;
+ readOnlyMode?: boolean;
}
export const HttpProvider: React.FC = (props) => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss
index f6c83888413d3..e867e9cf5a445 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.scss
@@ -81,4 +81,15 @@
padding: $euiSize;
}
}
+
+ &__readOnlyMode {
+ margin: -$euiSizeM 0 $euiSizeL;
+
+ @include euiBreakpoint('m') {
+ margin: 0 0 $euiSizeL;
+ }
+ @include euiBreakpoint('xs', 's') {
+ margin: 0;
+ }
+ }
}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx
index 623e6e47167d2..7b876d81527fa 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx
@@ -6,7 +6,7 @@
import React from 'react';
import { shallow } from 'enzyme';
-import { EuiPageSideBar, EuiButton, EuiPageBody } from '@elastic/eui';
+import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui';
import { Layout, INavContext } from './layout';
@@ -55,6 +55,12 @@ describe('Layout', () => {
expect(wrapper.find(EuiPageSideBar).prop('className')).not.toContain('--isOpen');
});
+ it('renders a read-only mode callout', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+
it('renders children', () => {
const wrapper = shallow(
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx
index e122c4d5cfdfa..ef8216e8b6711 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx
@@ -7,7 +7,7 @@
import React, { useState } from 'react';
import classNames from 'classnames';
-import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton } from '@elastic/eui';
+import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import './layout.scss';
@@ -15,6 +15,7 @@ import './layout.scss';
interface ILayoutProps {
navigation: React.ReactNode;
restrictWidth?: boolean;
+ readOnlyMode?: boolean;
}
export interface INavContext {
@@ -22,7 +23,12 @@ export interface INavContext {
}
export const NavContext = React.createContext({});
-export const Layout: React.FC = ({ children, navigation, restrictWidth }) => {
+export const Layout: React.FC = ({
+ children,
+ navigation,
+ restrictWidth,
+ readOnlyMode,
+}) => {
const [isNavOpen, setIsNavOpen] = useState(false);
const toggleNavigation = () => setIsNavOpen(!isNavOpen);
const closeNavigation = () => setIsNavOpen(false);
@@ -56,6 +62,17 @@ export const Layout: React.FC = ({ children, navigation, restrictW
{navigation}
+ {readOnlyMode && (
+
+ )}
{children}
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
index f88a00f63f487..94bd1d529b65f 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts
@@ -22,6 +22,7 @@ export interface IAppActions {
}
export const AppLogic = kea>({
+ path: ['enterprise_search', 'workplace_search', 'app_logic'],
actions: {
initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({
workplaceSearch,
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
index 39280ad6f4be4..fc1943264d72b 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx
@@ -12,6 +12,7 @@ import { Redirect } from 'react-router-dom';
import { shallow } from 'enzyme';
import { useValues, useActions } from 'kea';
+import { Layout } from '../shared/layout';
import { SetupGuide } from './views/setup_guide';
import { ErrorState } from './views/error_state';
import { Overview } from './views/overview';
@@ -53,6 +54,7 @@ describe('WorkplaceSearchConfigured', () => {
it('renders with layout', () => {
const wrapper = shallow();
+ expect(wrapper.find(Layout).prop('readOnlyMode')).toBeFalsy();
expect(wrapper.find(Overview)).toHaveLength(1);
});
@@ -60,9 +62,9 @@ describe('WorkplaceSearchConfigured', () => {
const initializeAppData = jest.fn();
(useActions as jest.Mock).mockImplementation(() => ({ initializeAppData }));
- shallow();
+ shallow();
- expect(initializeAppData).toHaveBeenCalledWith({ readOnlyMode: true });
+ expect(initializeAppData).toHaveBeenCalledWith({ isFederatedAuth: true });
});
it('does not re-initialize app data', () => {
@@ -82,4 +84,12 @@ describe('WorkplaceSearchConfigured', () => {
expect(wrapper.find(ErrorState)).toHaveLength(2);
});
+
+ it('passes readOnlyMode state', () => {
+ (useValues as jest.Mock).mockImplementation(() => ({ readOnlyMode: true }));
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(Layout).prop('readOnlyMode')).toEqual(true);
+ });
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
index 6a51b49869eaf..a68dfaf8ea471 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx
@@ -31,7 +31,7 @@ export const WorkplaceSearch: React.FC = (props) => {
export const WorkplaceSearchConfigured: React.FC = (props) => {
const { hasInitialized } = useValues(AppLogic);
const { initializeAppData } = useActions(AppLogic);
- const { errorConnecting } = useValues(HttpLogic);
+ const { errorConnecting, readOnlyMode } = useValues(HttpLogic);
useEffect(() => {
if (!hasInitialized) initializeAppData(props);
@@ -46,7 +46,7 @@ export const WorkplaceSearchConfigured: React.FC = (props) => {
{errorConnecting ? : }
- }>
+ } readOnlyMode={readOnlyMode}>
{errorConnecting ? (
) : (
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
index 787d5295db1cf..a156b8a8009f9 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts
@@ -31,6 +31,7 @@ export interface IOverviewValues extends IOverviewServerData {
}
export const OverviewLogic = kea>({
+ path: ['enterprise_search', 'workplace_search', 'overview_logic'],
actions: {
setServerData: (serverData) => serverData,
initializeOverview: () => null,
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts
index 0c1e81e3aba46..3d0a3181f8ab8 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts
@@ -5,7 +5,7 @@
*/
import { mockConfig, mockLogger } from '../__mocks__';
-import { JSON_HEADER } from '../../common/constants';
+import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants';
import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler';
@@ -18,6 +18,9 @@ const responseMock = {
custom: jest.fn(),
customError: jest.fn(),
};
+const mockExpectedResponseHeaders = {
+ [READ_ONLY_MODE_HEADER]: 'false',
+};
describe('EnterpriseSearchRequestHandler', () => {
const enterpriseSearchRequestHandler = new EnterpriseSearchRequestHandler({
@@ -58,6 +61,7 @@ describe('EnterpriseSearchRequestHandler', () => {
expect(responseMock.custom).toHaveBeenCalledWith({
body: responseBody,
statusCode: 200,
+ headers: mockExpectedResponseHeaders,
});
});
@@ -112,11 +116,12 @@ describe('EnterpriseSearchRequestHandler', () => {
await makeAPICall(requestHandler);
EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/example');
- expect(responseMock.custom).toHaveBeenCalledWith({ body: {}, statusCode: 201 });
+ expect(responseMock.custom).toHaveBeenCalledWith({
+ body: {},
+ statusCode: 201,
+ headers: mockExpectedResponseHeaders,
+ });
});
-
- // TODO: It's possible we may also pass back headers at some point
- // from Enterprise Search, e.g. the x-read-only mode header
});
});
@@ -140,6 +145,7 @@ describe('EnterpriseSearchRequestHandler', () => {
message: 'some error message',
attributes: { errors: ['some error message'] },
},
+ headers: mockExpectedResponseHeaders,
});
});
@@ -156,6 +162,7 @@ describe('EnterpriseSearchRequestHandler', () => {
message: 'one,two,three',
attributes: { errors: ['one', 'two', 'three'] },
},
+ headers: mockExpectedResponseHeaders,
});
});
@@ -171,6 +178,7 @@ describe('EnterpriseSearchRequestHandler', () => {
message: 'Bad Request',
attributes: { errors: ['Bad Request'] },
},
+ headers: mockExpectedResponseHeaders,
});
});
@@ -186,6 +194,7 @@ describe('EnterpriseSearchRequestHandler', () => {
message: 'Bad Request',
attributes: { errors: ['Bad Request'] },
},
+ headers: mockExpectedResponseHeaders,
});
});
@@ -201,6 +210,7 @@ describe('EnterpriseSearchRequestHandler', () => {
message: 'Not Found',
attributes: { errors: ['Not Found'] },
},
+ headers: mockExpectedResponseHeaders,
});
});
});
@@ -215,12 +225,33 @@ describe('EnterpriseSearchRequestHandler', () => {
expect(responseMock.customError).toHaveBeenCalledWith({
statusCode: 502,
body: expect.stringContaining('Enterprise Search encountered an internal server error'),
+ headers: mockExpectedResponseHeaders,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'Enterprise Search Server Error 500 at : "something crashed!"'
);
});
+ it('handleReadOnlyModeError()', async () => {
+ EnterpriseSearchAPI.mockReturn(
+ { errors: ['Read only mode'] },
+ { status: 503, headers: { ...JSON_HEADER, [READ_ONLY_MODE_HEADER]: 'true' } }
+ );
+ const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/api/503' });
+
+ await makeAPICall(requestHandler);
+ EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/503');
+
+ expect(responseMock.customError).toHaveBeenCalledWith({
+ statusCode: 503,
+ body: expect.stringContaining('Enterprise Search is in read-only mode'),
+ headers: { [READ_ONLY_MODE_HEADER]: 'true' },
+ });
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'Cannot perform action: Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.'
+ );
+ });
+
it('handleInvalidDataError()', async () => {
EnterpriseSearchAPI.mockReturn({ results: false });
const requestHandler = enterpriseSearchRequestHandler.createRequest({
@@ -234,6 +265,7 @@ describe('EnterpriseSearchRequestHandler', () => {
expect(responseMock.customError).toHaveBeenCalledWith({
statusCode: 502,
body: 'Invalid data received from Enterprise Search',
+ headers: mockExpectedResponseHeaders,
});
expect(mockLogger.error).toHaveBeenCalledWith(
'Invalid data received from : {"results":false}'
@@ -250,6 +282,7 @@ describe('EnterpriseSearchRequestHandler', () => {
expect(responseMock.customError).toHaveBeenCalledWith({
statusCode: 502,
body: 'Error connecting to Enterprise Search: Failed',
+ headers: mockExpectedResponseHeaders,
});
expect(mockLogger.error).toHaveBeenCalled();
});
@@ -265,6 +298,7 @@ describe('EnterpriseSearchRequestHandler', () => {
expect(responseMock.customError).toHaveBeenCalledWith({
statusCode: 502,
body: 'Cannot authenticate Enterprise Search user',
+ headers: mockExpectedResponseHeaders,
});
expect(mockLogger.error).toHaveBeenCalled();
});
@@ -279,6 +313,18 @@ describe('EnterpriseSearchRequestHandler', () => {
});
});
+ it('setResponseHeaders', async () => {
+ EnterpriseSearchAPI.mockReturn('anything' as any, {
+ headers: { [READ_ONLY_MODE_HEADER]: 'true' },
+ });
+ const requestHandler = enterpriseSearchRequestHandler.createRequest({ path: '/' });
+ await makeAPICall(requestHandler);
+
+ expect(enterpriseSearchRequestHandler.headers).toEqual({
+ [READ_ONLY_MODE_HEADER]: 'true',
+ });
+ });
+
it('isEmptyObj', async () => {
expect(enterpriseSearchRequestHandler.isEmptyObj({})).toEqual(true);
expect(enterpriseSearchRequestHandler.isEmptyObj({ empty: false })).toEqual(false);
@@ -304,9 +350,10 @@ const EnterpriseSearchAPI = {
...expectedParams,
});
},
- mockReturn(response: object, options?: object) {
+ mockReturn(response: object, options?: any) {
fetchMock.mockImplementation(() => {
- return Promise.resolve(new Response(JSON.stringify(response), options));
+ const headers = Object.assign({}, mockExpectedResponseHeaders, options?.headers);
+ return Promise.resolve(new Response(JSON.stringify(response), { ...options, headers }));
});
},
mockReturnError() {
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts
index 00d5eaf5d6a83..6b65c16c832fd 100644
--- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts
@@ -14,7 +14,7 @@ import {
Logger,
} from 'src/core/server';
import { ConfigType } from '../index';
-import { JSON_HEADER } from '../../common/constants';
+import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants';
interface IConstructorDependencies {
config: ConfigType;
@@ -46,6 +46,7 @@ export interface IEnterpriseSearchRequestHandler {
export class EnterpriseSearchRequestHandler {
private enterpriseSearchUrl: string;
private log: Logger;
+ private headers: Record = {};
constructor({ config, log }: IConstructorDependencies) {
this.log = log;
@@ -80,6 +81,9 @@ export class EnterpriseSearchRequestHandler {
// Call the Enterprise Search API
const apiResponse = await fetch(url, { method, headers, body });
+ // Handle response headers
+ this.setResponseHeaders(apiResponse);
+
// Handle authentication redirects
if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) {
return this.handleAuthenticationError(response);
@@ -88,7 +92,13 @@ export class EnterpriseSearchRequestHandler {
// Handle 400-500+ responses from the Enterprise Search server
const { status } = apiResponse;
if (status >= 500) {
- return this.handleServerError(response, apiResponse, url);
+ if (this.headers[READ_ONLY_MODE_HEADER] === 'true') {
+ // Handle 503 read-only mode errors
+ return this.handleReadOnlyModeError(response);
+ } else {
+ // Handle unexpected server errors
+ return this.handleServerError(response, apiResponse, url);
+ }
} else if (status >= 400) {
return this.handleClientError(response, apiResponse);
}
@@ -100,7 +110,11 @@ export class EnterpriseSearchRequestHandler {
}
// Pass successful responses back to the front-end
- return response.custom({ statusCode: status, body: json });
+ return response.custom({
+ statusCode: status,
+ headers: this.headers,
+ body: json,
+ });
} catch (e) {
// Catch connection/auth errors
return this.handleConnectionError(response, e);
@@ -160,7 +174,7 @@ export class EnterpriseSearchRequestHandler {
const { status } = apiResponse;
const body = await this.getErrorResponseBody(apiResponse);
- return response.customError({ statusCode: status, body });
+ return response.customError({ statusCode: status, headers: this.headers, body });
}
async handleServerError(response: KibanaResponseFactory, apiResponse: Response, url: string) {
@@ -172,14 +186,22 @@ export class EnterpriseSearchRequestHandler {
'Enterprise Search encountered an internal server error. Please contact your system administrator if the problem persists.';
this.log.error(`Enterprise Search Server Error ${status} at <${url}>: ${message}`);
- return response.customError({ statusCode: 502, body: errorMessage });
+ return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage });
+ }
+
+ handleReadOnlyModeError(response: KibanaResponseFactory) {
+ const errorMessage =
+ 'Enterprise Search is in read-only mode. Actions that create, update, or delete information are disabled.';
+
+ this.log.error(`Cannot perform action: ${errorMessage}`);
+ return response.customError({ statusCode: 503, headers: this.headers, body: errorMessage });
}
handleInvalidDataError(response: KibanaResponseFactory, url: string, json: object) {
const errorMessage = 'Invalid data received from Enterprise Search';
this.log.error(`Invalid data received from <${url}>: ${JSON.stringify(json)}`);
- return response.customError({ statusCode: 502, body: errorMessage });
+ return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage });
}
handleConnectionError(response: KibanaResponseFactory, e: Error) {
@@ -188,14 +210,26 @@ export class EnterpriseSearchRequestHandler {
this.log.error(errorMessage);
if (e instanceof Error) this.log.debug(e.stack as string);
- return response.customError({ statusCode: 502, body: errorMessage });
+ return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage });
}
handleAuthenticationError(response: KibanaResponseFactory) {
const errorMessage = 'Cannot authenticate Enterprise Search user';
this.log.error(errorMessage);
- return response.customError({ statusCode: 502, body: errorMessage });
+ return response.customError({ statusCode: 502, headers: this.headers, body: errorMessage });
+ }
+
+ /**
+ * Set response headers
+ *
+ * Currently just forwards the read-only mode header, but we can expand this
+ * in the future to pass more headers from Enterprise Search as we need them
+ */
+
+ setResponseHeaders(apiResponse: Response) {
+ const readOnlyMode = apiResponse.headers.get(READ_ONLY_MODE_HEADER);
+ this.headers[READ_ONLY_MODE_HEADER] = readOnlyMode as 'true' | 'false';
}
/**
diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts
index 24aae3a69ee5d..e89cf06ec8621 100644
--- a/x-pack/plugins/features/server/feature_registry.test.ts
+++ b/x-pack/plugins/features/server/feature_registry.test.ts
@@ -57,7 +57,7 @@ describe('FeatureRegistry', () => {
read: {
savedObject: {
all: [],
- read: ['config', 'url'],
+ read: ['config', 'url', 'telemetry'],
},
ui: [],
},
@@ -230,7 +230,7 @@ describe('FeatureRegistry', () => {
expect(allPrivilege?.savedObject.all).toEqual(['telemetry']);
});
- it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => {
+ it(`automatically grants access to config, url, and telemetry saved objects`, () => {
const feature: KibanaFeatureConfig = {
id: 'test-feature',
name: 'Test Feature',
@@ -263,7 +263,7 @@ describe('FeatureRegistry', () => {
const allPrivilege = result[0].privileges?.all;
const readPrivilege = result[0].privileges?.read;
expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']);
- expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']);
+ expect(readPrivilege?.savedObject.read).toEqual(['config', 'telemetry', 'url']);
});
it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => {
@@ -332,7 +332,7 @@ describe('FeatureRegistry', () => {
const readPrivilege = result[0].privileges!.read;
expect(allPrivilege?.savedObject.all).toEqual(['telemetry']);
expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']);
- expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']);
+ expect(readPrivilege?.savedObject.read).toEqual(['config', 'url', 'telemetry']);
});
it(`does not allow duplicate features to be registered`, () => {
diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts
index d357bdb782797..e9e556ba22fd2 100644
--- a/x-pack/plugins/features/server/feature_registry.ts
+++ b/x-pack/plugins/features/server/feature_registry.ts
@@ -97,7 +97,12 @@ function applyAutomaticReadPrivilegeGrants(
) {
readPrivileges.forEach((readPrivilege) => {
if (readPrivilege) {
- readPrivilege.savedObject.read = uniq([...readPrivilege.savedObject.read, 'config', 'url']);
+ readPrivilege.savedObject.read = uniq([
+ ...readPrivilege.savedObject.read,
+ 'config',
+ 'telemetry',
+ 'url',
+ ]);
}
});
}
diff --git a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
index 0217f039e08ba..7bb9954fa3048 100644
--- a/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
+++ b/x-pack/plugins/global_search_bar/public/components/__snapshots__/search_bar.test.tsx.snap
@@ -7,8 +7,13 @@ Array [
"className": "euiSelectableTemplateSitewide__listItem",
"key": "Canvas",
"label": "Canvas",
+ "meta": Array [
+ Object {
+ "text": "Kibana",
+ },
+ ],
"prepend": undefined,
- "title": "Canvasundefinedundefined",
+ "title": "Canvas • Kibana",
"url": "/app/test/Canvas",
},
Object {
@@ -16,8 +21,13 @@ Array [
"className": "euiSelectableTemplateSitewide__listItem",
"key": "Discover",
"label": "Discover",
+ "meta": Array [
+ Object {
+ "text": "Kibana",
+ },
+ ],
"prepend": undefined,
- "title": "Discoverundefinedundefined",
+ "title": "Discover • Kibana",
"url": "/app/test/Discover",
},
Object {
@@ -25,8 +35,13 @@ Array [
"className": "euiSelectableTemplateSitewide__listItem",
"key": "Graph",
"label": "Graph",
+ "meta": Array [
+ Object {
+ "text": "Kibana",
+ },
+ ],
"prepend": undefined,
- "title": "Graphundefinedundefined",
+ "title": "Graph • Kibana",
"url": "/app/test/Graph",
},
]
@@ -39,8 +54,13 @@ Array [
"className": "euiSelectableTemplateSitewide__listItem",
"key": "Discover",
"label": "Discover",
+ "meta": Array [
+ Object {
+ "text": "Kibana",
+ },
+ ],
"prepend": undefined,
- "title": "Discoverundefinedundefined",
+ "title": "Discover • Kibana",
"url": "/app/test/Discover",
},
Object {
diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx
index 0d1e8725b4911..11fbc7931e620 100644
--- a/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx
+++ b/x-pack/plugins/global_search_bar/public/components/search_bar.test.tsx
@@ -21,6 +21,7 @@ type Result = { id: string; type: string } | string;
const createResult = (result: Result): GlobalSearchResult => {
const id = typeof result === 'string' ? result : result.id;
const type = typeof result === 'string' ? 'application' : result.type;
+ const meta = type === 'application' ? { categoryLabel: 'Kibana' } : { categoryLabel: null };
return {
id,
@@ -28,6 +29,7 @@ const createResult = (result: Result): GlobalSearchResult => {
title: id,
url: `/app/test/${id}`,
score: 42,
+ meta,
};
};
@@ -74,7 +76,7 @@ describe('SearchBar', () => {
expect(findSpy).toHaveBeenCalledTimes(1);
expect(findSpy).toHaveBeenCalledWith('', {});
expect(getSelectableProps(component).options).toMatchSnapshot();
- await wait(() => getSearchProps(component).onSearch('d'));
+ await wait(() => getSearchProps(component).onKeyUpCapture({ currentTarget: { value: 'd' } }));
jest.runAllTimers();
component.update();
expect(getSelectableProps(component).options).toMatchSnapshot();
diff --git a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx
index d00349e21a7e4..e41f9243198ad 100644
--- a/x-pack/plugins/global_search_bar/public/components/search_bar.tsx
+++ b/x-pack/plugins/global_search_bar/public/components/search_bar.tsx
@@ -52,14 +52,20 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) {
if (!isMounted()) return;
_setOptions([
- ..._options.map((option) => ({
- key: option.id,
- label: option.title,
- url: option.url,
- ...(option.icon && { icon: { type: option.icon } }),
- ...(option.type &&
- option.type !== 'application' && { meta: [{ text: cleanMeta(option.type) }] }),
- })),
+ ..._options.map(({ id, title, url, icon, type, meta }) => {
+ const option: EuiSelectableTemplateSitewideOption = {
+ key: id,
+ label: title,
+ url,
+ };
+
+ if (icon) option.icon = { type: icon };
+
+ if (type === 'application') option.meta = [{ text: meta?.categoryLabel as string }];
+ else option.meta = [{ text: cleanMeta(type) }];
+
+ return option;
+ }),
]);
},
[isMounted, _setOptions]
@@ -133,7 +139,8 @@ export function SearchBar({ globalSearch, navigateToUrl }: Props) {
onChange={onChange}
options={options}
searchProps={{
- onSearch: setSearchValue,
+ onKeyUpCapture: (e: React.KeyboardEvent) =>
+ setSearchValue(e.currentTarget.value),
'data-test-subj': 'header-search',
inputRef: setSearchRef,
compressed: true,
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts
index a2d5c7c8d5308..b3bf071948956 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts
@@ -69,6 +69,8 @@ export * from './other_type_name_parameter';
export * from './other_type_json_parameter';
+export * from './meta_parameter';
+
export * from './ignore_above_parameter';
export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer];
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx
new file mode 100644
index 0000000000000..c8af296318b61
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { FunctionComponent } from 'react';
+
+import { i18n } from '@kbn/i18n';
+
+import { documentationService } from '../../../../../services/documentation';
+import { UseField, JsonEditorField } from '../../../shared_imports';
+import { getFieldConfig } from '../../../lib';
+import { EditFieldFormRow } from '../fields/edit_field';
+
+interface Props {
+ defaultToggleValue: boolean;
+}
+
+export const MetaParameter: FunctionComponent = ({ defaultToggleValue }) => (
+
+
+
+);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx
index ba9c75baa1987..1550485ebad93 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/binary_type.tsx
@@ -5,14 +5,25 @@
*/
import React from 'react';
-import { StoreParameter, DocValuesParameter } from '../../field_parameters';
+import { NormalizedField, ParameterName, Field as FieldType } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { StoreParameter, DocValuesParameter, MetaParameter } from '../../field_parameters';
import { AdvancedParametersSection } from '../edit_field';
-export const BinaryType = () => {
+const getDefaultToggleValue = (param: ParameterName, field: FieldType) => {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+};
+
+interface Props {
+ field: NormalizedField;
+}
+
+export const BinaryType = ({ field }: Props) => {
return (
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx
index 962606b2f4ffd..1ee2bf22edb44 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/boolean_type.tsx
@@ -16,11 +16,13 @@ import {
DocValuesParameter,
BoostParameter,
NullValueParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
+ case 'meta':
case 'boost': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -90,6 +92,8 @@ export const BooleanType = ({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx
index 74331cb1b6b22..748dc54838270 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/completion_type.tsx
@@ -10,11 +10,12 @@ import { i18n } from '@kbn/i18n';
import { NormalizedField, Field as FieldType } from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import { UseField, Field } from '../../../../shared_imports';
-import { AnalyzersParameter } from '../../field_parameters';
+import { AnalyzersParameter, MetaParameter } from '../../field_parameters';
import { EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
+ case 'meta':
case 'max_input_length': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -88,6 +89,8 @@ export const CompletionType = ({ field }: Props) => {
)}
formFieldPath="preserve_position_increments"
/>
+
+
);
};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.tsx
new file mode 100644
index 0000000000000..aa8aefba921e7
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/constant_keyword_type.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 { UseField, Field } from '../../../../shared_imports';
+import { getFieldConfig } from '../../../../lib';
+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 (
+ <>
+
+ {/* Value field */}
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx
index 0c067d09046d7..35382506a3cd9 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/date_type.tsx
@@ -19,6 +19,7 @@ import {
IgnoreMalformedParameter,
FormatParameter,
LocaleParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
@@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
case 'locale':
case 'format':
+ case 'meta':
case 'boost': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -73,6 +75,8 @@ export const DateType = ({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx
index e96426ece27e8..b1545d44885c8 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx
@@ -18,6 +18,7 @@ import {
NullValueParameter,
SimilarityParameter,
SplitQueriesOnWhitespaceParameter,
+ MetaParameter,
IgnoreAboveParameter,
} from '../../field_parameters';
import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
@@ -30,6 +31,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
case 'boost':
case 'ignore_above':
+ case 'meta':
case 'similarity': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -83,6 +85,8 @@ export const FlattenedType = React.memo(({ field }: Props) => {
defaultToggleValue={getDefaultToggleValue('null_value', field.source)}
/>
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx
index 997e866da35f0..0f28c5080d26d 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/geo_point_type.tsx
@@ -14,11 +14,14 @@ import {
IgnoreMalformedParameter,
NullValueParameter,
IgnoreZValueParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
+ case 'meta':
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
case 'null_value': {
return field.null_value !== undefined;
}
@@ -65,6 +68,8 @@ export const GeoPointType = ({ field }: Props) => {
config={getFieldConfig('null_value_geo_point')}
/>
+
+
>
);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx
new file mode 100644
index 0000000000000..1ff97a8d72a21
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/histogram_type.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React from 'react';
+
+import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
+import { getFieldConfig } from '../../../../lib';
+import { IgnoreMalformedParameter, MetaParameter } from '../../field_parameters';
+import { AdvancedParametersSection } from '../edit_field';
+
+interface Props {
+ field: NormalizedField;
+}
+
+const getDefaultToggleValue = (param: ParameterName, field: FieldType) => {
+ return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
+};
+
+export const HistogramType = ({ field }: Props) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts
index d84d9c6ea40cf..8fcd02e4a362e 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts
@@ -28,6 +28,8 @@ 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';
@@ -54,6 +56,8 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = {
other: OtherType,
nested: NestedType,
join: JoinType,
+ histogram: HistogramType,
+ constant_keyword: ConstantKeywordType,
rank_feature: RankFeatureType,
wildcard: WildcardType,
};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
index 3d78205934eea..6ad3c9c5d0bd4 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/numeric_type.tsx
@@ -18,6 +18,7 @@ import {
CoerceNumberParameter,
IgnoreMalformedParameter,
CopyToParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
import { PARAMETERS_DEFINITION } from '../../../../constants';
@@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
case 'copy_to':
case 'boost':
+ case 'meta':
case 'ignore_malformed': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -95,6 +97,8 @@ export const NumericType = ({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
index f87d1f9400101..9a37f55ac8e9d 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/range_type.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import { NormalizedField, Field as FieldType } from '../../../../types';
+import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types';
import { getFieldConfig } from '../../../../lib';
import {
StoreParameter,
@@ -14,11 +14,12 @@ import {
CoerceNumberParameter,
FormatParameter,
LocaleParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
import { FormDataProvider } from '../../../../shared_imports';
-const getDefaultToggleValue = (param: 'locale' | 'format' | 'boost', field: FieldType) => {
+const getDefaultToggleValue = (param: ParameterName, field: FieldType) => {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
};
@@ -57,6 +58,8 @@ export const RangeType = ({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx
index dafbebd24b3fa..3fa456c33f5e9 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/search_as_you_type.tsx
@@ -15,6 +15,7 @@ import {
SimilarityParameter,
TermVectorParameter,
MaxShingleSizeParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, AdvancedParametersSection } from '../edit_field';
@@ -26,6 +27,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
case 'similarity':
case 'term_vector':
+ case 'meta':
case 'max_shingle_size': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -65,6 +67,8 @@ export const SearchAsYouType = React.memo(({ field }: Props) => {
/>
+
+
>
);
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx
index c4ed11097b609..07def791096e7 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/text_type.tsx
@@ -28,6 +28,7 @@ import {
CopyToParameter,
TermVectorParameter,
FieldDataParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
@@ -40,6 +41,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
case 'boost':
case 'position_increment_gap':
case 'similarity':
+ case 'meta':
case 'term_vector': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -47,7 +49,7 @@ const getDefaultToggleValue = (param: string, field: FieldType) => {
return field.search_analyzer !== undefined && field.search_analyzer !== field.analyzer;
}
case 'copy_to': {
- return field.null_value !== undefined && field.null_value !== '';
+ return field[param] !== undefined && field[param] !== '';
}
case 'indexPrefixes': {
if (field.index_prefixes === undefined) {
@@ -241,6 +243,8 @@ export const TextType = React.memo(({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx
index 42854673269ae..5cc2addba53b8 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/token_count_type.tsx
@@ -20,12 +20,14 @@ import {
BoostParameter,
AnalyzerParameter,
NullValueParameter,
+ MetaParameter,
} from '../../field_parameters';
import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field';
const getDefaultToggleValue = (param: string, field: FieldType) => {
switch (param) {
case 'analyzer':
+ case 'meta':
case 'boost': {
return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue;
}
@@ -107,6 +109,8 @@ export const TokenCountType = ({ field }: Props) => {
+
+
>
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx
index a8844c7a9b270..a4d3bf3832d5c 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx
@@ -71,6 +71,26 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {
),
},
+ constant_keyword: {
+ value: 'constant_keyword',
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.constantKeywordDescription', {
+ defaultMessage: 'Constant keyword',
+ }),
+ documentation: {
+ main: '/keyword.html#constant-keyword-field-type',
+ },
+ description: () => (
+
+ {'keyword'},
+ }}
+ />
+
+ ),
+ },
numeric: {
value: 'numeric',
label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.numericDescription', {
@@ -699,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',
@@ -822,6 +859,7 @@ export const MAIN_TYPES: MainType[] = [
'binary',
'boolean',
'completion',
+ 'constant_keyword',
'date',
'date_nanos',
'dense_vector',
@@ -842,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/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx
index f2148f1f657a6..fd17dc1b8fd1e 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx
@@ -29,7 +29,7 @@ import { INDEX_DEFAULT } from './default_values';
import { TYPE_DEFINITION } from './data_types_definition';
const { toInt } = fieldFormatters;
-const { emptyField, containsCharsField, numberGreaterThanField } = fieldValidators;
+const { emptyField, containsCharsField, numberGreaterThanField, isJsonField } = fieldValidators;
const commonErrorMessages = {
smallerThanZero: i18n.translate(
@@ -404,6 +404,88 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio
},
schema: t.string,
},
+ value: {
+ fieldConfig: {
+ defaultValue: '',
+ type: FIELD_TYPES.TEXT,
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.valueLabel', {
+ defaultMessage: 'Value',
+ }),
+ },
+ schema: t.string,
+ },
+ meta: {
+ fieldConfig: {
+ defaultValue: '',
+ label: i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.metaLabel', {
+ defaultMessage: 'Metadata',
+ }),
+ helpText: (
+ {JSON.stringify({ arbitrary_key: 'anything_goes' })},
+ }}
+ />
+ ),
+ validations: [
+ {
+ validator: isJsonField(
+ i18n.translate('xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorJsonError', {
+ defaultMessage: 'Invalid JSON.',
+ }),
+ { allowEmptyString: true }
+ ),
+ },
+ {
+ validator: ({ value }: ValidationFuncArg) => {
+ if (typeof value !== 'string' || value.trim() === '') {
+ return;
+ }
+
+ const json = JSON.parse(value);
+ const valuesAreNotString = Object.values(json).some((v) => typeof v !== 'string');
+
+ if (Array.isArray(json)) {
+ return {
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorArraysNotAllowedError',
+ {
+ defaultMessage: 'Arrays are not allowed.',
+ }
+ ),
+ };
+ } else if (valuesAreNotString) {
+ return {
+ message: i18n.translate(
+ 'xpack.idxMgmt.mappingsEditor.parameters.metaFieldEditorOnlyStringValuesAllowedError',
+ {
+ defaultMessage: 'Values must be a string.',
+ }
+ ),
+ };
+ }
+ },
+ },
+ ],
+ deserializer: (value: any) => {
+ if (value === '') {
+ return value;
+ }
+ return JSON.stringify(value, null, 2);
+ },
+ serializer: (value: string) => {
+ const parsed = JSON.parse(value);
+ // If an empty object was passed, strip out this value entirely.
+ if (!Object.keys(parsed).length) {
+ return undefined;
+ }
+ return parsed;
+ },
+ },
+ schema: t.any,
+ },
max_input_length: {
fieldConfig: {
defaultValue: 50,
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 fd0e4ed32bfe8..97dca49fc93ed 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts
@@ -59,6 +59,8 @@ export type MainType =
| 'geo_point'
| 'geo_shape'
| 'token_count'
+ | 'histogram'
+ | 'constant_keyword'
| 'wildcard'
/**
* 'other' is a special type that only exists inside of MappingsEditor as a placeholder
@@ -146,7 +148,9 @@ export type ParameterName =
| 'dims'
| 'depth_limit'
| 'relations'
- | 'max_shingle_size';
+ | 'max_shingle_size'
+ | 'value'
+ | 'meta';
export interface Parameter {
fieldConfig: FieldConfig;
diff --git a/x-pack/plugins/index_management/public/application/services/documentation.ts b/x-pack/plugins/index_management/public/application/services/documentation.ts
index afc9c76f1afbe..c52b958d94ae1 100644
--- a/x-pack/plugins/index_management/public/application/services/documentation.ts
+++ b/x-pack/plugins/index_management/public/application/services/documentation.ts
@@ -123,6 +123,10 @@ class DocumentationService {
return `${this.esDocsBase}/ignore-malformed.html`;
}
+ public getMetaLink() {
+ return `${this.esDocsBase}/mapping-field-meta.html`;
+ }
+
public getFormatLink() {
return `${this.esDocsBase}/mapping-date-format.html`;
}
diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
index 605e4db230ce5..b9789b770eb2e 100644
--- a/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/agent/handlers.ts
@@ -30,6 +30,7 @@ import {
import * as AgentService from '../../services/agents';
import * as APIKeyService from '../../services/api_keys';
import { appContextService } from '../../services/app_context';
+import { defaultIngestErrorHandler } from '../../errors';
export const getAgentHandler: RequestHandler = ({
onClick={() => {
setOpen(!open);
}}
+ title={title}
hasArrow={false}
isDisabled={isDisabled}
groupPosition={groupPosition}
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
index 7e2e8f0453588..2114d63fcfacd 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
@@ -7,7 +7,7 @@
import React from 'react';
import { mountWithIntl as mount, shallowWithIntl as shallow } from 'test_utils/enzyme_helpers';
import { EuiButtonGroupProps, EuiSuperSelect, EuiButtonGroup } from '@elastic/eui';
-import { LayerContextMenu, XyToolbar } from './xy_config_panel';
+import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
import { ToolbarPopover } from '../shared_components';
import { AxisSettingsPopover } from './axis_settings_popover';
import { FramePublicAPI } from '../types';
@@ -171,4 +171,48 @@ describe('XY Config panels', () => {
expect(component.find(AxisSettingsPopover).length).toEqual(3);
});
});
+
+ describe('Dimension Editor', () => {
+ test('shows the correct axis side options when in horizontal mode', () => {
+ const state = testState();
+ const component = mount(
+
+ );
+
+ const options = component
+ .find(EuiButtonGroup)
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
+
+ expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Bottom', 'Top']);
+ });
+
+ test('shows the default axis side options when not in horizontal mode', () => {
+ const state = testState();
+ const component = mount(
+
+ );
+
+ const options = component
+ .find(EuiButtonGroup)
+ .first()
+ .prop('options') as EuiButtonGroupProps['options'];
+
+ expect(options!.map(({ label }) => label)).toEqual(['Auto', 'Left', 'Right']);
+ });
+ });
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index bc98bf53d9f12..4aa5bd62c05a5 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -274,9 +274,15 @@ export function XyToolbar(props: VisualizationToolbarProps) {
group.groupId === 'left') || {}).length === 0
}
@@ -310,9 +316,15 @@ export function XyToolbar(props: VisualizationToolbarProps) {
toggleAxisTitleVisibility={onAxisTitlesVisibilitySettingsChange}
/>
group.groupId === 'right') || {}).length === 0
}
@@ -345,6 +357,7 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps)
const { state, setState, layerId, accessor } = props;
const index = state.layers.findIndex((l) => l.layerId === layerId);
const layer = state.layers[index];
+ const isHorizontal = isHorizontalChart(state.layers);
const axisMode =
(layer.yConfig &&
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) ||
@@ -377,15 +390,23 @@ export function DimensionEditor(props: VisualizationDimensionEditorProps)
},
{
id: `${idPrefix}left`,
- label: i18n.translate('xpack.lens.xyChart.axisSide.left', {
- defaultMessage: 'Left',
- }),
+ label: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.axisSide.bottom', {
+ defaultMessage: 'Bottom',
+ })
+ : i18n.translate('xpack.lens.xyChart.axisSide.left', {
+ defaultMessage: 'Left',
+ }),
},
{
id: `${idPrefix}right`,
- label: i18n.translate('xpack.lens.xyChart.axisSide.right', {
- defaultMessage: 'Right',
- }),
+ label: isHorizontal
+ ? i18n.translate('xpack.lens.xyChart.axisSide.top', {
+ defaultMessage: 'Top',
+ })
+ : i18n.translate('xpack.lens.xyChart.axisSide.right', {
+ defaultMessage: 'Right',
+ }),
},
]}
idSelected={`${idPrefix}${axisMode}`}
diff --git a/x-pack/plugins/ml/common/constants/app.ts b/x-pack/plugins/ml/common/constants/app.ts
index 97dd7a7b0fef5..3d54e9e150fef 100644
--- a/x-pack/plugins/ml/common/constants/app.ts
+++ b/x-pack/plugins/ml/common/constants/app.ts
@@ -4,6 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { i18n } from '@kbn/i18n';
+
export const PLUGIN_ID = 'ml';
export const PLUGIN_ICON = 'machineLearningApp';
export const PLUGIN_ICON_SOLUTION = 'logoKibana';
+export const ML_APP_NAME = i18n.translate('xpack.ml.navMenu.mlAppNameText', {
+ defaultMessage: 'Machine Learning',
+});
diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
index 830537cbadbc8..9a7af2496c03f 100644
--- a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts
@@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export const ANALYSIS_CONFIG_TYPE = {
+ OUTLIER_DETECTION: 'outlier_detection',
+ REGRESSION: 'regression',
+ CLASSIFICATION: 'classification',
+} as const;
export const DEFAULT_RESULTS_FIELD = 'ml';
diff --git a/x-pack/plugins/ml/common/constants/ml_url_generator.ts b/x-pack/plugins/ml/common/constants/ml_url_generator.ts
index 44f33aa329e7a..541b8af6fc0fc 100644
--- a/x-pack/plugins/ml/common/constants/ml_url_generator.ts
+++ b/x-pack/plugins/ml/common/constants/ml_url_generator.ts
@@ -31,8 +31,16 @@ export const ML_PAGES = {
* Open index data visualizer viewer page
*/
DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer',
+ ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`,
ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`,
+ ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`,
SETTINGS: 'settings',
CALENDARS_MANAGE: 'settings/calendars_list',
+ CALENDARS_NEW: 'settings/calendars_list/new_calendar',
+ CALENDARS_EDIT: 'settings/calendars_list/edit_calendar',
FILTER_LISTS_MANAGE: 'settings/filter_lists',
+ FILTER_LISTS_NEW: 'settings/filter_lists/new_filter_list',
+ FILTER_LISTS_EDIT: 'settings/filter_lists/edit_filter_list',
+ ACCESS_DENIED: 'access-denied',
+ OVERVIEW: 'overview',
} as const;
diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
index 96d6c81a3d309..5d0ecf96fb6b5 100644
--- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts
+++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts
@@ -6,6 +6,7 @@
import Boom from 'boom';
import { EsErrorBody } from '../util/errors';
+import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics';
export interface DeleteDataFrameAnalyticsWithIndexStatus {
success: boolean;
@@ -81,8 +82,4 @@ export interface DataFrameAnalyticsConfig {
allow_lazy_start?: boolean;
}
-export enum ANALYSIS_CONFIG_TYPE {
- OUTLIER_DETECTION = 'outlier_detection',
- REGRESSION = 'regression',
- CLASSIFICATION = 'classification',
-}
+export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE];
diff --git a/x-pack/plugins/ml/common/types/ml_url_generator.ts b/x-pack/plugins/ml/common/types/ml_url_generator.ts
index 234be8b6faf90..d176c22bdbb62 100644
--- a/x-pack/plugins/ml/common/types/ml_url_generator.ts
+++ b/x-pack/plugins/ml/common/types/ml_url_generator.ts
@@ -5,27 +5,21 @@
*/
import { RefreshInterval, TimeRange } from '../../../../../src/plugins/data/common/query';
-import { JobId } from '../../../reporting/common/types';
+import { JobId } from './anomaly_detection_jobs/job';
import { ML_PAGES } from '../constants/ml_url_generator';
+import { DataFrameAnalysisConfigType } from './data_frame_analytics';
type OptionalPageState = object | undefined;
export type MLPageState = PageState extends OptionalPageState
- ? { page: PageType; pageState?: PageState }
+ ? { page: PageType; pageState?: PageState; excludeBasePath?: boolean }
: PageState extends object
- ? { page: PageType; pageState: PageState }
- : { page: PageType };
-
-export const ANALYSIS_CONFIG_TYPE = {
- OUTLIER_DETECTION: 'outlier_detection',
- REGRESSION: 'regression',
- CLASSIFICATION: 'classification',
-} as const;
-
-type DataFrameAnalyticsType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE];
+ ? { page: PageType; pageState: PageState; excludeBasePath?: boolean }
+ : { page: PageType; excludeBasePath?: boolean };
export interface MlCommonGlobalState {
time?: TimeRange;
+ refreshInterval?: RefreshInterval;
}
export interface MlCommonAppState {
[key: string]: any;
@@ -42,16 +36,28 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState {
[key: string]: any;
}
-export interface MlGenericUrlState {
- page:
- | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
- | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE;
- pageState: MlGenericUrlPageState;
-}
+export type MlGenericUrlState = MLPageState<
+ | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
+ | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB
+ | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
+ | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
+ | typeof ML_PAGES.OVERVIEW
+ | typeof ML_PAGES.CALENDARS_MANAGE
+ | typeof ML_PAGES.CALENDARS_NEW
+ | typeof ML_PAGES.FILTER_LISTS_MANAGE
+ | typeof ML_PAGES.FILTER_LISTS_NEW
+ | typeof ML_PAGES.SETTINGS
+ | typeof ML_PAGES.ACCESS_DENIED
+ | typeof ML_PAGES.DATA_VISUALIZER
+ | typeof ML_PAGES.DATA_VISUALIZER_FILE
+ | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT,
+ MlGenericUrlPageState | undefined
+>;
export interface AnomalyDetectionQueryState {
jobId?: JobId;
groupIds?: string[];
+ globalState?: MlCommonGlobalState;
}
export type AnomalyDetectionUrlState = MLPageState<
@@ -86,7 +92,7 @@ export interface ExplorerUrlPageState {
/**
* Job IDs
*/
- jobIds: JobId[];
+ jobIds?: JobId[];
/**
* Optionally set the time range in the time picker.
*/
@@ -104,6 +110,7 @@ export interface ExplorerUrlPageState {
*/
mlExplorerSwimlane?: ExplorerAppState['mlExplorerSwimlane'];
mlExplorerFilter?: ExplorerAppState['mlExplorerFilter'];
+ globalState?: MlCommonGlobalState;
}
export type ExplorerUrlState = MLPageState;
@@ -122,6 +129,7 @@ export interface TimeSeriesExplorerAppState {
to?: string;
};
mlTimeSeriesExplorer?: {
+ forecastId?: string;
detectorIndex?: number;
entities?: Record;
};
@@ -131,10 +139,12 @@ export interface TimeSeriesExplorerAppState {
export interface TimeSeriesExplorerPageState
extends Pick,
Pick {
- jobIds: JobId[];
+ jobIds?: JobId[];
timeRange?: TimeRange;
detectorIndex?: number;
entities?: Record;
+ forecastId?: string;
+ globalState?: MlCommonGlobalState;
}
export type TimeSeriesExplorerUrlState = MLPageState<
@@ -145,6 +155,7 @@ export type TimeSeriesExplorerUrlState = MLPageState<
export interface DataFrameAnalyticsQueryState {
jobId?: JobId | JobId[];
groupIds?: string[];
+ globalState?: MlCommonGlobalState;
}
export type DataFrameAnalyticsUrlState = MLPageState<
@@ -152,17 +163,10 @@ export type DataFrameAnalyticsUrlState = MLPageState<
DataFrameAnalyticsQueryState | undefined
>;
-export interface DataVisualizerUrlState {
- page:
- | typeof ML_PAGES.DATA_VISUALIZER
- | typeof ML_PAGES.DATA_VISUALIZER_FILE
- | typeof ML_PAGES.DATA_VISUALIZER_INDEX_SELECT;
-}
-
export interface DataFrameAnalyticsExplorationQueryState {
ml: {
jobId: JobId;
- analysisType: DataFrameAnalyticsType;
+ analysisType: DataFrameAnalysisConfigType;
};
}
@@ -170,7 +174,24 @@ export type DataFrameAnalyticsExplorationUrlState = MLPageState<
typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
{
jobId: JobId;
- analysisType: DataFrameAnalyticsType;
+ analysisType: DataFrameAnalysisConfigType;
+ globalState?: MlCommonGlobalState;
+ }
+>;
+
+export type CalendarEditUrlState = MLPageState<
+ typeof ML_PAGES.CALENDARS_EDIT,
+ {
+ calendarId: string;
+ globalState?: MlCommonGlobalState;
+ }
+>;
+
+export type FilterEditUrlState = MLPageState<
+ typeof ML_PAGES.FILTER_LISTS_EDIT,
+ {
+ filterId: string;
+ globalState?: MlCommonGlobalState;
}
>;
@@ -183,5 +204,6 @@ export type MlUrlGeneratorState =
| TimeSeriesExplorerUrlState
| DataFrameAnalyticsUrlState
| DataFrameAnalyticsExplorationUrlState
- | DataVisualizerUrlState
+ | CalendarEditUrlState
+ | FilterEditUrlState
| MlGenericUrlState;
diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts
index d725984a47d66..d231ed4344389 100644
--- a/x-pack/plugins/ml/common/util/analytics_utils.ts
+++ b/x-pack/plugins/ml/common/util/analytics_utils.ts
@@ -9,8 +9,8 @@ import {
ClassificationAnalysis,
OutlierAnalysis,
RegressionAnalysis,
- ANALYSIS_CONFIG_TYPE,
} from '../types/data_frame_analytics';
+import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => {
const keys = Object.keys(arg);
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index fc673397ef177..2c5dbe108ab1e 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -16,7 +16,8 @@
"embeddable",
"uiActions",
"kibanaLegacy",
- "indexPatternManagement"
+ "indexPatternManagement",
+ "discover"
],
"optionalPlugins": [
"home",
diff --git a/x-pack/plugins/ml/public/application/app.tsx b/x-pack/plugins/ml/public/application/app.tsx
index c281dc4e9ae05..e3bcc53fe697f 100644
--- a/x-pack/plugins/ml/public/application/app.tsx
+++ b/x-pack/plugins/ml/public/application/app.tsx
@@ -20,6 +20,7 @@ import { MlSetupDependencies, MlStartDependencies } from '../plugin';
import { MlRouter } from './routing';
import { mlApiServicesProvider } from './services/ml_api_service';
import { HttpService } from './services/http_service';
+import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../common/constants/ml_url_generator';
export type MlDependencies = Omit &
MlStartDependencies;
@@ -50,11 +51,21 @@ export interface MlServicesContext {
export type MlGlobalServices = ReturnType;
const App: FC = ({ coreStart, deps, appMountParams }) => {
+ const redirectToMlAccessDeniedPage = async () => {
+ const accessDeniedPageUrl = await deps.share.urlGenerators
+ .getUrlGenerator(ML_APP_URL_GENERATOR)
+ .createUrl({
+ page: ML_PAGES.ACCESS_DENIED,
+ });
+ await coreStart.application.navigateToUrl(accessDeniedPageUrl);
+ };
+
const pageDeps = {
history: appMountParams.history,
indexPatterns: deps.data.indexPatterns,
config: coreStart.uiSettings!,
setBreadcrumbs: coreStart.chrome!.setBreadcrumbs,
+ redirectToMlAccessDeniedPage,
};
const services = {
appName: 'ML',
diff --git a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts
index 653eca126006d..cdd25821ea5ca 100644
--- a/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts
+++ b/x-pack/plugins/ml/public/application/capabilities/check_capabilities.ts
@@ -33,10 +33,12 @@ export function checkGetManagementMlJobsResolver() {
});
}
-export function checkGetJobsCapabilitiesResolver(): Promise {
+export function checkGetJobsCapabilitiesResolver(
+ redirectToMlAccessDeniedPage: () => Promise
+): Promise {
return new Promise((resolve, reject) => {
getCapabilities()
- .then(({ capabilities, isPlatinumOrTrialLicense }) => {
+ .then(async ({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a platinum or trial license is being able to get the transforms list.
// all other functionality is controlled by the return capabilities object.
@@ -46,21 +48,23 @@ export function checkGetJobsCapabilitiesResolver(): Promise {
if (_capabilities.canGetJobs || isPlatinumOrTrialLicense === false) {
return resolve(_capabilities);
} else {
- window.location.href = '#/access-denied';
+ await redirectToMlAccessDeniedPage();
return reject();
}
})
- .catch((e) => {
- window.location.href = '#/access-denied';
+ .catch(async (e) => {
+ await redirectToMlAccessDeniedPage();
return reject();
});
});
}
-export function checkCreateJobsCapabilitiesResolver(): Promise {
+export function checkCreateJobsCapabilitiesResolver(
+ redirectToJobsManagementPage: () => Promise
+): Promise {
return new Promise((resolve, reject) => {
getCapabilities()
- .then(({ capabilities, isPlatinumOrTrialLicense }) => {
+ .then(async ({ capabilities, isPlatinumOrTrialLicense }) => {
_capabilities = capabilities;
// if the license is basic (isPlatinumOrTrialLicense === false) then do not redirect,
// allow the promise to resolve as the separate license check will redirect then user to
@@ -69,34 +73,36 @@ export function checkCreateJobsCapabilitiesResolver(): Promise {
return resolve(_capabilities);
} else {
// if the user has no permission to create a job,
- // redirect them back to the Transforms Management page
- window.location.href = '#/jobs';
+ // redirect them back to the Anomaly Detection Management page
+ await redirectToJobsManagementPage();
return reject();
}
})
- .catch((e) => {
- window.location.href = '#/jobs';
+ .catch(async (e) => {
+ await redirectToJobsManagementPage();
return reject();
});
});
}
-export function checkFindFileStructurePrivilegeResolver(): Promise {
+export function checkFindFileStructurePrivilegeResolver(
+ redirectToMlAccessDeniedPage: () => Promise
+): Promise {
return new Promise((resolve, reject) => {
getCapabilities()
- .then(({ capabilities }) => {
+ .then(async ({ capabilities }) => {
_capabilities = capabilities;
// the minimum privilege for using ML with a basic license is being able to use the datavisualizer.
// all other functionality is controlled by the return _capabilities object
if (_capabilities.canFindFileStructure) {
return resolve(_capabilities);
} else {
- window.location.href = '#/access-denied';
+ await redirectToMlAccessDeniedPage();
return reject();
}
})
- .catch((e) => {
- window.location.href = '#/access-denied';
+ .catch(async (e) => {
+ await redirectToMlAccessDeniedPage();
return reject();
});
});
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
index 9eb44c71aa799..114a6b235d1ad 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/__snapshots__/annotations_table.test.js.snap
@@ -1,170 +1,527 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
-
- ",
+ "end_timestamp": 1455041968976,
+ "job_id": "farequote",
+ "modified_time": 1546417097181,
+ "modified_username": "",
+ "timestamp": 1455026177994,
+ "type": "annotation",
+ },
+ ]
+ }
+ intl={
+ Object {
+ "defaultFormats": Object {},
+ "defaultLocale": "en",
+ "formatDate": [Function],
+ "formatHTMLMessage": [Function],
+ "formatMessage": [Function],
+ "formatNumber": [Function],
+ "formatPlural": [Function],
+ "formatRelative": [Function],
+ "formatTime": [Function],
+ "formats": Object {
+ "date": Object {
+ "full": Object {
+ "day": "numeric",
+ "month": "long",
+ "weekday": "long",
+ "year": "numeric",
+ },
+ "long": Object {
+ "day": "numeric",
+ "month": "long",
+ "year": "numeric",
+ },
+ "medium": Object {
+ "day": "numeric",
+ "month": "short",
+ "year": "numeric",
+ },
+ "short": Object {
+ "day": "numeric",
+ "month": "numeric",
+ "year": "2-digit",
+ },
+ },
+ "number": Object {
+ "currency": Object {
+ "style": "currency",
+ },
+ "percent": Object {
+ "style": "percent",
+ },
+ },
+ "relative": Object {
+ "days": Object {
+ "units": "day",
+ },
+ "hours": Object {
+ "units": "hour",
+ },
+ "minutes": Object {
+ "units": "minute",
+ },
+ "months": Object {
+ "units": "month",
+ },
+ "seconds": Object {
+ "units": "second",
+ },
+ "years": Object {
+ "units": "year",
+ },
+ },
+ "time": Object {
+ "full": Object {
+ "hour": "numeric",
+ "minute": "numeric",
+ "second": "numeric",
+ "timeZoneName": "short",
+ },
+ "long": Object {
+ "hour": "numeric",
+ "minute": "numeric",
+ "second": "numeric",
+ "timeZoneName": "short",
+ },
+ "medium": Object {
+ "hour": "numeric",
+ "minute": "numeric",
+ "second": "numeric",
+ },
+ "short": Object {
+ "hour": "numeric",
+ "minute": "numeric",
+ },
+ },
+ },
+ "formatters": Object {
+ "getDateTimeFormat": [Function],
+ "getMessageFormat": [Function],
+ "getNumberFormat": [Function],
+ "getPluralFormat": [Function],
+ "getRelativeFormat": [Function],
+ },
+ "locale": "en",
+ "messages": Object {},
+ "now": [Function],
+ "onError": [Function],
+ "textComponent": Symbol(react.fragment),
+ "timeZone": null,
+ }
+ }
+ kibana={
+ Object {
+ "notifications": Object {
+ "toasts": Object {
+ "danger": [Function],
+ "show": [Function],
+ "success": [Function],
+ "warning": [Function],
+ },
+ },
+ "overlays": Object {
+ "openFlyout": [Function],
+ "openModal": [Function],
+ },
+ "services": Object {},
+ }
+ }
+/>
+`;
+
+exports[`AnnotationsTable Initialization with job config prop. 1`] = `
+",
- "end_timestamp": 1455041968976,
"job_id": "farequote",
- "modified_time": 1546417097181,
- "modified_username": "",
- "timestamp": 1455026177994,
- "type": "annotation",
+ "query": Object {
+ "bool": Object {
+ "adjust_pure_negative": true,
+ "boost": 1,
+ "must": Array [
+ Object {
+ "query_string": Object {
+ "analyze_wildcard": true,
+ "auto_generate_synonyms_phrase_query": true,
+ "boost": 1,
+ "default_operator": "or",
+ "enable_position_increments": true,
+ "escape": false,
+ "fields": Array [],
+ "fuzziness": "AUTO",
+ "fuzzy_max_expansions": 50,
+ "fuzzy_prefix_length": 0,
+ "fuzzy_transpositions": true,
+ "max_determinized_states": 10000,
+ "phrase_slop": 0,
+ "query": "*",
+ "type": "best_fields",
+ },
+ },
+ ],
+ },
+ },
+ "query_delay": "115823ms",
+ "scroll_size": 1000,
+ "state": "stopped",
},
- ]
- }
- pagination={
- Object {
- "pageSizeOptions": Array [
- 5,
- 10,
- 25,
- ],
- }
- }
- responsive={true}
- rowProps={[Function]}
- search={
- Object {
- "box": Object {
- "incremental": true,
- "schema": true,
- },
- "defaultQuery": "event:(user or delayed_data)",
- "filters": Array [
- Object {
- "field": "event",
- "multiSelect": "or",
- "name": "Event",
- "options": Array [],
- "type": "field_value_selection",
- },
- ],
- }
- }
- sorting={
- Object {
- "sort": Object {
- "direction": "asc",
- "field": "timestamp",
+ "description": "",
+ "established_model_memory": 42102,
+ "finished_time": 1546418359427,
+ "job_id": "farequote",
+ "job_type": "anomaly_detector",
+ "job_version": "7.0.0",
+ "model_plot_config": Object {
+ "enabled": true,
},
- }
+ "model_size_stats": Object {
+ "bucket_allocation_failures_count": 0,
+ "job_id": "farequote",
+ "log_time": 1546418359000,
+ "memory_status": "ok",
+ "model_bytes": 42102,
+ "result_type": "model_size_stats",
+ "timestamp": 1455232500000,
+ "total_by_field_count": 3,
+ "total_over_field_count": 0,
+ "total_partition_field_count": 2,
+ },
+ "model_snapshot_id": "1546418359",
+ "model_snapshot_min_version": "6.4.0",
+ "model_snapshot_retention_days": 1,
+ "results_index_name": "shared",
+ "state": "closed",
+ },
+ ]
+ }
+ kibana={
+ Object {
+ "notifications": Object {
+ "toasts": Object {
+ "danger": [Function],
+ "show": [Function],
+ "success": [Function],
+ "warning": [Function],
+ },
+ },
+ "overlays": Object {
+ "openFlyout": [Function],
+ "openModal": [Function],
+ },
+ "services": Object {},
}
- tableLayout="fixed"
- />
-
-`;
-
-exports[`AnnotationsTable Initialization with job config prop. 1`] = `
-
-
-
-
-
+ }
+/>
`;
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
-
+
`;
diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
index 9dabfce163dbb..d5025fd3c3649 100644
--- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
+++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js
@@ -13,7 +13,6 @@
import uniq from 'lodash/uniq';
import PropTypes from 'prop-types';
-import rison from 'rison-node';
import React, { Component, Fragment } from 'react';
import memoizeOne from 'memoize-one';
import {
@@ -54,12 +53,15 @@ import {
ANNOTATION_EVENT_USER,
ANNOTATION_EVENT_DELAYED_DATA,
} from '../../../../../common/constants/annotations';
+import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public';
+import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { PLUGIN_ID } from '../../../../../common/constants/app';
const CURRENT_SERIES = 'current_series';
/**
* Table component for rendering the lists of annotations for an ML job.
*/
-export class AnnotationsTable extends Component {
+class AnnotationsTableUI extends Component {
static propTypes = {
annotations: PropTypes.array,
jobs: PropTypes.array,
@@ -199,7 +201,17 @@ export class AnnotationsTable extends Component {
}
}
- openSingleMetricView = (annotation = {}) => {
+ openSingleMetricView = async (annotation = {}) => {
+ const {
+ services: {
+ application: { navigateToApp },
+
+ share: {
+ urlGenerators: { getUrlGenerator },
+ },
+ },
+ } = this.props.kibana;
+
// Creates the link to the Single Metric Viewer.
// Set the total time range from the start to the end of the annotation.
const job = this.getJob(annotation.job_id);
@@ -210,30 +222,10 @@ export class AnnotationsTable extends Component {
);
const from = new Date(dataCounts.earliest_record_timestamp).toISOString();
const to = new Date(resultLatest).toISOString();
-
- const globalSettings = {
- ml: {
- jobIds: [job.job_id],
- },
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
- },
- time: {
- from,
- to,
- mode: 'absolute',
- },
- };
-
- const appState = {
- query: {
- query_string: {
- analyze_wildcard: true,
- query: '*',
- },
- },
+ const timeRange = {
+ from,
+ to,
+ mode: 'absolute',
};
let mlTimeSeriesExplorer = {};
const entityCondition = {};
@@ -247,11 +239,11 @@ export class AnnotationsTable extends Component {
};
if (annotation.timestamp < dataCounts.earliest_record_timestamp) {
- globalSettings.time.from = new Date(annotation.timestamp).toISOString();
+ timeRange.from = new Date(annotation.timestamp).toISOString();
}
if (annotation.end_timestamp > dataCounts.latest_record_timestamp) {
- globalSettings.time.to = new Date(annotation.end_timestamp).toISOString();
+ timeRange.to = new Date(annotation.end_timestamp).toISOString();
}
}
@@ -274,14 +266,34 @@ export class AnnotationsTable extends Component {
entityCondition[annotation.by_field_name] = annotation.by_field_value;
}
mlTimeSeriesExplorer.entities = entityCondition;
- appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer;
-
- const _g = rison.encode(globalSettings);
- const _a = rison.encode(appState);
+ // appState.mlTimeSeriesExplorer = mlTimeSeriesExplorer;
+
+ const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
+ const singleMetricViewerLink = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.SINGLE_METRIC_VIEWER,
+ pageState: {
+ timeRange,
+ refreshInterval: {
+ display: 'Off',
+ pause: false,
+ value: 0,
+ },
+ jobIds: [job.job_id],
+ query: {
+ query_string: {
+ analyze_wildcard: true,
+ query: '*',
+ },
+ },
+ ...mlTimeSeriesExplorer,
+ },
+ excludeBasePath: true,
+ });
- const url = `?_g=${_g}&_a=${_a}`;
- addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, url);
- window.open(`#/timeseriesexplorer${url}`, '_self');
+ addItemToRecentlyAccessed('timeseriesexplorer', job.job_id, singleMetricViewerLink);
+ await navigateToApp(PLUGIN_ID, {
+ path: singleMetricViewerLink,
+ });
};
onMouseOverRow = (record) => {
@@ -686,3 +698,5 @@ export class AnnotationsTable extends Component {
);
}
}
+
+export const AnnotationsTable = withKibana(AnnotationsTableUI);
diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js
index fdeab0c49e32b..6025dd1c7433e 100644
--- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js
+++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.js
@@ -29,6 +29,8 @@ import { getUrlForRecord, openCustomUrlWindow } from '../../util/custom_url_util
import { formatHumanReadableDateTimeSeconds } from '../../util/date_utils';
import { getIndexPatternIdFromName } from '../../util/index_utils';
import { replaceStringTokens } from '../../util/string_utils';
+import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../common/constants/ml_url_generator';
+import { PLUGIN_ID } from '../../../../common/constants/app';
/*
* Component for rendering the links menu inside a cell in the anomalies table.
*/
@@ -142,7 +144,18 @@ class LinksMenuUI extends Component {
}
};
- viewSeries = () => {
+ viewSeries = async () => {
+ const {
+ services: {
+ application: { navigateToApp },
+
+ share: {
+ urlGenerators: { getUrlGenerator },
+ },
+ },
+ } = this.props.kibana;
+ const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
+
const record = this.props.anomaly.source;
const bounds = this.props.bounds;
const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
@@ -171,44 +184,36 @@ class LinksMenuUI extends Component {
entityCondition[record.by_field_name] = record.by_field_value;
}
- // Use rison to build the URL .
- const _g = rison.encode({
- ml: {
+ const singleMetricViewerLink = await mlUrlGenerator.createUrl({
+ excludeBasePath: true,
+ page: ML_PAGES.SINGLE_METRIC_VIEWER,
+ pageState: {
jobIds: [record.job_id],
- },
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
- },
- time: {
- from: from,
- to: to,
- mode: 'absolute',
- },
- });
-
- const _a = rison.encode({
- mlTimeSeriesExplorer: {
+ refreshInterval: {
+ display: 'Off',
+ pause: false,
+ value: 0,
+ },
+ timeRange: {
+ from: from,
+ to: to,
+ mode: 'absolute',
+ },
zoom: {
from: zoomFrom,
to: zoomTo,
},
detectorIndex: record.detector_index,
entities: entityCondition,
- },
- query: {
query_string: {
analyze_wildcard: true,
query: '*',
},
},
});
-
- // Need to encode the _a parameter in case any entities contain unsafe characters such as '+'.
- let path = '#/timeseriesexplorer';
- path += `?_g=${_g}&_a=${encodeURIComponent(_a)}`;
- window.open(path, '_blank');
+ await navigateToApp(PLUGIN_ID, {
+ path: singleMetricViewerLink,
+ });
};
viewExamples = () => {
diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx
index 4a63a8cd7e716..d54a7fe81e858 100644
--- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.test.tsx
@@ -6,13 +6,22 @@
import React from 'react';
import { Router } from 'react-router-dom';
-import { render, fireEvent } from '@testing-library/react';
+import { render } from '@testing-library/react';
import { createBrowserHistory } from 'history';
import { I18nProvider } from '@kbn/i18n/react';
import { AnomalyResultsViewSelector } from './index';
+jest.mock('../../contexts/kibana', () => {
+ return {
+ useMlUrlGenerator: () => ({
+ createUrl: jest.fn(),
+ }),
+ useNavigateToPath: () => jest.fn(),
+ };
+});
+
describe('AnomalyResultsViewSelector', () => {
test('should create selector with correctly selected value', () => {
const history = createBrowserHistory();
@@ -31,27 +40,4 @@ describe('AnomalyResultsViewSelector', () => {
getByTestId('mlAnomalyResultsViewSelectorSingleMetricViewer').hasAttribute('checked')
).toBe(true);
});
-
- test('should open window to other results view when clicking on non-checked input', () => {
- // Create mock for window.open
- const mockedOpen = jest.fn();
- const originalOpen = window.open;
- window.open = mockedOpen;
-
- const history = createBrowserHistory();
-
- const { getByTestId } = render(
-
-
-
-
-
- );
-
- fireEvent.click(getByTestId('mlAnomalyResultsViewSelectorExplorer'));
- expect(mockedOpen).toHaveBeenCalledWith('#/explorer', '_self');
-
- // Clean-up window.open.
- window.open = originalOpen;
- });
});
diff --git a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx
index 78acb422851e3..c4c8f06bbbc3a 100644
--- a/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx
+++ b/x-pack/plugins/ml/public/application/components/anomaly_results_view_selector/anomaly_results_view_selector.tsx
@@ -5,21 +5,25 @@
*/
import React, { FC, useMemo } from 'react';
-import { encode } from 'rison-node';
import { EuiButtonGroup } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '../../util/url_state';
+import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
+import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
interface Props {
- viewId: 'timeseriesexplorer' | 'explorer';
+ viewId: typeof ML_PAGES.SINGLE_METRIC_VIEWER | typeof ML_PAGES.ANOMALY_EXPLORER;
}
// Component for rendering a set of buttons for switching between the Anomaly Detection results views.
export const AnomalyResultsViewSelector: FC = ({ viewId }) => {
+ const urlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
const toggleButtonsIcons = useMemo(
() => [
{
@@ -28,7 +32,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => {
defaultMessage: 'View results in the Single Metric Viewer',
}),
iconType: 'visLine',
- value: 'timeseriesexplorer',
+ value: ML_PAGES.SINGLE_METRIC_VIEWER,
'data-test-subj': 'mlAnomalyResultsViewSelectorSingleMetricViewer',
},
{
@@ -37,7 +41,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => {
defaultMessage: 'View results in the Anomaly Explorer',
}),
iconType: 'visTable',
- value: 'explorer',
+ value: ML_PAGES.ANOMALY_EXPLORER,
'data-test-subj': 'mlAnomalyResultsViewSelectorExplorer',
},
],
@@ -46,9 +50,14 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => {
const [globalState] = useUrlState('_g');
- const onChangeView = (newViewId: string) => {
- const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : '';
- window.open(`#/${newViewId}${fullGlobalStateString}`, '_self');
+ const onChangeView = async (newViewId: Props['viewId']) => {
+ const url = await urlGenerator.createUrl({
+ page: newViewId,
+ pageState: {
+ globalState,
+ },
+ });
+ await navigateToPath(url);
};
return (
@@ -60,7 +69,7 @@ export const AnomalyResultsViewSelector: FC = ({ viewId }) => {
data-test-subj="mlAnomalyResultsViewSelector"
options={toggleButtonsIcons}
idSelected={viewId}
- onChange={onChangeView}
+ onChange={(newViewId: string) => onChangeView(newViewId as Props['viewId'])}
isIconOnly
/>
);
diff --git a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts
index 368e758a027c4..b4668810b9421 100644
--- a/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts
+++ b/x-pack/plugins/ml/public/application/components/custom_hooks/use_create_ad_links.ts
@@ -22,16 +22,19 @@ export const useCreateADLinks = () => {
const userTimeSettings = useUiSettings().get(ANOMALY_DETECTION_DEFAULT_TIME_RANGE);
const createLinkWithUserDefaults = useCallback(
(location, jobList) => {
- const resultsPageUrl = mlJobService.createResultsUrlForJobs(
+ return mlJobService.createResultsUrlForJobs(
jobList,
location,
useUserTimeSettings === true && userTimeSettings !== undefined
? userTimeSettings
: undefined
);
- return `${basePath.get()}/app/ml${resultsPageUrl}`;
},
[basePath]
);
return { createLinkWithUserDefaults };
};
+
+export type CreateLinkWithUserDefaults = ReturnType<
+ typeof useCreateADLinks
+>['createLinkWithUserDefaults'];
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
index 22815fe593d57..6aad5d53c3a3c 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx
@@ -32,6 +32,7 @@ import { UseIndexDataReturnType } from './types';
import { DecisionPathPopover } from './feature_importance/decision_path_popover';
import { TopClasses } from '../../../../common/types/feature_importance';
import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics';
+import { DataFrameAnalysisConfigType } from '../../../../common/types/data_frame_analytics';
// TODO Fix row hovering + bar highlighting
// import { hoveredRow$ } from './column_chart';
@@ -44,7 +45,7 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => (
interface PropsWithoutHeader extends UseIndexDataReturnType {
baseline?: number;
- analysisType?: ANALYSIS_CONFIG_TYPE;
+ analysisType?: DataFrameAnalysisConfigType;
resultsField?: string;
dataTestSubj: string;
toastNotifications: CoreSetup['notifications']['toasts'];
diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
index 263337f93e9a8..7c4428db71b3b 100644
--- a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
+++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx
@@ -13,10 +13,11 @@ import { FeatureImportance, TopClasses } from '../../../../../common/types/featu
import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common';
import { ClassificationDecisionPath } from './decision_path_classification';
import { useMlKibana } from '../../../contexts/kibana';
+import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
interface DecisionPathPopoverProps {
featureImportance: FeatureImportance[];
- analysisType: ANALYSIS_CONFIG_TYPE;
+ analysisType: DataFrameAnalysisConfigType;
predictionFieldName?: string;
baseline?: number;
predictedValue?: number | string | undefined;
diff --git a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js
index 1f03dbe134756..279afc8c50339 100644
--- a/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js
+++ b/x-pack/plugins/ml/public/application/components/data_recognizer/recognized_result.js
@@ -9,11 +9,16 @@ import PropTypes from 'prop-types';
import { EuiIcon, EuiFlexItem } from '@elastic/eui';
import { CreateJobLinkCard } from '../create_job_link_card';
+import { useMlKibana } from '../../contexts/kibana';
export const RecognizedResult = ({ config, indexPattern, savedSearch }) => {
+ const {
+ services: {
+ http: { basePath },
+ },
+ } = useMlKibana();
const id = savedSearch === null ? `index=${indexPattern.id}` : `savedSearchId=${savedSearch.id}`;
-
- const href = `#/jobs/new_job/recognize?id=${config.id}&${id}`;
+ const href = `${basePath.get()}/app/ml/jobs/new_job/recognize?id=${config.id}&${id}`;
let logo = null;
// if a logo is available, use that, otherwise display the id
diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
index 3a4875fa243fd..671f0b196ce35 100644
--- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
+++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
@@ -4,15 +4,15 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { FC, useState } from 'react';
-import { encode } from 'rison-node';
+import React, { FC, useState, useEffect } from 'react';
-import { EuiTabs, EuiTab, EuiLink } from '@elastic/eui';
+import { EuiTabs, EuiTab } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-
-import { useUrlState } from '../../util/url_state';
-
import { TabId } from './navigation_menu';
+import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
+import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator';
+import { useUrlState } from '../../util/url_state';
+import { ML_APP_NAME } from '../../../../common/constants/app';
export interface Tab {
id: TabId;
@@ -66,20 +66,57 @@ function getTabs(disableLinks: boolean): Tab[] {
}
interface TabData {
testSubject: string;
- pathId?: string;
+ pathId?: MlUrlGeneratorState['page'];
+ name: string;
}
const TAB_DATA: Record = {
- overview: { testSubject: 'mlMainTab overview' },
+ overview: {
+ testSubject: 'mlMainTab overview',
+ name: i18n.translate('xpack.ml.overviewTabLabel', {
+ defaultMessage: 'Overview',
+ }),
+ },
// Note that anomaly detection jobs list is mapped to ml#/jobs.
- anomaly_detection: { testSubject: 'mlMainTab anomalyDetection', pathId: 'jobs' },
- data_frame_analytics: { testSubject: 'mlMainTab dataFrameAnalytics' },
- datavisualizer: { testSubject: 'mlMainTab dataVisualizer' },
- settings: { testSubject: 'mlMainTab settings' },
- 'access-denied': { testSubject: 'mlMainTab overview' },
+ anomaly_detection: {
+ testSubject: 'mlMainTab anomalyDetection',
+ name: i18n.translate('xpack.ml.anomalyDetectionTabLabel', {
+ defaultMessage: 'Anomaly Detection',
+ }),
+ pathId: 'jobs',
+ },
+ data_frame_analytics: {
+ testSubject: 'mlMainTab dataFrameAnalytics',
+ name: i18n.translate('xpack.ml.dataFrameAnalyticsTabLabel', {
+ defaultMessage: 'Data Frame Analytics',
+ }),
+ },
+ datavisualizer: {
+ testSubject: 'mlMainTab dataVisualizer',
+ name: i18n.translate('xpack.ml.dataVisualizerTabLabel', {
+ defaultMessage: 'Data Visualizer',
+ }),
+ },
+ settings: {
+ testSubject: 'mlMainTab settings',
+ name: i18n.translate('xpack.ml.settingsTabLabel', {
+ defaultMessage: 'Settings',
+ }),
+ },
+ 'access-denied': {
+ testSubject: 'mlMainTab overview',
+ name: i18n.translate('xpack.ml.accessDeniedTabLabel', {
+ defaultMessage: 'Access Denied',
+ }),
+ },
};
export const MainTabs: FC = ({ tabId, disableLinks }) => {
+ const {
+ services: {
+ chrome: { docTitle },
+ },
+ } = useMlKibana();
const [globalState] = useUrlState('_g');
const [selectedTabId, setSelectedTabId] = useState(tabId);
function onSelectedTabChanged(id: TabId) {
@@ -87,16 +124,40 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => {
}
const tabs = getTabs(disableLinks);
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
+ const redirectToTab = async (defaultPathId: MlUrlGeneratorState['page']) => {
+ const pageState =
+ globalState?.refreshInterval !== undefined
+ ? {
+ globalState: {
+ refreshInterval: globalState.refreshInterval,
+ },
+ }
+ : undefined;
+ // TODO - Fix ts so passing pageState won't default to MlGenericUrlState when pageState is passed in
+ // @ts-ignore
+ const path = await mlUrlGenerator.createUrl({
+ page: defaultPathId,
+ // only retain the refreshInterval part of globalState
+ // appState will not be considered.
+ pageState,
+ });
+
+ await navigateToPath(path, false);
+ };
+
+ useEffect(() => {
+ docTitle.change([TAB_DATA[selectedTabId].name, ML_APP_NAME]);
+ }, [selectedTabId]);
return (
{tabs.map((tab: Tab) => {
const { id, disabled } = tab;
const testSubject = TAB_DATA[id].testSubject;
- const defaultPathId = TAB_DATA[id].pathId || id;
- // globalState (e.g. selected jobs and time range) should be retained when changing pages.
- // appState will not be considered.
- const fullGlobalStateString = globalState !== undefined ? `?_g=${encode(globalState)}` : '';
+ const defaultPathId = (TAB_DATA[id].pathId || id) as MlUrlGeneratorState['page'];
return disabled ? (
@@ -104,21 +165,18 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => {
) : (
- {
+ onSelectedTabChanged(id);
+ redirectToTab(defaultPathId);
+ }}
+ isSelected={id === selectedTabId}
+ key={`tab-${id}-key`}
>
- onSelectedTabChanged(id)}
- isSelected={id === selectedTabId}
- key={`tab-${id}-key`}
- >
- {tab.name}
-
-
+ {tab.name}
+
);
})}
diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js
index 48e0da72f067c..eb12cb7679674 100644
--- a/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js
+++ b/x-pack/plugins/ml/public/application/components/rule_editor/scope_section.js
@@ -17,8 +17,19 @@ import { ScopeExpression } from './scope_expression';
import { checkPermission } from '../../capabilities/check_capabilities';
import { getScopeFieldDefaults } from './utils';
import { FormattedMessage } from '@kbn/i18n/react';
+import { ML_PAGES } from '../../../../common/constants/ml_url_generator';
+import { useMlUrlGenerator, useNavigateToPath } from '../../contexts/kibana';
function NoFilterListsCallOut() {
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+ const redirectToFilterManagementPage = async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.FILTER_LISTS_MANAGE,
+ });
+ await navigateToPath(path, true);
+ };
+
return (
+
useKibana();
export type MlKibanaReactContextValue = KibanaReactContextValue;
diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts
index 48385ad3ae6a8..d448185c914b8 100644
--- a/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts
+++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_create_url.ts
@@ -4,8 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { useCallback, useEffect, useState } from 'react';
import { useMlKibana } from './kibana_context';
import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator';
+import { MlUrlGeneratorState } from '../../../../common/types/ml_url_generator';
+import { useUrlState } from '../../util/url_state';
export const useMlUrlGenerator = () => {
const {
@@ -18,3 +21,59 @@ export const useMlUrlGenerator = () => {
return getUrlGenerator(ML_APP_URL_GENERATOR);
};
+
+export const useMlLink = (params: MlUrlGeneratorState): string => {
+ const [href, setHref] = useState(params.page);
+ const mlUrlGenerator = useMlUrlGenerator();
+
+ useEffect(() => {
+ let isCancelled = false;
+ const generateUrl = async (_params: MlUrlGeneratorState) => {
+ const url = await mlUrlGenerator.createUrl(_params);
+ if (!isCancelled) {
+ setHref(url);
+ }
+ };
+ generateUrl(params);
+ return () => {
+ isCancelled = true;
+ };
+ }, [params]);
+
+ return href;
+};
+
+export const useCreateAndNavigateToMlLink = (
+ page: MlUrlGeneratorState['page']
+): (() => Promise) => {
+ const mlUrlGenerator = useMlUrlGenerator();
+ const [globalState] = useUrlState('_g');
+
+ const {
+ services: {
+ application: { navigateToUrl },
+ },
+ } = useMlKibana();
+
+ const redirectToMlPage = useCallback(
+ async (_page: MlUrlGeneratorState['page']) => {
+ const pageState =
+ globalState?.refreshInterval !== undefined
+ ? {
+ globalState: {
+ refreshInterval: globalState.refreshInterval,
+ },
+ }
+ : undefined;
+
+ // TODO: fix ts only interpreting it as MlUrlGenericState if pageState is passed
+ // @ts-ignore
+ const url = await mlUrlGenerator.createUrl({ page: _page, pageState });
+ await navigateToUrl(url);
+ },
+ [mlUrlGenerator, navigateToUrl]
+ );
+
+ // returns the onClick callback
+ return useCallback(() => redirectToMlPage(page), [redirectToMlPage, page]);
+};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index 60681fb6e7bbe..d22bba7738db4 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -15,8 +15,8 @@ import { SavedSearchQuery } from '../../contexts/ml';
import {
AnalysisConfig,
ClassificationAnalysis,
+ DataFrameAnalysisConfigType,
RegressionAnalysis,
- ANALYSIS_CONFIG_TYPE,
} from '../../../../common/types/data_frame_analytics';
import {
isOutlierAnalysis,
@@ -26,6 +26,7 @@ import {
getDependentVar,
getPredictedFieldName,
} from '../../../../common/util/analytics_utils';
+import { ANALYSIS_CONFIG_TYPE } from '../../../../common/constants/data_frame_analytics';
export type IndexPattern = string;
export enum ANALYSIS_ADVANCED_FIELDS {
@@ -429,7 +430,7 @@ interface LoadEvalDataConfig {
predictionFieldName?: string;
searchQuery?: ResultsSearchQuery;
ignoreDefaultQuery?: boolean;
- jobType: ANALYSIS_CONFIG_TYPE;
+ jobType: DataFrameAnalysisConfigType;
requiresKeyword?: boolean;
}
@@ -550,7 +551,7 @@ export {
isRegressionAnalysis,
isClassificationAnalysis,
getPredictionFieldName,
- ANALYSIS_CONFIG_TYPE,
getDependentVar,
getPredictedFieldName,
+ ANALYSIS_CONFIG_TYPE,
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts
index 00d735d9a866e..83eebccd310e3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/index.ts
@@ -14,7 +14,6 @@ export {
UpdateDataFrameAnalyticsConfig,
IndexPattern,
REFRESH_ANALYTICS_LIST_STATE,
- ANALYSIS_CONFIG_TYPE,
OUTLIER_ANALYSIS_METHOD,
RegressionEvaluateResponse,
getValuesFromResponse,
@@ -26,6 +25,7 @@ export {
SEARCH_SIZE,
defaultSearchQuery,
SearchQuery,
+ ANALYSIS_CONFIG_TYPE,
} from './analytics';
export {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx
index 1e5dbee3499bd..1e6a616fedd64 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/job_type.tsx
@@ -8,7 +8,7 @@ import React, { Fragment, FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiSelect } from '@elastic/eui';
-import { ANALYSIS_CONFIG_TYPE } from '../../../../common';
+import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
index 88c89df86b29a..310cd4e3b3a79 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
@@ -16,6 +16,7 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com
import { CATEGORICAL_TYPES } from './form_options_validation';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
const containsClassificationFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) &&
@@ -32,13 +33,13 @@ const containsRegressionFieldsCb = ({ name, type }: Field) =>
const containsOutlierFieldsCb = ({ name, type }: Field) =>
!OMIT_FIELDS.includes(name) && name !== EVENT_RATE_FIELD_ID && BASIC_NUMERICAL_TYPES.has(type);
-const callbacks: Record boolean> = {
+const callbacks: Record boolean> = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: containsClassificationFieldsCb,
[ANALYSIS_CONFIG_TYPE.REGRESSION]: containsRegressionFieldsCb,
[ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION]: containsOutlierFieldsCb,
};
-const messages: Record = {
+const messages: Record = {
[ANALYSIS_CONFIG_TYPE.CLASSIFICATION]: (
= ({ jobId, analysisType }) => {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
index eea579ef1d064..84b1c4241aaf2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx
@@ -29,7 +29,6 @@ import {
SEARCH_SIZE,
defaultSearchQuery,
getAnalysisType,
- ANALYSIS_CONFIG_TYPE,
} from '../../../../common';
import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns';
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
@@ -39,6 +38,7 @@ import { IndexPatternPrompt } from '../index_pattern_prompt';
import { useExplorationResults } from './use_exploration_results';
import { useMlKibana } from '../../../../../contexts/kibana';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
const showingDocs = i18n.translate(
'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText',
@@ -195,7 +195,7 @@ export const ExplorationResultsTable: FC = React.memo(
{...classificationData}
dataTestSubj="mlExplorationDataGrid"
toastNotifications={getToastNotifications()}
- analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE}
+ analysisType={(analysisType as unknown) as DataFrameAnalysisConfigType}
/>
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
index c8349084dbda8..f4f01330271fc 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx
@@ -26,11 +26,12 @@ import { OutlierExploration } from './components/outlier_exploration';
import { RegressionExploration } from './components/regression_exploration';
import { ClassificationExploration } from './components/classification_exploration';
-import { ANALYSIS_CONFIG_TYPE } from '../../common/analytics';
+import { ANALYSIS_CONFIG_TYPE } from '../../../../../common/constants/data_frame_analytics';
+import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
export const Page: FC<{
jobId: string;
- analysisType: ANALYSIS_CONFIG_TYPE;
+ analysisType: DataFrameAnalysisConfigType;
}> = ({ jobId, analysisType }) => (
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx
index a3595b51d0a59..2363e6fbecc9d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_view/use_view_action.tsx
@@ -7,24 +7,32 @@
import React, { useCallback, useMemo } from 'react';
import { getAnalysisType } from '../../../../common/analytics';
-import { useNavigateToPath } from '../../../../../contexts/kibana';
+import { useMlUrlGenerator, useNavigateToPath } from '../../../../../contexts/kibana';
-import {
- getResultsUrl,
- DataFrameAnalyticsListAction,
- DataFrameAnalyticsListRow,
-} from '../analytics_list/common';
+import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common';
import { getViewLinkStatus } from './get_view_link_status';
import { viewActionButtonText, ViewButton } from './view_button';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
+import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
export type ViewAction = ReturnType;
export const useViewAction = () => {
+ const mlUrlGenerator = useMlUrlGenerator();
const navigateToPath = useNavigateToPath();
+ const redirectToTab = async (jobId: string, analysisType: DataFrameAnalysisConfigType) => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
+ pageState: { jobId, analysisType },
+ });
+
+ await navigateToPath(path, false);
+ };
+
const clickHandler = useCallback((item: DataFrameAnalyticsListRow) => {
- const analysisType = getAnalysisType(item.config.analysis);
- navigateToPath(getResultsUrl(item.id, analysisType));
+ const analysisType = getAnalysisType(item.config.analysis) as DataFrameAnalysisConfigType;
+ redirectToTab(item.id, analysisType);
}, []);
const action: DataFrameAnalyticsListAction = useMemo(
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 0c3bff58c25cd..2f8e087a6a3f0 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -15,12 +15,8 @@ import {
EuiSearchBarProps,
EuiSpacer,
} from '@elastic/eui';
-
-import {
- DataFrameAnalyticsId,
- useRefreshAnalyticsList,
- ANALYSIS_CONFIG_TYPE,
-} from '../../../../common';
+import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
+import { DataFrameAnalyticsId, useRefreshAnalyticsList } from '../../../../common';
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
index 994357412510d..37076d400f021 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/common.ts
@@ -9,11 +9,8 @@ import { EuiTableActionsColumnType, Query, Ast } from '@elastic/eui';
import { DATA_FRAME_TASK_STATE } from './data_frame_task_state';
export { DATA_FRAME_TASK_STATE };
-import {
- DataFrameAnalyticsId,
- DataFrameAnalyticsConfig,
- ANALYSIS_CONFIG_TYPE,
-} from '../../../../common';
+import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
export enum DATA_FRAME_MODE {
BATCH = 'batch',
@@ -111,10 +108,7 @@ export interface DataFrameAnalyticsListRow {
checkpointing: object;
config: DataFrameAnalyticsConfig;
id: DataFrameAnalyticsId;
- job_type:
- | ANALYSIS_CONFIG_TYPE.CLASSIFICATION
- | ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION
- | ANALYSIS_CONFIG_TYPE.REGRESSION;
+ job_type: DataFrameAnalysisConfigType;
mode: string;
state: DataFrameAnalyticsStats['state'];
stats: DataFrameAnalyticsStats;
@@ -137,10 +131,6 @@ export function isCompletedAnalyticsJob(stats: DataFrameAnalyticsStats) {
return stats.state === DATA_FRAME_TASK_STATE.STOPPED && progress === 100;
}
-export function getResultsUrl(jobId: string, analysisType: ANALYSIS_CONFIG_TYPE | string) {
- return `#/data_frame_analytics/exploration?_g=(ml:(jobId:${jobId},analysisType:${analysisType}))`;
-}
-
// The single Action type is not exported as is
// from EUI so we use that code to get the single
// Action type from the array of actions.
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
index ef1d373a55a12..1af99d2a1ed00 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_columns.tsx
@@ -19,8 +19,6 @@ import {
EuiLink,
RIGHT_ALIGNMENT,
} from '@elastic/eui';
-import { getJobIdUrl, TAB_IDS } from '../../../../../util/get_selected_ids_url';
-
import { getAnalysisType, DataFrameAnalyticsId } from '../../../../common';
import {
getDataFrameAnalyticsProgressPhase,
@@ -32,6 +30,8 @@ import {
DataFrameAnalyticsStats,
} from './common';
import { useActions } from './use_actions';
+import { useMlLink } from '../../../../../contexts/kibana';
+import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
enum TASK_STATE_COLOR {
analyzing = 'primary',
@@ -134,9 +134,14 @@ export const progressColumn = {
'data-test-subj': 'mlAnalyticsTableColumnProgress',
};
-export const getDFAnalyticsJobIdLink = (item: DataFrameAnalyticsListRow) => (
- {item.id}
-);
+export const DFAnalyticsJobIdLink = ({ item }: { item: DataFrameAnalyticsListRow }) => {
+ const href = useMlLink({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
+ pageState: { jobId: item.id },
+ });
+
+ return {item.id};
+};
export const useColumns = (
expandedRowItemIds: DataFrameAnalyticsId[],
@@ -145,7 +150,6 @@ export const useColumns = (
isMlEnabledInSpace: boolean = true
) => {
const { actions, modals } = useActions(isManagementTable);
-
function toggleDetails(item: DataFrameAnalyticsListRow) {
const index = expandedRowItemIds.indexOf(item.config.id);
if (index !== -1) {
@@ -200,7 +204,7 @@ export const useColumns = (
'data-test-subj': 'mlAnalyticsTableColumnId',
scope: 'row',
render: (item: DataFrameAnalyticsListRow) =>
- isManagementTable ? getDFAnalyticsJobIdLink(item) : item.id,
+ isManagementTable ? : item.id,
},
{
field: DataFrameAnalyticsListColumn.description,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
index 338b6444671a6..dbc7a23f2258b 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx
@@ -29,21 +29,23 @@ import { useInferenceApiService } from '../../../../../services/ml_api_service/i
import { ModelsTableToConfigMapping } from './index';
import { TIME_FORMAT } from '../../../../../../../common/constants/time_format';
import { DeleteModelsModal } from './delete_models_modal';
-import { useMlKibana, useNotifications } from '../../../../../contexts/kibana';
+import { useMlKibana, useMlUrlGenerator, useNotifications } from '../../../../../contexts/kibana';
import { ExpandedRow } from './expanded_row';
-import { getResultsUrl } from '../analytics_list/common';
import {
ModelConfigResponse,
ModelPipelines,
TrainedModelStat,
} from '../../../../../../../common/types/inference';
import {
+ getAnalysisType,
REFRESH_ANALYTICS_LIST_STATE,
refreshAnalyticsList$,
useRefreshAnalyticsList,
} from '../../../../common';
import { useTableSettings } from '../analytics_list/use_table_settings';
import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar';
+import { ML_PAGES } from '../../../../../../../common/constants/ml_url_generator';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
type Stats = Omit;
@@ -61,6 +63,7 @@ export const ModelsList: FC = () => {
application: { navigateToUrl, capabilities },
},
} = useMlKibana();
+ const urlGenerator = useMlUrlGenerator();
const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean;
@@ -278,12 +281,19 @@ export const ModelsList: FC = () => {
type: 'icon',
available: (item) => item.metadata?.analytics_config?.id,
onClick: async (item) => {
- await navigateToUrl(
- getResultsUrl(
- item.metadata?.analytics_config.id,
- Object.keys(item.metadata?.analytics_config.analysis)[0]
- )
- );
+ if (item.metadata?.analytics_config === undefined) return;
+
+ const url = await urlGenerator.createUrl({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
+ pageState: {
+ jobId: item.metadata?.analytics_config.id as string,
+ analysisType: getAnalysisType(
+ item.metadata?.analytics_config.analysis
+ ) as DataFrameAnalysisConfigType,
+ },
+ });
+
+ await navigateToUrl(url);
},
isPrimary: true,
},
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index 7cd9fcc052f1a..178638322bacd 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -33,13 +33,13 @@ import {
JOB_ID_MAX_LENGTH,
ALLOWED_DATA_UNITS,
} from '../../../../../../../common/constants/validation';
+import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
import {
getDependentVar,
getNumTopFeatureImportanceValues,
getTrainingPercent,
isRegressionAnalysis,
isClassificationAnalysis,
- ANALYSIS_CONFIG_TYPE,
NUM_TOP_FEATURE_IMPORTANCE_VALUES_MIN,
TRAINING_PERCENT_MIN,
TRAINING_PERCENT_MAX,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index 4926decaa7f9c..2a89c5a5fd686 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -8,13 +8,14 @@ import { DeepPartial, DeepReadonly } from '../../../../../../../common/types/com
import { checkPermission } from '../../../../../capabilities/check_capabilities';
import { mlNodesAvailable } from '../../../../../ml_nodes_check';
-import { ANALYSIS_CONFIG_TYPE, defaultSearchQuery } from '../../../../common/analytics';
+import { defaultSearchQuery, getAnalysisType } from '../../../../common/analytics';
import { CloneDataFrameAnalyticsConfig } from '../../components/action_clone';
import {
DataFrameAnalyticsConfig,
DataFrameAnalyticsId,
+ DataFrameAnalysisConfigType,
} from '../../../../../../../common/types/data_frame_analytics';
-
+import { ANALYSIS_CONFIG_TYPE } from '../../../../../../../common/constants/data_frame_analytics';
export enum DEFAULT_MODEL_MEMORY_LIMIT {
regression = '100mb',
outlier_detection = '50mb',
@@ -28,7 +29,7 @@ export const UNSET_CONFIG_ITEM = '--';
export type EsIndexName = string;
export type DependentVariable = string;
export type IndexPatternTitle = string;
-export type AnalyticsJobType = ANALYSIS_CONFIG_TYPE | undefined;
+export type AnalyticsJobType = DataFrameAnalysisConfigType | undefined;
type IndexPatternId = string;
export type SourceIndexMap = Record<
IndexPatternTitle,
@@ -290,7 +291,7 @@ export function getFormStateFromJobConfig(
analyticsJobConfig: Readonly,
isClone: boolean = true
): Partial {
- const jobType = Object.keys(analyticsJobConfig.analysis)[0] as ANALYSIS_CONFIG_TYPE;
+ const jobType = getAnalysisType(analyticsJobConfig.analysis) as DataFrameAnalysisConfigType;
const resultState: Partial = {
jobType,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
index 41f3bab8113f0..14427dd5c6ef2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
@@ -11,7 +11,7 @@ import {
GetDataFrameAnalyticsStatsResponseOk,
} from '../../../../../services/ml_api_service/data_frame_analytics';
import {
- ANALYSIS_CONFIG_TYPE,
+ getAnalysisType,
REFRESH_ANALYTICS_LIST_STATE,
refreshAnalyticsList$,
} from '../../../../common';
@@ -25,6 +25,7 @@ import {
isDataFrameAnalyticsStopped,
} from '../../components/analytics_list/common';
import { AnalyticStatsBarStats } from '../../../../../components/stats_bar';
+import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
export const isGetDataFrameAnalyticsStatsResponseOk = (
arg: any
@@ -143,7 +144,7 @@ export const getAnalyticsFactory = (
checkpointing: {},
config,
id: config.id,
- job_type: Object.keys(config.analysis)[0] as ANALYSIS_CONFIG_TYPE,
+ job_type: getAnalysisType(config.analysis) as DataFrameAnalysisConfigType,
mode: DATA_FRAME_MODE.BATCH,
state: stats.state,
stats,
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx
index 769b83c03110b..7c30dc0cac690 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/datavisualizer_selector.tsx
@@ -52,7 +52,10 @@ function startTrialDescription() {
export const DatavisualizerSelector: FC = () => {
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
const {
- services: { licenseManagement },
+ services: {
+ licenseManagement,
+ http: { basePath },
+ },
} = useMlKibana();
const navigateToPath = useNavigateToPath();
@@ -183,7 +186,10 @@ export const DatavisualizerSelector: FC = () => {
}
description={startTrialDescription()}
footer={
-
+
= ({
to: 'now',
});
const [showCreateJobLink, setShowCreateJobLink] = useState(false);
- const [globalStateString, setGlobalStateString] = useState('');
+ const [globalState, setGlobalState] = useState();
+
+ const [discoverLink, setDiscoverLink] = useState('');
const {
services: {
http: { basePath },
},
} = useMlKibana();
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
+ const {
+ services: {
+ share: {
+ urlGenerators: { getUrlGenerator },
+ },
+ },
+ } = useMlKibana();
+
+ useEffect(() => {
+ let unmounted = false;
+
+ const getDiscoverUrl = async (): Promise => {
+ const state: DiscoverUrlGeneratorState = {
+ indexPatternId,
+ };
+
+ if (globalState?.time) {
+ state.timeRange = globalState.time;
+ }
+ if (!unmounted) {
+ const discoverUrlGenerator = getUrlGenerator(DISCOVER_APP_URL_GENERATOR);
+ const discoverUrl = await discoverUrlGenerator.createUrl(state);
+ setDiscoverLink(discoverUrl);
+ }
+ };
+ getDiscoverUrl();
+
+ return () => {
+ unmounted = true;
+ };
+ }, [indexPatternId, getUrlGenerator]);
+
+ const openInDataVisualizer = useCallback(async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER,
+ pageState: {
+ index: indexPatternId,
+ globalState,
+ },
+ });
+ await navigateToPath(path);
+ }, [indexPatternId, globalState]);
+
+ const redirectToADCreateJobsSelectTypePage = useCallback(async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE,
+ pageState: {
+ index: indexPatternId,
+ globalState,
+ },
+ });
+ await navigateToPath(path);
+ }, [indexPatternId, globalState]);
useEffect(() => {
setShowCreateJobLink(checkPermission('canCreateJob') && mlNodesAvailable());
@@ -49,11 +113,13 @@ export const ResultsLinks: FC = ({
}, []);
useEffect(() => {
- const _g =
- timeFieldName !== undefined
- ? `&_g=(time:(from:'${duration.from}',mode:quick,to:'${duration.to}'))`
- : '';
- setGlobalStateString(_g);
+ const _globalState: MlCommonGlobalState = {
+ time: {
+ from: duration.from,
+ to: duration.to,
+ },
+ };
+ setGlobalState(_globalState);
}, [duration]);
async function updateTimeValues(recheck = true) {
@@ -89,7 +155,7 @@ export const ResultsLinks: FC = ({
/>
}
description=""
- href={`${basePath.get()}/app/discover#/?&_a=(index:'${indexPatternId}')${globalStateString}`}
+ href={discoverLink}
/>
)}
@@ -108,7 +174,7 @@ export const ResultsLinks: FC = ({
/>
}
description=""
- href={`#/jobs/new_job/step/job_type?index=${indexPatternId}${globalStateString}`}
+ onClick={redirectToADCreateJobsSelectTypePage}
/>
)}
@@ -124,7 +190,7 @@ export const ResultsLinks: FC = ({
/>
}
description=""
- href={`#/jobs/new_job/datavisualizer?index=${indexPatternId}${globalStateString}`}
+ onClick={openInDataVisualizer}
/>
)}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
index 1f2c97b128e3f..ab738ca0f1545 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/actions_panel/actions_panel.tsx
@@ -9,11 +9,11 @@ import React, { FC, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiText, EuiTitle, EuiFlexGroup } from '@elastic/eui';
-
+import { Link } from 'react-router-dom';
import { IndexPattern } from '../../../../../../../../../src/plugins/data/public';
import { CreateJobLinkCard } from '../../../../components/create_job_link_card';
import { DataRecognizer } from '../../../../components/data_recognizer';
-import { getBasePath } from '../../../../util/dependency_cache';
+import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
interface Props {
indexPattern: IndexPattern;
@@ -21,7 +21,6 @@ interface Props {
export const ActionsPanel: FC = ({ indexPattern }) => {
const [recognizerResultsCount, setRecognizerResultsCount] = useState(0);
- const basePath = getBasePath();
const recognizerResults = {
count: 0,
@@ -29,12 +28,7 @@ export const ActionsPanel: FC = ({ indexPattern }) => {
setRecognizerResultsCount(recognizerResults.count);
},
};
-
- function openAdvancedJobWizard() {
- // TODO - pass the search string to the advanced job page as well as the index pattern
- // (add in with new advanced job wizard?)
- window.open(`${basePath.get()}/app/ml/jobs/new_job/advanced?index=${indexPattern.id}`, '_self');
- }
+ const createJobLink = `/${ML_PAGES.ANOMALY_DETECTION_CREATE_JOB}/advanced?index=${indexPattern.id}`;
// Note we use display:none for the DataRecognizer section as it needs to be
// passed the recognizerResults object, and then run the recognizer check which
@@ -78,19 +72,19 @@ export const ActionsPanel: FC = ({ indexPattern }) => {
-
+
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap
index c6503a639997d..826f7b707cfdf 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/__snapshots__/explorer_no_jobs_found.test.js.snap
@@ -3,17 +3,20 @@
exports[`ExplorerNoInfluencersFound snapshot 1`] = `
-
-
+
+
+
+
}
data-test-subj="mlNoJobsFound"
iconType="alert"
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js
index 6f391f9746f23..029ca0475015f 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.js
@@ -7,25 +7,40 @@
/*
* React component for rendering EuiEmptyPrompt when no jobs were found.
*/
-
+import { Link } from 'react-router-dom';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
-
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { useMlLink } from '../../../contexts/kibana/use_create_url';
-export const ExplorerNoJobsFound = () => (
-
-
-
- }
- actions={
-
-
-
- }
- data-test-subj="mlNoJobsFound"
- />
-);
+export const ExplorerNoJobsFound = () => {
+ const ADJobsManagementUrl = useMlLink({
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ excludeBasePath: true,
+ });
+ return (
+
+
+
+ }
+ actions={
+
+
+
+
+
+ }
+ data-test-subj="mlNoJobsFound"
+ />
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js
index bcb11cad9674c..c9645b787a8e0 100644
--- a/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js
+++ b/x-pack/plugins/ml/public/application/explorer/components/explorer_no_jobs_found/explorer_no_jobs_found.test.js
@@ -8,6 +8,9 @@ import React from 'react';
import { shallow } from 'enzyme';
import { ExplorerNoJobsFound } from './explorer_no_jobs_found';
+jest.mock('../../../contexts/kibana/use_create_url', () => ({
+ useMlLink: jest.fn().mockReturnValue('/jobs'),
+}));
describe('ExplorerNoInfluencersFound', () => {
test('snapshot', () => {
const wrapper = shallow();
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
index 4fb783bfb6006..8f03b1903800a 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.js
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
import {
EuiButtonEmpty,
@@ -28,6 +28,10 @@ import { CHART_TYPE } from '../explorer_constants';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { MlTooltipComponent } from '../../components/chart_tooltip';
+import { withKibana } from '../../../../../../../src/plugins/kibana_react/public';
+import { ML_APP_URL_GENERATOR } from '../../../../common/constants/ml_url_generator';
+import { PLUGIN_ID } from '../../../../common/constants/app';
+import { addItemToRecentlyAccessed } from '../../util/recently_accessed';
const textTooManyBuckets = i18n.translate('xpack.ml.explorer.charts.tooManyBucketsDescription', {
defaultMessage:
@@ -51,7 +55,23 @@ function getChartId(series) {
}
// Wrapper for a single explorer chart
-function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel }) {
+function ExplorerChartContainer({
+ series,
+ severity,
+ tooManyBuckets,
+ wrapLabel,
+ navigateToApp,
+ mlUrlGenerator,
+}) {
+ const redirectToSingleMetricViewer = useCallback(async () => {
+ const singleMetricViewerLink = await getExploreSeriesLink(mlUrlGenerator, series);
+ addItemToRecentlyAccessed('timeseriesexplorer', series.jobId, singleMetricViewerLink);
+
+ await navigateToApp(PLUGIN_ID, {
+ path: singleMetricViewerLink,
+ });
+ }, [mlUrlGenerator]);
+
const { detectorLabel, entityFields } = series;
const chartType = getChartType(series);
@@ -106,7 +126,7 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel })
iconSide="right"
iconType="visLine"
size="xs"
- onClick={() => window.open(getExploreSeriesLink(series), '_blank')}
+ onClick={redirectToSingleMetricViewer}
>
@@ -150,12 +170,24 @@ function ExplorerChartContainer({ series, severity, tooManyBuckets, wrapLabel })
}
// Flex layout wrapper for all explorer charts
-export const ExplorerChartsContainer = ({
+export const ExplorerChartsContainerUI = ({
chartsPerRow,
seriesToPlot,
severity,
tooManyBuckets,
+ kibana,
}) => {
+ const {
+ services: {
+ application: { navigateToApp },
+
+ share: {
+ urlGenerators: { getUrlGenerator },
+ },
+ },
+ } = kibana;
+ const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
+
// doesn't allow a setting of `columns={1}` when chartsPerRow would be 1.
// If that's the case we trick it doing that with the following settings:
const chartsWidth = chartsPerRow === 1 ? 'calc(100% - 20px)' : 'auto';
@@ -177,9 +209,13 @@ export const ExplorerChartsContainer = ({
severity={severity}
tooManyBuckets={tooManyBuckets}
wrapLabel={wrapLabel}
+ navigateToApp={navigateToApp}
+ mlUrlGenerator={mlUrlGenerator}
/>
))}
);
};
+
+export const ExplorerChartsContainer = withKibana(ExplorerChartsContainerUI);
diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js
index 8257ac2b3a703..2da212c8f2f29 100644
--- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js
+++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container.test.js
@@ -40,6 +40,12 @@ jest.mock('../../services/job_service', () => ({
},
}));
+jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({
+ withKibana: (comp) => {
+ return comp;
+ },
+}));
+
describe('ExplorerChartsContainer', () => {
const mockedGetBBox = { x: 0, y: -11.5, width: 12.1875, height: 14.5 };
const originalGetBBox = SVGElement.prototype.getBBox;
@@ -47,10 +53,22 @@ describe('ExplorerChartsContainer', () => {
beforeEach(() => (SVGElement.prototype.getBBox = () => mockedGetBBox));
afterEach(() => (SVGElement.prototype.getBBox = originalGetBBox));
+ const kibanaContextMock = {
+ services: {
+ application: { navigateToApp: jest.fn() },
+ share: {
+ urlGenerators: { getUrlGenerator: jest.fn() },
+ },
+ },
+ };
test('Minimal Initialization', () => {
const wrapper = shallow(
-
+
);
@@ -71,10 +89,11 @@ describe('ExplorerChartsContainer', () => {
],
chartsPerRow: 1,
tooManyBuckets: false,
+ severity: 10,
};
const wrapper = mount(
-
+
);
@@ -98,10 +117,11 @@ describe('ExplorerChartsContainer', () => {
],
chartsPerRow: 1,
tooManyBuckets: false,
+ severity: 10,
};
const wrapper = mount(
-
+
);
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
index d0d0442dd4aee..85a342838a506 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_actions/results.js
@@ -5,13 +5,20 @@
*/
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useMemo } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useCreateADLinks } from '../../../../components/custom_hooks/use_create_ad_links';
+import { Link } from 'react-router-dom';
+import { useMlKibana } from '../../../../contexts/kibana';
-export function ResultLinks({ jobs }) {
+export function ResultLinks({ jobs, isManagementTable }) {
+ const {
+ services: {
+ http: { basePath },
+ },
+ } = useMlKibana();
const openJobsInSingleMetricViewerText = i18n.translate(
'xpack.ml.jobsList.resultActions.openJobsInSingleMetricViewerText',
{
@@ -37,29 +44,59 @@ export function ResultLinks({ jobs }) {
const singleMetricEnabled = jobs.length === 1 && jobs[0].isSingleMetricViewerJob;
const jobActionsDisabled = jobs.length === 1 && jobs[0].deleting === true;
const { createLinkWithUserDefaults } = useCreateADLinks();
+ const timeSeriesExplorerLink = useMemo(
+ () => createLinkWithUserDefaults('timeseriesexplorer', jobs),
+ [jobs]
+ );
+ const anomalyExplorerLink = useMemo(() => createLinkWithUserDefaults('explorer', jobs), [jobs]);
+
return (
{singleMetricVisible && (
+ {isManagementTable ? (
+
+ ) : (
+
+
+
+ )}
+
+ )}
+
+ {isManagementTable ? (
-
- )}
-
-
+ ) : (
+
+
+
+ )}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
index 8f89c4a049189..73b212b97b4cc 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/extract_job_details.js
@@ -5,10 +5,10 @@
*/
import React from 'react';
-import { EuiLink } from '@elastic/eui';
import { detectorToString } from '../../../../util/string_utils';
import { formatValues, filterObjects } from './format_values';
import { i18n } from '@kbn/i18n';
+import { Link } from 'react-router-dom';
export function extractJobDetails(job) {
if (Object.keys(job).length === 0) {
@@ -61,7 +61,7 @@ export function extractJobDetails(job) {
if (job.calendars) {
calendars.items = job.calendars.map((c) => [
'',
- {c},
+ {c},
]);
// remove the calendars list from the general section
// so not to show it twice.
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js
index b6157c8694a18..b32070fff73aa 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/job_details/forecasts_table/forecasts_table.js
@@ -5,8 +5,6 @@
*/
import PropTypes from 'prop-types';
-import rison from 'rison-node';
-
import React, { Component } from 'react';
import {
@@ -30,13 +28,19 @@ import {
getLatestDataOrBucketTimestamp,
isTimeSeriesViewJob,
} from '../../../../../../../common/util/job_utils';
+import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
+import {
+ ML_APP_URL_GENERATOR,
+ ML_PAGES,
+} from '../../../../../../../common/constants/ml_url_generator';
+import { PLUGIN_ID } from '../../../../../../../common/constants/app';
const MAX_FORECASTS = 500;
/**
* Table component for rendering the lists of forecasts run on an ML job.
*/
-export class ForecastsTable extends Component {
+export class ForecastsTableUI extends Component {
constructor(props) {
super(props);
this.state = {
@@ -78,7 +82,17 @@ export class ForecastsTable extends Component {
}
}
- openSingleMetricView(forecast) {
+ async openSingleMetricView(forecast) {
+ const {
+ services: {
+ application: { navigateToApp },
+
+ share: {
+ urlGenerators: { getUrlGenerator },
+ },
+ },
+ } = this.props.kibana;
+
// Creates the link to the Single Metric Viewer.
// Set the total time range from the start of the job data to the end of the forecast,
const dataCounts = this.props.job.data_counts;
@@ -93,31 +107,7 @@ export class ForecastsTable extends Component {
? new Date(forecast.forecast_end_timestamp).toISOString()
: new Date(resultLatest).toISOString();
- const _g = rison.encode({
- ml: {
- jobIds: [this.props.job.job_id],
- },
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
- },
- time: {
- from,
- to,
- mode: 'absolute',
- },
- });
-
- const appState = {
- query: {
- query_string: {
- analyze_wildcard: true,
- query: '*',
- },
- },
- };
-
+ let mlTimeSeriesExplorer = {};
if (forecast !== undefined) {
// Set the zoom to show duration before the forecast equal to the length of the forecast.
const forecastDurationMs =
@@ -126,8 +116,7 @@ export class ForecastsTable extends Component {
forecast.forecast_start_timestamp - forecastDurationMs,
jobEarliest
);
-
- appState.mlTimeSeriesExplorer = {
+ mlTimeSeriesExplorer = {
forecastId: forecast.forecast_id,
zoom: {
from: new Date(zoomFrom).toISOString(),
@@ -136,11 +125,39 @@ export class ForecastsTable extends Component {
};
}
- const _a = rison.encode(appState);
-
- const url = `?_g=${_g}&_a=${_a}`;
- addItemToRecentlyAccessed('timeseriesexplorer', this.props.job.job_id, url);
- window.open(`#/timeseriesexplorer${url}`, '_self');
+ const mlUrlGenerator = getUrlGenerator(ML_APP_URL_GENERATOR);
+ const singleMetricViewerForecastLink = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.SINGLE_METRIC_VIEWER,
+ pageState: {
+ timeRange: {
+ from,
+ to,
+ mode: 'absolute',
+ },
+ refreshInterval: {
+ display: 'Off',
+ pause: false,
+ value: 0,
+ },
+ jobIds: [this.props.job.job_id],
+ query: {
+ query_string: {
+ analyze_wildcard: true,
+ query: '*',
+ },
+ },
+ ...mlTimeSeriesExplorer,
+ },
+ excludeBasePath: true,
+ });
+ addItemToRecentlyAccessed(
+ 'timeseriesexplorer',
+ this.props.job.job_id,
+ singleMetricViewerForecastLink
+ );
+ await navigateToApp(PLUGIN_ID, {
+ path: singleMetricViewerForecastLink,
+ });
}
render() {
@@ -322,6 +339,8 @@ export class ForecastsTable extends Component {
);
}
}
-ForecastsTable.propTypes = {
+ForecastsTableUI.propTypes = {
job: PropTypes.object.isRequired,
};
+
+export const ForecastsTable = withKibana(ForecastsTableUI);
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js
index a5469357ba1a1..8b5d6009cc61e 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_description.js
@@ -8,7 +8,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { JobGroup } from '../job_group';
-import { getGroupIdsUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
+import { AnomalyDetectionJobIdLink } from './job_id_link';
export function JobDescription({ job, isManagementTable }) {
return (
@@ -17,11 +17,7 @@ export function JobDescription({ job, isManagementTable }) {
{job.description}
{job.groups.map((group) => {
if (isManagementTable === true) {
- return (
-
-
-
- );
+ return ;
}
return ;
})}
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx
new file mode 100644
index 0000000000000..0e84619899d71
--- /dev/null
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/job_id_link.tsx
@@ -0,0 +1,63 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiLink } from '@elastic/eui';
+import { useMlKibana, useMlUrlGenerator } from '../../../../contexts/kibana';
+import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
+import { AnomalyDetectionQueryState } from '../../../../../../common/types/ml_url_generator';
+// @ts-ignore
+import { JobGroup } from '../job_group';
+
+interface JobIdLink {
+ id: string;
+}
+
+interface GroupIdLink {
+ groupId: string;
+ children: string;
+}
+
+type AnomalyDetectionJobIdLinkProps = JobIdLink | GroupIdLink;
+
+function isGroupIdLink(props: JobIdLink | GroupIdLink): props is GroupIdLink {
+ return (props as GroupIdLink).groupId !== undefined;
+}
+export const AnomalyDetectionJobIdLink = (props: AnomalyDetectionJobIdLinkProps) => {
+ const mlUrlGenerator = useMlUrlGenerator();
+ const {
+ services: {
+ application: { navigateToUrl },
+ },
+ } = useMlKibana();
+
+ const redirectToJobsManagementPage = async () => {
+ const pageState: AnomalyDetectionQueryState = {};
+ if (isGroupIdLink(props)) {
+ pageState.groupIds = [props.groupId];
+ } else {
+ pageState.jobId = props.id;
+ }
+ const url = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ pageState,
+ });
+ await navigateToUrl(url);
+ };
+ if (isGroupIdLink(props)) {
+ return (
+ redirectToJobsManagementPage()}>
+
+
+ );
+ } else {
+ return (
+ redirectToJobsManagementPage()}>
+ {props.id}
+
+ );
+ }
+};
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js
index fa4ea09b89ff9..8bc0057b27d6d 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/jobs_list/jobs_list.js
@@ -14,12 +14,12 @@ import { toLocaleString } from '../../../../util/string_utils';
import { ResultLinks, actionsMenuContent } from '../job_actions';
import { JobDescription } from './job_description';
import { JobIcon } from '../../../../components/job_message_icon';
-import { getJobIdUrl, TAB_IDS } from '../../../../util/get_selected_ids_url';
import { TIME_FORMAT } from '../../../../../../common/constants/time_format';
-import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiLink, EuiScreenReaderOnly } from '@elastic/eui';
+import { EuiBadge, EuiBasicTable, EuiButtonIcon, EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { AnomalyDetectionJobIdLink } from './job_id_link';
const PAGE_SIZE = 10;
const PAGE_SIZE_OPTIONS = [10, 25, 50];
@@ -71,7 +71,7 @@ export class JobsList extends Component {
return id;
}
- return {id};
+ return ;
}
getPageOfJobs(index, size, sortField, sortDirection) {
@@ -241,7 +241,7 @@ export class JobsList extends Component {
name: i18n.translate('xpack.ml.jobsList.actionsLabel', {
defaultMessage: 'Actions',
}),
- render: (item) => ,
+ render: (item) => ,
},
];
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js
index fdffa8b38ae04..81effe8d3ebeb 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/new_job_button/new_job_button.js
@@ -11,13 +11,13 @@ import React from 'react';
import { EuiButton } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
-
-function newJob() {
- window.location.href = `#/jobs/new_job`;
-}
+import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
export function NewJobButton() {
const buttonEnabled = checkPermission('canCreateJob') && mlNodesAvailable();
+ const newJob = useCreateAndNavigateToMlLink(ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX);
+
return (
{
const {
@@ -73,7 +74,7 @@ export const CalendarsSelection: FC = () => {
};
const manageCalendarsHref = getUrlForApp(PLUGIN_ID, {
- path: '/settings/calendars_list',
+ path: ML_PAGES.CALENDARS_MANAGE,
});
return (
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx
index 669b8837e74b5..021039c06e320 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx
@@ -39,7 +39,10 @@ import { JobSectionTitle, DatafeedSectionTitle } from './components/common';
export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => {
const {
- services: { notifications },
+ services: {
+ notifications,
+ http: { basePath },
+ },
} = useMlKibana();
const navigateToPath = useNavigateToPath();
@@ -108,7 +111,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) =>
jobCreator.end,
isSingleMetricJobCreator(jobCreator) === true ? 'timeseriesexplorer' : 'explorer'
);
- window.open(url, '_blank');
+ navigateToPath(`${basePath.get()}/app/ml/${url}`);
}
function clickResetJob() {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
index 69df2773f9f8d..cedaaa3b5dfaa 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/index_or_search/preconfigured_job_redirect.ts
@@ -4,19 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ApplicationStart } from 'kibana/public';
import { IndexPatternsContract } from '../../../../../../../../../src/plugins/data/public';
import { mlJobService } from '../../../../services/job_service';
import { loadIndexPatterns, getIndexPatternIdFromName } from '../../../../util/index_utils';
import { CombinedJob } from '../../../../../../common/types/anomaly_detection_jobs';
import { CREATED_BY_LABEL, JOB_TYPE } from '../../../../../../common/constants/new_job';
-export async function preConfiguredJobRedirect(indexPatterns: IndexPatternsContract) {
+export async function preConfiguredJobRedirect(
+ indexPatterns: IndexPatternsContract,
+ basePath: string,
+ navigateToUrl: ApplicationStart['navigateToUrl']
+) {
const { job } = mlJobService.tempJobCloningObjects;
if (job) {
try {
await loadIndexPatterns(indexPatterns);
const redirectUrl = getWizardUrlFromCloningJob(job);
- window.location.href = `#/${redirectUrl}`;
+ await navigateToUrl(`${basePath}/app/ml/${redirectUrl}`);
return Promise.reject();
} catch (error) {
return Promise.resolve();
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
index be0135ec3f1e0..1a91f6d51ed4d 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/job_type/page.tsx
@@ -19,6 +19,7 @@ import {
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { useNavigateToPath } from '../../../../contexts/kibana';
+
import { useMlContext } from '../../../../contexts/ml';
import { isSavedSearchSavedObject } from '../../../../../../common/types/kibana';
import { DataRecognizer } from '../../../../components/data_recognizer';
@@ -26,10 +27,15 @@ import { addItemToRecentlyAccessed } from '../../../../util/recently_accessed';
import { timeBasedIndexCheck } from '../../../../util/index_utils';
import { CreateJobLinkCard } from '../../../../components/create_job_link_card';
import { CategorizationIcon } from './categorization_job_icon';
+import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
+import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
export const Page: FC = () => {
const mlContext = useMlContext();
const navigateToPath = useNavigateToPath();
+ const onSelectDifferentIndex = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
+ );
const [recognizerResultsCount, setRecognizerResultsCount] = useState(0);
@@ -193,7 +199,7 @@ export const Page: FC = () => {
defaultMessage="Anomaly detection can only be run over indices which are time based."
/>
-
+
= ({ moduleId, existingGroupIds }) => {
const {
services: { notifications },
} = useMlKibana();
+ const urlGenerator = useMlUrlGenerator();
+
// #region State
const [jobPrefix, setJobPrefix] = useState('');
const [jobs, setJobs] = useState([]);
@@ -185,14 +189,20 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => {
})
);
setKibanaObjects(merge(kibanaObjects, kibanaResponse));
- setResultsUrl(
- mlJobService.createResultsUrl(
- jobsResponse.filter(({ success }) => success).map(({ id }) => id),
- resultTimeRange.start,
- resultTimeRange.end,
- 'explorer'
- )
- );
+
+ const url = await urlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_EXPLORER,
+ pageState: {
+ jobIds: jobsResponse.filter(({ success }) => success).map(({ id }) => id),
+ timeRange: {
+ from: moment(resultTimeRange.start).format(TIME_FORMAT),
+ to: moment(resultTimeRange.end).format(TIME_FORMAT),
+ mode: 'absolute',
+ },
+ },
+ });
+
+ setResultsUrl(url);
const failedJobsCount = jobsResponse.reduce((count, { success }) => {
return success ? count : count + 1;
}, 0);
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts
index e3b0fd4cefe0c..97a03fa21035f 100644
--- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts
+++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/resolvers.ts
@@ -6,33 +6,40 @@
import { i18n } from '@kbn/i18n';
import { getToastNotifications, getSavedObjectsClient } from '../../../util/dependency_cache';
-import { mlJobService } from '../../../services/job_service';
import { ml } from '../../../services/ml_api_service';
import { KibanaObjects } from './page';
+import { NavigateToPath } from '../../../contexts/kibana';
+import { CreateLinkWithUserDefaults } from '../../../components/custom_hooks/use_create_ad_links';
/**
* Checks whether the jobs in a data recognizer module have been created.
* Redirects to the Anomaly Explorer to view the jobs if they have been created,
* or the recognizer job wizard for the module if not.
*/
-export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string): Promise {
+export function checkViewOrCreateJobs(
+ moduleId: string,
+ indexPatternId: string,
+ createLinkWithUserDefaults: CreateLinkWithUserDefaults,
+ navigateToPath: NavigateToPath
+): Promise {
return new Promise((resolve, reject) => {
// Load the module, and check if the job(s) in the module have been created.
// If so, load the jobs in the Anomaly Explorer.
// Otherwise open the data recognizer wizard for the module.
// Always want to call reject() so as not to load original page.
ml.dataRecognizerModuleJobsExist({ moduleId })
- .then((resp: any) => {
+ .then(async (resp: any) => {
if (resp.jobsExist === true) {
- const resultsPageUrl = mlJobService.createResultsUrlForJobs(resp.jobs, 'explorer');
- window.location.href = resultsPageUrl;
+ // also honor user's time filter setting in Advanced Settings
+ const url = createLinkWithUserDefaults('explorer', resp.jobs);
+ await navigateToPath(url);
reject();
} else {
- window.location.href = `#/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`;
+ await navigateToPath(`/jobs/new_job/recognize?id=${moduleId}&index=${indexPatternId}`);
reject();
}
})
- .catch((err: Error) => {
+ .catch(async (err: Error) => {
// eslint-disable-next-line no-console
console.error(`Error checking whether jobs in module ${moduleId} exists`, err);
const toastNotifications = getToastNotifications();
@@ -46,8 +53,7 @@ export function checkViewOrCreateJobs(moduleId: string, indexPatternId: string):
'An error occurred trying to check whether the jobs in the module have been created.',
}),
});
-
- window.location.href = '#/jobs';
+ await navigateToPath(`/jobs`);
reject();
});
});
diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
index 0af6030df28b1..9c9096dfdfc21 100644
--- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
+++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx
@@ -31,6 +31,7 @@ import { getDocLinks } from '../../../../util/dependency_cache';
import { JobsListView } from '../../../../jobs/jobs_list/components/jobs_list_view/index';
import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/analytics_management/components/analytics_list';
import { AccessDeniedPage } from '../access_denied_page';
+import { SharePluginStart } from '../../../../../../../../../src/plugins/share/public';
interface Tab {
'data-test-subj': string;
@@ -75,8 +76,9 @@ function getTabs(isMlEnabledInSpace: boolean): Tab[] {
export const JobsListPage: FC<{
coreStart: CoreStart;
+ share: SharePluginStart;
history: ManagementAppMountParams['history'];
-}> = ({ coreStart, history }) => {
+}> = ({ coreStart, share, history }) => {
const [initialized, setInitialized] = useState(false);
const [accessDenied, setAccessDenied] = useState(false);
const [isMlEnabledInSpace, setIsMlEnabledInSpace] = useState(false);
@@ -136,7 +138,7 @@ export const JobsListPage: FC<{
return (
-
+
{
- ReactDOM.render(React.createElement(JobsListPage, { coreStart, history }), element);
+ ReactDOM.render(React.createElement(JobsListPage, { coreStart, history, share }), element);
return () => {
unmountComponentAtNode(element);
clearCache();
@@ -30,7 +32,7 @@ export async function mountApp(
core: CoreSetup,
params: ManagementAppMountParams
) {
- const [coreStart] = await core.getStartServices();
+ const [coreStart, pluginsStart] = await core.getStartServices();
setDependencyCache({
docLinks: coreStart.docLinks!,
@@ -41,5 +43,5 @@ export async function mountApp(
params.setBreadcrumbs(getJobsListBreadcrumbs());
- return renderApp(params.element, params.history, coreStart);
+ return renderApp(params.element, params.history, coreStart, pluginsStart.share);
}
diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
index 1792999eee4c2..d0cfd16d7562f 100644
--- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
+++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
@@ -9,7 +9,7 @@ import { ml } from '../services/ml_api_service';
let mlNodeCount: number = 0;
let userHasPermissionToViewMlNodeCount: boolean = false;
-export async function checkMlNodesAvailable() {
+export async function checkMlNodesAvailable(redirectToJobsManagementPage: () => Promise) {
try {
const nodes = await getMlNodeCount();
if (nodes.count !== undefined && nodes.count > 0) {
@@ -20,7 +20,7 @@ export async function checkMlNodesAvailable() {
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
- window.location.href = '#/jobs';
+ await redirectToJobsManagementPage();
Promise.reject();
}
}
diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx
index 395a570083c0d..4f0cbc0adddf2 100644
--- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/actions.tsx
@@ -4,30 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useCallback, FC } from 'react';
+import React, { FC, useMemo } from 'react';
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { useNavigateToPath } from '../../../contexts/kibana';
+import { Link } from 'react-router-dom';
+import { useMlLink } from '../../../contexts/kibana';
import { getAnalysisType } from '../../../data_frame_analytics/common/analytics';
-import {
- getResultsUrl,
- DataFrameAnalyticsListRow,
-} from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
+import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { getViewLinkStatus } from '../../../data_frame_analytics/pages/analytics_management/components/action_view/get_view_link_status';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
interface Props {
item: DataFrameAnalyticsListRow;
}
export const ViewLink: FC = ({ item }) => {
- const navigateToPath = useNavigateToPath();
-
- const clickHandler = useCallback(() => {
- const analysisType = getAnalysisType(item.config.analysis);
- navigateToPath(getResultsUrl(item.id, analysisType));
- }, []);
-
const { disabled, tooltipContent } = getViewLinkStatus(item);
const viewJobResultsButtonText = i18n.translate(
@@ -38,23 +31,34 @@ export const ViewLink: FC = ({ item }) => {
);
const tooltipText = disabled === false ? viewJobResultsButtonText : tooltipContent;
+ const analysisType = useMemo(() => getAnalysisType(item.config.analysis), [item]);
+
+ const viewAnalyticsResultsLink = useMlLink({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
+ pageState: {
+ jobId: item.id,
+ analysisType: analysisType as DataFrameAnalysisConfigType,
+ },
+ excludeBasePath: true,
+ });
return (
-
- {i18n.translate('xpack.ml.overview.analytics.viewActionName', {
- defaultMessage: 'View',
- })}
-
+
+
+ {i18n.translate('xpack.ml.overview.analytics.viewActionName', {
+ defaultMessage: 'View',
+ })}
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
index be8038cc5049d..4d810c47415a7 100644
--- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
@@ -23,6 +23,8 @@ import { AnalyticsTable } from './table';
import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar';
+import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
interface Props {
jobCreationDisabled: boolean;
@@ -35,6 +37,16 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => {
const [errorMessage, setErrorMessage] = useState(undefined);
const [isInitialized, setIsInitialized] = useState(false);
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
+ const redirectToDataFrameAnalyticsManagementPage = async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
+ });
+ await navigateToPath(path, true);
+ };
+
const getAnalytics = getAnalyticsFactory(
setAnalytics,
setAnalyticsStats,
@@ -75,7 +87,6 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => {
{isInitialized === false && (
)}
-
{errorMessage === undefined && isInitialized === true && analytics.length === 0 && (
= ({ jobCreationDisabled }) => {
}
actions={
= ({ jobCreationDisabled }) => {
)}
{isInitialized === true && analytics.length > 0 && (
<>
+
@@ -136,7 +148,7 @@ export const AnalyticsPanel: FC = ({ jobCreationDisabled }) => {
defaultMessage: 'Refresh',
})}
-
+
{i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
index a71141d0356d0..dfba7c9651266 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/actions.tsx
@@ -7,6 +7,7 @@
import React, { FC } from 'react';
import { EuiToolTip, EuiButtonEmpty } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { Link } from 'react-router-dom';
import { MlSummaryJobs } from '../../../../../common/types/anomaly_detection_jobs';
import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';
@@ -26,19 +27,20 @@ export const ExplorerLink: FC = ({ jobsList }) => {
return (
-
- {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', {
- defaultMessage: 'View',
- })}
-
+
+
+ {i18n.translate('xpack.ml.overview.anomalyDetection.viewActionName', {
+ defaultMessage: 'View',
+ })}
+
+
);
};
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
index 0bfd2c2e49232..1cb6bab7fd768 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
@@ -16,12 +16,13 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
-import { useMlKibana } from '../../../contexts/kibana';
+import { useMlKibana, useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana';
import { AnomalyDetectionTable } from './table';
import { ml } from '../../../services/ml_api_service';
import { getGroupsFromJobs, getStatsBarData, getJobsWithTimerange } from './utils';
import { Dictionary } from '../../../../../common/types/common';
import { MlSummaryJobs, MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
export type GroupsDictionary = Dictionary;
@@ -39,8 +40,6 @@ type MaxScoresByGroup = Dictionary<{
index?: number;
}>;
-const createJobLink = '#/jobs/new_job/step/index_or_search';
-
function getDefaultAnomalyScores(groups: Group[]): MaxScoresByGroup {
const anomalyScores: MaxScoresByGroup = {};
groups.forEach((group) => {
@@ -58,6 +57,23 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => {
const {
services: { notifications },
} = useMlKibana();
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
+ const redirectToJobsManagementPage = async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ });
+ await navigateToPath(path, true);
+ };
+
+ const redirectToCreateJobSelectIndexPage = async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX,
+ });
+ await navigateToPath(path, true);
+ };
+
const [isLoading, setIsLoading] = useState(false);
const [groups, setGroups] = useState({});
const [groupsCount, setGroupsCount] = useState(0);
@@ -157,7 +173,7 @@ export const AnomalyDetectionPanel: FC = ({ jobCreationDisabled }) => {
return (
{typeof errorMessage !== 'undefined' && errorDisplay}
- {isLoading && }
+ {isLoading && }
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount === 0 && (
= ({ jobCreationDisabled }) => {
actions={
= ({ jobCreationDisabled }) => {
defaultMessage: 'Refresh',
})}
-
+
{i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
defaultMessage: 'Manage jobs',
})}
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
index 945116b0534bb..8515431d49b17 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
@@ -88,7 +88,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData
{i18n.translate('xpack.ml.overview.anomalyDetection.tableMaxScore', {
defaultMessage: 'Max anomaly score',
- })}{' '}
+ })}
@@ -203,6 +203,7 @@ export const AnomalyDetectionTable: FC = ({ items, jobsList, statsBarData
return (
+
diff --git a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts
index d0a4f999af758..398ec5b4759d2 100644
--- a/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts
+++ b/x-pack/plugins/ml/public/application/routing/breadcrumbs.ts
@@ -54,6 +54,20 @@ export const CREATE_JOB_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
href: '/jobs/new_job',
});
+export const CALENDAR_MANAGEMENT_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
+ text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', {
+ defaultMessage: 'Calendar management',
+ }),
+ href: '/settings/calendars_list',
+});
+
+export const FILTER_LISTS_BREADCRUMB: ChromeBreadcrumb = Object.freeze({
+ text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', {
+ defaultMessage: 'Filter lists',
+ }),
+ href: '/settings/filter_lists',
+});
+
const breadcrumbs = {
ML_BREADCRUMB,
SETTINGS_BREADCRUMB,
@@ -61,6 +75,8 @@ const breadcrumbs = {
DATA_FRAME_ANALYTICS_BREADCRUMB,
DATA_VISUALIZER_BREADCRUMB,
CREATE_JOB_BREADCRUMB,
+ CALENDAR_MANAGEMENT_BREADCRUMB,
+ FILTER_LISTS_BREADCRUMB,
};
type Breadcrumb = keyof typeof breadcrumbs;
@@ -76,10 +92,12 @@ export const breadcrumbOnClickFactory = (
export const getBreadcrumbWithUrlForApp = (
breadcrumbName: Breadcrumb,
- navigateToPath: NavigateToPath
+ navigateToPath: NavigateToPath,
+ basePath: string
): EuiBreadcrumb => {
return {
- ...breadcrumbs[breadcrumbName],
+ text: breadcrumbs[breadcrumbName].text,
+ href: `${basePath}/app/ml${breadcrumbs[breadcrumbName].href}`,
onClick: breadcrumbOnClickFactory(breadcrumbs[breadcrumbName].href, navigateToPath),
};
};
diff --git a/x-pack/plugins/ml/public/application/routing/resolvers.ts b/x-pack/plugins/ml/public/application/routing/resolvers.ts
index 958221df8a636..9cebb67166a66 100644
--- a/x-pack/plugins/ml/public/application/routing/resolvers.ts
+++ b/x-pack/plugins/ml/public/application/routing/resolvers.ts
@@ -21,13 +21,17 @@ export interface ResolverResults {
interface BasicResolverDependencies {
indexPatterns: IndexPatternsContract;
+ redirectToMlAccessDeniedPage: () => Promise;
}
-export const basicResolvers = ({ indexPatterns }: BasicResolverDependencies): Resolvers => ({
+export const basicResolvers = ({
+ indexPatterns,
+ redirectToMlAccessDeniedPage,
+}: BasicResolverDependencies): Resolvers => ({
checkFullLicense,
getMlNodeCount,
loadMlServerInfo,
loadIndexPatterns: () => loadIndexPatterns(indexPatterns),
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
loadSavedSearches,
});
diff --git a/x-pack/plugins/ml/public/application/routing/router.tsx b/x-pack/plugins/ml/public/application/routing/router.tsx
index 22a17c4ea089a..7cb3a2f07c2ee 100644
--- a/x-pack/plugins/ml/public/application/routing/router.tsx
+++ b/x-pack/plugins/ml/public/application/routing/router.tsx
@@ -12,7 +12,7 @@ import { AppMountParameters, IUiSettingsClient, ChromeStart } from 'kibana/publi
import { ChromeBreadcrumb } from 'kibana/public';
import { IndexPatternsContract } from 'src/plugins/data/public';
-import { useNavigateToPath } from '../contexts/kibana';
+import { useMlKibana, useNavigateToPath } from '../contexts/kibana';
import { MlContext, MlContextValue } from '../contexts/ml';
import { UrlStateProvider } from '../util/url_state';
@@ -39,6 +39,7 @@ interface PageDependencies {
history: AppMountParameters['history'];
indexPatterns: IndexPatternsContract;
setBreadcrumbs: ChromeStart['setBreadcrumbs'];
+ redirectToMlAccessDeniedPage: () => Promise;
}
export const PageLoader: FC<{ context: MlContextValue }> = ({ context, children }) => {
@@ -75,10 +76,16 @@ const MlRoutes: FC<{
pageDeps: PageDependencies;
}> = ({ pageDeps }) => {
const navigateToPath = useNavigateToPath();
+ const {
+ services: {
+ http: { basePath },
+ },
+ } = useMlKibana();
+
return (
<>
{Object.entries(routes).map(([name, routeFactory]) => {
- const route = routeFactory(navigateToPath);
+ const route = routeFactory(navigateToPath, basePath.get());
return (
({
+export const analyticsJobsCreationRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/data_frame_analytics/new_job',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameManagementLabel', {
defaultMessage: 'Data Frame Analytics',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
index 47cc002ab4d83..f9f2ebe48f4aa 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_job_exploration.tsx
@@ -10,21 +10,25 @@ import { decode } from 'rison-node';
import { i18n } from '@kbn/i18n';
-import { NavigateToPath } from '../../../contexts/kibana';
+import { NavigateToPath, useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_exploration';
-import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common/analytics';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
-export const analyticsJobExplorationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const analyticsJobExplorationRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/data_frame_analytics/exploration',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameExplorationLabel', {
defaultMessage: 'Exploration',
@@ -38,16 +42,31 @@ const PageWrapper: FC = ({ location, deps }) => {
const { context } = useResolver('', undefined, deps.config, basicResolvers(deps));
const { _g }: Record = parse(location.search, { sort: false });
+ const urlGenerator = useMlUrlGenerator();
+ const {
+ services: {
+ application: { navigateToUrl },
+ },
+ } = useMlKibana();
+
+ const redirectToAnalyticsManagementPage = async () => {
+ const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE });
+ await navigateToUrl(url);
+ };
+
let globalState: any = null;
try {
globalState = decode(_g);
} catch (error) {
// eslint-disable-next-line no-console
- console.error('Could not parse global state');
- window.location.href = '#data_frame_analytics';
+ console.error(
+ 'Could not parse global state. Redirecting to Data Frame Analytics Management Page.'
+ );
+ redirectToAnalyticsManagementPage();
+ return <>>;
}
const jobId: string = globalState.ml.jobId;
- const analysisType: ANALYSIS_CONFIG_TYPE = globalState.ml.analysisType;
+ const analysisType: DataFrameAnalysisConfigType = globalState.ml.analysisType;
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
index b6ef9ea81b4ba..80706a82121d5 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/analytics_jobs_list.tsx
@@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_management';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const analyticsJobsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const analyticsJobsListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/data_frame_analytics',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.dataFrameListLabel', {
defaultMessage: 'Job Management',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx
index 7bf7784d1b559..b1fd6e93a744c 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/data_frame_analytics/models_list.tsx
@@ -15,12 +15,15 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_management';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const modelsListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const modelsListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/data_frame_analytics/models',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_FRAME_ANALYTICS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.modelsListLabel', {
defaultMessage: 'Model Management',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
index efe5c3cba04a5..f40b754a23ccb 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/datavisualizer.tsx
@@ -21,19 +21,25 @@ import { checkBasicLicense } from '../../../license';
import { checkFindFileStructurePrivilegeResolver } from '../../../capabilities/check_capabilities';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const selectorRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const selectorRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/datavisualizer',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC = ({ location, deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver(undefined, undefined, deps.config, {
checkBasicLicense,
- checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver,
+ checkFindFileStructurePrivilege: () =>
+ checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
});
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
index 485af52c45a55..837616a8a76d2 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/file_based.tsx
@@ -24,12 +24,15 @@ import { loadIndexPatterns } from '../../../util/index_utils';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const fileBasedRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/filedatavisualizer',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataVisualizer.fileBasedLabel', {
defaultMessage: 'File',
@@ -40,10 +43,13 @@ export const fileBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute =
});
const PageWrapper: FC = ({ location, deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver('', undefined, deps.config, {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
- checkFindFileStructurePrivilege: checkFindFileStructurePrivilegeResolver,
+ checkFindFileStructurePrivilege: () =>
+ checkFindFileStructurePrivilegeResolver(redirectToMlAccessDeniedPage),
});
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
index 358b8773e3460..e3d0e5050fca5 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/datavisualizer/index_based.tsx
@@ -20,13 +20,18 @@ import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_ca
import { loadIndexPatterns } from '../../../util/index_utils';
import { checkMlNodesAvailable } from '../../../ml_nodes_check';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
-export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const indexBasedRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/datavisualizer',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('DATA_VISUALIZER_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.dataFrameAnalyticsBreadcrumbs.indexLabel', {
defaultMessage: 'Index',
@@ -37,12 +42,17 @@ export const indexBasedRouteFactory = (navigateToPath: NavigateToPath): MlRoute
});
const PageWrapper: FC = ({ location, deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
+
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
const { context } = useResolver(index, savedSearchId, deps.config, {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
- checkMlNodesAvailable,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
return (
diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
index 30b9bc2af219f..00d64a2f1bd1d 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx
@@ -35,12 +35,15 @@ import { useTimefilter } from '../../contexts/kibana';
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
import { JOB_ID } from '../../../../common/constants/anomalies';
-export const explorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const explorerRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/explorer',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.anomalyExplorerLabel', {
defaultMessage: 'Anomaly Explorer',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
index 38a7900916ba8..2863e59508e35 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/jobs_list.tsx
@@ -20,12 +20,12 @@ import { JobsPage } from '../../jobs/jobs_list';
import { useTimefilter } from '../../contexts/kibana';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
-export const jobListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/jobs',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.jobManagementLabel', {
defaultMessage: 'Job Management',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
index d8605c4cc9115..0ef3b384dcf5d 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/index_or_search.tsx
@@ -8,7 +8,7 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { NavigateToPath } from '../../../contexts/kibana';
+import { NavigateToPath, useMlKibana } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
@@ -19,6 +19,8 @@ import { checkBasicLicense } from '../../../license';
import { loadIndexPatterns } from '../../../util/index_utils';
import { checkGetJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
import { checkMlNodesAvailable } from '../../../ml_nodes_check';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
+import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
enum MODE {
NEW_JOB,
@@ -30,9 +32,9 @@ interface IndexOrSearchPageProps extends PageProps {
mode: MODE;
}
-const getBreadcrumbs = (navigateToPath: NavigateToPath) => [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
+const getBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabel', {
defaultMessage: 'Create job',
@@ -41,7 +43,10 @@ const getBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-export const indexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const indexOrSearchRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/step/index_or_search',
render: (props, deps) => (
),
- breadcrumbs: getBreadcrumbs(navigateToPath),
+ breadcrumbs: getBreadcrumbs(navigateToPath, basePath),
});
-export const dataVizIndexOrSearchRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const dataVizIndexOrSearchRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/datavisualizer_index_select',
render: (props, deps) => (
),
- breadcrumbs: getBreadcrumbs(navigateToPath),
+ breadcrumbs: getBreadcrumbs(navigateToPath, basePath),
});
const PageWrapper: FC = ({ nextStepPath, deps, mode }) => {
+ const {
+ services: {
+ http: { basePath },
+ application: { navigateToUrl },
+ },
+ } = useMlKibana();
+ const { redirectToMlAccessDeniedPage } = deps;
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
+
const newJobResolvers = {
...basicResolvers(deps),
- preConfiguredJobRedirect: () => preConfiguredJobRedirect(deps.indexPatterns),
+ preConfiguredJobRedirect: () =>
+ preConfiguredJobRedirect(deps.indexPatterns, basePath.get(), navigateToUrl),
};
const dataVizResolvers = {
checkBasicLicense,
loadIndexPatterns: () => loadIndexPatterns(deps.indexPatterns),
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
- checkMlNodesAvailable,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
};
const { context } = useResolver(
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
index b8ab29d40fa1f..543e01fbd326d 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/job_type.tsx
@@ -17,12 +17,12 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../jobs/new_job/pages/job_type';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const jobTypeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const jobTypeRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/jobs/new_job/step/job_type',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectJobType', {
defaultMessage: 'Create job',
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
index 6be58828ee1a5..654d7184cfcf2 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/recognize.tsx
@@ -9,7 +9,7 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { NavigateToPath } from '../../../contexts/kibana';
+import { NavigateToPath, useNavigateToPath } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
import { useResolver } from '../../use_resolver';
@@ -18,14 +18,18 @@ import { Page } from '../../../jobs/new_job/recognize';
import { checkViewOrCreateJobs } from '../../../jobs/new_job/recognize/resolvers';
import { mlJobService } from '../../../services/job_service';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { useCreateADLinks } from '../../../components/custom_hooks/use_create_ad_links';
-export const recognizeRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const recognizeRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/recognize',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.selectIndexOrSearchLabelRecognize', {
defaultMessage: 'Recognized index',
@@ -60,10 +64,14 @@ const CheckViewOrCreateWrapper: FC = ({ location, deps }) => {
const { id: moduleId, index: indexPatternId }: Record = parse(location.search, {
sort: false,
});
+ const { createLinkWithUserDefaults } = useCreateADLinks();
+
+ const navigateToPath = useNavigateToPath();
// the single resolver checkViewOrCreateJobs redirects only. so will always reject
useResolver(undefined, undefined, deps.config, {
- checkViewOrCreateJobs: () => checkViewOrCreateJobs(moduleId, indexPatternId),
+ checkViewOrCreateJobs: () =>
+ checkViewOrCreateJobs(moduleId, indexPatternId, createLinkWithUserDefaults, navigateToPath),
});
return null;
};
diff --git a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
index 35085fd557577..8a82a9a8dbc49 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/new_job/wizard.tsx
@@ -19,19 +19,21 @@ import { mlJobService } from '../../../services/job_service';
import { loadNewJobCapabilities } from '../../../services/new_job_capabilities_service';
import { checkCreateJobsCapabilitiesResolver } from '../../../capabilities/check_capabilities';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
interface WizardPageProps extends PageProps {
jobType: JOB_TYPE;
}
-const getBaseBreadcrumbs = (navigateToPath: NavigateToPath) => [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath),
+const getBaseBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('CREATE_JOB_BREADCRUMB', navigateToPath, basePath),
];
-const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
- ...getBaseBreadcrumbs(navigateToPath),
+const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ ...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.singleMetricLabel', {
defaultMessage: 'Single metric',
@@ -40,8 +42,8 @@ const getSingleMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
- ...getBaseBreadcrumbs(navigateToPath),
+const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ ...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.multiMetricLabel', {
defaultMessage: 'Multi-metric',
@@ -50,8 +52,8 @@ const getMultiMetricBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [
- ...getBaseBreadcrumbs(navigateToPath),
+const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ ...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.populationLabel', {
defaultMessage: 'Population',
@@ -60,8 +62,8 @@ const getPopulationBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [
- ...getBaseBreadcrumbs(navigateToPath),
+const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ ...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.advancedConfigurationLabel', {
defaultMessage: 'Advanced configuration',
@@ -70,8 +72,8 @@ const getAdvancedBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [
- ...getBaseBreadcrumbs(navigateToPath),
+const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath, basePath: string) => [
+ ...getBaseBreadcrumbs(navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.jobsBreadcrumbs.categorizationLabel', {
defaultMessage: 'Categorization',
@@ -80,41 +82,60 @@ const getCategorizationBreadcrumbs = (navigateToPath: NavigateToPath) => [
},
];
-export const singleMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const singleMetricRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/single_metric',
render: (props, deps) => ,
- breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath),
+ breadcrumbs: getSingleMetricBreadcrumbs(navigateToPath, basePath),
});
-export const multiMetricRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const multiMetricRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/multi_metric',
render: (props, deps) => ,
- breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath),
+ breadcrumbs: getMultiMetricBreadcrumbs(navigateToPath, basePath),
});
-export const populationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const populationRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/population',
render: (props, deps) => ,
- breadcrumbs: getPopulationBreadcrumbs(navigateToPath),
+ breadcrumbs: getPopulationBreadcrumbs(navigateToPath, basePath),
});
-export const advancedRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const advancedRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/advanced',
render: (props, deps) => ,
- breadcrumbs: getAdvancedBreadcrumbs(navigateToPath),
+ breadcrumbs: getAdvancedBreadcrumbs(navigateToPath, basePath),
});
-export const categorizationRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const categorizationRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/jobs/new_job/categorization',
render: (props, deps) => ,
- breadcrumbs: getCategorizationBreadcrumbs(navigateToPath),
+ breadcrumbs: getCategorizationBreadcrumbs(navigateToPath, basePath),
});
const PageWrapper: FC = ({ location, jobType, deps }) => {
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
+
const { index, savedSearchId }: Record = parse(location.search, { sort: false });
const { context, results } = useResolver(index, savedSearchId, deps.config, {
...basicResolvers(deps),
- privileges: checkCreateJobsCapabilitiesResolver,
+ privileges: () => checkCreateJobsCapabilitiesResolver(redirectToJobsManagementPage),
jobCaps: () => loadNewJobCapabilities(index, savedSearchId, deps.indexPatterns),
existingJobsAndGroups: mlJobService.getJobAndGroupIds,
});
diff --git a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
index 174e9804b9689..0e07b0edfbe56 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/overview.tsx
@@ -22,11 +22,14 @@ import { loadMlServerInfo } from '../../services/ml_server_info';
import { useTimefilter } from '../../contexts/kibana';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../breadcrumbs';
-export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const overviewRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/overview',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.overview.overviewLabel', {
defaultMessage: 'Overview',
@@ -37,9 +40,11 @@ export const overviewRouteFactory = (navigateToPath: NavigateToPath): MlRoute =>
});
const PageWrapper: FC = ({ deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
loadMlServerInfo,
});
@@ -52,7 +57,7 @@ const PageWrapper: FC = ({ deps }) => {
);
};
-export const appRootRouteFactory = (): MlRoute => ({
+export const appRootRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/',
render: () => ,
breadcrumbs: [],
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
index f2ae57f1ec961..2460971239618 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_list.tsx
@@ -10,7 +10,6 @@
*/
import React, { FC } from 'react';
-import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
@@ -25,27 +24,27 @@ import {
} from '../../../capabilities/check_capabilities';
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { CalendarsList } from '../../../settings/calendars';
-import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const calendarListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const calendarListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/calendars_list',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
- {
- text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagementLabel', {
- defaultMessage: 'Calendar management',
- }),
- onClick: breadcrumbOnClickFactory('/settings/calendars_list', navigateToPath),
- },
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC = ({ deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
index a5c30e1eaaacc..4e0a8340590a4 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/calendar_new_edit.tsx
@@ -26,6 +26,8 @@ import {
import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { NewCalendar } from '../../../settings/calendars';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
enum MODE {
NEW,
@@ -36,12 +38,16 @@ interface NewCalendarPageProps extends PageProps {
mode: MODE;
}
-export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const newCalendarRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/calendars_list/new_calendar',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.createLabel', {
defaultMessage: 'Create',
@@ -51,12 +57,16 @@ export const newCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute
],
});
-export const editCalendarRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const editCalendarRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/calendars_list/edit_calendar/:calendarId',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('CALENDAR_MANAGEMENT_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.calendarManagement.editLabel', {
defaultMessage: 'Edit',
@@ -72,11 +82,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => {
const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/);
calendarId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined;
}
+ const { redirectToMlAccessDeniedPage } = deps;
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
- checkMlNodesAvailable,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
index d734e18d72bab..4e39cfce82e36 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list.tsx
@@ -10,7 +10,6 @@
*/
import React, { FC } from 'react';
-import { i18n } from '@kbn/i18n';
import { NavigateToPath } from '../../../contexts/kibana';
@@ -26,27 +25,27 @@ import {
import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { FilterLists } from '../../../settings/filter_lists';
-import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const filterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const filterListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/filter_lists',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
- {
- text: i18n.translate('xpack.ml.settings.breadcrumbs.filterListsLabel', {
- defaultMessage: 'Filter lists',
- }),
- onClick: breadcrumbOnClickFactory('/settings/filter_lists', navigateToPath),
- },
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC = ({ deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
index c6f17bc7f6f68..5fe56b024e413 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/filter_list_new_edit.tsx
@@ -27,6 +27,8 @@ import { checkMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes';
import { EditFilterList } from '../../../settings/filter_lists';
import { breadcrumbOnClickFactory, getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
+import { useCreateAndNavigateToMlLink } from '../../../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
enum MODE {
NEW,
@@ -37,12 +39,17 @@ interface NewFilterPageProps extends PageProps {
mode: MODE;
}
-export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const newFilterListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/filter_lists/new_filter_list',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
+
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.createLabel', {
defaultMessage: 'Create',
@@ -52,12 +59,16 @@ export const newFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRou
],
});
-export const editFilterListRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const editFilterListRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings/filter_lists/edit_filter_list/:filterId',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('FILTER_LISTS_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.settings.breadcrumbs.filterLists.editLabel', {
defaultMessage: 'Edit',
@@ -73,11 +84,15 @@ const PageWrapper: FC = ({ location, mode, deps }) => {
const pathMatch: string[] | null = location.pathname.match(/.+\/(.+)$/);
filterId = pathMatch && pathMatch.length > 1 ? pathMatch[1] : undefined;
}
+ const { redirectToMlAccessDeniedPage } = deps;
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
- checkMlNodesAvailable,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
+ checkMlNodesAvailable: () => checkMlNodesAvailable(redirectToJobsManagementPage),
});
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
diff --git a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
index 3f4b269851469..3159c2ae88166 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/settings/settings.tsx
@@ -26,19 +26,24 @@ import { getMlNodeCount } from '../../../ml_nodes_check/check_ml_nodes';
import { AnomalyDetectionSettingsContext, Settings } from '../../../settings';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
-export const settingsRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const settingsRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/settings',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('SETTINGS_BREADCRUMB', navigateToPath, basePath),
],
});
const PageWrapper: FC = ({ deps }) => {
+ const { redirectToMlAccessDeniedPage } = deps;
+
const { context } = useResolver(undefined, undefined, deps.config, {
checkFullLicense,
- checkGetJobsCapabilities: checkGetJobsCapabilitiesResolver,
+ checkGetJobsCapabilities: () => checkGetJobsCapabilitiesResolver(redirectToMlAccessDeniedPage),
getMlNodeCount,
});
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
index 11ec074bac1db..b60a265560455 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.test.tsx
@@ -19,6 +19,11 @@ jest.mock('../../contexts/kibana/kibana_context', () => {
useMlKibana: () => {
return {
services: {
+ chrome: { docTitle: { change: jest.fn() } },
+ application: { getUrlForApp: jest.fn(), navigateToUrl: jest.fn() },
+ share: {
+ urlGenerators: { getUrlGenerator: jest.fn() },
+ },
uiSettings: { get: jest.fn() },
data: {
query: {
diff --git a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
index 817c975415997..03588872d6be0 100644
--- a/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
+++ b/x-pack/plugins/ml/public/application/routing/routes/timeseriesexplorer.tsx
@@ -39,12 +39,15 @@ import { basicResolvers } from '../resolvers';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
-export const timeSeriesExplorerRouteFactory = (navigateToPath: NavigateToPath): MlRoute => ({
+export const timeSeriesExplorerRouteFactory = (
+ navigateToPath: NavigateToPath,
+ basePath: string
+): MlRoute => ({
path: '/timeseriesexplorer',
render: (props, deps) => ,
breadcrumbs: [
- getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath),
- getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath),
+ getBreadcrumbWithUrlForApp('ML_BREADCRUMB', navigateToPath, basePath),
+ getBreadcrumbWithUrlForApp('ANOMALY_DETECTION_BREADCRUMB', navigateToPath, basePath),
{
text: i18n.translate('xpack.ml.anomalyDetection.singleMetricViewerLabel', {
defaultMessage: 'Single Metric Viewer',
diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.ts b/x-pack/plugins/ml/public/application/routing/use_resolver.ts
index 4967e3a684a6b..e4cd90145bee4 100644
--- a/x-pack/plugins/ml/public/application/routing/use_resolver.ts
+++ b/x-pack/plugins/ml/public/application/routing/use_resolver.ts
@@ -16,6 +16,8 @@ import { createSearchItems } from '../jobs/new_job/utils/new_job_utils';
import { ResolverResults, Resolvers } from './resolvers';
import { MlContextValue } from '../contexts/ml';
import { useNotifications } from '../contexts/kibana';
+import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../common/constants/ml_url_generator';
export const useResolver = (
indexPatternId: string | undefined,
@@ -34,6 +36,9 @@ export const useResolver = (
const [context, setContext] = useState(null);
const [results, setResults] = useState(tempResults);
+ const redirectToJobsManagementPage = useCreateAndNavigateToMlLink(
+ ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE
+ );
useEffect(() => {
(async () => {
@@ -73,7 +78,7 @@ export const useResolver = (
defaultMessage: 'An error has occurred',
}),
});
- window.location.href = '#/';
+ await redirectToJobsManagementPage();
}
} else {
setContext({});
diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js
index dfa1b5f4e68cd..ea97492ae0f5a 100644
--- a/x-pack/plugins/ml/public/application/services/job_service.js
+++ b/x-pack/plugins/ml/public/application/services/job_service.js
@@ -797,7 +797,6 @@ function createResultsUrl(jobIds, start, end, resultsPage, mode = 'absolute') {
let path = '';
if (resultsPage !== undefined) {
- path += '#/';
path += resultsPage;
}
diff --git a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx
index 16d7e1605263c..57caa56b2f10e 100644
--- a/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx
+++ b/x-pack/plugins/ml/public/application/settings/anomaly_detection_settings.tsx
@@ -25,6 +25,8 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { AnomalyDetectionSettingsContext } from './anomaly_detection_settings_context';
import { useNotifications } from '../contexts/kibana';
import { ml } from '../services/ml_api_service';
+import { ML_PAGES } from '../../../common/constants/ml_url_generator';
+import { useCreateAndNavigateToMlLink } from '../contexts/kibana/use_create_url';
export const AnomalyDetectionSettings: FC = () => {
const [calendarsCount, setCalendarsCount] = useState(0);
@@ -35,6 +37,10 @@ export const AnomalyDetectionSettings: FC = () => {
);
const { toasts } = useNotifications();
+ const redirectToCalendarList = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_MANAGE);
+ const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW);
+ const redirectToFilterLists = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_MANAGE);
+ const redirectToNewFilterListPage = useCreateAndNavigateToMlLink(ML_PAGES.FILTER_LISTS_NEW);
useEffect(() => {
loadSummaryStats();
@@ -126,7 +132,7 @@ export const AnomalyDetectionSettings: FC = () => {
flush="left"
size="l"
color="primary"
- href="#/settings/calendars_list"
+ onClick={redirectToCalendarList}
isDisabled={canGetCalendars === false}
>
{
flush="left"
size="l"
color="primary"
- href="#/settings/calendars_list/new_calendar"
+ onClick={redirectToNewCalendarPage}
isDisabled={canCreateCalendar === false}
>
{
@@ -194,7 +200,7 @@ export const AnomalyDetectionSettings: FC = () => {
flush="left"
size="l"
color="primary"
- href="#/settings/filter_lists"
+ onClick={redirectToFilterLists}
isDisabled={canGetFilters === false}
>
{
data-test-subj="mlFilterListsCreateButton"
size="l"
color="primary"
- href="#/settings/filter_lists/new_filter_list"
+ onClick={redirectToNewFilterListPage}
isDisabled={canCreateFilter === false}
>
+
+
+ }
+ labelType="label"
+ >
+
+
+
+ }
+ labelType="label"
+ >
+
+
@@ -137,7 +200,6 @@ exports[`CalendarForm Renders calendar form 1`] = `
grow={false}
>
@@ -215,7 +218,7 @@ export const CalendarForm = ({
-
+
({
+ useCreateAndNavigateToMlLink: jest.fn(),
+}));
const testProps = {
calendarId: '',
canCreateCalendar: true,
@@ -31,6 +34,7 @@ const testProps = {
selectedGroupOptions: [],
selectedJobOptions: [],
showNewEventModal: jest.fn(),
+ isGlobalCalendar: false,
};
describe('CalendarForm', () => {
diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js
index 1fe16e4588bd7..a5eb212ba127e 100644
--- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js
+++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.js
@@ -20,6 +20,7 @@ import { ImportModal } from './import_modal';
import { ml } from '../../../services/ml_api_service';
import { withKibana } from '../../../../../../../../src/plugins/kibana_react/public';
import { GLOBAL_CALENDAR } from '../../../../../common/constants/calendars';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
class NewCalendarUI extends Component {
static propTypes = {
@@ -55,6 +56,16 @@ class NewCalendarUI extends Component {
this.formSetup();
}
+ returnToCalendarsManagementPage = async () => {
+ const {
+ services: {
+ http: { basePath },
+ application: { navigateToUrl },
+ },
+ } = this.props.kibana;
+ await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.CALENDARS_MANAGE}`, true);
+ };
+
async formSetup() {
try {
const { jobIds, groupIds, calendars } = await getCalendarSettingsData();
@@ -146,7 +157,7 @@ class NewCalendarUI extends Component {
try {
await ml.addCalendar(calendar);
- window.location = '#/settings/calendars_list';
+ await this.returnToCalendarsManagementPage();
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });
@@ -167,7 +178,7 @@ class NewCalendarUI extends Component {
try {
await ml.updateCalendar(calendar);
- window.location = '#/settings/calendars_list';
+ await this.returnToCalendarsManagementPage();
} catch (error) {
console.log('Error saving calendar', error);
this.setState({ saving: false });
diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js
index 2cff255bd1ce3..068d443300088 100644
--- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js
+++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_calendar.test.js
@@ -4,6 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
+jest.mock('../../../contexts/kibana/use_create_url', () => ({
+ useCreateAndNavigateToMlLink: jest.fn(),
+}));
+
jest.mock('../../../components/navigation_menu', () => ({
NavigationMenu: () => ,
}));
diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap
index cc1c524c19b57..50cacd7b3545a 100644
--- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap
+++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/__snapshots__/table.test.js.snap
@@ -77,7 +77,6 @@ exports[`CalendarsListTable renders the table with all calendars 1`] = `
"toolsRight": Array [
diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js
index 77331c4a987dc..6b4403aef7c7b 100644
--- a/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js
+++ b/x-pack/plugins/ml/public/application/settings/calendars/list/table/table.js
@@ -7,12 +7,14 @@
import PropTypes from 'prop-types';
import React from 'react';
-import { EuiButton, EuiLink, EuiInMemoryTable } from '@elastic/eui';
-
+import { EuiButton, EuiInMemoryTable } from '@elastic/eui';
+import { Link } from 'react-router-dom';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { GLOBAL_CALENDAR } from '../../../../../../common/constants/calendars';
+import { useCreateAndNavigateToMlLink } from '../../../../contexts/kibana/use_create_url';
+import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
export const CalendarsListTable = ({
calendarsList,
@@ -24,6 +26,8 @@ export const CalendarsListTable = ({
mlNodesAvailable,
itemsSelected,
}) => {
+ const redirectToNewCalendarPage = useCreateAndNavigateToMlLink(ML_PAGES.CALENDARS_NEW);
+
const sorting = {
sort: {
field: 'calendar_id',
@@ -46,12 +50,9 @@ export const CalendarsListTable = ({
truncateText: true,
scope: 'row',
render: (id) => (
-
+
{id}
-
+
),
'data-test-subj': 'mlCalendarListColumnId',
},
@@ -101,7 +102,7 @@ export const CalendarsListTable = ({
size="s"
data-test-subj="mlCalendarButtonCreate"
key="new_calendar_button"
- href="#/settings/calendars_list/new_calendar"
+ onClick={redirectToNewCalendarPage}
isDisabled={canCreateCalendar === false || mlNodesAvailable === false}
>
@@ -115,6 +116,7 @@ export const CalendarsListTable = ({
canDeleteCalendar === false || mlNodesAvailable === false || itemsSelected === false
}
data-test-subj="mlCalendarButtonDelete"
+ key="delete_calendar_button"
>
({
+ useCreateAndNavigateToMlLink: jest.fn(),
+}));
const calendars = [
{
@@ -42,7 +47,11 @@ describe('CalendarsListTable', () => {
});
test('New button enabled if permission available', () => {
- const wrapper = mountWithIntl();
+ const wrapper = mountWithIntl(
+
+
+
+ );
const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]');
const button = buttons.find('EuiButton');
@@ -56,7 +65,11 @@ describe('CalendarsListTable', () => {
canCreateCalendar: false,
};
- const wrapper = mountWithIntl();
+ const wrapper = mountWithIntl(
+
+
+
+ );
const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]');
const button = buttons.find('EuiButton');
@@ -70,7 +83,11 @@ describe('CalendarsListTable', () => {
mlNodesAvailable: false,
};
- const wrapper = mountWithIntl();
+ const wrapper = mountWithIntl(
+
+
+
+ );
const buttons = wrapper.find('[data-test-subj="mlCalendarButtonCreate"]');
const button = buttons.find('EuiButton');
diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js
index 41b7aa63f55ef..681c54ca9eee0 100644
--- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js
+++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js
@@ -34,6 +34,7 @@ import { ItemsGrid } from '../../../components/items_grid';
import { NavigationMenu } from '../../../components/navigation_menu';
import { isValidFilterListId, saveFilterList } from './utils';
import { ml } from '../../../services/ml_api_service';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
const DEFAULT_ITEMS_PER_PAGE = 50;
@@ -67,10 +68,6 @@ function getActivePage(activePageState, itemsPerPage, numMatchingItems) {
return activePage;
}
-function returnToFiltersList() {
- window.location.href = `#/settings/filter_lists`;
-}
-
export class EditFilterListUI extends Component {
static displayName = 'EditFilterList';
static propTypes = {
@@ -105,6 +102,16 @@ export class EditFilterListUI extends Component {
}
}
+ returnToFiltersList = async () => {
+ const {
+ services: {
+ http: { basePath },
+ application: { navigateToUrl },
+ },
+ } = this.props.kibana;
+ await navigateToUrl(`${basePath.get()}/app/ml/${ML_PAGES.FILTER_LISTS_MANAGE}`, true);
+ };
+
loadFilterList = (filterId) => {
ml.filters
.filters({ filterId })
@@ -279,7 +286,7 @@ export class EditFilterListUI extends Component {
saveFilterList(filterId, description, items, loadedFilter)
.then((savedFilter) => {
this.setLoadedFilterState(savedFilter);
- returnToFiltersList();
+ this.returnToFiltersList();
})
.catch((resp) => {
console.log(`Error saving filter ${filterId}:`, resp);
@@ -355,7 +362,7 @@ export class EditFilterListUI extends Component {
/>
-
+ this.returnToFiltersList()}>
@@ -84,12 +88,9 @@ function getColumns() {
defaultMessage: 'ID',
}),
render: (id) => (
-
+
{id}
-
+
),
sortable: true,
scope: 'row',
@@ -213,7 +214,7 @@ export function FilterListsTable({
isSelectable={true}
data-test-subj="mlFilterListsTable"
rowProps={(item) => ({
- 'data-test-subj': `mlCalendarListRow row-${item.filter_id}`,
+ 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`,
})}
/>
diff --git a/x-pack/plugins/ml/public/application/settings/settings.test.tsx b/x-pack/plugins/ml/public/application/settings/settings.test.tsx
index f16bf62632152..a5e69f233e2df 100644
--- a/x-pack/plugins/ml/public/application/settings/settings.test.tsx
+++ b/x-pack/plugins/ml/public/application/settings/settings.test.tsx
@@ -22,6 +22,10 @@ jest.mock('../contexts/kibana', () => ({
},
}));
+jest.mock('../contexts/kibana/use_create_url', () => ({
+ useCreateAndNavigateToMlLink: jest.fn(),
+}));
+
describe('Settings', () => {
function runCheckButtonsDisabledTest(
canGetFilters: boolean,
diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx
index deecb9fb45b51..88bf769aa2936 100644
--- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx
+++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/timeseriesexplorer_no_jobs_found/timeseriesexplorer_no_jobs_found.tsx
@@ -12,26 +12,40 @@ import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { useMlUrlGenerator, useNavigateToPath } from '../../../contexts/kibana';
+import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
-export const TimeseriesexplorerNoJobsFound = () => (
-
-
-
- }
- actions={
-
-
-
- }
- />
-);
+export const TimeseriesexplorerNoJobsFound = () => {
+ const mlUrlGenerator = useMlUrlGenerator();
+ const navigateToPath = useNavigateToPath();
+
+ const redirectToJobsManagementPage = async () => {
+ const path = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ });
+ await navigateToPath(path, true);
+ };
+
+ return (
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+ );
+};
diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.js b/x-pack/plugins/ml/public/application/util/chart_utils.js
index 4ec7c5cb6d819..ca55bb10b13d5 100644
--- a/x-pack/plugins/ml/public/application/util/chart_utils.js
+++ b/x-pack/plugins/ml/public/application/util/chart_utils.js
@@ -8,11 +8,9 @@ import d3 from 'd3';
import { calculateTextWidth } from './string_utils';
import { MULTI_BUCKET_IMPACT } from '../../../common/constants/multi_bucket_impact';
import moment from 'moment';
-import rison from 'rison-node';
-
import { getTimefilter } from './dependency_cache';
-
import { CHART_TYPE } from '../explorer/explorer_constants';
+import { ML_PAGES } from '../../../common/constants/ml_url_generator';
export const LINE_CHART_ANOMALY_RADIUS = 7;
export const MULTI_BUCKET_SYMBOL_SIZE = 100; // In square pixels for use with d3 symbol.size
@@ -212,7 +210,7 @@ export function getChartType(config) {
return chartType;
}
-export function getExploreSeriesLink(series) {
+export async function getExploreSeriesLink(mlUrlGenerator, series) {
// Open the Single Metric dashboard over the same overall bounds and
// zoomed in to the same time as the current chart.
const timefilter = getTimefilter();
@@ -227,46 +225,44 @@ export function getExploreSeriesLink(series) {
// to identify the particular series to view.
// Initially pass them in the mlTimeSeriesExplorer part of the AppState.
// TODO - do we want to pass the entities via the filter?
- const entityCondition = {};
- series.entityFields.forEach((entity) => {
- entityCondition[entity.fieldName] = entity.fieldValue;
- });
+ let entityCondition;
+ if (series.entityFields.length > 0) {
+ entityCondition = {};
+ series.entityFields.forEach((entity) => {
+ entityCondition[entity.fieldName] = entity.fieldValue;
+ });
+ }
- // Use rison to build the URL .
- const _g = rison.encode({
- ml: {
+ const url = await mlUrlGenerator.createUrl({
+ page: ML_PAGES.SINGLE_METRIC_VIEWER,
+ pageState: {
jobIds: [series.jobId],
- },
- refreshInterval: {
- display: 'Off',
- pause: false,
- value: 0,
- },
- time: {
- from: from,
- to: to,
- mode: 'absolute',
- },
- });
-
- const _a = rison.encode({
- mlTimeSeriesExplorer: {
+ refreshInterval: {
+ display: 'Off',
+ pause: false,
+ value: 0,
+ },
+ timeRange: {
+ from: from,
+ to: to,
+ mode: 'absolute',
+ },
zoom: {
from: zoomFrom,
to: zoomTo,
},
detectorIndex: series.detectorIndex,
entities: entityCondition,
- },
- query: {
- query_string: {
- analyze_wildcard: true,
- query: '*',
+ query: {
+ query_string: {
+ analyze_wildcard: true,
+ query: '*',
+ },
},
},
+ excludeBasePath: true,
});
-
- return `#/timeseriesexplorer?_g=${_g}&_a=${encodeURIComponent(_a)}`;
+ return url;
}
export function showMultiBucketAnomalyMarker(point) {
diff --git a/x-pack/plugins/ml/public/application/util/chart_utils.test.js b/x-pack/plugins/ml/public/application/util/chart_utils.test.js
index b7cf11c088a1e..955dd7cbea0a1 100644
--- a/x-pack/plugins/ml/public/application/util/chart_utils.test.js
+++ b/x-pack/plugins/ml/public/application/util/chart_utils.test.js
@@ -35,7 +35,6 @@ import { render } from '@testing-library/react';
import {
chartLimits,
getChartType,
- getExploreSeriesLink,
getTickValues,
getXTransform,
isLabelLengthAboveThreshold,
@@ -238,20 +237,6 @@ describe('ML - chart utils', () => {
});
});
- describe('getExploreSeriesLink', () => {
- test('get timeseriesexplorer link', () => {
- const link = getExploreSeriesLink(seriesConfig);
- const expectedLink =
- `#/timeseriesexplorer?_g=(ml:(jobIds:!(population-03)),` +
- `refreshInterval:(display:Off,pause:!f,value:0),time:(from:'2017-02-23T00:00:00.000Z',mode:absolute,` +
- `to:'2017-02-23T23:59:59.999Z'))&_a=(mlTimeSeriesExplorer%3A(detectorIndex%3A0%2Centities%3A` +
- `(nginx.access.remote_ip%3A'72.57.0.53')%2Czoom%3A(from%3A'2017-02-19T20%3A00%3A00.000Z'%2Cto%3A'2017-02-27T04%3A00%3A00.000Z'))` +
- `%2Cquery%3A(query_string%3A(analyze_wildcard%3A!t%2Cquery%3A'*')))`;
-
- expect(link).toBe(expectedLink);
- });
- });
-
describe('numTicks', () => {
test('returns 10 for 1000', () => {
expect(numTicks(1000)).toBe(10);
diff --git a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts b/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts
deleted file mode 100644
index 806626577008e..0000000000000
--- a/x-pack/plugins/ml/public/application/util/get_selected_ids_url.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-import rison from 'rison-node';
-import { getBasePath } from './dependency_cache';
-
-export enum TAB_IDS {
- DATA_FRAME_ANALYTICS = 'data_frame_analytics',
- ANOMALY_DETECTION = 'jobs',
-}
-
-function getSelectedIdsUrl(tabId: TAB_IDS, settings: { [key: string]: string | string[] }): string {
- // Create url for filtering by job id or group ids for kibana management table
- const encoded = rison.encode(settings);
- const url = `?mlManagement=${encoded}`;
- const basePath = getBasePath();
-
- return `${basePath.get()}/app/ml#/${tabId}${url}`;
-}
-
-// Create url for filtering by group ids for kibana management table
-export function getGroupIdsUrl(tabId: TAB_IDS, ids: string[]): string {
- const settings = {
- groupIds: ids,
- };
-
- return getSelectedIdsUrl(tabId, settings);
-}
-
-// Create url for filtering by job id for kibana management table
-export function getJobIdUrl(tabId: TAB_IDS, id: string): string {
- const settings = {
- jobId: id,
- };
-
- return getSelectedIdsUrl(tabId, settings);
-}
diff --git a/x-pack/plugins/ml/public/application/util/recently_accessed.ts b/x-pack/plugins/ml/public/application/util/recently_accessed.ts
index ab879e421cb09..04ccd84c561bb 100644
--- a/x-pack/plugins/ml/public/application/util/recently_accessed.ts
+++ b/x-pack/plugins/ml/public/application/util/recently_accessed.ts
@@ -37,7 +37,7 @@ export function addItemToRecentlyAccessed(page: string, itemId: string, url: str
return;
}
- url = `ml#/${page}/${url}`;
+ url = url.startsWith('/') ? `/app/ml${url}` : `/app/ml/${page}/${url}`;
const recentlyAccessed = getRecentlyAccessed();
recentlyAccessed.add(url, `ML - ${itemId} - ${pageLabel}`, id);
}
diff --git a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
index c4aebb108e7b9..6a44756412fe3 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/anomaly_detection_urls_generator.ts
@@ -11,13 +11,14 @@ import {
ExplorerAppState,
ExplorerGlobalState,
ExplorerUrlState,
+ MlCommonGlobalState,
MlGenericUrlState,
TimeSeriesExplorerAppState,
TimeSeriesExplorerGlobalState,
TimeSeriesExplorerUrlState,
} from '../../common/types/ml_url_generator';
import { ML_PAGES } from '../../common/constants/ml_url_generator';
-import { createIndexBasedMlUrl } from './common';
+import { createGenericMlUrl } from './common';
import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public';
/**
* Creates URL to the Anomaly Detection Job management page
@@ -30,18 +31,29 @@ export function createAnomalyDetectionJobManagementUrl(
if (!params || isEmpty(params)) {
return url;
}
- const { jobId, groupIds } = params;
- const queryState: AnomalyDetectionQueryState = {
- jobId,
- groupIds,
- };
+ const { jobId, groupIds, globalState } = params;
+ if (jobId || groupIds) {
+ const queryState: AnomalyDetectionQueryState = {
+ jobId,
+ groupIds,
+ };
- url = setStateToKbnUrl(
- 'mlManagement',
- queryState,
- { useHash: false, storeInHashQuery: false },
- url
- );
+ url = setStateToKbnUrl(
+ 'mlManagement',
+ queryState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ }
+
+ if (globalState) {
+ url = setStateToKbnUrl>(
+ '_g',
+ globalState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ }
return url;
}
@@ -49,13 +61,24 @@ export function createAnomalyDetectionCreateJobSelectType(
appBasePath: string,
pageState: MlGenericUrlState['pageState']
): string {
- return createIndexBasedMlUrl(
+ return createGenericMlUrl(
appBasePath,
ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE,
pageState
);
}
+export function createAnomalyDetectionCreateJobSelectIndex(
+ appBasePath: string,
+ pageState: MlGenericUrlState['pageState']
+): string {
+ return createGenericMlUrl(
+ appBasePath,
+ ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX,
+ pageState
+ );
+}
+
/**
* Creates URL to the Anomaly Explorer page
*/
@@ -75,36 +98,35 @@ export function createExplorerUrl(
query,
mlExplorerSwimlane = {},
mlExplorerFilter = {},
+ globalState,
} = params;
const appState: Partial = {
mlExplorerSwimlane,
mlExplorerFilter,
};
+ let queryState: Partial = {};
+ if (globalState) queryState = globalState;
if (query) appState.query = query;
-
if (jobIds) {
- const queryState: Partial = {
- ml: {
- jobIds,
- },
+ queryState.ml = {
+ jobIds,
};
-
- if (timeRange) queryState.time = timeRange;
- if (refreshInterval) queryState.refreshInterval = refreshInterval;
-
- url = setStateToKbnUrl>(
- '_g',
- queryState,
- { useHash: false, storeInHashQuery: false },
- url
- );
- url = setStateToKbnUrl>(
- '_a',
- appState,
- { useHash: false, storeInHashQuery: false },
- url
- );
}
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+ if (timeRange) queryState.time = timeRange;
+
+ url = setStateToKbnUrl>(
+ '_g',
+ queryState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ url = setStateToKbnUrl>(
+ '_a',
+ appState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
return url;
}
@@ -120,19 +142,36 @@ export function createSingleMetricViewerUrl(
if (!params) {
return url;
}
- const { timeRange, jobIds, refreshInterval, zoom, query, detectorIndex, entities } = params;
+ const {
+ timeRange,
+ jobIds,
+ refreshInterval,
+ zoom,
+ query,
+ detectorIndex,
+ forecastId,
+ entities,
+ globalState,
+ } = params;
+
+ let queryState: Partial = {};
+ if (globalState) queryState = globalState;
- const queryState: TimeSeriesExplorerGlobalState = {
- ml: {
+ if (jobIds) {
+ queryState.ml = {
jobIds,
- },
- refreshInterval,
- time: timeRange,
- };
+ };
+ }
+ if (refreshInterval) queryState.refreshInterval = refreshInterval;
+ if (timeRange) queryState.time = timeRange;
const appState: Partial = {};
const mlTimeSeriesExplorer: Partial = {};
+ if (forecastId !== undefined) {
+ mlTimeSeriesExplorer.forecastId = forecastId;
+ }
+
if (detectorIndex !== undefined) {
mlTimeSeriesExplorer.detectorIndex = detectorIndex;
}
@@ -146,7 +185,7 @@ export function createSingleMetricViewerUrl(
appState.query = {
query_string: query,
};
- url = setStateToKbnUrl(
+ url = setStateToKbnUrl>(
'_g',
queryState,
{ useHash: false, storeInHashQuery: false },
diff --git a/x-pack/plugins/ml/public/ml_url_generator/common.ts b/x-pack/plugins/ml/public/ml_url_generator/common.ts
index 57cfc52045282..f929e513e618a 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/common.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/common.ts
@@ -19,37 +19,40 @@ export function extractParams(urlState: UrlState) {
* Creates generic index based search ML url
* e.g. `jobs/new_job/datavisualizer?index=3da93760-e0af-11ea-9ad3-3bcfc330e42a`
*/
-export function createIndexBasedMlUrl(
+export function createGenericMlUrl(
appBasePath: string,
page: MlGenericUrlState['page'],
pageState: MlGenericUrlState['pageState']
): string {
- const { globalState, appState, index, savedSearchId, ...restParams } = pageState;
let url = `${appBasePath}/${page}`;
- if (index !== undefined && savedSearchId === undefined) {
- url = `${url}?index=${index}`;
- }
- if (index === undefined && savedSearchId !== undefined) {
- url = `${url}?savedSearchId=${savedSearchId}`;
- }
+ if (pageState) {
+ const { globalState, appState, index, savedSearchId, ...restParams } = pageState;
+ if (index !== undefined && savedSearchId === undefined) {
+ url = `${url}?index=${index}`;
+ }
+ if (index === undefined && savedSearchId !== undefined) {
+ url = `${url}?savedSearchId=${savedSearchId}`;
+ }
- if (!isEmpty(restParams)) {
- Object.keys(restParams).forEach((key) => {
- url = setStateToKbnUrl(
- key,
- restParams[key],
- { useHash: false, storeInHashQuery: false },
- url
- );
- });
- }
+ if (!isEmpty(restParams)) {
+ Object.keys(restParams).forEach((key) => {
+ url = setStateToKbnUrl(
+ key,
+ restParams[key],
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ });
+ }
- if (globalState) {
- url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url);
- }
- if (appState) {
- url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url);
+ if (globalState) {
+ url = setStateToKbnUrl('_g', globalState, { useHash: false, storeInHashQuery: false }, url);
+ }
+ if (appState) {
+ url = setStateToKbnUrl('_a', appState, { useHash: false, storeInHashQuery: false }, url);
+ }
}
+
return url;
}
diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
index 8cf10a2acb64f..88761edf241a9 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/data_frame_analytics_urls_generator.ts
@@ -12,6 +12,7 @@ import {
DataFrameAnalyticsExplorationUrlState,
DataFrameAnalyticsQueryState,
DataFrameAnalyticsUrlState,
+ MlCommonGlobalState,
} from '../../common/types/ml_url_generator';
import { ML_PAGES } from '../../common/constants/ml_url_generator';
import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public';
@@ -23,18 +24,28 @@ export function createDataFrameAnalyticsJobManagementUrl(
let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE}`;
if (mlUrlGeneratorState) {
- const { jobId, groupIds } = mlUrlGeneratorState;
- const queryState: Partial = {
- jobId,
- groupIds,
- };
+ const { jobId, groupIds, globalState } = mlUrlGeneratorState;
+ if (jobId || groupIds) {
+ const queryState: Partial = {
+ jobId,
+ groupIds,
+ };
- url = setStateToKbnUrl>(
- 'mlManagement',
- queryState,
- { useHash: false, storeInHashQuery: false },
- url
- );
+ url = setStateToKbnUrl>(
+ 'mlManagement',
+ queryState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ }
+ if (globalState) {
+ url = setStateToKbnUrl>(
+ '_g',
+ globalState,
+ { useHash: false, storeInHashQuery: false },
+ url
+ );
+ }
}
return url;
@@ -50,12 +61,14 @@ export function createDataFrameAnalyticsExplorationUrl(
let url = `${appBasePath}/${ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION}`;
if (mlUrlGeneratorState) {
- const { jobId, analysisType } = mlUrlGeneratorState;
+ const { jobId, analysisType, globalState } = mlUrlGeneratorState;
+
const queryState: DataFrameAnalyticsExplorationQueryState = {
ml: {
jobId,
analysisType,
},
+ ...globalState,
};
url = setStateToKbnUrl(
diff --git a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts
deleted file mode 100644
index 24693df5025d9..0000000000000
--- a/x-pack/plugins/ml/public/ml_url_generator/data_visualizer_urls_generator.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-/**
- * Creates URL to the Data Visualizer page
- */
-import { DataVisualizerUrlState, MlGenericUrlState } from '../../common/types/ml_url_generator';
-import { createIndexBasedMlUrl } from './common';
-import { ML_PAGES } from '../../common/constants/ml_url_generator';
-
-export function createDataVisualizerUrl(
- appBasePath: string,
- { page }: DataVisualizerUrlState
-): string {
- return `${appBasePath}/${page}`;
-}
-
-/**
- * Creates URL to the Index Data Visualizer
- */
-export function createIndexDataVisualizerUrl(
- appBasePath: string,
- pageState: MlGenericUrlState['pageState']
-): string {
- return createIndexBasedMlUrl(appBasePath, ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER, pageState);
-}
diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
index 55bc6d3668de7..754f5bec57a07 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.test.ts
@@ -6,7 +6,7 @@
import { MlUrlGenerator } from './ml_url_generator';
import { ML_PAGES } from '../../common/constants/ml_url_generator';
-import { ANALYSIS_CONFIG_TYPE } from '../../common/types/ml_url_generator';
+import { ANALYSIS_CONFIG_TYPE } from '../../common/constants/data_frame_analytics';
describe('MlUrlGenerator', () => {
const urlGenerator = new MlUrlGenerator({
diff --git a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts
index b69260d8d4157..abec5cc2b7d1e 100644
--- a/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts
+++ b/x-pack/plugins/ml/public/ml_url_generator/ml_url_generator.ts
@@ -16,6 +16,7 @@ import { MlUrlGeneratorState } from '../../common/types/ml_url_generator';
import {
createAnomalyDetectionJobManagementUrl,
createAnomalyDetectionCreateJobSelectType,
+ createAnomalyDetectionCreateJobSelectIndex,
createExplorerUrl,
createSingleMetricViewerUrl,
} from './anomaly_detection_urls_generator';
@@ -23,10 +24,8 @@ import {
createDataFrameAnalyticsJobManagementUrl,
createDataFrameAnalyticsExplorationUrl,
} from './data_frame_analytics_urls_generator';
-import {
- createIndexDataVisualizerUrl,
- createDataVisualizerUrl,
-} from './data_visualizer_urls_generator';
+import { createGenericMlUrl } from './common';
+import { createEditCalendarUrl, createEditFilterUrl } from './settings_urls_generator';
declare module '../../../../../src/plugins/share/public' {
export interface UrlGeneratorStateMapping {
@@ -44,8 +43,12 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition => {
- const appBasePath = this.params.appBasePath;
+ public readonly createUrl = async (
+ mlUrlGeneratorParams: MlUrlGeneratorState
+ ): Promise => {
+ const { excludeBasePath, ...mlUrlGeneratorState } = mlUrlGeneratorParams;
+ const appBasePath = excludeBasePath === true ? '' : this.params.appBasePath;
+
switch (mlUrlGeneratorState.page) {
case ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE:
return createAnomalyDetectionJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState);
@@ -56,18 +59,39 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition {
defaultMessage: 'Import your own CSV, NDJSON, or log file.',
}),
icon: 'document',
- path: '/app/ml#/filedatavisualizer',
+ path: '/app/ml/filedatavisualizer',
showOnHomePage: true,
category: FeatureCatalogueCategory.DATA,
order: 520,
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/logo.json
new file mode 100644
index 0000000000000..2e57038bbc639
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/logo.json
@@ -0,0 +1,3 @@
+{
+ "icon": "metricsApp"
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/manifest.json
new file mode 100644
index 0000000000000..29ac288c0649f
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/manifest.json
@@ -0,0 +1,38 @@
+{
+ "id": "metrics_ui_hosts",
+ "title": "Metrics Hosts",
+ "description": "Detect anomalous memory and network behavior on hosts.",
+ "type": "Metricbeat Data",
+ "logoFile": "logo.json",
+ "jobs": [
+ {
+ "id": "hosts_memory_usage",
+ "file": "hosts_memory_usage.json"
+ },
+ {
+ "id": "hosts_network_in",
+ "file": "hosts_network_in.json"
+ },
+ {
+ "id": "hosts_network_out",
+ "file": "hosts_network_out.json"
+ }
+ ],
+ "datafeeds": [
+ {
+ "id": "datafeed-hosts_memory_usage",
+ "file": "datafeed_hosts_memory_usage.json",
+ "job_id": "hosts_memory_usage"
+ },
+ {
+ "id": "datafeed-hosts_network_in",
+ "file": "datafeed_hosts_network_in.json",
+ "job_id": "hosts_network_in"
+ },
+ {
+ "id": "datafeed-hosts_network_out",
+ "file": "datafeed_hosts_network_out.json",
+ "job_id": "hosts_network_out"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_memory_usage.json
new file mode 100644
index 0000000000000..db883a6ce36f9
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_memory_usage.json
@@ -0,0 +1,16 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "system.memory"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_in.json
new file mode 100644
index 0000000000000..7eb430632a81f
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_in.json
@@ -0,0 +1,40 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "system.network"}}
+ ]
+ }
+ },
+ "chunking_config": {
+ "mode": "manual",
+ "time_span": "900s"
+ },
+ "aggregations": {
+ "host.name": {"terms": {"field": "host.name", "size": 100},
+ "aggregations": {
+ "buckets": {
+ "date_histogram": {"field": "@timestamp","fixed_interval": "5m"},
+ "aggregations": {
+ "@timestamp": {"max": {"field": "@timestamp"}},
+ "bytes_in_max": {"max": {"field": "system.network.in.bytes"}},
+ "bytes_in_derivative": {"derivative": {"buckets_path": "bytes_in_max"}},
+ "positive_only":{
+ "bucket_script": {
+ "buckets_path": {"in_derivative": "bytes_in_derivative.value"},
+ "script": "params.in_derivative > 0.0 ? params.in_derivative : 0.0"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_out.json
new file mode 100644
index 0000000000000..427cb678ce663
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/datafeed_hosts_network_out.json
@@ -0,0 +1,40 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "system.network"}}
+ ]
+ }
+ },
+ "chunking_config": {
+ "mode": "manual",
+ "time_span": "900s"
+ },
+ "aggregations": {
+ "host.name": {"terms": {"field": "host.name", "size": 100},
+ "aggregations": {
+ "buckets": {
+ "date_histogram": {"field": "@timestamp","fixed_interval": "5m"},
+ "aggregations": {
+ "@timestamp": {"max": {"field": "@timestamp"}},
+ "bytes_out_max": {"max": {"field": "system.network.out.bytes"}},
+ "bytes_out_derivative": {"derivative": {"buckets_path": "bytes_out_max"}},
+ "positive_only":{
+ "bucket_script": {
+ "buckets_path": {"out_derivative": "bytes_out_derivative.value"},
+ "script": "params.out_derivative > 0.0 ? params.out_derivative : 0.0"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json
new file mode 100644
index 0000000000000..186c9dcdb27e5
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json
@@ -0,0 +1,50 @@
+{
+ "job_type": "anomaly_detector",
+ "groups": [
+ "hosts",
+ "metrics"
+ ],
+ "description": "Metrics: Hosts - Identify unusual spikes in memory usage across hosts.",
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max('system.memory.actual.used.pct')",
+ "function": "max",
+ "field_name": "system.memory.actual.used.pct",
+ "custom_rules": [
+ {
+ "actions": [
+ "skip_result"
+ ],
+ "conditions": [
+ {
+ "applies_to": "actual",
+ "operator": "lt",
+ "value": 0.1
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "influencers": [
+ "host.name"
+ ]
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "64mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-hosts",
+ "custom_urls": [
+ {
+ "url_name": "Host Metrics",
+ "url_value": "metrics/detail/host/$host.name$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json
new file mode 100644
index 0000000000000..0054d90b1df33
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_in.json
@@ -0,0 +1,37 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Metrics: Hosts - Identify unusual spikes in inbound traffic across hosts.",
+ "groups": [
+ "hosts",
+ "metrics"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max(bytes_in_derivative)",
+ "function": "max",
+ "field_name": "bytes_in_derivative"
+ }
+ ],
+ "influencers": [
+ "host.name"
+ ],
+ "summary_count_field_name": "doc_count"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-hosts",
+ "custom_urls": [
+ {
+ "url_name": "Host Metrics",
+ "url_value": "metrics/detail/host/$host.name$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json
new file mode 100644
index 0000000000000..601cc3807c441
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_network_out.json
@@ -0,0 +1,37 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Metrics: Hosts - Identify unusual spikes in outbound traffic across hosts.",
+ "groups": [
+ "hosts",
+ "metrics"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max(bytes_out_derivative)",
+ "function": "max",
+ "field_name": "bytes_out_derivative"
+ }
+ ],
+ "influencers": [
+ "host.name"
+ ],
+ "summary_count_field_name": "doc_count"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-hosts",
+ "custom_urls": [
+ {
+ "url_name": "Host Metrics",
+ "url_value": "metrics/detail/host/$host.name$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/logo.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/logo.json
new file mode 100644
index 0000000000000..63105a28c0ab1
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/logo.json
@@ -0,0 +1,3 @@
+{
+ "icon": "metricsApp"
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/manifest.json
new file mode 100644
index 0000000000000..15336069e092b
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/manifest.json
@@ -0,0 +1,38 @@
+{
+ "id": "metrics_ui_k8s",
+ "title": "Metrics Kubernetes",
+ "description": "Detect anomalous memory and network behavior on Kubernetes pods.",
+ "type": "Metricbeat Data",
+ "logoFile": "logo.json",
+ "jobs": [
+ {
+ "id": "k8s_memory_usage",
+ "file": "k8s_memory_usage.json"
+ },
+ {
+ "id": "k8s_network_in",
+ "file": "k8s_network_in.json"
+ },
+ {
+ "id": "k8s_network_out",
+ "file": "k8s_network_out.json"
+ }
+ ],
+ "datafeeds": [
+ {
+ "id": "datafeed-k8s_memory_usage",
+ "file": "datafeed_k8s_memory_usage.json",
+ "job_id": "k8s_memory_usage"
+ },
+ {
+ "id": "datafeed-k8s_network_in",
+ "file": "datafeed_k8s_network_in.json",
+ "job_id": "k8s_network_in"
+ },
+ {
+ "id": "datafeed-k8s_network_out",
+ "file": "datafeed_k8s_network_out.json",
+ "job_id": "k8s_network_out"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_memory_usage.json
new file mode 100644
index 0000000000000..14590f743528e
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_memory_usage.json
@@ -0,0 +1,17 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "kubernetes.pod.uid"}},
+ {"exists": {"field": "kubernetes.pod.memory"}}
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_in.json
new file mode 100644
index 0000000000000..4fa4c603ea049
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_in.json
@@ -0,0 +1,44 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "kubernetes.pod.network"}}
+ ]
+ }
+ },
+ "chunking_config": {
+ "mode": "manual",
+ "time_span": "900s"
+ },
+ "aggregations": {
+ "kubernetes.namespace": {"terms": {"field": "kubernetes.namespace", "size": 25},
+ "aggregations": {
+ "kubernetes.pod.uid": {"terms": {"field": "kubernetes.pod.uid", "size": 100},
+ "aggregations": {
+ "buckets": {
+ "date_histogram": {"field": "@timestamp","fixed_interval": "5m"},
+ "aggregations": {
+ "@timestamp": {"max": {"field": "@timestamp"}},
+ "bytes_in_max": {"max": {"field": "kubernetes.pod.network.rx.bytes"}},
+ "bytes_in_derivative": {"derivative": {"buckets_path": "bytes_in_max"}},
+ "positive_only":{
+ "bucket_script": {
+ "buckets_path": {"in_derivative": "bytes_in_derivative.value"},
+ "script": "params.in_derivative > 0.0 ? params.in_derivative : 0.0"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_out.json
new file mode 100644
index 0000000000000..633dd6bf490e7
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/datafeed_k8s_network_out.json
@@ -0,0 +1,44 @@
+{
+ "job_id": "JOB_ID",
+ "indices": [
+ "INDEX_PATTERN_NAME"
+ ],
+ "indices_options": {
+ "allow_no_indices": true
+ },
+ "query": {
+ "bool": {
+ "must": [
+ {"exists": {"field": "kubernetes.pod.network"}}
+ ]
+ }
+ },
+ "chunking_config": {
+ "mode": "manual",
+ "time_span": "900s"
+ },
+ "aggregations": {
+ "kubernetes.namespace": {"terms": {"field": "kubernetes.namespace", "size": 25},
+ "aggregations": {
+ "kubernetes.pod.uid": {"terms": {"field": "kubernetes.pod.uid", "size": 100},
+ "aggregations": {
+ "buckets": {
+ "date_histogram": {"field": "@timestamp","fixed_interval": "5m"},
+ "aggregations": {
+ "@timestamp": {"max": {"field": "@timestamp"}},
+ "bytes_out_max": {"max": {"field": "kubernetes.pod.network.tx.bytes"}},
+ "bytes_out_derivative": {"derivative": {"buckets_path": "bytes_out_max"}},
+ "positive_only":{
+ "bucket_script": {
+ "buckets_path": {"pos_derivative": "bytes_out_derivative.value"},
+ "script": "params.pos_derivative > 0.0 ? params.pos_derivative : 0.0"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json
new file mode 100644
index 0000000000000..d3f58086e2fd5
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json
@@ -0,0 +1,53 @@
+{
+ "job_type": "anomaly_detector",
+ "groups": [
+ "k8s",
+ "metrics"
+ ],
+ "description": "Metrics: Kubernetes - Identify unusual spikes in memory usage across Kubernetes pods.",
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max('kubernetes.pod.memory.usage.node.pct')",
+ "function": "max",
+ "field_name": "kubernetes.pod.memory.usage.node.pct",
+ "partition_field_name": "kubernetes.namespace",
+ "custom_rules": [
+ {
+ "actions": [
+ "skip_result"
+ ],
+ "conditions": [
+ {
+ "applies_to": "actual",
+ "operator": "lt",
+ "value": 0.1
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "influencers": [
+ "kubernetes.namespace",
+ "kubernetes.node.name",
+ "kubernetes.pod.uid"
+ ]
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "64mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-k8s",
+ "custom_urls": [
+ {
+ "url_name": "Pod Metrics",
+ "url_value": "metrics/detail/pod/$kubernetes.pod.uid$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json
new file mode 100644
index 0000000000000..212b2681beb77
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_in.json
@@ -0,0 +1,39 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Metrics: Kubernetes - Identify unusual spikes in inbound traffic across Kubernetes pods.",
+ "groups": [
+ "k8s",
+ "metrics"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max(bytes_in_derivative)",
+ "function": "max",
+ "field_name": "bytes_in_derivative",
+ "partition_field_name": "kubernetes.namespace"
+ }
+ ],
+ "influencers": [
+ "kubernetes.namespace",
+ "kubernetes.pod.uid"
+ ],
+ "summary_count_field_name": "doc_count"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-k8s",
+ "custom_urls": [
+ {
+ "url_name": "Pod Metrics",
+ "url_value": "metrics/detail/pod/$kubernetes.pod.uid$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json
new file mode 100644
index 0000000000000..b06b0ed5089ef
--- /dev/null
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_network_out.json
@@ -0,0 +1,39 @@
+{
+ "job_type": "anomaly_detector",
+ "description": "Metrics: Kubernetes - Identify unusual spikes in outbound traffic across Kubernetes pods.",
+ "groups": [
+ "k8s",
+ "metrics"
+ ],
+ "analysis_config": {
+ "bucket_span": "15m",
+ "detectors": [
+ {
+ "detector_description": "max(bytes_out_derivative)",
+ "function": "max",
+ "field_name": "bytes_out_derivative",
+ "partition_field_name": "kubernetes.namespace"
+ }
+ ],
+ "influencers": [
+ "kubernetes.namespace",
+ "kubernetes.pod.uid"
+ ],
+ "summary_count_field_name": "doc_count"
+ },
+ "data_description": {
+ "time_field": "@timestamp"
+ },
+ "analysis_limits": {
+ "model_memory_limit": "32mb"
+ },
+ "custom_settings": {
+ "created_by": "ml-module-metrics-ui-k8s",
+ "custom_urls": [
+ {
+ "url_name": "Pod Metrics",
+ "url_value": "metrics/detail/pod/$kubernetes.pod.uid$?metricTime=(autoReload:!f,refreshInterval:5000,time:(from:%27$earliest$%27,interval:%3E%3D1m,to:%27$latest$%27))"
+ }
+ ]
+ }
+}
diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
index 245b7e0819c7d..bb0323ed9ae78 100644
--- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
+++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json
@@ -1,6 +1,6 @@
{
"job_type": "anomaly_detector",
- "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privliege elevation via locally run exploits or malware activity.",
+ "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privilege elevation via locally run exploits or malware activity.",
"groups": [
"security",
"auditbeat",
diff --git a/x-pack/plugins/reporting/common/schema_utils.ts b/x-pack/plugins/reporting/common/schema_utils.ts
new file mode 100644
index 0000000000000..f9b5c90e3c366
--- /dev/null
+++ b/x-pack/plugins/reporting/common/schema_utils.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ByteSizeValue } from '@kbn/config-schema';
+import moment from 'moment';
+
+/*
+ * For cleaner code: use these functions when a config schema value could be
+ * one type or another. This allows you to treat the value as one type.
+ */
+
+export const durationToNumber = (value: number | moment.Duration): number => {
+ if (typeof value === 'number') {
+ return value;
+ }
+ return value.asMilliseconds();
+};
+
+export const byteSizeValueToNumber = (value: number | ByteSizeValue) => {
+ if (typeof value === 'number') {
+ return value;
+ }
+
+ return value.getValueInBytes();
+};
diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx
index 65db13f22788b..f326d365351f2 100644
--- a/x-pack/plugins/reporting/public/components/report_listing.tsx
+++ b/x-pack/plugins/reporting/public/components/report_listing.tsx
@@ -6,8 +6,8 @@
import {
EuiBasicTable,
- EuiFlexItem,
EuiFlexGroup,
+ EuiFlexItem,
EuiPageContent,
EuiSpacer,
EuiText,
@@ -23,6 +23,7 @@ import { Subscription } from 'rxjs';
import { ApplicationStart, ToastsSetup } from 'src/core/public';
import { ILicense, LicensingPluginSetup } from '../../../licensing/public';
import { Poller } from '../../common/poller';
+import { durationToNumber } from '../../common/schema_utils';
import { JobStatuses } from '../../constants';
import { checkLicense } from '../lib/license_check';
import { JobQueueEntry, ReportingAPIClient } from '../lib/reporting_api_client';
@@ -183,17 +184,19 @@ class ReportListingUi extends Component {
public componentDidMount() {
this.mounted = true;
+ const { pollConfig, license$ } = this.props;
+ const pollFrequencyInMillis = durationToNumber(pollConfig.jobsRefresh.interval);
this.poller = new Poller({
functionToPoll: () => {
return this.fetchJobs();
},
- pollFrequencyInMillis: this.props.pollConfig.jobsRefresh.interval,
+ pollFrequencyInMillis,
trailing: false,
continuePollingOnError: true,
- pollFrequencyErrorMultiplier: this.props.pollConfig.jobsRefresh.intervalErrorMultiplier,
+ pollFrequencyErrorMultiplier: pollConfig.jobsRefresh.intervalErrorMultiplier,
});
this.poller.start();
- this.licenseSubscription = this.props.license$.subscribe(this.licenseHandler);
+ this.licenseSubscription = license$.subscribe(this.licenseHandler);
}
private licenseHandler = (license: ILicense) => {
diff --git a/x-pack/plugins/reporting/public/plugin.tsx b/x-pack/plugins/reporting/public/plugin.tsx
index d003d4c581699..a134377e194b8 100644
--- a/x-pack/plugins/reporting/public/plugin.tsx
+++ b/x-pack/plugins/reporting/public/plugin.tsx
@@ -26,6 +26,7 @@ import {
import { ManagementSetup } from '../../../../src/plugins/management/public';
import { SharePluginSetup } from '../../../../src/plugins/share/public';
import { LicensingPluginSetup } from '../../licensing/public';
+import { durationToNumber } from '../common/schema_utils';
import { JobId, JobStatusBuckets, ReportingConfigType } from '../common/types';
import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../constants';
import { getGeneralErrorToast } from './components';
@@ -158,8 +159,7 @@ export class ReportingPublicPlugin implements Plugin {
const { http, notifications } = core;
const apiClient = new ReportingAPIClient(http);
const streamHandler = new StreamHandler(notifications, apiClient);
- const { interval } = this.config.poll.jobsRefresh;
-
+ const interval = durationToNumber(this.config.poll.jobsRefresh.interval);
Rx.timer(0, interval)
.pipe(
takeUntil(this.stop$), // stop the interval when stop method is called
diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
index 88be86d1ecc30..6897f07c45e2b 100644
--- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
+++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts
@@ -21,6 +21,7 @@ import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber';
import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators';
import { getChromiumDisconnectedError } from '../';
import { BROWSER_TYPE } from '../../../../common/constants';
+import { durationToNumber } from '../../../../common/schema_utils';
import { CaptureConfig } from '../../../../server/types';
import { LevelLogger } from '../../../lib';
import { safeChildProcess } from '../../safe_child_process';
@@ -90,7 +91,7 @@ export class HeadlessChromiumDriverFactory {
// Set the default timeout for all navigation methods to the openUrl timeout (30 seconds)
// All waitFor methods have their own timeout config passed in to them
- page.setDefaultTimeout(this.captureConfig.timeouts.openUrl);
+ page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl));
logger.debug(`Browser page driver created`);
} catch (err) {
diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts
index a89b952702e1b..b9c6f8e7591e3 100644
--- a/x-pack/plugins/reporting/server/config/index.ts
+++ b/x-pack/plugins/reporting/server/config/index.ts
@@ -17,6 +17,8 @@ export const config: PluginConfigDescriptor = {
unused('capture.concurrency'),
unused('capture.settleTime'),
unused('capture.timeout'),
+ unused('poll.jobCompletionNotifier.intervalErrorMultiplier'),
+ unused('poll.jobsRefresh.intervalErrorMultiplier'),
unused('kibanaApp'),
],
};
diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts
index 69e4d443cf040..9fc3d4329879e 100644
--- a/x-pack/plugins/reporting/server/config/schema.test.ts
+++ b/x-pack/plugins/reporting/server/config/schema.test.ts
@@ -8,101 +8,242 @@ import { ConfigSchema } from './schema';
describe('Reporting Config Schema', () => {
it(`context {"dev":false,"dist":false} produces correct config`, () => {
- expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchObject({
- capture: {
- browser: {
- autoDownload: true,
- chromium: { proxy: { enabled: false } },
- type: 'chromium',
+ expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchInlineSnapshot(`
+ Object {
+ "capture": Object {
+ "browser": Object {
+ "autoDownload": true,
+ "chromium": Object {
+ "proxy": Object {
+ "enabled": false,
+ },
+ },
+ "type": "chromium",
+ },
+ "loadDelay": "PT3S",
+ "maxAttempts": 1,
+ "networkPolicy": Object {
+ "enabled": true,
+ "rules": Array [
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "http:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "https:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "ws:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "wss:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "data:",
+ },
+ Object {
+ "allow": false,
+ "host": undefined,
+ "protocol": undefined,
+ },
+ ],
+ },
+ "timeouts": Object {
+ "openUrl": "PT1M",
+ "renderComplete": "PT30S",
+ "waitForElements": "PT30S",
+ },
+ "viewport": Object {
+ "height": 1200,
+ "width": 1950,
+ },
+ "zoom": 2,
+ },
+ "csv": Object {
+ "checkForFormulas": true,
+ "enablePanelActionDownload": true,
+ "escapeFormulaValues": false,
+ "maxSizeBytes": ByteSizeValue {
+ "valueInBytes": 10485760,
+ },
+ "scroll": Object {
+ "duration": "30s",
+ "size": 500,
+ },
+ "useByteOrderMarkEncoding": false,
},
- loadDelay: 3000,
- maxAttempts: 1,
- networkPolicy: {
- enabled: true,
- rules: [
- { allow: true, host: undefined, protocol: 'http:' },
- { allow: true, host: undefined, protocol: 'https:' },
- { allow: true, host: undefined, protocol: 'ws:' },
- { allow: true, host: undefined, protocol: 'wss:' },
- { allow: true, host: undefined, protocol: 'data:' },
- { allow: false, host: undefined, protocol: undefined },
+ "enabled": true,
+ "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "index": ".reporting",
+ "kibanaServer": Object {},
+ "poll": Object {
+ "jobCompletionNotifier": Object {
+ "interval": 10000,
+ "intervalErrorMultiplier": 5,
+ },
+ "jobsRefresh": Object {
+ "interval": 5000,
+ "intervalErrorMultiplier": 5,
+ },
+ },
+ "queue": Object {
+ "indexInterval": "week",
+ "pollEnabled": true,
+ "pollInterval": "PT3S",
+ "pollIntervalErrorMultiplier": 10,
+ "timeout": "PT2M",
+ },
+ "roles": Object {
+ "allow": Array [
+ "reporting_user",
],
},
- viewport: { height: 1200, width: 1950 },
- zoom: 2,
- },
- csv: {
- checkForFormulas: true,
- enablePanelActionDownload: true,
- maxSizeBytes: 10485760,
- scroll: { duration: '30s', size: 500 },
- },
- encryptionKey: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
- index: '.reporting',
- kibanaServer: {},
- poll: {
- jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 },
- jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 },
- },
- queue: {
- indexInterval: 'week',
- pollEnabled: true,
- pollInterval: 3000,
- pollIntervalErrorMultiplier: 10,
- timeout: 120000,
- },
- roles: { allow: ['reporting_user'] },
- });
+ }
+ `);
});
it(`context {"dev":false,"dist":true} produces correct config`, () => {
- expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchObject({
- capture: {
- browser: {
- autoDownload: false,
- chromium: {
- inspect: false,
- proxy: { enabled: false },
- },
- type: 'chromium',
+ expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchInlineSnapshot(`
+ Object {
+ "capture": Object {
+ "browser": Object {
+ "autoDownload": false,
+ "chromium": Object {
+ "inspect": false,
+ "proxy": Object {
+ "enabled": false,
+ },
+ },
+ "type": "chromium",
+ },
+ "loadDelay": "PT3S",
+ "maxAttempts": 3,
+ "networkPolicy": Object {
+ "enabled": true,
+ "rules": Array [
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "http:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "https:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "ws:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "wss:",
+ },
+ Object {
+ "allow": true,
+ "host": undefined,
+ "protocol": "data:",
+ },
+ Object {
+ "allow": false,
+ "host": undefined,
+ "protocol": undefined,
+ },
+ ],
+ },
+ "timeouts": Object {
+ "openUrl": "PT1M",
+ "renderComplete": "PT30S",
+ "waitForElements": "PT30S",
+ },
+ "viewport": Object {
+ "height": 1200,
+ "width": 1950,
+ },
+ "zoom": 2,
+ },
+ "csv": Object {
+ "checkForFormulas": true,
+ "enablePanelActionDownload": true,
+ "escapeFormulaValues": false,
+ "maxSizeBytes": ByteSizeValue {
+ "valueInBytes": 10485760,
+ },
+ "scroll": Object {
+ "duration": "30s",
+ "size": 500,
+ },
+ "useByteOrderMarkEncoding": false,
},
- loadDelay: 3000,
- maxAttempts: 3,
- networkPolicy: {
- enabled: true,
- rules: [
- { allow: true, host: undefined, protocol: 'http:' },
- { allow: true, host: undefined, protocol: 'https:' },
- { allow: true, host: undefined, protocol: 'ws:' },
- { allow: true, host: undefined, protocol: 'wss:' },
- { allow: true, host: undefined, protocol: 'data:' },
- { allow: false, host: undefined, protocol: undefined },
+ "enabled": true,
+ "index": ".reporting",
+ "kibanaServer": Object {},
+ "poll": Object {
+ "jobCompletionNotifier": Object {
+ "interval": 10000,
+ "intervalErrorMultiplier": 5,
+ },
+ "jobsRefresh": Object {
+ "interval": 5000,
+ "intervalErrorMultiplier": 5,
+ },
+ },
+ "queue": Object {
+ "indexInterval": "week",
+ "pollEnabled": true,
+ "pollInterval": "PT3S",
+ "pollIntervalErrorMultiplier": 10,
+ "timeout": "PT2M",
+ },
+ "roles": Object {
+ "allow": Array [
+ "reporting_user",
],
},
- viewport: { height: 1200, width: 1950 },
- zoom: 2,
- },
- csv: {
- checkForFormulas: true,
- enablePanelActionDownload: true,
- maxSizeBytes: 10485760,
- scroll: { duration: '30s', size: 500 },
- },
- index: '.reporting',
- kibanaServer: {},
- poll: {
- jobCompletionNotifier: { interval: 10000, intervalErrorMultiplier: 5 },
- jobsRefresh: { interval: 5000, intervalErrorMultiplier: 5 },
- },
- queue: {
- indexInterval: 'week',
- pollEnabled: true,
- pollInterval: 3000,
- pollIntervalErrorMultiplier: 10,
- timeout: 120000,
- },
- roles: { allow: ['reporting_user'] },
- });
+ }
+ `);
+ });
+
+ it('allows Duration values for certain keys', () => {
+ expect(ConfigSchema.validate({ queue: { timeout: '2m' } }).queue.timeout).toMatchInlineSnapshot(
+ `"PT2M"`
+ );
+
+ expect(
+ ConfigSchema.validate({ capture: { loadDelay: '3s' } }).capture.loadDelay
+ ).toMatchInlineSnapshot(`"PT3S"`);
+
+ expect(
+ ConfigSchema.validate({
+ capture: { timeouts: { openUrl: '1m', waitForElements: '30s', renderComplete: '10s' } },
+ }).capture.timeouts
+ ).toMatchInlineSnapshot(`
+ Object {
+ "openUrl": "PT1M",
+ "renderComplete": "PT10S",
+ "waitForElements": "PT30S",
+ }
+ `);
+ });
+
+ it('allows ByteSizeValue values for certain keys', () => {
+ expect(ConfigSchema.validate({ csv: { maxSizeBytes: '12mb' } }).csv.maxSizeBytes)
+ .toMatchInlineSnapshot(`
+ ByteSizeValue {
+ "valueInBytes": 12582912,
+ }
+ `);
});
it(`allows optional settings`, () => {
diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts
index a81ffd754946b..8276e8b49d348 100644
--- a/x-pack/plugins/reporting/server/config/schema.ts
+++ b/x-pack/plugins/reporting/server/config/schema.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { schema, TypeOf } from '@kbn/config-schema';
+import { ByteSizeValue, schema, TypeOf } from '@kbn/config-schema';
import moment from 'moment';
const KibanaServerSchema = schema.object({
@@ -33,9 +33,13 @@ const KibanaServerSchema = schema.object({
const QueueSchema = schema.object({
indexInterval: schema.string({ defaultValue: 'week' }),
pollEnabled: schema.boolean({ defaultValue: true }),
- pollInterval: schema.number({ defaultValue: 3000 }),
+ pollInterval: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ seconds: 3 }),
+ }),
pollIntervalErrorMultiplier: schema.number({ defaultValue: 10 }),
- timeout: schema.number({ defaultValue: moment.duration(2, 'm').asMilliseconds() }),
+ timeout: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ minutes: 2 }),
+ }),
});
const RulesSchema = schema.object({
@@ -46,9 +50,15 @@ const RulesSchema = schema.object({
const CaptureSchema = schema.object({
timeouts: schema.object({
- openUrl: schema.number({ defaultValue: 60000 }),
- waitForElements: schema.number({ defaultValue: 30000 }),
- renderComplete: schema.number({ defaultValue: 30000 }),
+ openUrl: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ minutes: 1 }),
+ }),
+ waitForElements: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ seconds: 30 }),
+ }),
+ renderComplete: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ seconds: 30 }),
+ }),
}),
networkPolicy: schema.object({
enabled: schema.boolean({ defaultValue: true }),
@@ -68,9 +78,9 @@ const CaptureSchema = schema.object({
width: schema.number({ defaultValue: 1950 }),
height: schema.number({ defaultValue: 1200 }),
}),
- loadDelay: schema.number({
- defaultValue: moment.duration(3, 's').asMilliseconds(),
- }), // TODO: use schema.duration
+ loadDelay: schema.oneOf([schema.number(), schema.duration()], {
+ defaultValue: moment.duration({ seconds: 3 }),
+ }),
browser: schema.object({
autoDownload: schema.conditional(
schema.contextRef('dist'),
@@ -116,13 +126,13 @@ const CsvSchema = schema.object({
checkForFormulas: schema.boolean({ defaultValue: true }),
escapeFormulaValues: schema.boolean({ defaultValue: false }),
enablePanelActionDownload: schema.boolean({ defaultValue: true }),
- maxSizeBytes: schema.number({
- defaultValue: 1024 * 1024 * 10, // 10MB
- }), // TODO: use schema.byteSize
+ maxSizeBytes: schema.oneOf([schema.number(), schema.byteSize()], {
+ defaultValue: ByteSizeValue.parse('10mb'),
+ }),
useByteOrderMarkEncoding: schema.boolean({ defaultValue: false }),
scroll: schema.object({
duration: schema.string({
- defaultValue: '30s',
+ defaultValue: '30s', // this value is passed directly to ES, so string only format is preferred
validate(value) {
if (!/^[0-9]+(d|h|m|s|ms|micros|nanos)$/.test(value)) {
return 'must be a duration string';
@@ -146,18 +156,16 @@ const RolesSchema = schema.object({
const IndexSchema = schema.string({ defaultValue: '.reporting' });
+// Browser side polling: job completion notifier, management table auto-refresh
+// NOTE: can not use schema.duration, a bug prevents it being passed to the browser correctly
const PollSchema = schema.object({
jobCompletionNotifier: schema.object({
- interval: schema.number({
- defaultValue: moment.duration(10, 's').asMilliseconds(),
- }), // TODO: use schema.duration
- intervalErrorMultiplier: schema.number({ defaultValue: 5 }),
+ interval: schema.number({ defaultValue: 10000 }),
+ intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused
}),
jobsRefresh: schema.object({
- interval: schema.number({
- defaultValue: moment.duration(5, 's').asMilliseconds(),
- }), // TODO: use schema.duration
- intervalErrorMultiplier: schema.number({ defaultValue: 5 }),
+ interval: schema.number({ defaultValue: 5000 }),
+ intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused
}),
});
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
index 754bc7bc75cb5..a0d8ff0852544 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_conditional_headers.test.ts
@@ -4,10 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import sinon from 'sinon';
import { ReportingConfig } from '../../';
import { ReportingCore } from '../../core';
-import { createMockReportingCore } from '../../test_helpers';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockReportingCore,
+} from '../../test_helpers';
import { BasePayload } from '../../types';
import { TaskPayloadPDF } from '../printable_pdf/types';
import { getConditionalHeaders, getCustomLogo } from './';
@@ -15,17 +18,10 @@ import { getConditionalHeaders, getCustomLogo } from './';
let mockConfig: ReportingConfig;
let mockReportingPlugin: ReportingCore;
-const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({
- get: mockConfigGet,
- kbnConfig: { get: mockConfigGet },
-});
-
beforeEach(async () => {
- const mockConfigGet = sinon
- .stub()
- .withArgs('kibanaServer', 'hostname')
- .returns('custom-hostname');
- mockConfig = getMockConfig(mockConfigGet);
+ const reportingConfig = { kibanaServer: { hostname: 'custom-hostname' } };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
mockReportingPlugin = await createMockReportingCore(mockConfig);
});
@@ -84,10 +80,9 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav
const mockGetSavedObjectsClient = jest.fn();
mockReportingPlugin.getSavedObjectsClient = mockGetSavedObjectsClient;
- const mockConfigGet = sinon.stub();
- mockConfigGet.withArgs('kibanaServer', 'hostname').returns('localhost');
- mockConfigGet.withArgs('server', 'basePath').returns('/sbp');
- mockConfig = getMockConfig(mockConfigGet);
+ const reportingConfig = { kibanaServer: { hostname: 'localhost' }, server: { basePath: '/sbp' } };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
const permittedHeaders = {
foo: 'bar',
@@ -134,25 +129,12 @@ test(`uses basePath from server if job doesn't have a basePath when creating sav
});
describe('config formatting', () => {
- test(`lowercases server.host`, async () => {
- const mockConfigGet = sinon.stub().withArgs('server', 'host').returns('COOL-HOSTNAME');
- mockConfig = getMockConfig(mockConfigGet);
-
- const conditionalHeaders = await getConditionalHeaders({
- job: {} as BasePayload,
- filteredHeaders: {},
- config: mockConfig,
- });
- expect(conditionalHeaders.conditions.hostname).toEqual('cool-hostname');
- });
-
test(`lowercases kibanaServer.hostname`, async () => {
- const mockConfigGet = sinon
- .stub()
- .withArgs('kibanaServer', 'hostname')
- .returns('GREAT-HOSTNAME');
- mockConfig = getMockConfig(mockConfigGet);
- const conditionalHeaders = await getConditionalHeaders({
+ const reportingConfig = { kibanaServer: { hostname: 'GREAT-HOSTNAME' } };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
+
+ const conditionalHeaders = getConditionalHeaders({
job: {
title: 'cool-job-bro',
type: 'csv',
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts
index 8c02fdd69de8b..ec4e54632eef5 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_custom_logo.test.ts
@@ -4,18 +4,20 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { ReportingCore } from '../../core';
-import { createMockReportingCore } from '../../test_helpers';
+import { ReportingConfig, ReportingCore } from '../../';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockReportingCore,
+} from '../../test_helpers';
import { TaskPayloadPDF } from '../printable_pdf/types';
import { getConditionalHeaders, getCustomLogo } from './';
-const mockConfigGet = jest.fn().mockImplementation((key: string) => {
- return 'localhost';
-});
-const mockConfig = { get: mockConfigGet, kbnConfig: { get: mockConfigGet } };
-
+let mockConfig: ReportingConfig;
let mockReportingPlugin: ReportingCore;
+
beforeEach(async () => {
+ mockConfig = createMockConfig(createMockConfigSchema());
mockReportingPlugin = await createMockReportingCore(mockConfig);
});
diff --git a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts
index 355536000326e..fae66b26a83e0 100644
--- a/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/common/get_full_urls.test.ts
@@ -5,6 +5,7 @@
*/
import { ReportingConfig } from '../../';
+import { createMockConfig } from '../../test_helpers';
import { TaskPayloadPNG } from '../png/types';
import { TaskPayloadPDF } from '../printable_pdf/types';
import { getFullUrls } from './get_full_urls';
@@ -15,12 +16,6 @@ interface FullUrlsOpts {
}
let mockConfig: ReportingConfig;
-const getMockConfig = (mockConfigGet: jest.Mock) => {
- return {
- get: mockConfigGet,
- kbnConfig: { get: mockConfigGet },
- };
-};
beforeEach(() => {
const reportingConfig: Record = {
@@ -29,10 +24,7 @@ beforeEach(() => {
'kibanaServer.protocol': 'http',
'server.basePath': '/sbp',
};
- const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => {
- return reportingConfig[keys.join('.') as string];
- });
- mockConfig = getMockConfig(mockConfigGet);
+ mockConfig = createMockConfig(reportingConfig);
});
const getMockJob = (base: object) => base as TaskPayloadPNG & TaskPayloadPDF;
diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
index 15432d0cbd147..72b42143a24f7 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts
@@ -6,6 +6,7 @@
import nodeCrypto from '@elastic/node-crypto';
import { ElasticsearchServiceSetup, IUiSettingsClient } from 'kibana/server';
+import moment from 'moment';
// @ts-ignore
import Puid from 'puid';
import sinon from 'sinon';
@@ -73,6 +74,7 @@ describe('CSV Execute Job', function () {
beforeEach(async function () {
configGetStub = sinon.stub();
+ configGetStub.withArgs('queue', 'timeout').returns(moment.duration('2m'));
configGetStub.withArgs('index').returns('.reporting-foo-test');
configGetStub.withArgs('encryptionKey').returns(encryptionKey);
configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB
diff --git a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
index 06aa2434afc3f..e383f21143149 100644
--- a/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
+++ b/x-pack/plugins/reporting/server/export_types/csv/generate_csv/index.ts
@@ -6,11 +6,12 @@
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'src/core/server';
-import { getFieldFormats } from '../../../services';
import { ReportingConfig } from '../../../';
import { CancellationToken } from '../../../../../../plugins/reporting/common';
import { CSV_BOM_CHARS } from '../../../../common/constants';
+import { byteSizeValueToNumber } from '../../../../common/schema_utils';
import { LevelLogger } from '../../../lib';
+import { getFieldFormats } from '../../../services';
import { IndexPatternSavedObject, SavedSearchGeneratorResult } from '../types';
import { checkIfRowsHaveFormulas } from './check_cells_for_formulas';
import { createEscapeValue } from './escape_value';
@@ -64,7 +65,7 @@ export function createGenerateCsv(logger: LevelLogger) {
);
const escapeValue = createEscapeValue(settings.quoteValues, settings.escapeFormulaValues);
const bom = config.get('csv', 'useByteOrderMarkEncoding') ? CSV_BOM_CHARS : '';
- const builder = new MaxSizeStringBuilder(settings.maxSizeBytes, bom);
+ const builder = new MaxSizeStringBuilder(byteSizeValueToNumber(settings.maxSizeBytes), bom);
const { fields, metaFields, conflictedTypesFields } = job;
const header = `${fields.map(escapeValue).join(settings.separator)}\n`;
diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts
index fdc51dc1c9c87..e7322bdc0d408 100644
--- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts
+++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts
@@ -10,7 +10,11 @@ import * as Rx from 'rxjs';
import { ReportingCore } from '../../../';
import { CancellationToken } from '../../../../common';
import { cryptoFactory, LevelLogger } from '../../../lib';
-import { createMockReportingCore } from '../../../test_helpers';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockReportingCore,
+} from '../../../test_helpers';
import { generatePdfObservableFactory } from '../lib/generate_pdf';
import { TaskPayloadPDF } from '../types';
import { runTaskFnFactory } from './';
@@ -39,20 +43,16 @@ const encryptHeaders = async (headers: Record) => {
const getBasePayload = (baseObj: any) => baseObj as TaskPayloadPDF;
beforeEach(async () => {
- const kbnConfig = {
- 'server.basePath': '/sbp',
- };
const reportingConfig = {
+ 'server.basePath': '/sbp',
index: '.reports-test',
encryptionKey: mockEncryptionKey,
'kibanaServer.hostname': 'localhost',
'kibanaServer.port': 5601,
'kibanaServer.protocol': 'http',
};
- const mockReportingConfig = {
- get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')],
- kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] },
- };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ const mockReportingConfig = createMockConfig(mockSchema);
mockReporting = await createMockReportingCore(mockReportingConfig);
@@ -79,7 +79,7 @@ test(`passes browserTimezone to generatePdf`, async () => {
const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock;
generatePdfObservable.mockReturnValue(Rx.of(Buffer.from('')));
- const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
+ const runTask = runTaskFnFactory(mockReporting, getMockLogger());
const browserTimezone = 'UTC';
await runTask(
'pdfJobId',
@@ -98,7 +98,7 @@ test(`passes browserTimezone to generatePdf`, async () => {
test(`returns content_type of application/pdf`, async () => {
const logger = getMockLogger();
- const runTask = await runTaskFnFactory(mockReporting, logger);
+ const runTask = runTaskFnFactory(mockReporting, logger);
const encryptedHeaders = await encryptHeaders({});
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
@@ -117,7 +117,7 @@ test(`returns content of generatePdf getBuffer base64 encoded`, async () => {
const generatePdfObservable = await generatePdfObservableFactory(mockReporting);
(generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));
- const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
+ const runTask = runTaskFnFactory(mockReporting, getMockLogger());
const encryptedHeaders = await encryptHeaders({});
const { content } = await runTask(
'pdfJobId',
diff --git a/x-pack/plugins/reporting/server/lib/create_worker.test.ts b/x-pack/plugins/reporting/server/lib/create_worker.test.ts
index 85188c07eeb20..1fcd750849331 100644
--- a/x-pack/plugins/reporting/server/lib/create_worker.test.ts
+++ b/x-pack/plugins/reporting/server/lib/create_worker.test.ts
@@ -6,7 +6,12 @@
import * as sinon from 'sinon';
import { ReportingConfig, ReportingCore } from '../../server';
-import { createMockReportingCore } from '../test_helpers';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockLevelLogger,
+ createMockReportingCore,
+} from '../test_helpers';
import { createWorkerFactory } from './create_worker';
// @ts-ignore
import { Esqueue } from './esqueue';
@@ -14,16 +19,13 @@ import { Esqueue } from './esqueue';
import { ClientMock } from './esqueue/__tests__/fixtures/legacy_elasticsearch';
import { ExportTypesRegistry } from './export_types_registry';
-const configGetStub = sinon.stub();
-configGetStub.withArgs('queue').returns({
- pollInterval: 3300,
- pollIntervalErrorMultiplier: 10,
-});
-configGetStub.withArgs('server', 'name').returns('test-server-123');
-configGetStub.withArgs('server', 'uuid').returns('g9ymiujthvy6v8yrh7567g6fwzgzftzfr');
+const logger = createMockLevelLogger();
+const reportingConfig = {
+ queue: { pollInterval: 3300, pollIntervalErrorMultiplier: 10 },
+ server: { name: 'test-server-123', uuid: 'g9ymiujthvy6v8yrh7567g6fwzgzftzfr' },
+};
const executeJobFactoryStub = sinon.stub();
-const getMockLogger = sinon.stub();
const getMockExportTypesRegistry = (
exportTypes: any[] = [{ runTaskFnFactory: executeJobFactoryStub }]
@@ -39,18 +41,18 @@ describe('Create Worker', () => {
let client: ClientMock;
beforeEach(async () => {
- mockConfig = { get: configGetStub, kbnConfig: { get: configGetStub } };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
mockReporting = await createMockReportingCore(mockConfig);
mockReporting.getExportTypesRegistry = () => getMockExportTypesRegistry();
- // @ts-ignore over-riding config manually
- mockReporting.config = mockConfig;
+
client = new ClientMock();
queue = new Esqueue('reporting-queue', { client });
executeJobFactoryStub.reset();
});
test('Creates a single Esqueue worker for Reporting', async () => {
- const createWorker = createWorkerFactory(mockReporting, getMockLogger());
+ const createWorker = createWorkerFactory(mockReporting, logger);
const registerWorkerSpy = sinon.spy(queue, 'registerWorker');
await createWorker(queue);
@@ -82,7 +84,7 @@ Object {
{ runTaskFnFactory: executeJobFactoryStub },
]);
mockReporting.getExportTypesRegistry = () => exportTypesRegistry;
- const createWorker = createWorkerFactory(mockReporting, getMockLogger());
+ const createWorker = createWorkerFactory(mockReporting, logger);
const registerWorkerSpy = sinon.spy(queue, 'registerWorker');
await createWorker(queue);
diff --git a/x-pack/plugins/reporting/server/lib/create_worker.ts b/x-pack/plugins/reporting/server/lib/create_worker.ts
index dd5c560455274..c1c88dd8a54ba 100644
--- a/x-pack/plugins/reporting/server/lib/create_worker.ts
+++ b/x-pack/plugins/reporting/server/lib/create_worker.ts
@@ -6,6 +6,7 @@
import { CancellationToken } from '../../common';
import { PLUGIN_ID } from '../../common/constants';
+import { durationToNumber } from '../../common/schema_utils';
import { ReportingCore } from '../../server';
import { LevelLogger } from '../../server/lib';
import { ExportTypeDefinition, JobSource, RunTaskFn } from '../../server/types';
@@ -57,7 +58,7 @@ export function createWorkerFactory(reporting: ReportingCore, log
const workerOptions = {
kibanaName,
kibanaId,
- interval: queueConfig.pollInterval,
+ interval: durationToNumber(queueConfig.pollInterval),
intervalErrorMultiplier: queueConfig.pollIntervalErrorMultiplier,
};
const worker = queue.registerWorker(PLUGIN_ID, workerFn, workerOptions);
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts
index 49c690e8c024d..89cb4221c96b2 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts
@@ -5,9 +5,10 @@
*/
import { i18n } from '@kbn/i18n';
+import { durationToNumber } from '../../../common/schema_utils';
+import { LevelLogger, startTrace } from '../';
import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig } from '../../types';
-import { LevelLogger, startTrace } from '../';
import { LayoutInstance } from '../layouts';
import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants';
@@ -31,9 +32,10 @@ export const getNumberOfItems = async (
// the dashboard is using the `itemsCountAttribute` attribute to let us
// know how many items to expect since gridster incrementally adds panels
// we have to use this hint to wait for all of them
+ const timeout = durationToNumber(captureConfig.timeouts.waitForElements);
await browser.waitForSelector(
`${renderCompleteSelector},[${itemsCountAttribute}]`,
- { timeout: captureConfig.timeouts.waitForElements },
+ { timeout },
{ context: CONTEXT_READMETADATA },
logger
);
@@ -59,6 +61,7 @@ export const getNumberOfItems = async (
logger
);
} catch (err) {
+ logger.error(err);
throw new Error(
i18n.translate('xpack.reporting.screencapture.readVisualizationsError', {
defaultMessage: `An error occurred when trying to read the page for visualization panel info. You may need to increase '{configKey}'. {error}`,
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts
index f893951815e9e..2fc711d4d6f07 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts
@@ -43,6 +43,7 @@ export const injectCustomCss = async (
logger
);
} catch (err) {
+ logger.error(err);
throw new Error(
i18n.translate('xpack.reporting.screencapture.injectCss', {
defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`,
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts
index 3749e4372bdab..5b671e9f5b47e 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts
@@ -15,12 +15,17 @@ jest.mock('../../browsers/chromium/puppeteer', () => ({
}),
}));
+import moment from 'moment';
import * as Rx from 'rxjs';
-import { LevelLogger } from '../';
-import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
import { HeadlessChromiumDriver } from '../../browsers';
-import { createMockBrowserDriverFactory, createMockLayoutInstance } from '../../test_helpers';
-import { CaptureConfig, ConditionalHeaders } from '../../types';
+import {
+ createMockBrowserDriverFactory,
+ createMockConfig,
+ createMockConfigSchema,
+ createMockLayoutInstance,
+ createMockLevelLogger,
+} from '../../test_helpers';
+import { ConditionalHeaders } from '../../types';
import { ElementsPositionAndAttribute } from './';
import * as contexts from './constants';
import { screenshotsObservableFactory } from './observable';
@@ -28,11 +33,22 @@ import { screenshotsObservableFactory } from './observable';
/*
* Mocks
*/
-const mockLogger = jest.fn(loggingSystemMock.create);
-const logger = new LevelLogger(mockLogger());
+const logger = createMockLevelLogger();
-const mockConfig = { timeouts: { openUrl: 13 } } as CaptureConfig;
-const mockLayout = createMockLayoutInstance(mockConfig);
+const reportingConfig = {
+ capture: {
+ loadDelay: moment.duration(2, 's'),
+ timeouts: {
+ openUrl: moment.duration(2, 'm'),
+ waitForElements: moment.duration(20, 's'),
+ renderComplete: moment.duration(10, 's'),
+ },
+ },
+};
+const mockSchema = createMockConfigSchema(reportingConfig);
+const mockConfig = createMockConfig(mockSchema);
+const captureConfig = mockConfig.get('capture');
+const mockLayout = createMockLayoutInstance(captureConfig);
/*
* Tests
@@ -45,7 +61,7 @@ describe('Screenshot Observable Pipeline', () => {
});
it('pipelines a single url into screenshot and timeRange', async () => {
- const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory);
+ const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const result = await getScreenshots$({
logger,
urls: ['/welcome/home/start/index.htm'],
@@ -106,7 +122,7 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
- const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory);
+ const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const result = await getScreenshots$({
logger,
urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'],
@@ -205,7 +221,7 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
- const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory);
+ const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
logger,
@@ -300,7 +316,7 @@ describe('Screenshot Observable Pipeline', () => {
});
// test
- const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory);
+ const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
logger,
@@ -333,7 +349,7 @@ describe('Screenshot Observable Pipeline', () => {
mockLayout.getViewport = () => null;
// test
- const getScreenshots$ = screenshotsObservableFactory(mockConfig, mockBrowserDriverFactory);
+ const getScreenshots$ = screenshotsObservableFactory(captureConfig, mockBrowserDriverFactory);
const getScreenshot = async () => {
return await getScreenshots$({
logger,
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts
index c21ef3b91fab3..e28f50851f4d9 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts
@@ -5,9 +5,10 @@
*/
import { i18n } from '@kbn/i18n';
+import { durationToNumber } from '../../../common/schema_utils';
+import { LevelLogger, startTrace } from '../';
import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig, ConditionalHeaders } from '../../types';
-import { LevelLogger, startTrace } from '../';
export const openUrl = async (
captureConfig: CaptureConfig,
@@ -19,16 +20,14 @@ export const openUrl = async (
): Promise => {
const endTrace = startTrace('open_url', 'wait');
try {
+ const timeout = durationToNumber(captureConfig.timeouts.openUrl);
await browser.open(
url,
- {
- conditionalHeaders,
- waitForSelector: pageLoadSelector,
- timeout: captureConfig.timeouts.openUrl,
- },
+ { conditionalHeaders, waitForSelector: pageLoadSelector, timeout },
logger
);
} catch (err) {
+ logger.error(err);
throw new Error(
i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', {
defaultMessage: `An error occurred when trying to open the Kibana URL. You may need to increase '{configKey}'. {error}`,
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts
index f36a7b6f73664..edd4f71b2adac 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts
@@ -5,6 +5,7 @@
*/
import { i18n } from '@kbn/i18n';
+import { durationToNumber } from '../../../common/schema_utils';
import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig } from '../../types';
import { LevelLogger, startTrace } from '../';
@@ -67,7 +68,7 @@ export const waitForRenderComplete = async (
return Promise.all(renderedTasks).then(hackyWaitForVisualizations);
},
- args: [layout.selectors.renderComplete, captureConfig.loadDelay],
+ args: [layout.selectors.renderComplete, durationToNumber(captureConfig.loadDelay)],
},
{ context: CONTEXT_WAITFORRENDER },
logger
diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts
index 779d00442522d..5f86a2b3bf00b 100644
--- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts
+++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts
@@ -5,8 +5,9 @@
*/
import { i18n } from '@kbn/i18n';
-import { HeadlessChromiumDriver } from '../../browsers';
+import { durationToNumber } from '../../../common/schema_utils';
import { LevelLogger, startTrace } from '../';
+import { HeadlessChromiumDriver } from '../../browsers';
import { CaptureConfig } from '../../types';
import { LayoutInstance } from '../layouts';
import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants';
@@ -25,7 +26,7 @@ const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => {
export const waitForVisualizations = async (
captureConfig: CaptureConfig,
browser: HeadlessChromiumDriver,
- itemsCount: number,
+ toEqual: number,
layout: LayoutInstance,
logger: LevelLogger
): Promise => {
@@ -35,29 +36,26 @@ export const waitForVisualizations = async (
logger.debug(
i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', {
defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`,
- values: { itemsCount },
+ values: { itemsCount: toEqual },
})
);
try {
+ const timeout = durationToNumber(captureConfig.timeouts.renderComplete);
await browser.waitFor(
- {
- fn: getCompletedItemsCount,
- args: [{ renderCompleteSelector }],
- toEqual: itemsCount,
- timeout: captureConfig.timeouts.renderComplete,
- },
+ { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout },
{ context: CONTEXT_WAITFORELEMENTSTOBEINDOM },
logger
);
- logger.debug(`found ${itemsCount} rendered elements in the DOM`);
+ logger.debug(`found ${toEqual} rendered elements in the DOM`);
} catch (err) {
+ logger.error(err);
throw new Error(
i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', {
defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. You may need to increase '{configKey}'. {error}`,
values: {
- count: itemsCount,
+ count: toEqual,
configKey: 'xpack.reporting.capture.timeouts.renderComplete',
error: err,
},
diff --git a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts
index 71ce0b1e572f8..7b8b851f5bd72 100644
--- a/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts
+++ b/x-pack/plugins/reporting/server/lib/store/index_timestamp.ts
@@ -8,7 +8,6 @@ import moment, { unitOfTime } from 'moment';
export const intervals = ['year', 'month', 'week', 'day', 'hour', 'minute'];
-// TODO: This helper function can be removed by using `schema.duration` objects in the reporting config schema
export function indexTimestamp(intervalStr: string, separator = '-') {
const startOf = intervalStr as unitOfTime.StartOf;
if (separator.match(/[a-z]/i)) throw new Error('Interval separator can not be a letter');
diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts
index b87466ca289cf..8dc4edd200052 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.test.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts
@@ -7,15 +7,15 @@
import sinon from 'sinon';
import { ElasticsearchServiceSetup } from 'src/core/server';
import { ReportingConfig, ReportingCore } from '../..';
-import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockLevelLogger,
+ createMockReportingCore,
+} from '../../test_helpers';
import { Report } from './report';
import { ReportingStore } from './store';
-const getMockConfig = (mockConfigGet: sinon.SinonStub) => ({
- get: mockConfigGet,
- kbnConfig: { get: mockConfigGet },
-});
-
describe('ReportingStore', () => {
const mockLogger = createMockLevelLogger();
let mockConfig: ReportingConfig;
@@ -25,10 +25,12 @@ describe('ReportingStore', () => {
const mockElasticsearch = { legacy: { client: { callAsInternalUser: callClusterStub } } };
beforeEach(async () => {
- const mockConfigGet = sinon.stub();
- mockConfigGet.withArgs('index').returns('.reporting-test');
- mockConfigGet.withArgs('queue', 'indexInterval').returns('week');
- mockConfig = getMockConfig(mockConfigGet);
+ const reportingConfig = {
+ index: '.reporting-test',
+ queue: { indexInterval: 'week' },
+ };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
mockCore = await createMockReportingCore(mockConfig);
callClusterStub.reset();
@@ -67,15 +69,17 @@ describe('ReportingStore', () => {
priority: 10,
started_at: undefined,
status: 'pending',
- timeout: undefined,
+ timeout: 120000,
});
});
it('throws if options has invalid indexInterval', async () => {
- const mockConfigGet = sinon.stub();
- mockConfigGet.withArgs('index').returns('.reporting-test');
- mockConfigGet.withArgs('queue', 'indexInterval').returns('centurially');
- mockConfig = getMockConfig(mockConfigGet);
+ const reportingConfig = {
+ index: '.reporting-test',
+ queue: { indexInterval: 'centurially' },
+ };
+ const mockSchema = createMockConfigSchema(reportingConfig);
+ mockConfig = createMockConfig(mockSchema);
mockCore = await createMockReportingCore(mockConfig);
const store = new ReportingStore(mockCore, mockLogger);
@@ -159,7 +163,7 @@ describe('ReportingStore', () => {
priority: 10,
started_at: undefined,
status: 'pending',
- timeout: undefined,
+ timeout: 120000,
});
});
@@ -190,7 +194,7 @@ describe('ReportingStore', () => {
priority: 10,
started_at: undefined,
status: 'pending',
- timeout: undefined,
+ timeout: 120000,
});
});
});
diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts
index b1309cbdeb94d..0aae8b567bcdb 100644
--- a/x-pack/plugins/reporting/server/lib/store/store.ts
+++ b/x-pack/plugins/reporting/server/lib/store/store.ts
@@ -5,6 +5,7 @@
*/
import { ElasticsearchServiceSetup } from 'src/core/server';
+import { durationToNumber } from '../../../common/schema_utils';
import { LevelLogger, statuses } from '../';
import { ReportingCore } from '../../';
import { BaseParams, BaseParamsEncryptedFields, ReportingUser } from '../../types';
@@ -45,7 +46,7 @@ export class ReportingStore {
this.indexPrefix = config.get('index');
this.indexInterval = config.get('queue', 'indexInterval');
this.jobSettings = {
- timeout: config.get('queue', 'timeout'),
+ timeout: durationToNumber(config.get('queue', 'timeout')),
browser_type: config.get('capture', 'browser', 'type'),
max_attempts: config.get('capture', 'maxAttempts'),
priority: 10, // unused
diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts
index d323a281c06ff..3f2f472ab0623 100644
--- a/x-pack/plugins/reporting/server/plugin.test.ts
+++ b/x-pack/plugins/reporting/server/plugin.test.ts
@@ -32,8 +32,8 @@ describe('Reporting Plugin', () => {
beforeEach(async () => {
configSchema = createMockConfigSchema();
initContext = coreMock.createPluginInitializerContext(configSchema);
- coreSetup = await coreMock.createSetup(configSchema);
- coreStart = await coreMock.createStart();
+ coreSetup = coreMock.createSetup(configSchema);
+ coreStart = coreMock.createStart();
pluginSetup = ({
licensing: {},
features: featuresPluginMock.createSetup(),
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts
index f92fbfc7013cf..71ca0661a42a9 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts
@@ -33,7 +33,15 @@ describe('POST /diagnose/browser', () => {
const mockedCreateInterface: any = createInterface;
const config = {
- get: jest.fn().mockImplementation(() => ({})),
+ get: jest.fn().mockImplementation((...keys) => {
+ const key = keys.join('.');
+ switch (key) {
+ case 'queue.timeout':
+ return 120000;
+ case 'capture.browser.chromium.proxy':
+ return { enabled: false };
+ }
+ }),
kbnConfig: { get: jest.fn() },
};
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts
index 24b85220defb4..33620bc9a0038 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts
@@ -54,25 +54,30 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger
validate: {},
},
userHandler(async (user, context, req, res) => {
- const logs = await browserStartLogs(reporting, logger).toPromise();
- const knownIssues = Object.keys(logsToHelpMap) as Array;
+ try {
+ const logs = await browserStartLogs(reporting, logger).toPromise();
+ const knownIssues = Object.keys(logsToHelpMap) as Array;
- const boundSuccessfully = logs.includes(`DevTools listening on`);
- const help = knownIssues.reduce((helpTexts: string[], knownIssue) => {
- const helpText = logsToHelpMap[knownIssue];
- if (logs.includes(knownIssue)) {
- helpTexts.push(helpText);
- }
- return helpTexts;
- }, []);
+ const boundSuccessfully = logs.includes(`DevTools listening on`);
+ const help = knownIssues.reduce((helpTexts: string[], knownIssue) => {
+ const helpText = logsToHelpMap[knownIssue];
+ if (logs.includes(knownIssue)) {
+ helpTexts.push(helpText);
+ }
+ return helpTexts;
+ }, []);
- const response: DiagnosticResponse = {
- success: boundSuccessfully && !help.length,
- help,
- logs,
- };
+ const response: DiagnosticResponse = {
+ success: boundSuccessfully && !help.length,
+ help,
+ logs,
+ };
- return res.ok({ body: response });
+ return res.ok({ body: response });
+ } catch (err) {
+ logger.error(err);
+ return res.custom({ statusCode: 500 });
+ }
})
);
};
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts
index 624397246656d..a112d04f38c7b 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts
@@ -35,7 +35,15 @@ describe('POST /diagnose/config', () => {
} as unknown) as any;
config = {
- get: jest.fn(),
+ get: jest.fn().mockImplementation((...keys) => {
+ const key = keys.join('.');
+ switch (key) {
+ case 'queue.timeout':
+ return 120000;
+ case 'csv.maxSizeBytes':
+ return 1024;
+ }
+ }),
kbnConfig: { get: jest.fn() },
};
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts
index 198ba63e2614d..95c3a05bbf680 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts
@@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ByteSizeValue } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
-import numeral from '@elastic/numeral';
import { defaults, get } from 'lodash';
import { ReportingCore } from '../..';
import { API_DIAGNOSE_URL } from '../../../common/constants';
@@ -16,6 +16,14 @@ import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_rout
const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes';
const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length';
+const numberToByteSizeValue = (value: number | ByteSizeValue) => {
+ if (typeof value === 'number') {
+ return new ByteSizeValue(value);
+ }
+
+ return value;
+};
+
export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => {
const setupDeps = reporting.getPluginSetupDeps();
const userHandler = authorizedUserPreRoutingFactory(reporting);
@@ -42,12 +50,10 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger)
'http.max_content_length',
'100mb'
);
- const elasticSearchMaxContentBytes = numeral().unformat(
- elasticSearchMaxContent.toUpperCase()
- );
- const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes');
+ const elasticSearchMaxContentBytes = ByteSizeValue.parse(elasticSearchMaxContent);
+ const kibanaMaxContentBytes = numberToByteSizeValue(config.get('csv', 'maxSizeBytes'));
- if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) {
+ if (kibanaMaxContentBytes.isGreaterThan(elasticSearchMaxContentBytes)) {
const maxContentSizeWarning = i18n.translate(
'xpack.reporting.diagnostic.configSizeMismatch',
{
@@ -55,8 +61,8 @@ export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger)
`xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` +
`Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`,
values: {
- kibanaMaxContentBytes,
- elasticSearchMaxContentBytes,
+ kibanaMaxContentBytes: kibanaMaxContentBytes.getValueInBytes(),
+ elasticSearchMaxContentBytes: elasticSearchMaxContentBytes.getValueInBytes(),
KIBANA_MAX_SIZE_BYTES_PATH,
ES_MAX_SIZE_BYTES_PATH,
},
diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts
index ec4ab0446ae5f..287da0d2ed5ec 100644
--- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts
+++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts
@@ -33,7 +33,11 @@ describe('POST /diagnose/screenshot', () => {
};
const config = {
- get: jest.fn(),
+ get: jest.fn().mockImplementation((...keys) => {
+ if (keys.join('.') === 'queue.timeout') {
+ return 120000;
+ }
+ }),
kbnConfig: { get: jest.fn() },
};
const mockLogger = createMockLevelLogger();
diff --git a/x-pack/plugins/reporting/server/routes/jobs.test.ts b/x-pack/plugins/reporting/server/routes/jobs.test.ts
index 2957bc76f4682..187c69f4a72ef 100644
--- a/x-pack/plugins/reporting/server/routes/jobs.test.ts
+++ b/x-pack/plugins/reporting/server/routes/jobs.test.ts
@@ -10,9 +10,8 @@ import { setupServer } from 'src/core/server/test_utils';
import supertest from 'supertest';
import { ReportingCore } from '..';
import { ReportingInternalSetup } from '../core';
-import { LevelLogger } from '../lib';
import { ExportTypesRegistry } from '../lib/export_types_registry';
-import { createMockReportingCore } from '../test_helpers';
+import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers';
import { ExportTypeDefinition } from '../types';
import { registerJobInfoRoutes } from './jobs';
@@ -25,11 +24,7 @@ describe('GET /api/reporting/jobs/download', () => {
let exportTypesRegistry: ExportTypesRegistry;
let core: ReportingCore;
- const config = { get: jest.fn(), kbnConfig: { get: jest.fn() } };
- const mockLogger = ({
- error: jest.fn(),
- debug: jest.fn(),
- } as unknown) as jest.Mocked;
+ const config = createMockConfig(createMockConfigSchema());
const getHits = (...sources: any) => {
return {
@@ -86,8 +81,6 @@ describe('GET /api/reporting/jobs/download', () => {
});
afterEach(async () => {
- mockLogger.debug.mockReset();
- mockLogger.error.mockReset();
await server.stop();
});
diff --git a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts
index 50780a577af02..932ebfdd22bbc 100644
--- a/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts
+++ b/x-pack/plugins/reporting/server/routes/lib/authorized_user_pre_routing.test.ts
@@ -4,24 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { KibanaRequest, RequestHandlerContext, KibanaResponseFactory } from 'kibana/server';
+import { KibanaRequest, KibanaResponseFactory, RequestHandlerContext } from 'kibana/server';
import { coreMock, httpServerMock } from 'src/core/server/mocks';
import { ReportingCore } from '../../';
-import { createMockReportingCore } from '../../test_helpers';
-import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing';
import { ReportingInternalSetup } from '../../core';
+import {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockReportingCore,
+} from '../../test_helpers';
+import { authorizedUserPreRoutingFactory } from './authorized_user_pre_routing';
let mockCore: ReportingCore;
-const kbnConfig = {
- 'server.basePath': '/sbp',
-};
-const reportingConfig = {
- 'roles.allow': ['reporting_user'],
-};
-const mockReportingConfig = {
- get: (...keys: string[]) => (reportingConfig as any)[keys.join('.')] || 'whoah!',
- kbnConfig: { get: (...keys: string[]) => (kbnConfig as any)[keys.join('.')] },
-};
+const mockConfig: any = { 'server.basePath': '/sbp', 'roles.allow': ['reporting_user'] };
+const mockReportingConfigSchema = createMockConfigSchema(mockConfig);
+const mockReportingConfig = createMockConfig(mockReportingConfigSchema);
const getMockContext = () =>
(({
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts
index f2785bce10964..d6996d2caf1bc 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import moment from 'moment';
import { Page } from 'puppeteer';
import * as Rx from 'rxjs';
import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers';
@@ -15,6 +16,7 @@ import { CaptureConfig } from '../types';
interface CreateMockBrowserDriverFactoryOpts {
evaluate: jest.Mock, any[]>;
waitForSelector: jest.Mock, any[]>;
+ waitFor: jest.Mock, any[]>;
screenshot: jest.Mock, any[]>;
open: jest.Mock, any[]>;
getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock;
@@ -86,6 +88,7 @@ const getCreatePage = (driver: HeadlessChromiumDriver) =>
const defaultOpts: CreateMockBrowserDriverFactoryOpts = {
evaluate: mockBrowserEvaluate,
waitForSelector: mockWaitForSelector,
+ waitFor: jest.fn(),
screenshot: mockScreenshot,
open: jest.fn(),
getCreatePage,
@@ -96,7 +99,11 @@ export const createMockBrowserDriverFactory = async (
opts: Partial = {}
): Promise => {
const captureConfig: CaptureConfig = {
- timeouts: { openUrl: 30000, waitForElements: 30000, renderComplete: 30000 },
+ timeouts: {
+ openUrl: moment.duration(60, 's'),
+ waitForElements: moment.duration(30, 's'),
+ renderComplete: moment.duration(30, 's'),
+ },
browser: {
type: 'chromium',
chromium: {
@@ -108,18 +115,14 @@ export const createMockBrowserDriverFactory = async (
},
networkPolicy: { enabled: true, rules: [] },
viewport: { width: 800, height: 600 },
- loadDelay: 2000,
+ loadDelay: moment.duration(2, 's'),
zoom: 2,
maxAttempts: 1,
};
const binaryPath = '/usr/local/share/common/secure/super_awesome_binary';
- const mockBrowserDriverFactory = await chromium.createDriverFactory(
- binaryPath,
- captureConfig,
- logger
- );
- const mockPage = {} as Page;
+ const mockBrowserDriverFactory = chromium.createDriverFactory(binaryPath, captureConfig, logger);
+ const mockPage = ({ setViewport: () => {} } as unknown) as Page;
const mockBrowserDriver = new HeadlessChromiumDriver(mockPage, {
inspect: true,
networkPolicy: captureConfig.networkPolicy,
@@ -127,6 +130,7 @@ export const createMockBrowserDriverFactory = async (
// mock the driver methods as either default mocks or passed-in
mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore
+ mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor;
mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate;
mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot;
mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open;
diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
index 559726e0b8a99..6ec35db5caec6 100644
--- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts
@@ -9,14 +9,16 @@ jest.mock('../usage');
jest.mock('../browsers');
jest.mock('../lib/create_queue');
+import _ from 'lodash';
import * as Rx from 'rxjs';
-import { featuresPluginMock } from '../../../features/server/mocks';
import { ReportingConfig, ReportingCore } from '../';
+import { featuresPluginMock } from '../../../features/server/mocks';
import {
chromium,
HeadlessChromiumDriverFactory,
initializeBrowserDriverFactory,
} from '../browsers';
+import { ReportingConfigType } from '../config';
import { ReportingInternalSetup, ReportingInternalStart } from '../core';
import { ReportingStore } from '../lib';
import { ReportingStartDeps } from '../types';
@@ -57,12 +59,58 @@ const createMockPluginStart = (
};
};
-export const createMockConfigSchema = (overrides?: any) => ({
- index: '.reporting',
- kibanaServer: { hostname: 'localhost', port: '80' },
- capture: { browser: { chromium: { disableSandbox: true } } },
- ...overrides,
-});
+interface ReportingConfigTestType {
+ index: string;
+ encryptionKey: string;
+ queue: Partial;
+ kibanaServer: Partial;
+ csv: Partial;
+ capture: any;
+ server?: any;
+}
+
+export const createMockConfigSchema = (
+ overrides: Partial = {}
+): ReportingConfigTestType => {
+ // deeply merge the defaults and the provided partial schema
+ return {
+ index: '.reporting',
+ encryptionKey: 'cool-encryption-key-where-did-you-find-it',
+ ...overrides,
+ kibanaServer: {
+ hostname: 'localhost',
+ port: 80,
+ ...overrides.kibanaServer,
+ },
+ capture: {
+ browser: {
+ chromium: {
+ disableSandbox: true,
+ },
+ },
+ ...overrides.capture,
+ },
+ queue: {
+ timeout: 120000,
+ ...overrides.queue,
+ },
+ csv: {
+ ...overrides.csv,
+ },
+ };
+};
+
+export const createMockConfig = (
+ reportingConfig: Partial
+): ReportingConfig => {
+ const mockConfigGet = jest.fn().mockImplementation((...keys: string[]) => {
+ return _.get(reportingConfig, keys.join('.'));
+ });
+ return {
+ get: mockConfigGet,
+ kbnConfig: { get: mockConfigGet },
+ };
+};
export const createMockStartDeps = (startMock?: any): ReportingStartDeps => ({
data: startMock.data,
diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts
index 2d5ef9fdd768d..96357dc915eef 100644
--- a/x-pack/plugins/reporting/server/test_helpers/index.ts
+++ b/x-pack/plugins/reporting/server/test_helpers/index.ts
@@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { createMockServer } from './create_mock_server';
-export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin';
export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory';
export { createMockLayoutInstance } from './create_mock_layoutinstance';
export { createMockLevelLogger } from './create_mock_levellogger';
+export {
+ createMockConfig,
+ createMockConfigSchema,
+ createMockReportingCore,
+} from './create_mock_reportingplugin';
+export { createMockServer } from './create_mock_server';
diff --git a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts
index ed2abef2542de..fc2dce441c621 100644
--- a/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts
+++ b/x-pack/plugins/reporting/server/usage/reporting_usage_collector.test.ts
@@ -8,8 +8,8 @@ import * as Rx from 'rxjs';
import sinon from 'sinon';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { ReportingConfig, ReportingCore } from '../';
-import { createMockReportingCore } from '../test_helpers';
import { getExportTypesRegistry } from '../lib/export_types_registry';
+import { createMockConfig, createMockConfigSchema, createMockReportingCore } from '../test_helpers';
import { ReportingSetupDeps } from '../types';
import { FeaturesAvailability } from './';
import {
@@ -54,17 +54,13 @@ function getPluginsMock(
} as unknown) as ReportingSetupDeps & { usageCollection: UsageCollectionSetup };
}
-const getMockReportingConfig = () => ({
- get: () => {},
- kbnConfig: { get: () => '' },
-});
const getResponseMock = (base = {}) => base;
describe('license checks', () => {
let mockConfig: ReportingConfig;
let mockCore: ReportingCore;
beforeAll(async () => {
- mockConfig = getMockReportingConfig();
+ mockConfig = createMockConfig(createMockConfigSchema());
mockCore = await createMockReportingCore(mockConfig);
});
@@ -189,7 +185,7 @@ describe('data modeling', () => {
let mockConfig: ReportingConfig;
let mockCore: ReportingCore;
beforeAll(async () => {
- mockConfig = getMockReportingConfig();
+ mockConfig = createMockConfig(createMockConfigSchema());
mockCore = await createMockReportingCore(mockConfig);
});
test('with normal looking usage data', async () => {
@@ -455,7 +451,7 @@ describe('data modeling', () => {
describe('Ready for collection observable', () => {
test('converts observable to promise', async () => {
- const mockConfig = getMockReportingConfig();
+ const mockConfig = createMockConfig(createMockConfigSchema());
const mockReporting = await createMockReportingCore(mockConfig);
const usageCollection = getMockUsageCollection();
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
index d466ebd69737e..8672a8b8f6849 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.test.js
@@ -6,21 +6,16 @@
import { registerRollupSearchStrategy } from './register_rollup_search_strategy';
describe('Register Rollup Search Strategy', () => {
- let routeDependencies;
let addSearchStrategy;
+ let getRollupService;
beforeEach(() => {
- routeDependencies = {
- router: jest.fn().mockName('router'),
- elasticsearchService: jest.fn().mockName('elasticsearchService'),
- elasticsearch: jest.fn().mockName('elasticsearch'),
- };
-
addSearchStrategy = jest.fn().mockName('addSearchStrategy');
+ getRollupService = jest.fn().mockName('getRollupService');
});
test('should run initialization', () => {
- registerRollupSearchStrategy(routeDependencies, addSearchStrategy);
+ registerRollupSearchStrategy(addSearchStrategy, getRollupService);
expect(addSearchStrategy).toHaveBeenCalled();
});
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
index 333863979ba95..22dafbb71d802 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/register_rollup_search_strategy.ts
@@ -4,27 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { ILegacyScopedClusterClient } from 'src/core/server';
import {
- AbstractSearchRequest,
DefaultSearchCapabilities,
AbstractSearchStrategy,
+ ReqFacade,
} from '../../../../../../src/plugins/vis_type_timeseries/server';
-import { CallWithRequestFactoryShim } from '../../types';
import { getRollupSearchStrategy } from './rollup_search_strategy';
-import { getRollupSearchRequest } from './rollup_search_request';
import { getRollupSearchCapabilities } from './rollup_search_capabilities';
export const registerRollupSearchStrategy = (
- callWithRequestFactory: CallWithRequestFactoryShim,
- addSearchStrategy: (searchStrategy: any) => void
+ addSearchStrategy: (searchStrategy: any) => void,
+ getRollupService: (reg: ReqFacade) => Promise
) => {
- const RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
const RollupSearchCapabilities = getRollupSearchCapabilities(DefaultSearchCapabilities);
const RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
- RollupSearchRequest,
RollupSearchCapabilities,
- callWithRequestFactory
+ getRollupService
);
addSearchStrategy(new RollupSearchStrategy());
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js
deleted file mode 100644
index 2ea0612140946..0000000000000
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.test.js
+++ /dev/null
@@ -1,53 +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 { getRollupSearchRequest } from './rollup_search_request';
-
-class AbstractSearchRequest {
- indexPattern = 'indexPattern';
- callWithRequest = jest.fn(({ body }) => Promise.resolve(body));
-}
-
-describe('Rollup search request', () => {
- let RollupSearchRequest;
-
- beforeEach(() => {
- RollupSearchRequest = getRollupSearchRequest(AbstractSearchRequest);
- });
-
- test('should create instance of RollupSearchRequest', () => {
- const rollupSearchRequest = new RollupSearchRequest();
-
- expect(rollupSearchRequest).toBeInstanceOf(AbstractSearchRequest);
- expect(rollupSearchRequest.search).toBeDefined();
- expect(rollupSearchRequest.callWithRequest).toBeDefined();
- });
-
- test('should send one request for single search', async () => {
- const rollupSearchRequest = new RollupSearchRequest();
- const searches = [{ body: 'body', index: 'index' }];
-
- await rollupSearchRequest.search(searches);
-
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledTimes(1);
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledWith('rollup.search', {
- body: 'body',
- index: 'index',
- rest_total_hits_as_int: true,
- });
- });
-
- test('should send multiple request for multi search', async () => {
- const rollupSearchRequest = new RollupSearchRequest();
- const searches = [
- { body: 'body', index: 'index' },
- { body: 'body1', index: 'index' },
- ];
-
- await rollupSearchRequest.search(searches);
-
- expect(rollupSearchRequest.callWithRequest).toHaveBeenCalledTimes(2);
- });
-});
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts
deleted file mode 100644
index 7e12d5286f34c..0000000000000
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_request.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-const SEARCH_METHOD = 'rollup.search';
-
-interface Search {
- index: string;
- body: {
- [key: string]: any;
- };
-}
-
-export const getRollupSearchRequest = (AbstractSearchRequest: any) =>
- class RollupSearchRequest extends AbstractSearchRequest {
- async search(searches: Search[]) {
- const requests = searches.map(({ body, index }) =>
- this.callWithRequest(SEARCH_METHOD, {
- body,
- index,
- rest_total_hits_as_int: true,
- })
- );
-
- return await Promise.all(requests);
- }
- };
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
index 63f4628e36bfe..f3da7ed3fdd17 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.test.js
@@ -7,13 +7,32 @@ import { getRollupSearchStrategy } from './rollup_search_strategy';
describe('Rollup Search Strategy', () => {
let RollupSearchStrategy;
- let RollupSearchRequest;
let RollupSearchCapabilities;
let callWithRequest;
let rollupResolvedData;
- const server = 'server';
- const request = 'request';
+ const request = {
+ requestContext: {
+ core: {
+ elasticsearch: {
+ client: {
+ asCurrentUser: {
+ rollup: {
+ getRollupIndexCaps: jest.fn().mockImplementation(() => rollupResolvedData),
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ const getRollupService = jest.fn().mockImplementation(() => {
+ return {
+ callAsCurrentUser: async () => {
+ return rollupResolvedData;
+ },
+ };
+ });
const indexPattern = 'indexPattern';
beforeEach(() => {
@@ -33,19 +52,17 @@ describe('Rollup Search Strategy', () => {
}
}
- RollupSearchRequest = jest.fn();
RollupSearchCapabilities = jest.fn(() => 'capabilities');
- callWithRequest = jest.fn().mockImplementation(() => rollupResolvedData);
RollupSearchStrategy = getRollupSearchStrategy(
AbstractSearchStrategy,
- RollupSearchRequest,
- RollupSearchCapabilities
+ RollupSearchCapabilities,
+ getRollupService
);
});
test('should create instance of RollupSearchRequest', () => {
- const rollupSearchStrategy = new RollupSearchStrategy(server);
+ const rollupSearchStrategy = new RollupSearchStrategy();
expect(rollupSearchStrategy.name).toBe('rollup');
});
@@ -55,7 +72,7 @@ describe('Rollup Search Strategy', () => {
const rollupIndex = 'rollupIndex';
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
rollupSearchStrategy.getRollupData = jest.fn(() => ({
[rollupIndex]: {
rollup_jobs: [
@@ -104,7 +121,7 @@ describe('Rollup Search Strategy', () => {
let rollupSearchStrategy;
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
});
test('should return rollup data', async () => {
@@ -112,10 +129,7 @@ describe('Rollup Search Strategy', () => {
const rollupData = await rollupSearchStrategy.getRollupData(request, indexPattern);
- expect(callWithRequest).toHaveBeenCalledWith('rollup.rollupIndexCapabilities', {
- indexPattern,
- });
- expect(rollupSearchStrategy.getCallWithRequestInstance).toHaveBeenCalledWith(request);
+ expect(getRollupService).toHaveBeenCalled();
expect(rollupData).toBe('data');
});
@@ -135,7 +149,7 @@ describe('Rollup Search Strategy', () => {
const rollupIndex = 'rollupIndex';
beforeEach(() => {
- rollupSearchStrategy = new RollupSearchStrategy(server);
+ rollupSearchStrategy = new RollupSearchStrategy();
fieldsCapabilities = {
[rollupIndex]: {
aggs: {
diff --git a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
index 885836780f1a9..e7794caf8697b 100644
--- a/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
+++ b/x-pack/plugins/rollup/server/lib/search_strategies/rollup_search_strategy.ts
@@ -4,15 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { keyBy, isString } from 'lodash';
-import { KibanaRequest } from 'src/core/server';
-
-import { CallWithRequestFactoryShim } from '../../types';
+import { ILegacyScopedClusterClient } from 'src/core/server';
+import { ReqFacade } from '../../../../../../src/plugins/vis_type_timeseries/server';
+import { ENHANCED_ES_SEARCH_STRATEGY } from '../../../../data_enhanced/server';
import { mergeCapabilitiesWithFields } from '../merge_capabilities_with_fields';
import { getCapabilitiesForRollupIndices } from '../map_capabilities';
-const ROLLUP_INDEX_CAPABILITIES_METHOD = 'rollup.rollupIndexCapabilities';
-
-const getRollupIndices = (rollupData: { [key: string]: any[] }) => Object.keys(rollupData);
+const getRollupIndices = (rollupData: { [key: string]: any }) => Object.keys(rollupData);
const isIndexPatternContainsWildcard = (indexPattern: string) => indexPattern.includes('*');
const isIndexPatternValid = (indexPattern: string) =>
@@ -20,28 +18,40 @@ const isIndexPatternValid = (indexPattern: string) =>
export const getRollupSearchStrategy = (
AbstractSearchStrategy: any,
- RollupSearchRequest: any,
RollupSearchCapabilities: any,
- callWithRequestFactory: CallWithRequestFactoryShim
+ getRollupService: (reg: ReqFacade) => Promise
) =>
class RollupSearchStrategy extends AbstractSearchStrategy {
name = 'rollup';
constructor() {
- // TODO: When vis_type_timeseries and AbstractSearchStrategy are migrated to the NP, it
- // shouldn't require elasticsearchService to be injected, and we can remove this null argument.
- super(null, callWithRequestFactory, RollupSearchRequest);
+ super(ENHANCED_ES_SEARCH_STRATEGY, 'rollup', { rest_total_hits_as_int: true });
}
- getRollupData(req: KibanaRequest, indexPattern: string) {
- const callWithRequest = this.getCallWithRequestInstance(req);
+ async search(req: ReqFacade, bodies: any[], options = {}) {
+ const rollupService = await getRollupService(req);
+ const requests: any[] = [];
+ bodies.forEach((body) => {
+ requests.push(
+ rollupService.callAsCurrentUser('rollup.search', {
+ ...body,
+ rest_total_hits_as_int: true,
+ })
+ );
+ });
+ return Promise.all(requests);
+ }
- return callWithRequest(ROLLUP_INDEX_CAPABILITIES_METHOD, {
- indexPattern,
- }).catch(() => Promise.resolve({}));
+ async getRollupData(req: ReqFacade, indexPattern: string) {
+ const rollupService = await getRollupService(req);
+ return rollupService
+ .callAsCurrentUser('rollup.rollupIndexCapabilities', {
+ indexPattern,
+ })
+ .catch(() => Promise.resolve({}));
}
- async checkForViability(req: KibanaRequest, indexPattern: string) {
+ async checkForViability(req: ReqFacade, indexPattern: string) {
let isViable = false;
let capabilities = null;
@@ -66,7 +76,7 @@ export const getRollupSearchStrategy = (
}
async getFieldsForWildcard(
- req: KibanaRequest,
+ req: ReqFacade,
indexPattern: string,
{
fieldsCapabilities,
diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts
index 8b3a6355f950d..fe193150fc1ca 100644
--- a/x-pack/plugins/rollup/server/plugin.ts
+++ b/x-pack/plugins/rollup/server/plugin.ts
@@ -17,17 +17,16 @@ import {
ILegacyCustomClusterClient,
Plugin,
Logger,
- KibanaRequest,
PluginInitializerContext,
ILegacyScopedClusterClient,
- LegacyAPICaller,
SharedGlobalConfig,
} from 'src/core/server';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
+import { ReqFacade } from '../../../../src/plugins/vis_type_timeseries/server';
import { PLUGIN, CONFIG_ROLLUPS } from '../common';
-import { Dependencies, CallWithRequestFactoryShim } from './types';
+import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { registerRollupUsageCollector } from './collectors';
@@ -132,19 +131,12 @@ export class RollupPlugin implements Plugin {
});
if (visTypeTimeseries) {
- // TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
- const callWithRequestFactoryShim = (
- elasticsearchServiceShim: CallWithRequestFactoryShim,
- request: KibanaRequest
- ): LegacyAPICaller => {
- return async (...args: Parameters) => {
- this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
- return await this.rollupEsClient.asScoped(request).callAsCurrentUser(...args);
- };
+ const getRollupService = async (request: ReqFacade) => {
+ this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices));
+ return this.rollupEsClient.asScoped(request);
};
-
const { addSearchStrategy } = visTypeTimeseries;
- registerRollupSearchStrategy(callWithRequestFactoryShim, addSearchStrategy);
+ registerRollupSearchStrategy(addSearchStrategy, getRollupService);
}
if (usageCollection) {
diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts
index 290d2df050099..b167806cf8d5d 100644
--- a/x-pack/plugins/rollup/server/types.ts
+++ b/x-pack/plugins/rollup/server/types.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { IRouter, LegacyAPICaller, KibanaRequest } from 'src/core/server';
+import { IRouter } from 'src/core/server';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server';
@@ -39,9 +39,3 @@ export interface RouteDependencies {
IndexPatternsFetcher: typeof IndexPatternsFetcher;
};
}
-
-// TODO: When vis_type_timeseries is fully migrated to the NP, it shouldn't require this shim.
-export type CallWithRequestFactoryShim = (
- elasticsearchServiceShim: CallWithRequestFactoryShim,
- request: KibanaRequest
-) => LegacyAPICaller;
diff --git a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
index 6194d6892d799..a45b1fd18a4b6 100644
--- a/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/cases.spec.ts
@@ -24,17 +24,16 @@ import {
ALL_CASES_TAGS_COUNT,
} from '../screens/all_cases';
import {
- ACTION,
CASE_DETAILS_DESCRIPTION,
CASE_DETAILS_PAGE_TITLE,
CASE_DETAILS_PUSH_TO_EXTERNAL_SERVICE_BTN,
CASE_DETAILS_STATUS,
CASE_DETAILS_TAGS,
- CASE_DETAILS_USER_ACTION,
+ CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME,
+ CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT,
CASE_DETAILS_USERNAMES,
PARTICIPANTS,
REPORTER,
- USER,
} from '../screens/case_details';
import { TIMELINE_DESCRIPTION, TIMELINE_QUERY, TIMELINE_TITLE } from '../screens/timeline';
@@ -84,8 +83,8 @@ describe('Cases', () => {
const expectedTags = case1.tags.join('');
cy.get(CASE_DETAILS_PAGE_TITLE).should('have.text', case1.name);
cy.get(CASE_DETAILS_STATUS).should('have.text', 'open');
- cy.get(CASE_DETAILS_USER_ACTION).eq(USER).should('have.text', case1.reporter);
- cy.get(CASE_DETAILS_USER_ACTION).eq(ACTION).should('have.text', 'added description');
+ cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME).should('have.text', case1.reporter);
+ cy.get(CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT).should('have.text', 'added description');
cy.get(CASE_DETAILS_DESCRIPTION).should(
'have.text',
`${case1.description} ${case1.timeline.title}`
diff --git a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
index 6af4d174b9583..3862a89a7d833 100644
--- a/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
+++ b/x-pack/plugins/security_solution/cypress/integration/timeline_attach_to_case.spec.ts
@@ -11,7 +11,7 @@ import {
addNewCase,
selectCase,
} from '../tasks/timeline';
-import { DESCRIPTION_INPUT } from '../screens/create_new_case';
+import { DESCRIPTION_INPUT, ADD_COMMENT_INPUT } from '../screens/create_new_case';
import { esArchiverLoad, esArchiverUnload } from '../tasks/es_archiver';
import { caseTimeline, TIMELINE_CASE_ID } from '../objects/case';
@@ -34,7 +34,7 @@ describe('attach timeline to case', () => {
cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
@@ -46,7 +46,7 @@ describe('attach timeline to case', () => {
cy.location('origin').then((origin) => {
cy.get(DESCRIPTION_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
@@ -66,9 +66,9 @@ describe('attach timeline to case', () => {
selectCase(TIMELINE_CASE_ID);
cy.location('origin').then((origin) => {
- cy.get(DESCRIPTION_INPUT).should(
+ cy.get(ADD_COMMENT_INPUT).should(
'have.text',
- `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:'${caseTimeline.id}',isOpen:!t))`
+ `[${caseTimeline.title}](${origin}/app/security/timelines?timeline=(id:%27${caseTimeline.id}%27,isOpen:!t))`
);
});
});
diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts
index f2cdaa6994356..7b995f5395543 100644
--- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts
@@ -4,9 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export const ACTION = 2;
-
-export const CASE_DETAILS_DESCRIPTION = '[data-test-subj="markdown-root"]';
+export const CASE_DETAILS_DESCRIPTION =
+ '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]';
export const CASE_DETAILS_PAGE_TITLE = '[data-test-subj="header-page-title"]';
@@ -17,14 +16,17 @@ export const CASE_DETAILS_STATUS = '[data-test-subj="case-view-status"]';
export const CASE_DETAILS_TAGS = '[data-test-subj="case-tags"]';
-export const CASE_DETAILS_TIMELINE_LINK_MARKDOWN = '[data-test-subj="markdown-timeline-link"]';
+export const CASE_DETAILS_TIMELINE_LINK_MARKDOWN =
+ '[data-test-subj="description-action"] [data-test-subj="user-action-markdown"] button';
+
+export const CASE_DETAILS_USER_ACTION_DESCRIPTION_EVENT =
+ '[data-test-subj="description-action"] .euiCommentEvent__headerEvent';
-export const CASE_DETAILS_USER_ACTION = '[data-test-subj="user-action-title"] .euiFlexItem';
+export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME =
+ '[data-test-subj="description-action"] .euiCommentEvent__headerUsername';
export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]';
export const PARTICIPANTS = 1;
export const REPORTER = 0;
-
-export const USER = 1;
diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
index 9431c054d96a4..4f348b4dcdbd1 100644
--- a/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
+++ b/x-pack/plugins/security_solution/cypress/screens/create_new_case.ts
@@ -4,11 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
+export const ADD_COMMENT_INPUT = '[data-test-subj="add-comment"] textarea';
+
export const BACK_TO_CASES_BTN = '[data-test-subj="backToCases"]';
-export const DESCRIPTION_INPUT = '[data-test-subj="textAreaInput"]';
+export const DESCRIPTION_INPUT = '[data-test-subj="caseDescription"] textarea';
-export const INSERT_TIMELINE_BTN = '[data-test-subj="insert-timeline-button"]';
+export const INSERT_TIMELINE_BTN = '.euiMarkdownEditorToolbar [aria-label="Insert timeline link"]';
export const LOADING_SPINNER = '[data-test-subj="create-case-loading-spinner"]';
diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
index 1d5d240c5c53d..f5013eed07d29 100644
--- a/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
+++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_case.ts
@@ -13,7 +13,6 @@ import {
INSERT_TIMELINE_BTN,
LOADING_SPINNER,
TAGS_INPUT,
- TIMELINE,
TIMELINE_SEARCHBOX,
TITLE_INPUT,
} from '../screens/create_new_case';
@@ -43,9 +42,6 @@ export const createNewCaseWithTimeline = (newCase: TestCase) => {
cy.get(INSERT_TIMELINE_BTN).click({ force: true });
cy.get(TIMELINE_SEARCHBOX).type(`${newCase.timeline.title}{enter}`);
- cy.get(TIMELINE).should('be.visible');
- cy.wait(300);
- cy.get(TIMELINE).eq(0).click({ force: true });
cy.get(SUBMIT_BTN).click({ force: true });
cy.get(LOADING_SPINNER).should('exist');
diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
index ef13c87a92dbb..14c42697dcbb4 100644
--- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.tsx
@@ -11,14 +11,14 @@ import styled from 'styled-components';
import { CommentRequest } from '../../../../../case/common/api';
import { usePostComment } from '../../containers/use_post_comment';
import { Case } from '../../containers/types';
-import { MarkdownEditorForm } from '../../../common/components/markdown_editor/form';
+import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import { Form, useForm, UseField, useFormData } from '../../../shared_imports';
import * as i18n from './translations';
import { schema } from './schema';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
const MySpinner = styled(EuiLoadingSpinner)`
position: absolute;
diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
index e1d7d98ba8c51..246df1c94b817 100644
--- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx
@@ -114,34 +114,41 @@ describe('CaseView ', () => {
expect(wrapper.find(`[data-test-subj="case-view-title"]`).first().prop('title')).toEqual(
data.title
);
+
expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual(
data.status
);
+
expect(
wrapper
- .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-coke"]`)
+ .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="tag-coke"]`)
.first()
.text()
).toEqual(data.tags[0]);
+
expect(
wrapper
- .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag-pepsi"]`)
+ .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="tag-pepsi"]`)
.first()
.text()
).toEqual(data.tags[1]);
+
expect(wrapper.find(`[data-test-subj="case-view-username"]`).first().text()).toEqual(
data.createdBy.username
);
+
expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false);
+
expect(wrapper.find(`[data-test-subj="case-view-createdAt"]`).first().prop('value')).toEqual(
data.createdAt
);
+
expect(
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`)
.first()
- .prop('raw')
- ).toEqual(data.description);
+ .text()
+ ).toBe(data.description);
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
index 3c3cc95218b03..a8babe729fde0 100644
--- a/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/create/index.tsx
@@ -31,10 +31,10 @@ import { schema } from './schema';
import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
import * as i18n from '../../translations';
-import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
+import { MarkdownEditorForm } from '../../../common/components/markdown_editor/eui_form';
import { useGetTags } from '../../containers/use_get_tags';
import { getCaseDetailsUrl } from '../../../common/components/link_to';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import { useTimelineClick } from '../../../common/utils/timeline/use_timeline_click';
export const CommonUseField = getUseField({ component: Field });
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
index 7c3fcde687033..a60167a18762f 100644
--- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx
@@ -58,6 +58,7 @@ describe('TagList ', () => {
fetchTags,
}));
});
+
it('Renders no tags, and then edit', () => {
const wrapper = mount(
@@ -69,6 +70,7 @@ describe('TagList ', () => {
expect(wrapper.find(`[data-test-subj="no-tags"]`).last().exists()).toBeFalsy();
expect(wrapper.find(`[data-test-subj="edit-tags"]`).last().exists()).toBeTruthy();
});
+
it('Edit tag on submit', async () => {
const wrapper = mount(
@@ -81,6 +83,7 @@ describe('TagList ', () => {
await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags));
});
});
+
it('Tag options render with new tags added', () => {
const wrapper = mount(
@@ -92,6 +95,7 @@ describe('TagList ', () => {
wrapper.find(`[data-test-subj="caseTags"] [data-test-subj="input"]`).first().prop('options')
).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]);
});
+
it('Cancels on cancel', async () => {
const props = {
...defaultProps,
@@ -102,17 +106,19 @@ describe('TagList ', () => {
);
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy();
+
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click');
await act(async () => {
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeFalsy();
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeFalsy();
wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click');
await waitFor(() => {
wrapper.update();
- expect(wrapper.find(`[data-test-subj="case-tag-pepsi"]`).last().exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="tag-pepsi"]`).last().exists()).toBeTruthy();
});
});
});
+
it('Renders disabled button', () => {
const props = { ...defaultProps, disabled: true };
const wrapper = mount(
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
index eeb7c49eceab5..4af781e3c31f4 100644
--- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.tsx
@@ -10,8 +10,6 @@ import {
EuiHorizontalRule,
EuiFlexGroup,
EuiFlexItem,
- EuiBadgeGroup,
- EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
@@ -25,6 +23,8 @@ import { schema } from './schema';
import { CommonUseField } from '../create';
import { useGetTags } from '../../containers/use_get_tags';
+import { Tags } from './tags';
+
interface TagListProps {
disabled?: boolean;
isLoading: boolean;
@@ -99,15 +99,7 @@ export const TagList = React.memo(
{tags.length === 0 && !isEditTags && {i18n.NO_TAGS}
}
-
- {tags.length > 0 &&
- !isEditTags &&
- tags.map((tag) => (
-
- {tag}
-
- ))}
-
+ {!isEditTags && }
{isEditTags && (
diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx
new file mode 100644
index 0000000000000..e257563ce751e
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/tags.tsx
@@ -0,0 +1,32 @@
+/*
+ * 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, { memo } from 'react';
+import { EuiBadgeGroup, EuiBadge, EuiBadgeGroupProps } from '@elastic/eui';
+
+interface TagsProps {
+ tags: string[];
+ color?: string;
+ gutterSize?: EuiBadgeGroupProps['gutterSize'];
+}
+
+const TagsComponent: React.FC = ({ tags, color = 'default', gutterSize }) => {
+ return (
+ <>
+ {tags.length > 0 && (
+
+ {tags.map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export const Tags = memo(TagsComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
index b5be84db59920..4e5c05f2f1404 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.test.tsx
@@ -14,7 +14,7 @@ import { connectorsMock } from '../../containers/configure/mock';
describe('User action tree helpers', () => {
const connectors = connectorsMock;
it('label title generated for update tags', () => {
- const action = getUserAction(['title'], 'update');
+ const action = getUserAction(['tags'], 'update');
const result: string | JSX.Element = getLabelTitle({
action,
connectors,
@@ -27,8 +27,11 @@ describe('User action tree helpers', () => {
` ${i18n.TAGS.toLowerCase()}`
);
- expect(wrapper.find(`[data-test-subj="ua-tag"]`).first().text()).toEqual(action.newValue);
+ expect(wrapper.find(`[data-test-subj="tag-${action.newValue}"]`).first().text()).toEqual(
+ action.newValue
+ );
});
+
it('label title generated for update title', () => {
const action = getUserAction(['title'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -44,6 +47,7 @@ describe('User action tree helpers', () => {
}"`
);
});
+
it('label title generated for update description', () => {
const action = getUserAction(['description'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -55,6 +59,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`);
});
+
it('label title generated for update status to open', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: 'open' };
const result: string | JSX.Element = getLabelTitle({
@@ -66,6 +71,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`);
});
+
it('label title generated for update status to closed', () => {
const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' };
const result: string | JSX.Element = getLabelTitle({
@@ -77,6 +83,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`);
});
+
it('label title generated for update comment', () => {
const action = getUserAction(['comment'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -88,6 +95,7 @@ describe('User action tree helpers', () => {
expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`);
});
+
it('label title generated for pushed incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const result: string | JSX.Element = getLabelTitle({
@@ -105,6 +113,7 @@ describe('User action tree helpers', () => {
JSON.parse(action.newValue).external_url
);
});
+
it('label title generated for needs update incident', () => {
const action = getUserAction(['pushed'], 'push-to-service');
const result: string | JSX.Element = getLabelTitle({
@@ -122,6 +131,7 @@ describe('User action tree helpers', () => {
JSON.parse(action.newValue).external_url
);
});
+
it('label title generated for update connector', () => {
const action = getUserAction(['connector_id'], 'update');
const result: string | JSX.Element = getLabelTitle({
@@ -136,6 +146,8 @@ describe('User action tree helpers', () => {
` ${i18n.TAGS.toLowerCase()}`
);
- expect(wrapper.find(`[data-test-subj="ua-tag"]`).first().text()).toEqual(action.newValue);
+ expect(wrapper.find(`[data-test-subj="tag-${action.newValue}"]`).first().text()).toEqual(
+ action.newValue
+ );
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
index e343c3da6cc8b..4d8bb9ba078e5 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/helpers.tsx
@@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiBadgeGroup, EuiBadge, EuiLink } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
import React from 'react';
import { CaseFullExternalService, Connector } from '../../../../../case/common/api';
import { CaseUserActions } from '../../containers/types';
+import { CaseServices } from '../../containers/use_get_case_user_actions';
import * as i18n from '../case_view/translations';
+import { Tags } from '../tag_list/tags';
interface LabelTitle {
action: CaseUserActions;
@@ -44,22 +46,21 @@ export const getLabelTitle = ({ action, connectors, field, firstPush }: LabelTit
return '';
};
-const getTagsLabelTitle = (action: CaseUserActions) => (
-
-
- {action.action === 'add' && i18n.ADDED_FIELD}
- {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
-
-
- {action.newValue != null &&
- action.newValue.split(',').map((tag) => (
-
- {tag}
-
- ))}
-
-
-);
+const getTagsLabelTitle = (action: CaseUserActions) => {
+ const tags = action.newValue != null ? action.newValue.split(',') : [];
+
+ return (
+
+
+ {action.action === 'add' && i18n.ADDED_FIELD}
+ {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()}
+
+
+
+
+
+ );
+};
const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => {
const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService;
@@ -78,3 +79,20 @@ const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean)
);
};
+
+export const getPushInfo = (
+ caseServices: CaseServices,
+ parsedValue: { connector_id: string; connector_name: string },
+ index: number
+) =>
+ parsedValue != null
+ ? {
+ firstPush: caseServices[parsedValue.connector_id].firstPushIndex === index,
+ parsedConnectorId: parsedValue.connector_id,
+ parsedConnectorName: parsedValue.connector_name,
+ }
+ : {
+ firstPush: false,
+ parsedConnectorId: 'none',
+ parsedConnectorName: 'none',
+ };
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
index d67c364bbda10..d2bb2fb243458 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx
@@ -6,6 +6,9 @@
import React from 'react';
import { mount } from 'enzyme';
+// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
+import { wait as waitFor } from '@testing-library/react';
+import { act } from 'react-dom/test-utils';
import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router';
import { getFormMock, useFormMock, useFormDataMock } from '../__mock__/form';
@@ -13,9 +16,6 @@ import { useUpdateComment } from '../../containers/use_update_comment';
import { basicCase, basicPush, getUserAction } from '../../containers/mock';
import { UserActionTree } from '.';
import { TestProviders } from '../../../common/mock';
-// we don't have the types for waitFor just yet, so using "as waitFor" until when we do
-import { wait as waitFor } from '@testing-library/react';
-import { act } from 'react-dom/test-utils';
const fetchUserActions = jest.fn();
const onUpdateField = jest.fn();
@@ -66,9 +66,10 @@ describe('UserActionTree ', () => {
expect(wrapper.find(`[data-test-subj="user-action-avatar"]`).first().prop('name')).toEqual(
defaultProps.data.createdBy.fullName
);
- expect(wrapper.find(`[data-test-subj="user-action-title"] strong`).first().text()).toEqual(
- defaultProps.data.createdBy.username
- );
+
+ expect(
+ wrapper.find(`[data-test-subj="description-action"] figcaption strong`).first().text()
+ ).toEqual(defaultProps.data.createdBy.username);
});
it('Renders service now update line with top and bottom when push is required', async () => {
@@ -76,6 +77,7 @@ describe('UserActionTree ', () => {
getUserAction(['pushed'], 'push-to-service'),
getUserAction(['comment'], 'update'),
];
+
const props = {
...defaultProps,
caseServices: {
@@ -90,20 +92,18 @@ describe('UserActionTree ', () => {
caseUserActions: ourActions,
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeTruthy();
});
-
- expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy();
- expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy();
});
it('Renders service now update line with top only when push is up to date', async () => {
@@ -122,20 +122,17 @@ describe('UserActionTree ', () => {
},
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
+ const wrapper = mount(
+
+
+
+
+
+ );
+ expect(wrapper.find(`[data-test-subj="top-footer"]`).exists()).toBeTruthy();
+ expect(wrapper.find(`[data-test-subj="bottom-footer"]`).exists()).toBeFalsy();
});
-
- expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy();
- expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy();
});
it('Outlines comment when update move to link is clicked', async () => {
@@ -145,89 +142,104 @@ describe('UserActionTree ', () => {
caseUserActions: ourActions,
};
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
- wrapper.update();
- });
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeFalsy();
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual('');
- wrapper
- .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`)
- .first()
- .simulate('click');
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual(ourActions[0].commentId);
+ wrapper
+ .find(
+ `[data-test-subj="comment-update-action-${ourActions[1].actionId}"] [data-test-subj="move-to-link-${props.data.comments[0].id}"]`
+ )
+ .first()
+ .simulate('click');
+
+ await waitFor(() => {
+ wrapper.update();
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${props.data.comments[0].id}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeTruthy();
+ });
+ });
});
it('Switches to markdown when edit is clicked and back to panel when canceled', async () => {
- const ourActions = [getUserAction(['comment'], 'create')];
- const props = {
- ...defaultProps,
- caseUserActions: ourActions,
- };
-
- const wrapper = mount(
-
-
-
-
-
- );
-
- await act(async () => {
- wrapper.update();
- });
+ await waitFor(() => {
+ const ourActions = [getUserAction(['comment'], 'create')];
+ const props = {
+ ...defaultProps,
+ caseUserActions: ourActions,
+ };
+
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(false);
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-ellipses"]`
)
- .exists()
- ).toEqual(false);
+ .first()
+ .simulate('click');
- wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`)
- .first()
- .simulate('click');
-
- wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`)
- .first()
- .simulate('click');
+ wrapper.update();
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-pencil"]`
)
- .exists()
- ).toEqual(true);
+ .first()
+ .simulate('click');
- wrapper
- .find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]`
- )
- .first()
- .simulate('click');
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(true);
- expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]`
)
- .exists()
- ).toEqual(false);
+ .first()
+ .simulate('click');
+
+ expect(
+ wrapper
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ )
+ .exists()
+ ).toEqual(false);
+ });
});
it('calls update comment when comment markdown is saved', async () => {
@@ -236,6 +248,7 @@ describe('UserActionTree ', () => {
...defaultProps,
caseUserActions: ourActions,
};
+
const wrapper = mount(
@@ -243,27 +256,35 @@ describe('UserActionTree ', () => {
);
+
wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`)
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-ellipses"]`
+ )
.first()
.simulate('click');
+
wrapper
- .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`)
+ .find(
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="property-actions-pencil"]`
+ )
.first()
.simulate('click');
+
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]`
)
.first()
.simulate('click');
+
await act(async () => {
await waitFor(() => {
wrapper.update();
expect(
wrapper
.find(
- `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
+ `[data-test-subj="comment-create-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]`
)
.exists()
).toEqual(false);
@@ -288,93 +309,101 @@ describe('UserActionTree ', () => {
);
+
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`)
.first()
.simulate('click');
+
wrapper
.find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`)
.first()
.simulate('click');
- wrapper
- .find(
- `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]`
- )
- .first()
- .simulate('click');
+
await act(async () => {
- await waitFor(() => {
- expect(
- wrapper
- .find(
- `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]`
- )
- .exists()
- ).toEqual(false);
- expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content });
- });
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="user-action-save-markdown"]`)
+ .first()
+ .simulate('click');
});
+
+ wrapper.update();
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown-form"]`)
+ .exists()
+ ).toEqual(false);
+
+ expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content });
});
it('quotes', async () => {
- const commentData = {
- comment: '',
- };
- const formHookMock = getFormMock(commentData);
- const setFieldValue = jest.fn();
- useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } }));
- const props = defaultProps;
- const wrapper = mount(
-
-
-
-
-
- );
-
await act(async () => {
+ const commentData = {
+ comment: '',
+ };
+ const setFieldValue = jest.fn();
+
+ const formHookMock = getFormMock(commentData);
+ useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } }));
+
+ const props = defaultProps;
+ const wrapper = mount(
+
+
+
+
+
+ );
+
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`)
+ .first()
+ .simulate('click');
+
await waitFor(() => {
- wrapper
- .find(
- `[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`
- )
- .first()
- .simulate('click');
wrapper.update();
});
- });
- wrapper
- .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`)
- .first()
- .simulate('click');
+ wrapper
+ .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`)
+ .first()
+ .simulate('click');
- expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`);
+ expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`);
+ });
});
it('Outlines comment when url param is provided', async () => {
- const commentId = 'neat-comment-id';
- const ourActions = [getUserAction(['comment'], 'create')];
- const props = {
- ...defaultProps,
- caseUserActions: ourActions,
- };
-
+ const commentId = 'basic-comment-id';
jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId });
- const wrapper = mount(
-
-
-
-
-
- );
await act(async () => {
- wrapper.update();
- });
+ const ourActions = [getUserAction(['comment'], 'create')];
+ const props = {
+ ...defaultProps,
+ caseUserActions: ourActions,
+ };
+
+ const wrapper = mount(
+
+
+
+
+
+ );
- expect(
- wrapper.find(`[data-test-subj="comment-create-action"]`).first().prop('idToOutline')
- ).toEqual(commentId);
+ await waitFor(() => {
+ wrapper.update();
+ });
+
+ expect(
+ wrapper
+ .find(`[data-test-subj="comment-create-action-${commentId}"]`)
+ .first()
+ .hasClass('outlined')
+ ).toBeTruthy();
+ });
});
});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
index d1263ab13f41b..bada15294de09 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx
@@ -3,25 +3,38 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+import classNames from 'classnames';
-import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiCommentList,
+ EuiCommentProps,
+} from '@elastic/eui';
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
-import * as i18n from '../case_view/translations';
+import * as i18n from './translations';
import { Case, CaseUserActions } from '../../containers/types';
import { useUpdateComment } from '../../containers/use_update_comment';
import { useCurrentUser } from '../../../common/lib/kibana';
import { AddComment, AddCommentRefObject } from '../add_comment';
-import { getLabelTitle } from './helpers';
-import { UserActionItem } from './user_action_item';
-import { UserActionMarkdown } from './user_action_markdown';
import { Connector } from '../../../../../case/common/api/cases';
import { CaseServices } from '../../containers/use_get_case_user_actions';
import { parseString } from '../../containers/utils';
import { OnUpdateFields } from '../case_view';
+import { getLabelTitle, getPushInfo } from './helpers';
+import { UserActionAvatar } from './user_action_avatar';
+import { UserActionMarkdown } from './user_action_markdown';
+import { UserActionTimestamp } from './user_action_timestamp';
+import { UserActionCopyLink } from './user_action_copy_link';
+import { UserActionMoveToReference } from './user_action_move_to_reference';
+import { UserActionUsername } from './user_action_username';
+import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
+import { UserActionContentToolbar } from './user_action_content_toolbar';
export interface UserActionTreeProps {
caseServices: CaseServices;
@@ -40,6 +53,31 @@ const MyEuiFlexGroup = styled(EuiFlexGroup)`
margin-bottom: 8px;
`;
+const MyEuiCommentList = styled(EuiCommentList)`
+ ${({ theme }) => `
+ & .userAction__comment.outlined .euiCommentEvent {
+ outline: solid 5px ${theme.eui.euiColorVis1_behindText};
+ margin: 0.5em;
+ transition: 0.8s;
+ }
+
+ & .euiComment.isEdit {
+ & .euiCommentEvent {
+ border: none;
+ box-shadow: none;
+ }
+
+ & .euiCommentEvent__body {
+ padding: 0;
+ }
+
+ & .euiCommentEvent__header {
+ display: none;
+ }
+ }
+ `}
+`;
+
const DESCRIPTION_ID = 'description';
const NEW_ID = 'newComment';
@@ -86,8 +124,7 @@ export const UserActionTree = React.memo(
updateCase,
});
},
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [caseData, handleManageMarkdownEditId, patchComment, updateCase]
+ [caseData.id, fetchUserActions, patchComment, updateCase]
);
const handleOutlineComment = useCallback(
@@ -172,117 +209,246 @@ export const UserActionTree = React.memo(
}
}
}, [commentId, initLoading, isLoadingUserActions, isLoadingIds, handleOutlineComment]);
- return (
- <>
- {i18n.ADDED_DESCRIPTION}>}
- fullName={caseData.createdBy.fullName ?? caseData.createdBy.username ?? ''}
- markdown={MarkdownDescription}
- onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)}
- onQuote={handleManageQuote.bind(null, caseData.description)}
- username={caseData.createdBy.username ?? i18n.UNKNOWN}
- />
- {caseUserActions.map((action, index) => {
- if (action.commentId != null && action.action === 'create') {
- const comment = caseData.comments.find((c) => c.id === action.commentId);
- if (comment != null) {
- return (
-
}>
+
+
+ );
+};
+
+export const UserActionCopyLink = memo(UserActionCopyLinkComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx
deleted file mode 100644
index eeb728aa7d1df..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_item.tsx
+++ /dev/null
@@ -1,197 +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 {
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPanel,
- EuiHorizontalRule,
- EuiText,
-} from '@elastic/eui';
-import React from 'react';
-import styled, { css } from 'styled-components';
-
-import { UserActionAvatar } from './user_action_avatar';
-import { UserActionTitle } from './user_action_title';
-import * as i18n from './translations';
-
-interface UserActionItemProps {
- caseConnectorName?: string;
- createdAt: string;
- 'data-test-subj'?: string;
- disabled: boolean;
- id: string;
- isEditable: boolean;
- isLoading: boolean;
- labelEditAction?: string;
- labelQuoteAction?: string;
- labelTitle?: JSX.Element;
- linkId?: string | null;
- fullName?: string | null;
- markdown?: React.ReactNode;
- onEdit?: (id: string) => void;
- onQuote?: (id: string) => void;
- username: string;
- updatedAt?: string | null;
- outlineComment?: (id: string) => void;
- showBottomFooter?: boolean;
- showTopFooter?: boolean;
- idToOutline?: string | null;
-}
-
-export const UserActionItemContainer = styled(EuiFlexGroup)`
- ${({ theme }) => css`
- & {
- background-image: linear-gradient(
- to right,
- transparent 0,
- transparent 15px,
- ${theme.eui.euiBorderColor} 15px,
- ${theme.eui.euiBorderColor} 17px,
- transparent 17px,
- transparent 100%
- );
- background-repeat: no-repeat;
- background-position: left ${theme.eui.euiSizeXXL};
- margin-bottom: ${theme.eui.euiSizeS};
- }
- .userAction__panel {
- margin-bottom: ${theme.eui.euiSize};
- }
- .userAction__circle {
- flex-shrink: 0;
- margin-right: ${theme.eui.euiSize};
- vertical-align: top;
- }
- .userAction_loadingAvatar {
- position: relative;
- margin-right: ${theme.eui.euiSizeXL};
- top: ${theme.eui.euiSizeM};
- left: ${theme.eui.euiSizeS};
- }
- .userAction__title {
- padding: ${theme.eui.euiSizeS} ${theme.eui.euiSizeL};
- background: ${theme.eui.euiColorLightestShade};
- border-bottom: ${theme.eui.euiBorderThin};
- border-radius: ${theme.eui.euiBorderRadius} ${theme.eui.euiBorderRadius} 0 0;
- }
- .euiText--small * {
- margin-bottom: 0;
- }
- `}
-`;
-
-const MyEuiPanel = styled(EuiPanel)<{ showoutline: string }>`
- flex-grow: 0;
- ${({ theme, showoutline }) =>
- showoutline === 'true'
- ? `
- outline: solid 5px ${theme.eui.euiColorVis1_behindText};
- margin: 0.5em;
- transition: 0.8s;
- `
- : ''}
-`;
-
-const PushedContainer = styled(EuiFlexItem)`
- ${({ theme }) => `
- margin-top: ${theme.eui.euiSizeS};
- margin-bottom: ${theme.eui.euiSizeXL};
- hr {
- margin: 5px;
- height: ${theme.eui.euiBorderWidthThick};
- }
- `}
-`;
-
-const PushedInfoContainer = styled.div`
- margin-left: 48px;
-`;
-
-export const UserActionItem = ({
- caseConnectorName,
- createdAt,
- disabled,
- 'data-test-subj': dataTestSubj,
- id,
- idToOutline,
- isEditable,
- isLoading,
- labelEditAction,
- labelQuoteAction,
- labelTitle,
- linkId,
- fullName,
- markdown,
- onEdit,
- onQuote,
- outlineComment,
- showBottomFooter,
- showTopFooter,
- username,
- updatedAt,
-}: UserActionItemProps) => (
-
-
-
-
- {(fullName && fullName.length > 0) || (username && username.length > 0) ? (
- 0 ? fullName : username ?? ''} />
- ) : (
-
- )}
-
-
- {isEditable && markdown}
- {!isEditable && (
-
- >}
- linkId={linkId}
- onEdit={onEdit}
- onQuote={onQuote}
- outlineComment={outlineComment}
- updatedAt={updatedAt}
- username={username}
- />
- {markdown}
-
- )}
-
-
-
- {showTopFooter && (
-
-
-
- {i18n.ALREADY_PUSHED_TO_SERVICE(`${caseConnectorName}`)}
-
-
-
- {showBottomFooter && (
-
-
- {i18n.REQUIRED_UPDATE_TO_SERVICE(`${caseConnectorName}`)}
-
-
- )}
-
- )}
-
-);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
index 6cf827ea55f1f..f1f7d40009045 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.test.tsx
@@ -17,8 +17,9 @@ const onChangeEditable = jest.fn();
const onSaveContent = jest.fn();
const timelineId = '1e10f150-949b-11ea-b63c-2bc51864784c';
+const timelineMarkdown = `[timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`;
const defaultProps = {
- content: `A link to a timeline [timeline](http://localhost:5601/app/security/timelines?timeline=(id:'${timelineId}',isOpen:!t))`,
+ content: `A link to a timeline ${timelineMarkdown}`,
id: 'markdown-id',
isEditable: false,
onChangeEditable,
@@ -40,7 +41,11 @@ describe('UserActionMarkdown ', () => {
);
- wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
+
+ wrapper
+ .find(`[data-test-subj="markdown-timeline-link-${timelineId}"]`)
+ .first()
+ .simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
@@ -59,8 +64,19 @@ describe('UserActionMarkdown ', () => {
);
- wrapper.find(`[data-test-subj="preview-tab"]`).first().simulate('click');
- wrapper.find(`[data-test-subj="markdown-timeline-link"]`).first().simulate('click');
+
+ // Preview button of Markdown editor
+ wrapper
+ .find(
+ `[data-test-subj="user-action-markdown-form"] .euiMarkdownEditorToolbar .euiButtonEmpty`
+ )
+ .first()
+ .simulate('click');
+
+ wrapper
+ .find(`[data-test-subj="markdown-timeline-link-${timelineId}"]`)
+ .first()
+ .simulate('click');
expect(queryTimelineByIdSpy).toBeCalledWith({
apolloClient: mockUseApolloClient(),
graphEventId: '',
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
index ac2ad179ec60c..45e46b2d7d2db 100644
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_markdown.tsx
@@ -4,18 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiButton,
+ EuiMarkdownFormat,
+} from '@elastic/eui';
import React, { useCallback } from 'react';
import styled from 'styled-components';
import * as i18n from '../case_view/translations';
-import { Markdown } from '../../../common/components/markdown';
-import { Form, useForm, UseField, useFormData } from '../../../shared_imports';
+import { Form, useForm, UseField } from '../../../shared_imports';
import { schema, Content } from './schema';
-import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover';
-import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline';
-import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form';
-import { useTimelineClick } from '../utils/use_timeline_click';
+import {
+ MarkdownEditorForm,
+ parsingPlugins,
+ processingPlugins,
+} from '../../../common/components/markdown_editor/eui_form';
const ContentWrapper = styled.div`
padding: ${({ theme }) => `${theme.eui.euiSizeM} ${theme.eui.euiSizeL}`};
@@ -43,24 +49,12 @@ export const UserActionMarkdown = ({
});
const fieldName = 'content';
- const { submit, setFieldValue } = form;
- const [{ content: contentFormValue }] = useFormData({ form, watch: [fieldName] });
-
- const onContentChange = useCallback((newValue) => setFieldValue(fieldName, newValue), [
- setFieldValue,
- ]);
-
- const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline(
- contentFormValue,
- onContentChange
- );
+ const { submit } = form;
const handleCancelAction = useCallback(() => {
onChangeEditable(id);
}, [id, onChangeEditable]);
- const handleTimelineClick = useTimelineClick();
-
const handleSaveAction = useCallback(async () => {
const { isValid, data } = await submit();
if (isValid) {
@@ -105,29 +99,24 @@ export const UserActionMarkdown = ({
path={fieldName}
component={MarkdownEditorForm}
componentProps={{
+ 'aria-label': 'Cases markdown editor',
+ value: content,
+ id,
bottomRightContent: renderButtons({
cancelAction: handleCancelAction,
saveAction: handleSaveAction,
}),
- onClickTimeline: handleTimelineClick,
- onCursorPositionUpdate: handleCursorChange,
- topRightContent: (
-
- ),
}}
/>
) : (
-
-
+
+
+ {content}
+
);
};
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx
new file mode 100644
index 0000000000000..5bb0f50ce25e5
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.test.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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 { mount, ReactWrapper } from 'enzyme';
+import { UserActionMoveToReference } from './user_action_move_to_reference';
+
+const outlineComment = jest.fn();
+const props = {
+ id: 'move-to-ref-id',
+ outlineComment,
+};
+
+describe('UserActionMoveToReference ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find(`[data-test-subj="move-to-link-${props.id}"]`).first().exists()
+ ).toBeTruthy();
+ });
+
+ it('calls outlineComment correctly', async () => {
+ wrapper.find(`[data-test-subj="move-to-link-${props.id}"]`).first().simulate('click');
+ expect(outlineComment).toHaveBeenCalledWith(props.id);
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx
new file mode 100644
index 0000000000000..39d016dd69520
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_move_to_reference.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo, useCallback } from 'react';
+import { EuiToolTip, EuiButtonIcon } from '@elastic/eui';
+
+import * as i18n from './translations';
+
+interface UserActionMoveToReferenceProps {
+ id: string;
+ outlineComment: (id: string) => void;
+}
+
+const UserActionMoveToReferenceComponent = ({
+ id,
+ outlineComment,
+}: UserActionMoveToReferenceProps) => {
+ const handleMoveToLink = useCallback(() => {
+ outlineComment(id);
+ }, [id, outlineComment]);
+
+ return (
+ {i18n.MOVE_TO_ORIGINAL_COMMENT}}>
+
+
+ );
+};
+
+export const UserActionMoveToReference = memo(UserActionMoveToReferenceComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.tsx
new file mode 100644
index 0000000000000..bd5da8aca7d4f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.test.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 from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionPropertyActions } from './user_action_property_actions';
+
+const props = {
+ id: 'property-actions-id',
+ editLabel: 'edit',
+ quoteLabel: 'quote',
+ disabled: false,
+ isLoading: false,
+ onEdit: jest.fn(),
+ onQuote: jest.fn(),
+};
+
+describe('UserActionPropertyActions ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
+ ).toBeFalsy();
+
+ expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeTruthy();
+ });
+
+ it('it shows the edit and quote buttons', async () => {
+ wrapper.find('[data-test-subj="property-actions-ellipses"]').first().simulate('click');
+ wrapper.find('[data-test-subj="property-actions-pencil"]').exists();
+ wrapper.find('[data-test-subj="property-actions-quote"]').exists();
+ });
+
+ it('it shows the spinner when loading', async () => {
+ wrapper = mount();
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-loading"]').first().exists()
+ ).toBeTruthy();
+
+ expect(wrapper.find('[data-test-subj="property-actions"]').first().exists()).toBeFalsy();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx
new file mode 100644
index 0000000000000..454880e93a27f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_property_actions.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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, { memo, useMemo, useCallback } from 'react';
+import { EuiLoadingSpinner } from '@elastic/eui';
+
+import { PropertyActions } from '../property_actions';
+
+interface UserActionPropertyActionsProps {
+ id: string;
+ editLabel: string;
+ quoteLabel: string;
+ disabled: boolean;
+ isLoading: boolean;
+ onEdit: (id: string) => void;
+ onQuote: (id: string) => void;
+}
+
+const UserActionPropertyActionsComponent = ({
+ id,
+ editLabel,
+ quoteLabel,
+ disabled,
+ isLoading,
+ onEdit,
+ onQuote,
+}: UserActionPropertyActionsProps) => {
+ const onEditClick = useCallback(() => onEdit(id), [id, onEdit]);
+ const onQuoteClick = useCallback(() => onQuote(id), [id, onQuote]);
+
+ const propertyActions = useMemo(() => {
+ return [
+ {
+ disabled,
+ iconType: 'pencil',
+ label: editLabel,
+ onClick: onEditClick,
+ },
+ {
+ disabled,
+ iconType: 'quote',
+ label: quoteLabel,
+ onClick: onQuoteClick,
+ },
+ ];
+ }, [disabled, editLabel, quoteLabel, onEditClick, onQuoteClick]);
+ return (
+ <>
+ {isLoading && }
+ {!isLoading && }
+ >
+ );
+};
+
+export const UserActionPropertyActions = memo(UserActionPropertyActionsComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx
new file mode 100644
index 0000000000000..a65806520c854
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.test.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { TestProviders } from '../../../common/mock';
+import { UserActionTimestamp } from './user_action_timestamp';
+
+jest.mock('@kbn/i18n/react', () => {
+ const originalModule = jest.requireActual('@kbn/i18n/react');
+ const FormattedRelative = jest.fn();
+ FormattedRelative.mockImplementationOnce(() => '2 days ago');
+ FormattedRelative.mockImplementation(() => '20 hours ago');
+
+ return {
+ ...originalModule,
+ FormattedRelative,
+ };
+});
+
+const props = {
+ createdAt: '2020-09-06T14:40:59.889Z',
+ updatedAt: '2020-09-07T14:40:59.889Z',
+};
+
+describe('UserActionTimestamp ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount(, { wrappingComponent: TestProviders });
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-creation-relative-time"]').first().exists()
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="user-action-title-edited-relative-time"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows only the created time when the updated time is missing', async () => {
+ const newWrapper = mount(, {
+ wrappingComponent: TestProviders,
+ });
+
+ expect(
+ newWrapper
+ .find('[data-test-subj="user-action-title-creation-relative-time"]')
+ .first()
+ .exists()
+ ).toBeTruthy();
+ expect(
+ newWrapper.find('[data-test-subj="user-action-title-edited-relative-time"]').first().exists()
+ ).toBeFalsy();
+ });
+
+ it('it shows the timestamp correctly', async () => {
+ const createdText = wrapper
+ .find('[data-test-subj="user-action-title-creation-relative-time"]')
+ .first()
+ .text();
+
+ const updatedText = wrapper
+ .find('[data-test-subj="user-action-title-edited-relative-time"]')
+ .first()
+ .text();
+
+ expect(`${createdText} (${updatedText})`).toBe('2 days ago (20 hours ago)');
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx
new file mode 100644
index 0000000000000..72dc5de9cdb3b
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_timestamp.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiTextColor } from '@elastic/eui';
+import { FormattedRelative } from '@kbn/i18n/react';
+
+import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip';
+import * as i18n from './translations';
+
+interface UserActionAvatarProps {
+ createdAt: string;
+ updatedAt?: string | null;
+}
+
+const UserActionTimestampComponent = ({ createdAt, updatedAt }: UserActionAvatarProps) => {
+ return (
+ <>
+
+
+
+ {updatedAt && (
+
+ {/* be careful of the extra space at the beginning of the parenthesis */}
+ {' ('}
+ {i18n.EDITED_FIELD}{' '}
+
+
+
+ {')'}
+
+ )}
+ >
+ );
+};
+
+export const UserActionTimestamp = memo(UserActionTimestampComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx
deleted file mode 100644
index 0bb02ce69a544..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.test.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import React from 'react';
-import { mount } from 'enzyme';
-import copy from 'copy-to-clipboard';
-import { Router, routeData, mockHistory } from '../__mock__/router';
-import { caseUserActions as basicUserActions } from '../../containers/mock';
-import { UserActionTitle } from './user_action_title';
-import { TestProviders } from '../../../common/mock';
-
-const outlineComment = jest.fn();
-const onEdit = jest.fn();
-const onQuote = jest.fn();
-
-jest.mock('copy-to-clipboard');
-const defaultProps = {
- createdAt: basicUserActions[0].actionAt,
- disabled: false,
- fullName: basicUserActions[0].actionBy.fullName,
- id: basicUserActions[0].actionId,
- isLoading: false,
- labelEditAction: 'labelEditAction',
- labelQuoteAction: 'labelQuoteAction',
- labelTitle: <>{'cool'}>,
- linkId: basicUserActions[0].commentId,
- onEdit,
- onQuote,
- outlineComment,
- updatedAt: basicUserActions[0].actionAt,
- username: basicUserActions[0].actionBy.username,
-};
-
-describe('UserActionTitle ', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId: '123' });
- });
-
- it('Calls copy when copy link is clicked', async () => {
- const wrapper = mount(
-
-
-
-
-
- );
- wrapper.find(`[data-test-subj="copy-link"]`).first().simulate('click');
- expect(copy).toBeCalledTimes(1);
- });
-});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx
deleted file mode 100644
index 9477299e563a8..0000000000000
--- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_title.tsx
+++ /dev/null
@@ -1,183 +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 {
- EuiLoadingSpinner,
- EuiFlexGroup,
- EuiFlexItem,
- EuiText,
- EuiButtonIcon,
- EuiToolTip,
-} from '@elastic/eui';
-import { FormattedRelative } from '@kbn/i18n/react';
-import copy from 'copy-to-clipboard';
-import { isEmpty } from 'lodash/fp';
-import React, { useMemo, useCallback } from 'react';
-import styled from 'styled-components';
-import { useParams } from 'react-router-dom';
-
-import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip';
-import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search';
-import { navTabs } from '../../../app/home/home_navigations';
-import { PropertyActions } from '../property_actions';
-import { SecurityPageName } from '../../../app/types';
-import * as i18n from './translations';
-
-const MySpinner = styled(EuiLoadingSpinner)`
- .euiLoadingSpinner {
- margin-top: 1px; // yes it matters!
- }
-`;
-
-interface UserActionTitleProps {
- createdAt: string;
- disabled: boolean;
- id: string;
- isLoading: boolean;
- labelEditAction?: string;
- labelQuoteAction?: string;
- labelTitle: JSX.Element;
- linkId?: string | null;
- fullName?: string | null;
- updatedAt?: string | null;
- username?: string | null;
- onEdit?: (id: string) => void;
- onQuote?: (id: string) => void;
- outlineComment?: (id: string) => void;
-}
-
-export const UserActionTitle = ({
- createdAt,
- disabled,
- fullName,
- id,
- isLoading,
- labelEditAction,
- labelQuoteAction,
- labelTitle,
- linkId,
- onEdit,
- onQuote,
- outlineComment,
- updatedAt,
- username = i18n.UNKNOWN,
-}: UserActionTitleProps) => {
- const { detailName: caseId } = useParams<{ detailName: string }>();
- const urlSearch = useGetUrlSearch(navTabs.case);
- const propertyActions = useMemo(() => {
- return [
- ...(labelEditAction != null && onEdit != null
- ? [
- {
- disabled,
- iconType: 'pencil',
- label: labelEditAction,
- onClick: () => onEdit(id),
- },
- ]
- : []),
- ...(labelQuoteAction != null && onQuote != null
- ? [
- {
- disabled,
- iconType: 'quote',
- label: labelQuoteAction,
- onClick: () => onQuote(id),
- },
- ]
- : []),
- ];
- }, [disabled, id, labelEditAction, onEdit, labelQuoteAction, onQuote]);
-
- const handleAnchorLink = useCallback(() => {
- copy(
- `${window.location.origin}${window.location.pathname}#${SecurityPageName.case}/${caseId}/${id}${urlSearch}`
- );
- }, [caseId, id, urlSearch]);
-
- const handleMoveToLink = useCallback(() => {
- if (outlineComment != null && linkId != null) {
- outlineComment(linkId);
- }
- }, [linkId, outlineComment]);
- return (
-
-
-
-
-
- {fullName ?? username}}>
- {username}
-
-
- {labelTitle}
-
-
-
-
-
- {updatedAt != null && (
-
-
- {'('}
- {i18n.EDITED_FIELD}{' '}
-
-
-
- {')'}
-
-
- )}
-
-
-
-
- {!isEmpty(linkId) && (
-
- {i18n.MOVE_TO_ORIGINAL_COMMENT}}>
-
-
-
- )}
-
- {i18n.COPY_REFERENCE_LINK}}>
-
-
-
- {propertyActions.length > 0 && (
-
- {isLoading && }
- {!isLoading && }
-
- )}
-
-
-
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx
new file mode 100644
index 0000000000000..008eb18aef074
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * 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 { mount, ReactWrapper } from 'enzyme';
+import { UserActionUsername } from './user_action_username';
+
+const props = {
+ username: 'elastic',
+ fullName: 'Elastic',
+};
+
+describe('UserActionUsername ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-tooltip"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows the username', async () => {
+ expect(wrapper.find('[data-test-subj="user-action-username-tooltip"]').text()).toBe('elastic');
+ });
+
+ test('it shows the fullname when hovering the username', () => {
+ // Use fake timers so we don't have to wait for the EuiToolTip timeout
+ jest.useFakeTimers();
+
+ wrapper.find('[data-test-subj="user-action-username-tooltip"]').first().simulate('mouseOver');
+
+ // Run the timers so the EuiTooltip will be visible
+ jest.runAllTimers();
+
+ wrapper.update();
+ expect(wrapper.find('.euiToolTipPopover').text()).toBe('Elastic');
+
+ // Clearing all mocks will also reset fake timers.
+ jest.clearAllMocks();
+ });
+
+ test('it shows the username when hovering the username and the fullname is missing', () => {
+ // Use fake timers so we don't have to wait for the EuiToolTip timeout
+ jest.useFakeTimers();
+
+ const newWrapper = mount();
+ newWrapper
+ .find('[data-test-subj="user-action-username-tooltip"]')
+ .first()
+ .simulate('mouseOver');
+
+ // Run the timers so the EuiTooltip will be visible
+ jest.runAllTimers();
+
+ newWrapper.update();
+ expect(newWrapper.find('.euiToolTipPopover').text()).toBe('elastic');
+
+ // Clearing all mocks will also reset fake timers.
+ jest.clearAllMocks();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx
new file mode 100644
index 0000000000000..dbc153ddbe577
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username.tsx
@@ -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 React, { memo } from 'react';
+import { EuiToolTip } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+
+interface UserActionUsernameProps {
+ username: string;
+ fullName?: string;
+}
+
+const UserActionUsernameComponent = ({ username, fullName }: UserActionUsernameProps) => {
+ return (
+ {isEmpty(fullName) ? username : fullName}}
+ data-test-subj="user-action-username-tooltip"
+ >
+ {username}
+
+ );
+};
+
+export const UserActionUsername = memo(UserActionUsernameComponent);
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx
new file mode 100644
index 0000000000000..f8403738c24ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.test.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { UserActionUsernameWithAvatar } from './user_action_username_with_avatar';
+
+const props = {
+ username: 'elastic',
+ fullName: 'Elastic',
+};
+
+describe('UserActionUsernameWithAvatar ', () => {
+ let wrapper: ReactWrapper;
+
+ beforeAll(() => {
+ wrapper = mount();
+ });
+
+ it('it renders', async () => {
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-with-avatar"]').first().exists()
+ ).toBeTruthy();
+ expect(
+ wrapper.find('[data-test-subj="user-action-username-avatar"]').first().exists()
+ ).toBeTruthy();
+ });
+
+ it('it shows the avatar', async () => {
+ expect(wrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe('E');
+ });
+
+ it('it shows the avatar without fullName', async () => {
+ const newWrapper = mount();
+ expect(newWrapper.find('[data-test-subj="user-action-username-avatar"]').first().text()).toBe(
+ 'e'
+ );
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx
new file mode 100644
index 0000000000000..e2326a3580e6f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/user_action_username_with_avatar.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { memo } from 'react';
+import { EuiFlexGroup, EuiFlexItem, EuiAvatar } from '@elastic/eui';
+import { isEmpty } from 'lodash/fp';
+
+import { UserActionUsername } from './user_action_username';
+
+interface UserActionUsernameWithAvatarProps {
+ username: string;
+ fullName?: string;
+}
+
+const UserActionUsernameWithAvatarComponent = ({
+ username,
+ fullName,
+}: UserActionUsernameWithAvatarProps) => {
+ return (
+
+
+
+
+
+
+
+
+ );
+};
+
+export const UserActionUsernameWithAvatar = memo(UserActionUsernameWithAvatarComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
index 403c8d838fa44..89fcc67bcd15f 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts
@@ -16,25 +16,40 @@ export { getDetectionEngineUrl } from './redirect_to_detection_engine';
export { getAppOverviewUrl } from './redirect_to_overview';
export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts';
export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network';
-export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines';
+export { getTimelinesUrl, getTimelineTabsUrl, getTimelineUrl } from './redirect_to_timelines';
export {
getCaseDetailsUrl,
getCaseUrl,
getCreateCaseUrl,
getConfigureCasesUrl,
+ getCaseDetailsUrlWithCommentId,
} from './redirect_to_case';
+interface FormatUrlOptions {
+ absolute: boolean;
+ skipSearch: boolean;
+}
+
+type FormatUrl = (path: string, options?: Partial) => string;
+
export const useFormatUrl = (page: SecurityPageName) => {
const { getUrlForApp } = useKibana().services.application;
const search = useGetUrlSearch(navTabs[page]);
- const formatUrl = useCallback(
- (path: string) => {
+ const formatUrl = useCallback(
+ (path: string, { absolute = false, skipSearch = false } = {}) => {
const pathArr = path.split('?');
const formattedPath = `${pathArr[0]}${
- isEmpty(pathArr[1]) ? search : `${pathArr[1]}${isEmpty(search) ? '' : `&${search}`}`
+ !skipSearch
+ ? isEmpty(pathArr[1])
+ ? search
+ : `?${pathArr[1]}${isEmpty(search) ? '' : `&${search}`}`
+ : isEmpty(pathArr[1])
+ ? ''
+ : `?${pathArr[1]}`
}`;
return getUrlForApp(`${APP_ID}:${page}`, {
path: formattedPath,
+ absolute,
});
},
[getUrlForApp, page, search]
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
index 7005460999fc7..3ef00635844f6 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_case.tsx
@@ -11,6 +11,17 @@ export const getCaseUrl = (search: string | null) => `${appendSearch(search ?? u
export const getCaseDetailsUrl = ({ id, search }: { id: string; search?: string | null }) =>
`/${encodeURIComponent(id)}${appendSearch(search ?? undefined)}`;
+export const getCaseDetailsUrlWithCommentId = ({
+ id,
+ commentId,
+ search,
+}: {
+ id: string;
+ commentId: string;
+ search?: string | null;
+}) =>
+ `/${encodeURIComponent(id)}/${encodeURIComponent(commentId)}${appendSearch(search ?? undefined)}`;
+
export const getCreateCaseUrl = (search?: string | null) =>
`/create${appendSearch(search ?? undefined)}`;
diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
index 75a2fa1efa414..58b9f940ceaa6 100644
--- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_timelines.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { isEmpty } from 'lodash/fp';
import { TimelineTypeLiteral } from '../../../../common/types/timeline';
import { appendSearch } from './helpers';
@@ -11,3 +12,8 @@ export const getTimelinesUrl = (search?: string) => `${appendSearch(search)}`;
export const getTimelineTabsUrl = (tabName: TimelineTypeLiteral, search?: string) =>
`/${tabName}${appendSearch(search)}`;
+
+export const getTimelineUrl = (id: string, graphEventId?: string) =>
+ `?timeline=(id:'${id}',isOpen:!t${
+ isEmpty(graphEventId) ? ')' : `,graphEventId:'${graphEventId}')`
+ }`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
new file mode 100644
index 0000000000000..481ed7892a8be
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useState, useCallback } from 'react';
+import styled from 'styled-components';
+import {
+ EuiMarkdownEditor,
+ EuiMarkdownEditorProps,
+ EuiFormRow,
+ EuiFlexItem,
+ EuiFlexGroup,
+ getDefaultEuiMarkdownParsingPlugins,
+ getDefaultEuiMarkdownProcessingPlugins,
+} from '@elastic/eui';
+import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
+
+import * as timelineMarkdownPlugin from './plugins/timeline';
+
+type MarkdownEditorFormProps = EuiMarkdownEditorProps & {
+ id: string;
+ field: FieldHook;
+ dataTestSubj: string;
+ idAria: string;
+ isDisabled?: boolean;
+ bottomRightContent?: React.ReactNode;
+};
+
+const BottomContentWrapper = styled(EuiFlexGroup)`
+ ${({ theme }) => `
+ padding: ${theme.eui.ruleMargins.marginSmall} 0;
+ `}
+`;
+
+export const parsingPlugins = getDefaultEuiMarkdownParsingPlugins();
+parsingPlugins.push(timelineMarkdownPlugin.parser);
+
+export const processingPlugins = getDefaultEuiMarkdownProcessingPlugins();
+processingPlugins[1][1].components.timeline = timelineMarkdownPlugin.renderer;
+
+export const MarkdownEditorForm: React.FC = ({
+ id,
+ field,
+ dataTestSubj,
+ idAria,
+ bottomRightContent,
+}) => {
+ const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
+ const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]);
+ const onParse = useCallback((err, { messages }) => {
+ setMarkdownErrorMessages(err ? [err] : messages);
+ }, []);
+
+ return (
+
+ <>
+
+ {bottomRightContent && (
+
+ {bottomRightContent}
+
+ )}
+ >
+
+ );
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx
deleted file mode 100644
index 2cc3fe05a2215..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/form.tsx
+++ /dev/null
@@ -1,67 +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 { EuiFormRow } from '@elastic/eui';
-import React, { useCallback } from 'react';
-
-import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports';
-import { CursorPosition, MarkdownEditor } from '.';
-
-interface IMarkdownEditorForm {
- bottomRightContent?: React.ReactNode;
- dataTestSubj: string;
- field: FieldHook;
- idAria: string;
- isDisabled: boolean;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
- placeholder?: string;
- topRightContent?: React.ReactNode;
-}
-export const MarkdownEditorForm = ({
- bottomRightContent,
- dataTestSubj,
- field,
- idAria,
- isDisabled = false,
- onClickTimeline,
- onCursorPositionUpdate,
- placeholder,
- topRightContent,
-}: IMarkdownEditorForm) => {
- const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field);
-
- const handleContentChange = useCallback(
- (newContent: string) => {
- field.setValue(newContent);
- },
- [field]
- );
-
- return (
-
-
-
- );
-};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx
deleted file mode 100644
index b5e5b01189418..0000000000000
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.test.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { mount } from 'enzyme';
-import React from 'react';
-
-import { MarkdownEditor } from '.';
-import { TestProviders } from '../../mock';
-
-describe('Markdown Editor', () => {
- const onChange = jest.fn();
- const onCursorPositionUpdate = jest.fn();
- const defaultProps = {
- content: 'hello world',
- onChange,
- onCursorPositionUpdate,
- };
- beforeEach(() => {
- jest.clearAllMocks();
- });
- test('it calls onChange with correct value', () => {
- const wrapper = mount(
-
-
-
- );
- const newValue = 'a new string';
- wrapper
- .find(`[data-test-subj="textAreaInput"]`)
- .first()
- .simulate('change', { target: { value: newValue } });
- expect(onChange).toBeCalledWith(newValue);
- });
- test('it calls onCursorPositionUpdate with correct args', () => {
- const wrapper = mount(
-
-
-
- );
- wrapper.find(`[data-test-subj="textAreaInput"]`).first().simulate('blur');
- expect(onCursorPositionUpdate).toBeCalledWith({
- start: 0,
- end: 0,
- });
- });
-});
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
index d4ad4a11b60a3..9f4141dbcae7d 100644
--- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/index.tsx
@@ -4,167 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import {
- EuiFlexGroup,
- EuiFlexItem,
- EuiLink,
- EuiPanel,
- EuiTabbedContent,
- EuiTextArea,
-} from '@elastic/eui';
-import React, { useMemo, useCallback, ChangeEvent } from 'react';
-import styled, { css } from 'styled-components';
-
-import { Markdown } from '../markdown';
-import * as i18n from './translations';
-import { MARKDOWN_HELP_LINK } from './constants';
-
-const TextArea = styled(EuiTextArea)`
- width: 100%;
-`;
-
-const Container = styled(EuiPanel)`
- ${({ theme }) => css`
- padding: 0;
- background: ${theme.eui.euiColorLightestShade};
- position: relative;
- .markdown-tabs-header {
- position: absolute;
- top: ${theme.eui.euiSizeS};
- right: ${theme.eui.euiSizeS};
- z-index: ${theme.eui.euiZContentMenu};
- }
- .euiTab {
- padding: 10px;
- }
- .markdown-tabs {
- width: 100%;
- }
- .markdown-tabs-footer {
- height: 41px;
- padding: 0 ${theme.eui.euiSizeM};
- .euiLink {
- font-size: ${theme.eui.euiSizeM};
- }
- }
- .euiFormRow__labelWrapper {
- position: absolute;
- top: -${theme.eui.euiSizeL};
- }
- .euiFormErrorText {
- padding: 0 ${theme.eui.euiSizeM};
- }
- `}
-`;
-
-const MarkdownContainer = styled(EuiPanel)`
- min-height: 150px;
- overflow: auto;
-`;
-
-export interface CursorPosition {
- start: number;
- end: number;
-}
-
-/** An input for entering a new case description */
-export const MarkdownEditor = React.memo<{
- bottomRightContent?: React.ReactNode;
- topRightContent?: React.ReactNode;
- content: string;
- isDisabled?: boolean;
- onChange: (description: string) => void;
- onClickTimeline?: (timelineId: string, graphEventId?: string) => void;
- onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void;
- placeholder?: string;
-}>(
- ({
- bottomRightContent,
- topRightContent,
- content,
- isDisabled = false,
- onChange,
- onClickTimeline,
- placeholder,
- onCursorPositionUpdate,
- }) => {
- const handleOnChange = useCallback(
- (evt: ChangeEvent) => {
- onChange(evt.target.value);
- },
- [onChange]
- );
-
- const setCursorPosition = useCallback(
- (e: React.ChangeEvent) => {
- if (onCursorPositionUpdate) {
- onCursorPositionUpdate({
- start: e!.target!.selectionStart ?? 0,
- end: e!.target!.selectionEnd ?? 0,
- });
- }
- },
- [onCursorPositionUpdate]
- );
-
- const tabs = useMemo(
- () => [
- {
- id: 'comment',
- name: i18n.MARKDOWN,
- content: (
-
- ),
- },
- {
- id: 'preview',
- name: i18n.PREVIEW,
- 'data-test-subj': 'preview-tab',
- content: (
-
-
-
- ),
- },
- ],
- [content, handleOnChange, isDisabled, onClickTimeline, placeholder, setCursorPosition]
- );
- return (
-
- {topRightContent && {topRightContent}
}
-
-
-
-
- {i18n.MARKDOWN_SYNTAX_HELP}
-
-
- {bottomRightContent && {bottomRightContent}}
-
-
- );
- }
-);
-
-MarkdownEditor.displayName = 'MarkdownEditor';
+export * from './types';
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts
new file mode 100644
index 0000000000000..917000a8ba21c
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/constants.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ID = 'timeline';
+export const PREFIX = `[`;
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts
new file mode 100644
index 0000000000000..701889013ee53
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/index.ts
@@ -0,0 +1,11 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { plugin } from './plugin';
+import { TimelineParser } from './parser';
+import { TimelineMarkDownRenderer } from './processor';
+
+export { plugin, TimelineParser as parser, TimelineMarkDownRenderer as renderer };
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts
new file mode 100644
index 0000000000000..d322a2c9e1929
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/parser.ts
@@ -0,0 +1,119 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Plugin } from '@elastic/eui/node_modules/unified';
+import { RemarkTokenizer } from '@elastic/eui';
+import { parse } from 'query-string';
+import { decodeRisonUrlState } from '../../../url_state/helpers';
+import { ID, PREFIX } from './constants';
+import * as i18n from './translations';
+
+export const TimelineParser: Plugin = function () {
+ const Parser = this.Parser;
+ const tokenizers = Parser.prototype.inlineTokenizers;
+ const methods = Parser.prototype.inlineMethods;
+
+ const parseTimeline: RemarkTokenizer = function (eat, value, silent) {
+ let index = 0;
+ const nextChar = value[index];
+
+ if (nextChar !== '[') {
+ return false;
+ }
+
+ if (silent) {
+ return true;
+ }
+
+ function readArg(open: string, close: string) {
+ if (value[index] !== open) {
+ throw new Error(i18n.NO_PARENTHESES);
+ }
+
+ index++;
+
+ let body = '';
+ let openBrackets = 0;
+
+ for (; index < value.length; index++) {
+ const char = value[index];
+
+ if (char === close && openBrackets === 0) {
+ index++;
+ return body;
+ } else if (char === close) {
+ openBrackets--;
+ } else if (char === open) {
+ openBrackets++;
+ }
+
+ body += char;
+ }
+
+ return '';
+ }
+
+ const timelineTitle = readArg('[', ']');
+ const timelineUrl = readArg('(', ')');
+ const now = eat.now();
+
+ if (!timelineTitle) {
+ this.file.info(i18n.NO_TIMELINE_NAME_FOUND, {
+ line: now.line,
+ column: now.column,
+ });
+ return false;
+ }
+
+ try {
+ const timelineSearch = timelineUrl.split('?');
+ const parseTimelineUrlSearch = parse(timelineSearch[1]) as { timeline: string };
+ const { id: timelineId = '', graphEventId = '' } = decodeRisonUrlState(
+ parseTimelineUrlSearch.timeline ?? ''
+ ) ?? { id: null, graphEventId: '' };
+
+ if (!timelineId) {
+ this.file.info(i18n.NO_TIMELINE_ID_FOUND, {
+ line: now.line,
+ column: now.column + timelineUrl.indexOf('id'),
+ });
+ return false;
+ }
+
+ return eat(`[${timelineTitle}](${timelineUrl})`)({
+ type: ID,
+ id: timelineId,
+ title: timelineTitle,
+ graphEventId,
+ });
+ } catch {
+ this.file.info(i18n.TIMELINE_URL_IS_NOT_VALID(timelineUrl), {
+ line: now.line,
+ column: now.column,
+ });
+ }
+
+ return false;
+ };
+
+ const tokenizeTimeline: RemarkTokenizer = function tokenizeTimeline(eat, value, silent) {
+ if (
+ value.startsWith(PREFIX) === false ||
+ (value.startsWith(PREFIX) === true && !value.includes('timelines?timeline=(id'))
+ ) {
+ return false;
+ }
+
+ return parseTimeline.call(this, eat, value, silent);
+ };
+
+ tokenizeTimeline.locator = (value: string, fromIndex: number) => {
+ return value.indexOf(PREFIX, fromIndex);
+ };
+
+ tokenizers.timeline = tokenizeTimeline;
+ methods.splice(methods.indexOf('url'), 0, ID);
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx
new file mode 100644
index 0000000000000..8d2488b269d76
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/plugin.tsx
@@ -0,0 +1,87 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useCallback, memo } from 'react';
+import {
+ EuiSelectableOption,
+ EuiModalBody,
+ EuiMarkdownEditorUiPlugin,
+ EuiCodeBlock,
+} from '@elastic/eui';
+
+import { TimelineType } from '../../../../../../common/types/timeline';
+import { SelectableTimeline } from '../../../../../timelines/components/timeline/selectable_timeline';
+import { OpenTimelineResult } from '../../../../../timelines/components/open_timeline/types';
+import { getTimelineUrl, useFormatUrl } from '../../../link_to';
+
+import { ID } from './constants';
+import * as i18n from './translations';
+import { SecurityPageName } from '../../../../../app/types';
+
+interface TimelineEditorProps {
+ onClosePopover: () => void;
+ onInsert: (markdown: string, config: { block: boolean }) => void;
+}
+
+const TimelineEditorComponent: React.FC = ({ onClosePopover, onInsert }) => {
+ const { formatUrl } = useFormatUrl(SecurityPageName.timelines);
+
+ const handleGetSelectableOptions = useCallback(
+ ({ timelines }: { timelines: OpenTimelineResult[] }) => [
+ ...timelines.map(
+ (t: OpenTimelineResult, index: number) =>
+ ({
+ description: t.description,
+ favorite: t.favorite,
+ label: t.title,
+ id: t.savedObjectId,
+ key: `${t.title}-${index}`,
+ title: t.title,
+ checked: undefined,
+ } as EuiSelectableOption)
+ ),
+ ],
+ []
+ );
+
+ return (
+
+ {
+ const url = formatUrl(getTimelineUrl(timelineId ?? '', graphEventId), {
+ absolute: true,
+ skipSearch: true,
+ });
+ onInsert(`[${timelineTitle}](${url})`, {
+ block: false,
+ });
+ }}
+ onClosePopover={onClosePopover}
+ timelineType={TimelineType.default}
+ />
+
+ );
+};
+
+const TimelineEditor = memo(TimelineEditorComponent);
+
+export const plugin: EuiMarkdownEditorUiPlugin = {
+ name: ID,
+ button: {
+ label: i18n.INSERT_TIMELINE,
+ iconType: 'timeline',
+ },
+ helpText: (
+
+ {'[title](url)'}
+
+ ),
+ editor: function editor({ node, onSave, onCancel }) {
+ return ;
+ },
+};
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
new file mode 100644
index 0000000000000..fb72b4368c8ea
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/processor.tsx
@@ -0,0 +1,34 @@
+/*
+ * 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, { useCallback, memo } from 'react';
+import { EuiToolTip, EuiLink, EuiMarkdownAstNodePosition } from '@elastic/eui';
+
+import { useTimelineClick } from '../../../../utils/timeline/use_timeline_click';
+import { TimelineProps } from './types';
+import * as i18n from './translations';
+
+export const TimelineMarkDownRendererComponent: React.FC<
+ TimelineProps & {
+ position: EuiMarkdownAstNodePosition;
+ }
+> = ({ id, title, graphEventId }) => {
+ const handleTimelineClick = useTimelineClick();
+ const onClickTimeline = useCallback(() => handleTimelineClick(id ?? '', graphEventId), [
+ id,
+ graphEventId,
+ handleTimelineClick,
+ ]);
+ return (
+
+
+ {title}
+
+
+ );
+};
+
+export const TimelineMarkDownRenderer = memo(TimelineMarkDownRendererComponent);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts
new file mode 100644
index 0000000000000..5a23b2a742157
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/translations.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const INSERT_TIMELINE = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.insertTimelineButtonLabel',
+ {
+ defaultMessage: 'Insert timeline link',
+ }
+);
+
+export const TIMELINE_ID = (timelineId: string) =>
+ i18n.translate('xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineId', {
+ defaultMessage: 'Timeline id: { timelineId }',
+ values: {
+ timelineId,
+ },
+ });
+
+export const NO_TIMELINE_NAME_FOUND = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineNameFoundErrorMsg',
+ {
+ defaultMessage: 'No timeline name found',
+ }
+);
+
+export const NO_TIMELINE_ID_FOUND = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noTimelineIdFoundErrorMsg',
+ {
+ defaultMessage: 'No timeline id found',
+ }
+);
+
+export const TIMELINE_URL_IS_NOT_VALID = (timelineUrl: string) =>
+ i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.toolTip.timelineUrlIsNotValidErrorMsg',
+ {
+ defaultMessage: 'Timeline URL is not valid => {timelineUrl}',
+ values: {
+ timelineUrl,
+ },
+ }
+ );
+
+export const NO_PARENTHESES = i18n.translate(
+ 'xpack.securitySolution.markdownEditor.plugins.timeline.noParenthesesErrorMsg',
+ {
+ defaultMessage: 'Expected left parentheses',
+ }
+);
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts
new file mode 100644
index 0000000000000..8b9111fc9fc7d
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/plugins/timeline/types.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ID } from './constants';
+
+export interface TimelineConfiguration {
+ id: string | null;
+ title: string;
+ graphEventId?: string;
+ [key: string]: string | null | undefined;
+}
+
+export interface TimelineProps extends TimelineConfiguration {
+ type: typeof ID;
+}
diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts b/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts
new file mode 100644
index 0000000000000..030def21ac36f
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/types.ts
@@ -0,0 +1,10 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface CursorPosition {
+ start: number;
+ end: number;
+}
diff --git a/x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx b/x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx
similarity index 100%
rename from x-pack/plugins/security_solution/public/cases/components/utils/use_timeline_click.tsx
rename to x-pack/plugins/security_solution/public/common/utils/timeline/use_timeline_click.tsx
diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
index d2c84883fa99b..66f95f5ce15d2 100644
--- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
+++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.tsx
@@ -36,7 +36,7 @@ import { schema } from './schema';
import * as I18n from './translations';
import { StepContentWrapper } from '../step_content_wrapper';
import { NextStep } from '../next_step';
-import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/form';
+import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/eui_form';
import { SeverityField } from '../severity_mapping';
import { RiskScoreField } from '../risk_score_mapping';
import { useFetchIndexPatterns } from '../../../containers/detection_engine/rules';
diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
index 642e86059ed6e..c8d9b46d5a0d2 100644
--- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
+++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap
@@ -297,13 +297,13 @@ Object {
class="sc-fzoyAV jWxvlI siemWrapperPage"
>