(async (cb) => {
const [coreStart] = await window.__coreProvider.setup.core.getStartServices();
cb(Boolean(coreStart.overlays));
})
diff --git a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts
index 6a0a5fed48e6d..3a618ceaeb22f 100644
--- a/test/plugin_functional/test_suites/core_plugins/ui_settings.ts
+++ b/test/plugin_functional/test_suites/core_plugins/ui_settings.ts
@@ -49,7 +49,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
expect(settingsValue).to.be('2');
- const settingsValueViaObservables = await browser.executeAsync(async (callback: Function) => {
+ const settingsValueViaObservables = await browser.executeAsync(async (callback) => {
window.__coreProvider.setup.core.uiSettings
.get$('ui_settings_plugin')
.subscribe((v) => callback(v));
diff --git a/test/plugin_functional/test_suites/embeddable_explorer/index.js b/test/plugin_functional/test_suites/embeddable_explorer/index.js
deleted file mode 100644
index b122d9740dc96..0000000000000
--- a/test/plugin_functional/test_suites/embeddable_explorer/index.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-export default function ({ getService, getPageObjects, loadTestFile }) {
- const browser = getService('browser');
- const appsMenu = getService('appsMenu');
- const esArchiver = getService('esArchiver');
- const kibanaServer = getService('kibanaServer');
- const PageObjects = getPageObjects(['common', 'header']);
-
- describe('embeddable explorer', function () {
- before(async () => {
- await esArchiver.loadIfNeeded('../functional/fixtures/es_archiver/dashboard/current/data');
- await esArchiver.load('../functional/fixtures/es_archiver/dashboard/current/kibana');
- await kibanaServer.uiSettings.replace({
- 'dateFormat:tz': 'Australia/North',
- defaultIndex: 'logstash-*',
- });
- await browser.setWindowSize(1300, 900);
- await PageObjects.common.navigateToApp('settings');
- await appsMenu.clickLink('Embeddable Explorer');
- });
-
- loadTestFile(require.resolve('./dashboard_container'));
- });
-}
diff --git a/vars/githubPr.groovy b/vars/githubPr.groovy
index 965fb1d4e108e..9dad2f88de2ec 100644
--- a/vars/githubPr.groovy
+++ b/vars/githubPr.groovy
@@ -186,6 +186,7 @@ def getNextCommentMessage(previousCommentInfo = [:]) {
}
messages << getTestFailuresMessage()
+ messages << ciStats.getMetricsReport()
if (info.builds && info.builds.size() > 0) {
messages << getHistoryText(info.builds)
diff --git a/x-pack/package.json b/x-pack/package.json
index 4f22c027c1a4c..2513a36f50014 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -128,7 +128,7 @@
"chance": "1.0.18",
"cheerio": "0.22.0",
"commander": "3.0.2",
- "copy-webpack-plugin": "^5.0.4",
+ "copy-webpack-plugin": "^6.0.2",
"cypress": "4.5.0",
"cypress-multi-reporters": "^1.2.3",
"enzyme": "^3.11.0",
diff --git a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
index f1633799ea583..2fae8bdab2b30 100644
--- a/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
+++ b/x-pack/plugins/apm/e2e/cypress/integration/snapshots.js
@@ -1,10 +1,10 @@
module.exports = {
- "APM": {
- "Transaction duration charts": {
- "1": "350 ms",
- "2": "175 ms",
- "3": "0 ms"
- }
+ APM: {
+ 'Transaction duration charts': {
+ '1': '350.0 ms',
+ '2': '175.0 ms',
+ '3': '0.0 ms',
+ },
},
- "__version": "4.5.0"
-}
+ __version: '4.5.0',
+};
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js
index 927779b571fd8..09fef5da16ae7 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js
@@ -40,7 +40,7 @@ describe('ServiceOverview -> List', () => {
expect(renderedColumns[0]).toMatchSnapshot();
expect(renderedColumns.slice(2)).toEqual([
'python',
- '92 ms',
+ '91.5 ms',
'86.9 tpm',
'12.6 err.',
]);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
index b9048f013cb25..90cc9af45273e 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx
@@ -12,7 +12,7 @@ import styled from 'styled-components';
import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services';
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
import { fontSizes, truncate } from '../../../../style/variables';
-import { asDecimal, convertTo } from '../../../../utils/formatters';
+import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters';
import { ManagedTable } from '../../../shared/ManagedTable';
import { EnvironmentBadge } from '../../../shared/EnvironmentBadge';
import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink';
@@ -81,11 +81,7 @@ export const SERVICE_COLUMNS = [
}),
sortable: true,
dataType: 'number',
- render: (time: number) =>
- convertTo({
- unit: 'milliseconds',
- microseconds: time,
- }).formatted,
+ render: (time: number) => asMillisecondDuration(time),
},
{
field: 'transactionsPerMinute',
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
index 3e6be107ce3a1..e89acca55d4fe 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap
@@ -203,7 +203,7 @@ NodeList [
- 1 ms
+ 0.6 ms
- 0 ms
+ 0.3 ms
|
> = [
}),
sortable: true,
dataType: 'number',
- render: (time: number) =>
- convertTo({
- unit: 'milliseconds',
- microseconds: time,
- }).formatted,
+ render: (time: number) => asMillisecondDuration(time),
},
{
field: 'transactionsPerMinute',
diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
index 61c8d3958b625..ae1b07bde0c87 100644
--- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.tsx
@@ -12,7 +12,7 @@ import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ITransactionGroup } from '../../../../../server/lib/transaction_groups/transform';
import { fontFamilyCode, truncate } from '../../../../style/variables';
-import { asDecimal, convertTo } from '../../../../utils/formatters';
+import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters';
import { ImpactBar } from '../../../shared/ImpactBar';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
@@ -29,12 +29,6 @@ interface Props {
isLoading: boolean;
}
-const toMilliseconds = (time: number) =>
- convertTo({
- unit: 'milliseconds',
- microseconds: time,
- }).formatted;
-
export function TransactionList({ items, isLoading }: Props) {
const columns: Array> = useMemo(
() => [
@@ -74,7 +68,7 @@ export function TransactionList({ items, isLoading }: Props) {
),
sortable: true,
dataType: 'number',
- render: (time: number) => toMilliseconds(time),
+ render: (time: number) => asMillisecondDuration(time),
},
{
field: 'p95',
@@ -86,7 +80,7 @@ export function TransactionList({ items, isLoading }: Props) {
),
sortable: true,
dataType: 'number',
- render: (time: number) => toMilliseconds(time),
+ render: (time: number) => asMillisecondDuration(time),
},
{
field: 'transactionsPerMinute',
diff --git a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
index 4c7d21d968088..b7ea026f80fde 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
+++ b/x-pack/plugins/apm/public/components/shared/charts/CustomPlot/test/__snapshots__/CustomPlot.test.js.snap
@@ -9,7 +9,7 @@ Array [
"text":
Avg.
- 468 ms
+ 467.6 ms
,
},
@@ -2744,7 +2744,7 @@ Array [
- 468 ms
+ 467.6 ms
@@ -5923,7 +5923,7 @@ Array [
- 468 ms
+ 467.6 ms
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
index 1d0a53843f538..f84b0cfeda369 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
+++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/Histogram.test.js
@@ -78,7 +78,7 @@ describe('Histogram', () => {
const tooltips = wrapper.find('Tooltip');
expect(tooltips.length).toBe(1);
- expect(tooltips.prop('header')).toBe('811 - 927 ms');
+ expect(tooltips.prop('header')).toBe('811.1 - 926.9 ms');
expect(tooltips.prop('tooltipPoints')).toEqual([
{ value: '49.0 occurrences' },
]);
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
index f1c7d4826fe0c..700602eb56929 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
+++ b/x-pack/plugins/apm/public/components/shared/charts/Histogram/__test__/__snapshots__/Histogram.test.js.snap
@@ -127,7 +127,7 @@ exports[`Histogram Initially should have default markup 1`] = `
textAnchor="middle"
transform="translate(0, 18)"
>
- 0 ms
+ 0.0 ms
- 500 ms
+ 500.0 ms
- 1,000 ms
+ 1,000.0 ms
- 1,500 ms
+ 1,500.0 ms
- 2,000 ms
+ 2,000.0 ms
- 2,500 ms
+ 2,500.0 ms
- 3,000 ms
+ 3,000.0 ms
@@ -1477,7 +1477,7 @@ exports[`Histogram when hovering over a non-empty bucket should have correct mar
- 811 - 927 ms
+ 811.1 - 926.9 ms
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
index d133ba5e715fd..2b6f0c7aa1319 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.test.tsx
@@ -12,7 +12,7 @@ import { act } from '@testing-library/react-hooks';
import { expectTextsInDocument } from '../../../../../utils/testHelpers';
describe('ErrorMarker', () => {
- const mark = {
+ const mark = ({
id: 'agent',
offset: 10000,
type: 'errorMark',
@@ -20,18 +20,24 @@ describe('ErrorMarker', () => {
error: {
trace: { id: '123' },
transaction: { id: '456' },
- error: { grouping_key: '123' },
+ error: {
+ grouping_key: '123',
+ log: {
+ message:
+ "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
+ },
+ },
service: { name: 'bar' },
},
serviceColor: '#fff',
- } as ErrorMark;
+ } as unknown) as ErrorMark;
function openPopover(errorMark: ErrorMark) {
const component = render();
act(() => {
fireEvent.click(component.getByTestId('popover'));
});
- expectTextsInDocument(component, ['10,000 μs']);
+ expectTextsInDocument(component, ['10.0 ms']);
return component;
}
function getKueryDecoded(url: string) {
@@ -76,4 +82,34 @@ describe('ErrorMarker', () => {
const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
expect(getKueryDecoded(errorLink.hash)).toEqual('kuery=');
});
+ it('truncates the error message text', () => {
+ const { trace, transaction, ...withoutTraceAndTransaction } = mark.error;
+ const newMark = {
+ ...mark,
+ error: withoutTraceAndTransaction,
+ } as ErrorMark;
+ const component = openPopover(newMark);
+ const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
+ expect(errorLink.innerHTML).toHaveLength(241);
+ });
+
+ describe('when the error message is not longer than 240 characters', () => {
+ it('truncates the error message text', () => {
+ const newMark = ({
+ ...mark,
+ error: {
+ ...mark.error,
+ error: {
+ grouping_key: '123',
+ log: {
+ message: 'Blah.',
+ },
+ },
+ },
+ } as unknown) as ErrorMark;
+ const component = openPopover(newMark);
+ const errorLink = component.getByTestId('errorLink') as HTMLAnchorElement;
+ expect(errorLink.innerHTML).toHaveLength(5);
+ });
+ });
});
diff --git a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
index 42f4f278b07bc..e3310c273a55b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/Timeline/Marker/ErrorMarker.tsx
@@ -34,6 +34,7 @@ const TimeLegend = styled(Legend)`
const ErrorLink = styled(ErrorDetailLink)`
display: block;
margin: ${px(units.half)} 0 ${px(units.half)} 0;
+ overflow-wrap: break-word;
`;
const Button = styled(Legend)`
@@ -42,6 +43,16 @@ const Button = styled(Legend)`
align-items: flex-end;
`;
+// We chose 240 characters because it fits most error messages and it's still easily readable on a screen.
+function truncateMessage(errorMessage?: string) {
+ const maxLength = 240;
+ if (typeof errorMessage === 'string' && errorMessage.length > maxLength) {
+ return errorMessage.substring(0, maxLength) + '…';
+ } else {
+ return errorMessage;
+ }
+}
+
export const ErrorMarker: React.FC = ({ mark }) => {
const { urlParams } = useUrlParams();
const [isPopoverOpen, showPopover] = useState(false);
@@ -73,6 +84,10 @@ export const ErrorMarker: React.FC = ({ mark }) => {
rangeTo,
};
+ const errorMessage =
+ error.error.log?.message || error.error.exception?.[0]?.message;
+ const truncatedErrorMessage = truncateMessage(errorMessage);
+
return (
= ({ mark }) => {
serviceName={error.service.name}
errorGroupId={error.error.grouping_key}
query={query}
+ title={errorMessage}
>
- {error.error.log?.message || error.error.exception?.[0]?.message}
+ {truncatedErrorMessage}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
index 3c1f4c54fc635..915b55f29ef80 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/TransactionCharts/index.tsx
@@ -120,7 +120,7 @@ export class TransactionCharts extends Component {
'xpack.apm.metrics.transactionChart.machineLearningTooltip',
{
defaultMessage:
- 'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores >= 75.',
+ 'The stream around the average duration shows the expected bounds. An annotation is shown for anomaly scores ≥ 75.',
}
)}
/>
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index 2f46e2090351b..e9de8fcd890d0 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -7,6 +7,7 @@
import { i18n } from '@kbn/i18n';
import { lazy } from 'react';
import { ConfigSchema } from '.';
+import { ObservabilityPluginSetup } from '../../observability/public';
import {
AppMountParameters,
CoreSetup,
@@ -33,10 +34,10 @@ import {
import { AlertType } from '../common/alert_types';
import { featureCatalogueEntry } from './featureCatalogueEntry';
import { createCallApmApi } from './services/rest/createCallApmApi';
-import { createStaticIndexPattern } from './services/rest/index_pattern';
import { setHelpExtension } from './setHelpExtension';
import { toggleAppLinkInNav } from './toggleAppLinkInNav';
import { setReadonlyBadge } from './updateBadge';
+import { createStaticIndexPattern } from './services/rest/index_pattern';
export type ApmPluginSetup = void;
export type ApmPluginStart = void;
@@ -48,6 +49,7 @@ export interface ApmPluginSetupDeps {
home: HomePublicPluginSetup;
licensing: LicensingPluginSetup;
triggers_actions_ui: TriggersAndActionsUIPublicPluginSetup;
+ observability?: ObservabilityPluginSetup;
}
export interface ApmPluginStartDeps {
@@ -64,6 +66,7 @@ export class ApmPlugin implements Plugin {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup, plugins: ApmPluginSetupDeps) {
+ createCallApmApi(core.http);
const config = this.initializerContext.config.get();
const pluginSetupDeps = plugins;
@@ -100,8 +103,6 @@ export class ApmPlugin implements Plugin {
});
}
public start(core: CoreStart, plugins: ApmPluginStartDeps) {
- createCallApmApi(core.http);
-
toggleAppLinkInNav(core, this.initializerContext.config.get());
plugins.triggers_actions_ui.alertTypeRegistry.register({
diff --git a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
index da9c32c84f36f..2f0a30a5019a9 100644
--- a/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
+++ b/x-pack/plugins/apm/public/selectors/__tests__/chartSelectors.test.ts
@@ -62,7 +62,7 @@ describe('chartSelectors', () => {
{ x: 0, y: 100 },
{ x: 1000, y: 200 },
],
- legendValue: '0 ms',
+ legendValue: '200 μs',
title: 'Avg.',
type: 'linemark',
},
diff --git a/x-pack/plugins/apm/public/selectors/chartSelectors.ts b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
index cfe1a6a60cd22..f8aed9dcf6d9f 100644
--- a/x-pack/plugins/apm/public/selectors/chartSelectors.ts
+++ b/x-pack/plugins/apm/public/selectors/chartSelectors.ts
@@ -18,7 +18,7 @@ import {
RectCoordinate,
TimeSeries,
} from '../../typings/timeseries';
-import { asDecimal, tpmUnit, convertTo } from '../utils/formatters';
+import { asDecimal, asDuration, tpmUnit } from '../utils/formatters';
import { IUrlParams } from '../context/UrlParamsContext/types';
import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries';
import { httpStatusCodeToColor } from '../utils/httpStatusCodeToColor';
@@ -72,10 +72,7 @@ export function getResponseTimeSeries({
}: TimeSeriesAPIResponse) {
const { overallAvgDuration } = apmTimeseries;
const { avg, p95, p99 } = apmTimeseries.responseTimes;
- const formattedDuration = convertTo({
- unit: 'milliseconds',
- microseconds: overallAvgDuration,
- }).formatted;
+ const formattedDuration = asDuration(overallAvgDuration);
const series: TimeSeries[] = [
{
diff --git a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts
index de3e3868de396..6d4b65d2aa9b4 100644
--- a/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts
+++ b/x-pack/plugins/apm/public/utils/formatters/__test__/duration.test.ts
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { asDuration, convertTo, toMicroseconds } from '../duration';
+import { asDuration, toMicroseconds, asMillisecondDuration } from '../duration';
describe('duration formatters', () => {
describe('asDuration', () => {
@@ -14,10 +14,10 @@ describe('duration formatters', () => {
expect(asDuration(1)).toEqual('1 μs');
expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs');
expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual(
- '1,000 ms'
+ '1,000.0 ms'
);
expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual(
- '10,000 ms'
+ '10,000.0 ms'
);
expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20.0 s');
expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('10.0 min');
@@ -30,96 +30,6 @@ describe('duration formatters', () => {
});
});
- describe('convertTo', () => {
- it('hours', () => {
- const unit = 'hours';
- const oneHourAsMicro = toMicroseconds(1, 'hours');
- const twoHourAsMicro = toMicroseconds(2, 'hours');
- expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({
- unit: 'h',
- value: '1.0',
- formatted: '1.0 h',
- });
- expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({
- unit: 'h',
- value: '2.0',
- formatted: '2.0 h',
- });
- expect(
- convertTo({ unit, microseconds: null, defaultValue: '1.2' })
- ).toEqual({ value: '1.2', formatted: '1.2' });
- });
-
- it('minutes', () => {
- const unit = 'minutes';
- const oneHourAsMicro = toMicroseconds(1, 'hours');
- const twoHourAsMicro = toMicroseconds(2, 'hours');
- expect(convertTo({ unit, microseconds: oneHourAsMicro })).toEqual({
- unit: 'min',
- value: '60.0',
- formatted: '60.0 min',
- });
- expect(convertTo({ unit, microseconds: twoHourAsMicro })).toEqual({
- unit: 'min',
- value: '120.0',
- formatted: '120.0 min',
- });
- expect(
- convertTo({ unit, microseconds: null, defaultValue: '10' })
- ).toEqual({ value: '10', formatted: '10' });
- });
-
- it('seconds', () => {
- const unit = 'seconds';
- const twentySecondsAsMicro = toMicroseconds(20, 'seconds');
- const thirtyFiveSecondsAsMicro = toMicroseconds(35, 'seconds');
- expect(convertTo({ unit, microseconds: twentySecondsAsMicro })).toEqual({
- unit: 's',
- value: '20.0',
- formatted: '20.0 s',
- });
- expect(
- convertTo({ unit, microseconds: thirtyFiveSecondsAsMicro })
- ).toEqual({ unit: 's', value: '35.0', formatted: '35.0 s' });
- expect(
- convertTo({ unit, microseconds: null, defaultValue: '10' })
- ).toEqual({ value: '10', formatted: '10' });
- });
-
- it('milliseconds', () => {
- const unit = 'milliseconds';
- const twentyMilliAsMicro = toMicroseconds(20, 'milliseconds');
- const thirtyFiveMilliAsMicro = toMicroseconds(35, 'milliseconds');
- expect(convertTo({ unit, microseconds: twentyMilliAsMicro })).toEqual({
- unit: 'ms',
- value: '20',
- formatted: '20 ms',
- });
- expect(
- convertTo({ unit, microseconds: thirtyFiveMilliAsMicro })
- ).toEqual({ unit: 'ms', value: '35', formatted: '35 ms' });
- expect(
- convertTo({ unit, microseconds: null, defaultValue: '10' })
- ).toEqual({ value: '10', formatted: '10' });
- });
-
- it('microseconds', () => {
- const unit = 'microseconds';
- expect(convertTo({ unit, microseconds: 20 })).toEqual({
- unit: 'μs',
- value: '20',
- formatted: '20 μs',
- });
- expect(convertTo({ unit, microseconds: 35 })).toEqual({
- unit: 'μs',
- value: '35',
- formatted: '35 μs',
- });
- expect(
- convertTo({ unit, microseconds: null, defaultValue: '10' })
- ).toEqual({ value: '10', formatted: '10' });
- });
- });
describe('toMicroseconds', () => {
it('transformes to microseconds', () => {
expect(toMicroseconds(1, 'hours')).toEqual(3600000000);
@@ -128,4 +38,10 @@ describe('duration formatters', () => {
expect(toMicroseconds(10, 'milliseconds')).toEqual(10000);
});
});
+
+ describe('asMilliseconds', () => {
+ it('converts to formatted decimal milliseconds', () => {
+ expect(asMillisecondDuration(0)).toEqual('0.0 ms');
+ });
+ });
});
diff --git a/x-pack/plugins/apm/public/utils/formatters/duration.ts b/x-pack/plugins/apm/public/utils/formatters/duration.ts
index af87f7d517cb9..a603faab37538 100644
--- a/x-pack/plugins/apm/public/utils/formatters/duration.ts
+++ b/x-pack/plugins/apm/public/utils/formatters/duration.ts
@@ -65,7 +65,7 @@ const durationUnit: DurationUnit = {
defaultMessage: 'ms',
}),
convert: (value: number) =>
- asInteger(moment.duration(value / 1000).asMilliseconds()),
+ asDecimal(moment.duration(value / 1000).asMilliseconds()),
},
microseconds: {
label: i18n.translate('xpack.apm.formatters.microsTimeUnitLabel', {
@@ -77,13 +77,8 @@ const durationUnit: DurationUnit = {
/**
* Converts a microseconds value into the unit defined.
- *
- * @param param0
- * { unit: "milliseconds" | "hours" | "minutes" | "seconds" | "microseconds", microseconds, defaultValue }
- *
- * @returns object { value, unit, formatted }
*/
-export function convertTo({
+function convertTo({
unit,
microseconds,
defaultValue = NOT_AVAILABLE_LABEL,
@@ -118,7 +113,7 @@ function getDurationUnitKey(max: number): DurationTimeUnit {
if (max > toMicroseconds(10, 'seconds')) {
return 'seconds';
}
- if (max > toMicroseconds(10, 'milliseconds')) {
+ if (max > toMicroseconds(1, 'milliseconds')) {
return 'milliseconds';
}
return 'microseconds';
@@ -135,10 +130,6 @@ export const getDurationFormatter: TimeFormatterBuilder = memoize(
/**
* Converts value and returns it formatted - 00 unit
- *
- * @param value
- * @param param1 { defaultValue }
- * @returns formated value - 00 unit
*/
export function asDuration(
value: Maybe,
@@ -151,3 +142,15 @@ export function asDuration(
const formatter = getDurationFormatter(value);
return formatter(value, { defaultValue }).formatted;
}
+
+/**
+ * Convert a microsecond value to decimal milliseconds. Normally we use
+ * `asDuration`, but this is used in places like tables where we always want
+ * the same units.
+ */
+export function asMillisecondDuration(time: number) {
+ return convertTo({
+ unit: 'milliseconds',
+ microseconds: time,
+ }).formatted;
+}
diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/.storybook/webpack.config.js
index 963cf831ef698..4d83a3d4fa70f 100644
--- a/x-pack/plugins/canvas/.storybook/webpack.config.js
+++ b/x-pack/plugins/canvas/.storybook/webpack.config.js
@@ -136,16 +136,18 @@ module.exports = async ({ config }) => {
// Copy the DLL files to the Webpack build for use in the Storybook UI
config.plugins.push(
- new CopyWebpackPlugin([
- {
- from: path.resolve(DLL_OUTPUT, 'dll.js'),
- to: 'dll.js',
- },
- {
- from: path.resolve(DLL_OUTPUT, 'dll.css'),
- to: 'dll.css',
- },
- ])
+ new CopyWebpackPlugin({
+ patterns: [
+ {
+ from: path.resolve(DLL_OUTPUT, 'dll.js'),
+ to: 'dll.js',
+ },
+ {
+ from: path.resolve(DLL_OUTPUT, 'dll.css'),
+ to: 'dll.css',
+ },
+ ],
+ })
);
config.plugins.push(
diff --git a/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts b/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts
deleted file mode 100644
index 4137293cf39c0..0000000000000
--- a/x-pack/plugins/cross_cluster_replication/server/lib/is_es_error.ts
+++ /dev/null
@@ -1,13 +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 * as legacyElasticsearch from 'elasticsearch';
-
-const esErrorsParent = legacyElasticsearch.errors._Abstract;
-
-export function isEsError(err: Error) {
- return err instanceof esErrorsParent;
-}
diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts
index f30378d874a9a..d9fe296553f38 100644
--- a/x-pack/plugins/cross_cluster_replication/server/plugin.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts
@@ -30,7 +30,7 @@ import { registerApiRoutes } from './routes';
import { License } from './services';
import { elasticsearchJsPlugin } from './client/elasticsearch_ccr';
import { CrossClusterReplicationConfig } from './config';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
interface CrossClusterReplicationContext {
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts
index b41b52e1764c8..0b5f04556596a 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_create_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts
index e610d09b44275..7468c643a3aa6 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_delete_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts
index dd102c45665cb..1aa7112c75276 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_fetch_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts
index d5889074651f5..980128027c2f9 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_get_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts
index 1eaac02918b88..5b27c77ca86de 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_pause_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts
index 9839761e701fc..afea0f631fe48 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_resume_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts
index 85f2270ec3aee..bdce84f6404b1 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/auto_follow_pattern/register_update_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts
index bba82b04ce9a0..ccf7c469fe780 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_create_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts
index 151ab84fabf4c..e1ec28a7c90b1 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_fetch_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts
index 42d04ca65b1cb..99c871d5d4f2d 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_get_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts
index 82cb88cbacea7..3d28d36ac6182 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_pause_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts
index 04167c5db3162..09975b262dca8 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_resume_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts
index 6302d5868b0db..5f0d148bfcae9 100644
--- a/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/routes/api/follower_index/register_unfollow_route.test.ts
@@ -7,7 +7,7 @@
import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
import { IRouter, kibanaResponseFactory, RequestHandler } from 'src/core/server';
-import { isEsError } from '../../../lib/is_es_error';
+import { isEsError } from '../../../shared_imports';
import { formatEsError } from '../../../lib/format_es_error';
import { License } from '../../../services';
import { mockRouteContext } from '../test_lib';
diff --git a/x-pack/plugins/remote_clusters/server/lib/is_es_error/index.ts b/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts
similarity index 76%
rename from x-pack/plugins/remote_clusters/server/lib/is_es_error/index.ts
rename to x-pack/plugins/cross_cluster_replication/server/shared_imports.ts
index a9a3c61472d8c..454beda5394c7 100644
--- a/x-pack/plugins/remote_clusters/server/lib/is_es_error/index.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/shared_imports.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsError } from './is_es_error';
+export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts
index 049d440e3d85d..c287acf86eb2b 100644
--- a/x-pack/plugins/cross_cluster_replication/server/types.ts
+++ b/x-pack/plugins/cross_cluster_replication/server/types.ts
@@ -9,7 +9,7 @@ import { LicensingPluginSetup } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server';
import { RemoteClustersPluginSetup } from '../../remote_clusters/server';
import { License } from './services';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
import { formatEsError } from './lib/format_es_error';
export interface Dependencies {
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
index 3013f9966f068..6b8820b92ba84 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.test.ts
@@ -25,6 +25,7 @@ describe('Async search strategy', () => {
mockCoreSetup = coreMock.createSetup();
mockDataStart = dataPluginMock.createStartContract();
(mockDataStart.search.getSearchStrategy as jest.Mock).mockReturnValue({ search: mockSearch });
+
mockCoreSetup.getStartServices.mockResolvedValue([
undefined as any,
{ data: mockDataStart },
@@ -92,6 +93,7 @@ describe('Async search strategy', () => {
await asyncSearch.search(mockRequest, mockOptions).toPromise();
+ expect(mockDataStart.search.getSearchStrategy).toBeCalledTimes(1);
expect(mockSearch).toBeCalledTimes(2);
expect(mockSearch.mock.calls[0][0]).toEqual(mockRequest);
expect(mockSearch.mock.calls[1][0]).toEqual({ id: 1, serverStrategy: 'foo' });
diff --git a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
index 7de4dd28ad3d7..49b27bba33a60 100644
--- a/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
+++ b/x-pack/plugins/data_enhanced/public/search/async_search_strategy.ts
@@ -70,7 +70,7 @@ export function asyncSearchStrategyProvider(
return timer(pollInterval).pipe(
// Send future requests using just the ID from the response
mergeMap(() => {
- return search({ id, serverStrategy }, options);
+ return syncSearch.search({ id, serverStrategy }, options);
})
);
}),
diff --git a/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts b/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts
deleted file mode 100644
index 2f514b93e5016..0000000000000
--- a/x-pack/plugins/index_lifecycle_management/server/lib/is_es_error.ts
+++ /dev/null
@@ -1,13 +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 * as legacyElasticsearch from 'elasticsearch';
-
-const esErrorsParent = legacyElasticsearch.errors._Abstract;
-
-export function isEsError(err: Error): boolean {
- return err instanceof esErrorsParent;
-}
diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts
index faeac67f62a21..79a9849e634b5 100644
--- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts
@@ -14,7 +14,7 @@ import { Dependencies } from './types';
import { registerApiRoutes } from './routes';
import { License } from './services';
import { IndexLifecycleManagementConfig } from './config';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
const indexLifecycleDataEnricher = async (indicesList: any, callAsCurrentUser: APICaller) => {
if (!indicesList || !indicesList.length) {
diff --git a/x-pack/plugins/ingest_pipelines/server/lib/index.ts b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts
similarity index 76%
rename from x-pack/plugins/ingest_pipelines/server/lib/index.ts
rename to x-pack/plugins/index_lifecycle_management/server/shared_imports.ts
index a9a3c61472d8c..454beda5394c7 100644
--- a/x-pack/plugins/ingest_pipelines/server/lib/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/shared_imports.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsError } from './is_es_error';
+export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
diff --git a/x-pack/plugins/index_lifecycle_management/server/types.ts b/x-pack/plugins/index_lifecycle_management/server/types.ts
index 734d05a82000e..8ea0956be0865 100644
--- a/x-pack/plugins/index_lifecycle_management/server/types.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/types.ts
@@ -10,7 +10,7 @@ import { LicensingPluginSetup } from '../../licensing/server';
import { IndexManagementPluginSetup } from '../../index_management/server';
import { License } from './services';
import { IndexLifecycleManagementConfig } from './config';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
export interface Dependencies {
licensing: LicensingPluginSetup;
diff --git a/x-pack/plugins/index_management/server/lib/is_es_error.ts b/x-pack/plugins/index_management/server/lib/is_es_error.ts
deleted file mode 100644
index 4137293cf39c0..0000000000000
--- a/x-pack/plugins/index_management/server/lib/is_es_error.ts
+++ /dev/null
@@ -1,13 +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 * as legacyElasticsearch from 'elasticsearch';
-
-const esErrorsParent = legacyElasticsearch.errors._Abstract;
-
-export function isEsError(err: Error) {
- return err instanceof esErrorsParent;
-}
diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts
index c1b9945c2cd1d..94af8415bfd55 100644
--- a/x-pack/plugins/index_management/server/plugin.ts
+++ b/x-pack/plugins/index_management/server/plugin.ts
@@ -24,7 +24,7 @@ import { PLUGIN } from '../common';
import { Dependencies } from './types';
import { ApiRoutes } from './routes';
import { License, IndexDataEnricher } from './services';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
import { elasticsearchJsPlugin } from './client/elasticsearch';
export interface DataManagementContext {
diff --git a/x-pack/plugins/watcher/server/lib/is_es_error/index.ts b/x-pack/plugins/index_management/server/shared_imports.ts
similarity index 76%
rename from x-pack/plugins/watcher/server/lib/is_es_error/index.ts
rename to x-pack/plugins/index_management/server/shared_imports.ts
index a9a3c61472d8c..454beda5394c7 100644
--- a/x-pack/plugins/watcher/server/lib/is_es_error/index.ts
+++ b/x-pack/plugins/index_management/server/shared_imports.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsError } from './is_es_error';
+export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts
index 1482d9225c7b5..dc151f510a043 100644
--- a/x-pack/plugins/index_management/server/types.ts
+++ b/x-pack/plugins/index_management/server/types.ts
@@ -7,7 +7,7 @@ import { ScopedClusterClient, IRouter } from 'src/core/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server';
import { License, IndexDataEnricher } from './services';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
export interface Dependencies {
security: SecurityPluginSetup;
diff --git a/x-pack/plugins/infra/common/alerting/metrics/index.ts b/x-pack/plugins/infra/common/alerting/metrics/index.ts
new file mode 100644
index 0000000000000..2c0a1bd9b2589
--- /dev/null
+++ b/x-pack/plugins/infra/common/alerting/metrics/index.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.
+ */
+
+export * from './types';
+export const INFRA_ALERT_PREVIEW_PATH = '/api/infra/alerting/preview';
+
+export const TOO_MANY_BUCKETS_PREVIEW_EXCEPTION = 'TOO_MANY_BUCKETS_PREVIEW_EXCEPTION';
+export interface TooManyBucketsPreviewExceptionMetadata {
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION: any;
+ maxBuckets: number;
+}
+export const isTooManyBucketsPreviewException = (
+ value: any
+): value is TooManyBucketsPreviewExceptionMetadata =>
+ Boolean(value && value.TOO_MANY_BUCKETS_PREVIEW_EXCEPTION);
diff --git a/x-pack/plugins/infra/common/alerting/metrics/types.ts b/x-pack/plugins/infra/common/alerting/metrics/types.ts
new file mode 100644
index 0000000000000..a6184080cb774
--- /dev/null
+++ b/x-pack/plugins/infra/common/alerting/metrics/types.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 * as rt from 'io-ts';
+
+// TODO: Have threshold and inventory alerts import these types from this file instead of from their
+// local directories
+export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
+export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
+
+export enum Comparator {
+ GT = '>',
+ LT = '<',
+ GT_OR_EQ = '>=',
+ LT_OR_EQ = '<=',
+ BETWEEN = 'between',
+ OUTSIDE_RANGE = 'outside',
+}
+
+export enum Aggregators {
+ COUNT = 'count',
+ AVERAGE = 'avg',
+ SUM = 'sum',
+ MIN = 'min',
+ MAX = 'max',
+ RATE = 'rate',
+ CARDINALITY = 'cardinality',
+ P95 = 'p95',
+ P99 = 'p99',
+}
+
+// Alert Preview API
+const baseAlertRequestParamsRT = rt.intersection([
+ rt.partial({
+ filterQuery: rt.union([rt.string, rt.undefined]),
+ sourceId: rt.string,
+ }),
+ rt.type({
+ lookback: rt.union([rt.literal('h'), rt.literal('d'), rt.literal('w'), rt.literal('M')]),
+ criteria: rt.array(rt.any),
+ alertInterval: rt.string,
+ }),
+]);
+
+const metricThresholdAlertPreviewRequestParamsRT = rt.intersection([
+ baseAlertRequestParamsRT,
+ rt.partial({
+ groupBy: rt.union([rt.string, rt.array(rt.string), rt.undefined]),
+ }),
+ rt.type({
+ alertType: rt.literal(METRIC_THRESHOLD_ALERT_TYPE_ID),
+ }),
+]);
+export type MetricThresholdAlertPreviewRequestParams = rt.TypeOf<
+ typeof metricThresholdAlertPreviewRequestParamsRT
+>;
+
+const inventoryAlertPreviewRequestParamsRT = rt.intersection([
+ baseAlertRequestParamsRT,
+ rt.type({
+ nodeType: rt.string,
+ alertType: rt.literal(METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID),
+ }),
+]);
+
+export const alertPreviewRequestParamsRT = rt.union([
+ metricThresholdAlertPreviewRequestParamsRT,
+ inventoryAlertPreviewRequestParamsRT,
+]);
+
+export const alertPreviewSuccessResponsePayloadRT = rt.type({
+ numberOfGroups: rt.number,
+ resultTotals: rt.type({
+ fired: rt.number,
+ noData: rt.number,
+ error: rt.number,
+ tooManyBuckets: rt.number,
+ }),
+});
diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
index d5d61733e8717..febf849ccc943 100644
--- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
+++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx
@@ -4,25 +4,36 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { debounce } from 'lodash';
+import { debounce, pick } from 'lodash';
+import * as rt from 'io-ts';
+import { HttpSetup } from 'src/core/public';
import React, { ChangeEvent, useCallback, useMemo, useEffect, useState } from 'react';
import {
EuiSpacer,
EuiText,
EuiFormRow,
+ EuiButton,
EuiButtonEmpty,
EuiCheckbox,
EuiToolTip,
EuiIcon,
EuiFieldSearch,
+ EuiSelect,
+ EuiFlexGroup,
+ EuiFlexItem,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { getIntervalInSeconds } from '../../../../server/utils/get_interval_in_seconds';
import {
Comparator,
Aggregators,
- // eslint-disable-next-line @kbn/eslint/no-restricted-paths
-} from '../../../../server/lib/alerting/metric_threshold/types';
+ INFRA_ALERT_PREVIEW_PATH,
+ alertPreviewRequestParamsRT,
+ alertPreviewSuccessResponsePayloadRT,
+ METRIC_THRESHOLD_ALERT_TYPE_ID,
+} from '../../../../common/alerting/metrics';
import {
ForLastExpression,
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
@@ -40,6 +51,7 @@ import { convertKueryToElasticSearchQuery } from '../../../utils/kuery';
import { ExpressionRow } from './expression_row';
import { AlertContextMeta, TimeUnit, MetricExpression } from '../types';
import { ExpressionChart } from './expression_chart';
+import { validateMetricThreshold } from './validation';
const FILTER_TYPING_DEBOUNCE_MS = 500;
@@ -54,6 +66,7 @@ interface Props {
alertOnNoData?: boolean;
};
alertsContext: AlertsContextValue;
+ alertInterval: string;
setAlertParams(key: string, value: any): void;
setAlertProperty(key: string, value: any): void;
}
@@ -66,8 +79,24 @@ const defaultExpression = {
timeUnit: 'm',
} as MetricExpression;
+async function getAlertPreview({
+ fetch,
+ params,
+}: {
+ fetch: HttpSetup['fetch'];
+ params: rt.TypeOf;
+}): Promise> {
+ return await fetch(`${INFRA_ALERT_PREVIEW_PATH}`, {
+ method: 'POST',
+ body: JSON.stringify({
+ ...params,
+ alertType: METRIC_THRESHOLD_ALERT_TYPE_ID,
+ }),
+ });
+}
+
export const Expressions: React.FC = (props) => {
- const { setAlertParams, alertParams, errors, alertsContext } = props;
+ const { setAlertParams, alertParams, errors, alertsContext, alertInterval } = props;
const { source, createDerivedIndexPattern } = useSourceViaHttp({
sourceId: 'default',
type: 'metrics',
@@ -75,6 +104,13 @@ export const Expressions: React.FC = (props) => {
toastWarning: alertsContext.toastNotifications.addWarning,
});
+ const [previewLookbackInterval, setPreviewLookbackInterval] = useState('h');
+ const [isPreviewLoading, setIsPreviewLoading] = useState(false);
+ const [previewError, setPreviewError] = useState(false);
+ const [previewResult, setPreviewResult] = useState | null>(null);
+
const [timeSize, setTimeSize] = useState(1);
const [timeUnit, setTimeUnit] = useState('m');
const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [
@@ -143,7 +179,7 @@ export const Expressions: React.FC = (props) => {
const onGroupByChange = useCallback(
(group: string | null | string[]) => {
- setAlertParams('groupBy', group || '');
+ setAlertParams('groupBy', group && group.length ? group : '');
},
[setAlertParams]
);
@@ -224,6 +260,33 @@ export const Expressions: React.FC = (props) => {
}
}, [alertsContext.metadata, derivedIndexPattern, setAlertParams]);
+ const onSelectPreviewLookbackInterval = useCallback((e) => {
+ setPreviewLookbackInterval(e.target.value);
+ setPreviewResult(null);
+ }, []);
+
+ const onClickPreview = useCallback(async () => {
+ setIsPreviewLoading(true);
+ setPreviewResult(null);
+ setPreviewError(false);
+ try {
+ const result = await getAlertPreview({
+ fetch: alertsContext.http.fetch,
+ params: {
+ ...pick(alertParams, 'criteria', 'groupBy', 'filterQuery'),
+ sourceId: alertParams.sourceId,
+ lookback: previewLookbackInterval as 'h' | 'd' | 'w' | 'M',
+ alertInterval,
+ },
+ });
+ setPreviewResult(result);
+ } catch (e) {
+ setPreviewError(true);
+ } finally {
+ setIsPreviewLoading(false);
+ }
+ }, [alertParams, alertInterval, alertsContext, previewLookbackInterval]);
+
useEffect(() => {
if (alertParams.criteria && alertParams.criteria.length) {
setTimeSize(alertParams.criteria[0].timeSize);
@@ -246,6 +309,23 @@ export const Expressions: React.FC = (props) => {
[onFilterChange]
);
+ const previewIntervalError = useMemo(() => {
+ const intervalInSeconds = getIntervalInSeconds(alertInterval);
+ const lookbackInSeconds = getIntervalInSeconds(`1${previewLookbackInterval}`);
+ if (intervalInSeconds >= lookbackInSeconds) {
+ return true;
+ }
+ return false;
+ }, [previewLookbackInterval, alertInterval]);
+
+ const isPreviewDisabled = useMemo(() => {
+ const validationResult = validateMetricThreshold({ criteria: alertParams.criteria } as any);
+ const hasValidationErrors = Object.values(validationResult.errors).some((result) =>
+ Object.values(result).some((arr) => Array.isArray(arr) && arr.length)
+ );
+ return hasValidationErrors || previewIntervalError;
+ }, [alertParams.criteria, previewIntervalError]);
+
return (
<>
@@ -381,10 +461,191 @@ export const Expressions: React.FC = (props) => {
}}
/>
+
+
+
+ <>
+
+
+
+
+
+
+ {i18n.translate('xpack.infra.metrics.alertFlyout.testAlertTrigger', {
+ defaultMessage: 'Test alert trigger',
+ })}
+
+
+
+
+ {previewResult && !previewIntervalError && !previewResult.resultTotals.tooManyBuckets && (
+ <>
+
+
+ {previewResult.resultTotals.fired},
+ lookback: previewOptions.find((e) => e.value === previewLookbackInterval)
+ ?.shortText,
+ }}
+ />{' '}
+ {alertParams.groupBy ? (
+ {previewResult.numberOfGroups},
+ groupName: alertParams.groupBy,
+ plural: previewResult.numberOfGroups !== 1 ? 's' : '',
+ }}
+ />
+ ) : (
+
+ )}
+
+ {alertParams.alertOnNoData && previewResult.resultTotals.noData ? (
+ <>
+
+
+ {previewResult.resultTotals.noData},
+ plural: previewResult.resultTotals.noData !== 1 ? 's' : '',
+ }}
+ />
+
+ >
+ ) : null}
+ {previewResult.resultTotals.error ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+ >
+ )}
+ {previewResult && previewResult.resultTotals.tooManyBuckets ? (
+ <>
+
+
+ FOR THE LAST,
+ }}
+ />
+
+ >
+ ) : null}
+ {previewIntervalError && (
+ <>
+
+
+ check every,
+ }}
+ />
+
+ >
+ )}
+ {previewError && (
+ <>
+
+
+
+
+ >
+ )}
+ >
+
+
>
);
};
+const previewOptions = [
+ {
+ value: 'h',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastHourLabel', {
+ defaultMessage: 'Last hour',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.hourLabel', {
+ defaultMessage: 'hour',
+ }),
+ },
+ {
+ value: 'd',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastDayLabel', {
+ defaultMessage: 'Last day',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.dayLabel', {
+ defaultMessage: 'day',
+ }),
+ },
+ {
+ value: 'w',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastWeekLabel', {
+ defaultMessage: 'Last week',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.weekLabel', {
+ defaultMessage: 'week',
+ }),
+ },
+ {
+ value: 'M',
+ text: i18n.translate('xpack.infra.metrics.alertFlyout.lastMonthLabel', {
+ defaultMessage: 'Last month',
+ }),
+ shortText: i18n.translate('xpack.infra.metrics.alertFlyout.monthLabel', {
+ defaultMessage: 'month',
+ }),
+ },
+];
+
+const firedTimeLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTime', {
+ defaultMessage: 'time',
+});
+const firedTimesLabel = i18n.translate('xpack.infra.metrics.alertFlyout.firedTimes', {
+ defaultMessage: 'times',
+});
+
// required for dynamic import
// eslint-disable-next-line import/no-default-export
export default Expressions;
diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts
index 06135c6532d77..6fbdeff950d1a 100644
--- a/x-pack/plugins/infra/server/infra_server.ts
+++ b/x-pack/plugins/infra/server/infra_server.ts
@@ -32,6 +32,7 @@ import {
import { initInventoryMetaRoute } from './routes/inventory_metadata';
import { initLogSourceConfigurationRoutes, initLogSourceStatusRoutes } from './routes/log_sources';
import { initSourceRoute } from './routes/source';
+import { initAlertPreviewRoute } from './routes/alerting';
export const initInfraServer = (libs: InfraBackendLibs) => {
const schema = makeExecutableSchema({
@@ -64,4 +65,5 @@ export const initInfraServer = (libs: InfraBackendLibs) => {
initInventoryMetaRoute(libs);
initLogSourceConfigurationRoutes(libs);
initLogSourceStatusRoutes(libs);
+ initAlertPreviewRoute(libs);
};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_percentile_aggregation.ts
similarity index 93%
rename from x-pack/plugins/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts
rename to x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_percentile_aggregation.ts
index 2c83f6ecfd705..3a5c53ca80880 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/create_percentile_aggregation.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_percentile_aggregation.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Aggregators } from './types';
+import { Aggregators } from '../types';
export const createPercentileAggregation = (
type: Aggregators.P95 | Aggregators.P99,
field: string
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
new file mode 100644
index 0000000000000..49b191c4e85c9
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
@@ -0,0 +1,190 @@
+/*
+ * 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 { mapValues, first, last, isNaN } from 'lodash';
+import {
+ TooManyBucketsPreviewExceptionMetadata,
+ isTooManyBucketsPreviewException,
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
+} from '../../../../../common/alerting/metrics';
+import { InfraSource } from '../../../../../common/http_api/source_api';
+import { InfraDatabaseSearchResponse } from '../../../adapters/framework/adapter_types';
+import { createAfterKeyHandler } from '../../../../utils/create_afterkey_handler';
+import { AlertServices, AlertExecutorOptions } from '../../../../../../alerts/server';
+import { getAllCompositeData } from '../../../../utils/get_all_composite_data';
+import { MetricExpressionParams, Comparator, Aggregators } from '../types';
+import { DOCUMENT_COUNT_I18N } from '../messages';
+import { getElasticsearchMetricQuery } from './metric_query';
+
+interface Aggregation {
+ aggregatedIntervals: {
+ buckets: Array<{
+ aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
+ doc_count: number;
+ }>;
+ };
+}
+
+interface CompositeAggregationsResponse {
+ groupings: {
+ buckets: Aggregation[];
+ };
+}
+
+export const evaluateAlert = (
+ callCluster: AlertServices['callCluster'],
+ params: AlertExecutorOptions['params'],
+ config: InfraSource['configuration'],
+ timeframe?: { start: number; end: number }
+) => {
+ const { criteria, groupBy, filterQuery } = params as {
+ criteria: MetricExpressionParams[];
+ groupBy: string | undefined | string[];
+ filterQuery: string | undefined;
+ };
+ return Promise.all(
+ criteria.map(async (criterion) => {
+ const currentValues = await getMetric(
+ callCluster,
+ criterion,
+ config.metricAlias,
+ config.fields.timestamp,
+ groupBy,
+ filterQuery,
+ timeframe
+ );
+ const { threshold, comparator } = criterion;
+ const comparisonFunction = comparatorMap[comparator];
+ return mapValues(
+ currentValues,
+ (values: number | number[] | null | TooManyBucketsPreviewExceptionMetadata) => {
+ if (isTooManyBucketsPreviewException(values)) throw values;
+ return {
+ ...criterion,
+ metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
+ currentValue: Array.isArray(values) ? last(values) : NaN,
+ shouldFire: Array.isArray(values)
+ ? values.map((value) => comparisonFunction(value, threshold))
+ : [false],
+ isNoData: values === null,
+ isError: isNaN(values),
+ };
+ }
+ );
+ })
+ );
+};
+
+const getMetric: (
+ callCluster: AlertServices['callCluster'],
+ params: MetricExpressionParams,
+ index: string,
+ timefield: string,
+ groupBy: string | undefined | string[],
+ filterQuery: string | undefined,
+ timeframe?: { start: number; end: number }
+) => Promise> = async function (
+ callCluster,
+ params,
+ index,
+ timefield,
+ groupBy,
+ filterQuery,
+ timeframe
+) {
+ const { aggType } = params;
+ const hasGroupBy = groupBy && groupBy.length;
+ const searchBody = getElasticsearchMetricQuery(
+ params,
+ timefield,
+ hasGroupBy ? groupBy : undefined,
+ filterQuery,
+ timeframe
+ );
+
+ try {
+ if (hasGroupBy) {
+ const bucketSelector = (
+ response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse>
+ ) => response.aggregations?.groupings?.buckets || [];
+ const afterKeyHandler = createAfterKeyHandler(
+ 'aggs.groupings.composite.after',
+ (response) => response.aggregations?.groupings?.after_key
+ );
+ const compositeBuckets = (await getAllCompositeData(
+ (body) => callCluster('search', { body, index }),
+ searchBody,
+ bucketSelector,
+ afterKeyHandler
+ )) as Array }>;
+ return compositeBuckets.reduce(
+ (result, bucket) => ({
+ ...result,
+ [Object.values(bucket.key)
+ .map((value) => value)
+ .join(', ')]: getValuesFromAggregations(bucket, aggType),
+ }),
+ {}
+ );
+ }
+ const result = await callCluster('search', {
+ body: searchBody,
+ index,
+ });
+
+ return { '*': getValuesFromAggregations(result.aggregations, aggType) };
+ } catch (e) {
+ if (timeframe) {
+ // This code should only ever be reached when previewing the alert, not executing it
+ const causedByType = e.body?.error?.caused_by?.type;
+ if (causedByType === 'too_many_buckets_exception') {
+ return {
+ '*': {
+ [TOO_MANY_BUCKETS_PREVIEW_EXCEPTION]: true,
+ maxBuckets: e.body.error.caused_by.max_buckets,
+ },
+ };
+ }
+ }
+ return { '*': NaN }; // Trigger an Error state
+ }
+};
+
+const getValuesFromAggregations = (
+ aggregations: Aggregation,
+ aggType: MetricExpressionParams['aggType']
+) => {
+ try {
+ const { buckets } = aggregations.aggregatedIntervals;
+ if (!buckets.length) return null; // No Data state
+ if (aggType === Aggregators.COUNT) {
+ return buckets.map((bucket) => bucket.doc_count);
+ }
+ if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
+ return buckets.map((bucket) => {
+ const values = bucket.aggregatedValue?.values || [];
+ const firstValue = first(values);
+ if (!firstValue) return null;
+ return firstValue.value;
+ });
+ }
+ return buckets.map((bucket) => bucket.aggregatedValue.value);
+ } catch (e) {
+ return NaN; // Error state
+ }
+};
+
+const comparatorMap = {
+ [Comparator.BETWEEN]: (value: number, [a, b]: number[]) =>
+ value >= Math.min(a, b) && value <= Math.max(a, b),
+ [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b,
+ // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is
+ // used; all other compartors will just destructure the first value in the array
+ [Comparator.GT]: (a: number, [b]: number[]) => a > b,
+ [Comparator.LT]: (a: number, [b]: number[]) => a < b,
+ [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b,
+ [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
new file mode 100644
index 0000000000000..5680035d9d609
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
@@ -0,0 +1,140 @@
+/*
+ * 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 { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
+import { MetricExpressionParams, Aggregators } from '../types';
+import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
+import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
+import { createPercentileAggregation } from './create_percentile_aggregation';
+
+const MINIMUM_BUCKETS = 5;
+
+const getParsedFilterQuery: (
+ filterQuery: string | undefined
+) => Record | Array> = (filterQuery) => {
+ if (!filterQuery) return {};
+ return JSON.parse(filterQuery).bool;
+};
+
+export const getElasticsearchMetricQuery = (
+ { metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
+ timefield: string,
+ groupBy?: string | string[],
+ filterQuery?: string,
+ timeframe?: { start: number; end: number }
+) => {
+ if (aggType === Aggregators.COUNT && metric) {
+ throw new Error('Cannot aggregate document count with a metric');
+ }
+ if (aggType !== Aggregators.COUNT && !metric) {
+ throw new Error('Can only aggregate without a metric if using the document count aggregator');
+ }
+ const interval = `${timeSize}${timeUnit}`;
+ const intervalAsSeconds = getIntervalInSeconds(interval);
+
+ const to = timeframe ? timeframe.end : Date.now();
+ // We need enough data for 5 buckets worth of data. We also need
+ // to convert the intervalAsSeconds to milliseconds.
+ const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS;
+
+ const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom;
+
+ const offset = getDateHistogramOffset(from, interval);
+
+ const aggregations =
+ aggType === Aggregators.COUNT
+ ? {}
+ : aggType === Aggregators.RATE
+ ? networkTraffic('aggregatedValue', metric)
+ : aggType === Aggregators.P95 || aggType === Aggregators.P99
+ ? createPercentileAggregation(aggType, metric)
+ : {
+ aggregatedValue: {
+ [aggType]: {
+ field: metric,
+ },
+ },
+ };
+
+ const baseAggs = {
+ aggregatedIntervals: {
+ date_histogram: {
+ field: timefield,
+ fixed_interval: interval,
+ offset,
+ extended_bounds: {
+ min: from,
+ max: to,
+ },
+ },
+ aggregations,
+ },
+ };
+
+ const aggs = groupBy
+ ? {
+ groupings: {
+ composite: {
+ size: 10,
+ sources: Array.isArray(groupBy)
+ ? groupBy.map((field, index) => ({
+ [`groupBy${index}`]: {
+ terms: { field },
+ },
+ }))
+ : [
+ {
+ groupBy0: {
+ terms: {
+ field: groupBy,
+ },
+ },
+ },
+ ],
+ },
+ aggs: baseAggs,
+ },
+ }
+ : baseAggs;
+
+ const rangeFilters = [
+ {
+ range: {
+ '@timestamp': {
+ gte: from,
+ lte: to,
+ format: 'epoch_millis',
+ },
+ },
+ },
+ ];
+
+ const metricFieldFilters = metric
+ ? [
+ {
+ exists: {
+ field: metric,
+ },
+ },
+ ]
+ : [];
+
+ const parsedFilterQuery = getParsedFilterQuery(filterQuery);
+
+ return {
+ query: {
+ bool: {
+ filter: [
+ ...rangeFilters,
+ ...metricFieldFilters,
+ ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []),
+ ],
+ ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}),
+ },
+ },
+ size: 0,
+ aggs,
+ };
+};
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index 8260ebed84622..f28137d980b9f 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -383,34 +383,6 @@ const executor = createMetricThresholdExecutor(mockLibs, 'test') as (opts: {
}) => Promise;
const services: AlertServicesMock = alertsMock.createAlertServices();
-services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
- if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
- const metric = body.query.bool.filter[1]?.exists.field;
- if (body.aggs.groupings) {
- if (body.aggs.groupings.composite.after) {
- return mocks.compositeEndResponse;
- }
- if (metric === 'test.metric.2') {
- return mocks.alternateCompositeResponse;
- }
- return mocks.basicCompositeResponse;
- }
- if (metric === 'test.metric.2') {
- return mocks.alternateMetricResponse;
- }
- return mocks.basicMetricResponse;
-});
-services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId: string) => {
- if (sourceId === 'alternate')
- return {
- id: 'alternate',
- attributes: { metricAlias: 'alternatebeat-*' },
- type,
- references: [],
- };
- return { id: 'default', attributes: { metricAlias: 'metricbeat-*' }, type, references: [] };
-});
-
services.callCluster.mockImplementation(async (_: string, { body, index }: any) => {
if (index === 'alternatebeat-*') return mocks.changedSourceIdResponse;
const metric = body.query.bool.filter[1]?.exists.field;
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 00a1d97dec811..4fe28fad68c85 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -3,263 +3,25 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { mapValues, first } from 'lodash';
+import { first, last } from 'lodash';
import { i18n } from '@kbn/i18n';
import moment from 'moment';
-import { InfraDatabaseSearchResponse } from '../../adapters/framework/adapter_types';
-import { createAfterKeyHandler } from '../../../utils/create_afterkey_handler';
-import { getAllCompositeData } from '../../../utils/get_all_composite_data';
-import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
-import { MetricExpressionParams, Comparator, Aggregators, AlertStates } from './types';
+import { AlertExecutorOptions } from '../../../../../alerts/server';
+import { InfraBackendLibs } from '../../infra_types';
+import { AlertStates } from './types';
+import { evaluateAlert } from './lib/evaluate_alert';
import {
buildErrorAlertReason,
buildFiredAlertReason,
buildNoDataAlertReason,
- DOCUMENT_COUNT_I18N,
stateToAlertMessage,
} from './messages';
-import { AlertServices, AlertExecutorOptions } from '../../../../../alerts/server';
-import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
-import { getDateHistogramOffset } from '../../snapshot/query_helpers';
-import { InfraBackendLibs } from '../../infra_types';
-import { createPercentileAggregation } from './create_percentile_aggregation';
-
-const TOTAL_BUCKETS = 5;
-
-interface Aggregation {
- aggregatedIntervals: {
- buckets: Array<{
- aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
- doc_count: number;
- }>;
- };
-}
-
-interface CompositeAggregationsResponse {
- groupings: {
- buckets: Aggregation[];
- };
-}
-
-const getCurrentValueFromAggregations = (
- aggregations: Aggregation,
- aggType: MetricExpressionParams['aggType']
-) => {
- try {
- const { buckets } = aggregations.aggregatedIntervals;
- if (!buckets.length) return null; // No Data state
- const mostRecentBucket = buckets[buckets.length - 1];
- if (aggType === Aggregators.COUNT) {
- return mostRecentBucket.doc_count;
- }
- if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
- const values = mostRecentBucket.aggregatedValue?.values || [];
- const firstValue = first(values);
- if (!firstValue) return null;
- return firstValue.value;
- }
- const { value } = mostRecentBucket.aggregatedValue;
- return value;
- } catch (e) {
- return undefined; // Error state
- }
-};
-
-const getParsedFilterQuery: (
- filterQuery: string | undefined
-) => Record | Array> = (filterQuery) => {
- if (!filterQuery) return {};
- return JSON.parse(filterQuery).bool;
-};
-
-export const getElasticsearchMetricQuery = (
- { metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
- timefield: string,
- groupBy?: string | string[],
- filterQuery?: string
-) => {
- if (aggType === Aggregators.COUNT && metric) {
- throw new Error('Cannot aggregate document count with a metric');
- }
- if (aggType !== Aggregators.COUNT && !metric) {
- throw new Error('Can only aggregate without a metric if using the document count aggregator');
- }
- const interval = `${timeSize}${timeUnit}`;
- const to = Date.now();
- const intervalAsSeconds = getIntervalInSeconds(interval);
- // We need enough data for 5 buckets worth of data. We also need
- // to convert the intervalAsSeconds to milliseconds.
- const from = to - intervalAsSeconds * 1000 * TOTAL_BUCKETS;
- const offset = getDateHistogramOffset(from, interval);
-
- const aggregations =
- aggType === Aggregators.COUNT
- ? {}
- : aggType === Aggregators.RATE
- ? networkTraffic('aggregatedValue', metric)
- : aggType === Aggregators.P95 || aggType === Aggregators.P99
- ? createPercentileAggregation(aggType, metric)
- : {
- aggregatedValue: {
- [aggType]: {
- field: metric,
- },
- },
- };
-
- const baseAggs = {
- aggregatedIntervals: {
- date_histogram: {
- field: timefield,
- fixed_interval: interval,
- offset,
- extended_bounds: {
- min: from,
- max: to,
- },
- },
- aggregations,
- },
- };
-
- const aggs = groupBy
- ? {
- groupings: {
- composite: {
- size: 10,
- sources: Array.isArray(groupBy)
- ? groupBy.map((field, index) => ({
- [`groupBy${index}`]: {
- terms: { field },
- },
- }))
- : [
- {
- groupBy0: {
- terms: {
- field: groupBy,
- },
- },
- },
- ],
- },
- aggs: baseAggs,
- },
- }
- : baseAggs;
-
- const rangeFilters = [
- {
- range: {
- '@timestamp': {
- gte: from,
- lte: to,
- format: 'epoch_millis',
- },
- },
- },
- ];
-
- const metricFieldFilters = metric
- ? [
- {
- exists: {
- field: metric,
- },
- },
- ]
- : [];
-
- const parsedFilterQuery = getParsedFilterQuery(filterQuery);
-
- return {
- query: {
- bool: {
- filter: [
- ...rangeFilters,
- ...metricFieldFilters,
- ...(Array.isArray(parsedFilterQuery) ? parsedFilterQuery : []),
- ],
- ...(!Array.isArray(parsedFilterQuery) ? parsedFilterQuery : {}),
- },
- },
- size: 0,
- aggs,
- };
-};
-
-const getMetric: (
- services: AlertServices,
- params: MetricExpressionParams,
- index: string,
- timefield: string,
- groupBy: string | undefined | string[],
- filterQuery: string | undefined
-) => Promise> = async function (
- { callCluster },
- params,
- index,
- timefield,
- groupBy,
- filterQuery
-) {
- const { aggType } = params;
- const searchBody = getElasticsearchMetricQuery(params, timefield, groupBy, filterQuery);
-
- try {
- if (groupBy) {
- const bucketSelector = (
- response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse>
- ) => response.aggregations?.groupings?.buckets || [];
- const afterKeyHandler = createAfterKeyHandler(
- 'aggs.groupings.composite.after',
- (response) => response.aggregations?.groupings?.after_key
- );
- const compositeBuckets = (await getAllCompositeData(
- (body) => callCluster('search', { body, index }),
- searchBody,
- bucketSelector,
- afterKeyHandler
- )) as Array }>;
- return compositeBuckets.reduce(
- (result, bucket) => ({
- ...result,
- [Object.values(bucket.key)
- .map((value) => value)
- .join(', ')]: getCurrentValueFromAggregations(bucket, aggType),
- }),
- {}
- );
- }
- const result = await callCluster('search', {
- body: searchBody,
- index,
- });
-
- return { '*': getCurrentValueFromAggregations(result.aggregations, aggType) };
- } catch (e) {
- return { '*': undefined }; // Trigger an Error state
- }
-};
-
-const comparatorMap = {
- [Comparator.BETWEEN]: (value: number, [a, b]: number[]) =>
- value >= Math.min(a, b) && value <= Math.max(a, b),
- [Comparator.OUTSIDE_RANGE]: (value: number, [a, b]: number[]) => value < a || value > b,
- // `threshold` is always an array of numbers in case the BETWEEN/OUTSIDE_RANGE comparator is
- // used; all other compartors will just destructure the first value in the array
- [Comparator.GT]: (a: number, [b]: number[]) => a > b,
- [Comparator.LT]: (a: number, [b]: number[]) => a < b,
- [Comparator.GT_OR_EQ]: (a: number, [b]: number[]) => a >= b,
- [Comparator.LT_OR_EQ]: (a: number, [b]: number[]) => a <= b,
-};
export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: string) =>
- async function ({ services, params }: AlertExecutorOptions) {
- const { criteria, groupBy, filterQuery, sourceId, alertOnNoData } = params as {
- criteria: MetricExpressionParams[];
- groupBy: string | undefined | string[];
- filterQuery: string | undefined;
+ async function (options: AlertExecutorOptions) {
+ const { services, params } = options;
+ const { criteria } = params;
+ const { sourceId, alertOnNoData } = params as {
sourceId?: string;
alertOnNoData: boolean;
};
@@ -269,39 +31,18 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
sourceId || 'default'
);
const config = source.configuration;
- const alertResults = await Promise.all(
- criteria.map((criterion) => {
- return (async () => {
- const currentValues = await getMetric(
- services,
- criterion,
- config.metricAlias,
- config.fields.timestamp,
- groupBy,
- filterQuery
- );
- const { threshold, comparator } = criterion;
- const comparisonFunction = comparatorMap[comparator];
- return mapValues(currentValues, (value) => ({
- ...criterion,
- metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
- currentValue: value,
- shouldFire:
- value !== undefined && value !== null && comparisonFunction(value, threshold),
- isNoData: value === null,
- isError: value === undefined,
- }));
- })();
- })
- );
+ const alertResults = await evaluateAlert(services.callCluster, params, config);
- // Because each alert result has the same group definitions, just grap the groups from the first one.
+ // Because each alert result has the same group definitions, just grab the groups from the first one.
const groups = Object.keys(first(alertResults));
for (const group of groups) {
const alertInstance = services.alertInstanceFactory(`${alertId}-${group}`);
// AND logic; all criteria must be across the threshold
- const shouldAlertFire = alertResults.every((result) => result[group].shouldFire);
+ const shouldAlertFire = alertResults.every((result) =>
+ // Grab the result of the most recent bucket
+ last(result[group].shouldFire)
+ );
// AND logic; because we need to evaluate all criteria, if one of them reports no data then the
// whole alert is in a No Data/Error state
const isNoData = alertResults.some((result) => result[group].isNoData);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
new file mode 100644
index 0000000000000..7aa8367f7678c
--- /dev/null
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/preview_metric_threshold_alert.ts
@@ -0,0 +1,168 @@
+/*
+ * 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 { first, zip } from 'lodash';
+import {
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
+ isTooManyBucketsPreviewException,
+} from '../../../../common/alerting/metrics';
+import { IScopedClusterClient } from '../../../../../../../src/core/server';
+import { InfraSource } from '../../../../common/http_api/source_api';
+import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds';
+import { MetricExpressionParams } from './types';
+import { evaluateAlert } from './lib/evaluate_alert';
+
+const MAX_ITERATIONS = 50;
+
+interface PreviewMetricThresholdAlertParams {
+ callCluster: IScopedClusterClient['callAsCurrentUser'];
+ params: {
+ criteria: MetricExpressionParams[];
+ groupBy: string | undefined | string[];
+ filterQuery: string | undefined;
+ };
+ config: InfraSource['configuration'];
+ lookback: 'h' | 'd' | 'w' | 'M';
+ alertInterval: string;
+ end?: number;
+ overrideLookbackIntervalInSeconds?: number;
+}
+
+export const previewMetricThresholdAlert: (
+ params: PreviewMetricThresholdAlertParams,
+ iterations?: number,
+ precalculatedNumberOfGroups?: number
+) => Promise> = async (
+ {
+ callCluster,
+ params,
+ config,
+ lookback,
+ alertInterval,
+ end = Date.now(),
+ overrideLookbackIntervalInSeconds,
+ },
+ iterations = 0,
+ precalculatedNumberOfGroups
+) => {
+ // There are three different "intervals" we're dealing with here, so to disambiguate:
+ // - The lookback interval, which is how long of a period of time we want to examine to count
+ // how many times the alert fired
+ // - The interval in the alert params, which we'll call the bucket interval; this is how large of
+ // a time bucket the alert uses to evaluate its result
+ // - The alert interval, which is how often the alert fires
+
+ const { timeSize, timeUnit } = params.criteria[0];
+ const bucketInterval = `${timeSize}${timeUnit}`;
+ const bucketIntervalInSeconds = getIntervalInSeconds(bucketInterval);
+
+ const lookbackInterval = `1${lookback}`;
+ const lookbackIntervalInSeconds =
+ overrideLookbackIntervalInSeconds ?? getIntervalInSeconds(lookbackInterval);
+
+ const start = end - lookbackIntervalInSeconds * 1000;
+ const timeframe = { start, end };
+
+ // Get a date histogram using the bucket interval and the lookback interval
+ try {
+ const alertResults = await evaluateAlert(callCluster, params, config, timeframe);
+ const groups = Object.keys(first(alertResults));
+
+ // Now determine how to interpolate this histogram based on the alert interval
+ const alertIntervalInSeconds = getIntervalInSeconds(alertInterval);
+ const alertResultsPerExecution = alertIntervalInSeconds / bucketIntervalInSeconds;
+ const previewResults = await Promise.all(
+ groups.map(async (group) => {
+ const tooManyBuckets = alertResults.some((alertResult) =>
+ isTooManyBucketsPreviewException(alertResult[group])
+ );
+ if (tooManyBuckets) {
+ return TOO_MANY_BUCKETS_PREVIEW_EXCEPTION;
+ }
+
+ const isNoData = alertResults.some((alertResult) => alertResult[group].isNoData);
+ if (isNoData) {
+ return null;
+ }
+ const isError = alertResults.some((alertResult) => alertResult[group].isError);
+ if (isError) {
+ return NaN;
+ }
+
+ // Interpolate the buckets returned by evaluateAlert and return a count of how many of these
+ // buckets would have fired the alert. If the alert interval and bucket interval are the same,
+ // this will be a 1:1 evaluation of the alert results. If these are different, the interpolation
+ // will skip some buckets or read some buckets more than once, depending on the differential
+ const numberOfResultBuckets = first(alertResults)[group].shouldFire.length;
+ const numberOfExecutionBuckets = Math.floor(
+ numberOfResultBuckets / alertResultsPerExecution
+ );
+ let numberOfTimesFired = 0;
+ for (let i = 0; i < numberOfExecutionBuckets; i++) {
+ const mappedBucketIndex = Math.floor(i * alertResultsPerExecution);
+ const allConditionsFiredInMappedBucket = alertResults.every(
+ (alertResult) => alertResult[group].shouldFire[mappedBucketIndex]
+ );
+ if (allConditionsFiredInMappedBucket) numberOfTimesFired++;
+ }
+ return numberOfTimesFired;
+ })
+ );
+ return previewResults;
+ } catch (e) {
+ if (isTooManyBucketsPreviewException(e)) {
+ // If there's too much data on the first request, recursively slice the lookback interval
+ // until all the data can be retrieved
+ const basePreviewParams = { callCluster, params, config, lookback, alertInterval };
+ const { maxBuckets } = e;
+ // If this is still the first iteration, try to get the number of groups in order to
+ // calculate max buckets. If this fails, just estimate based on 1 group
+ const currentAlertResults = !precalculatedNumberOfGroups
+ ? await evaluateAlert(callCluster, params, config)
+ : [];
+ const numberOfGroups =
+ precalculatedNumberOfGroups ?? Math.max(Object.keys(first(currentAlertResults)).length, 1);
+ const estimatedTotalBuckets =
+ (lookbackIntervalInSeconds / bucketIntervalInSeconds) * numberOfGroups;
+ // The minimum number of slices is 2. In case we underestimate the total number of buckets
+ // in the first iteration, we can bisect the remaining buckets on further recursions to get
+ // all the data needed
+ const slices = Math.max(Math.ceil(estimatedTotalBuckets / maxBuckets), 2);
+ const slicedLookback = Math.floor(lookbackIntervalInSeconds / slices);
+
+ // Bail out if it looks like this is going to take too long
+ if (slicedLookback <= 0 || iterations > MAX_ITERATIONS || slices > MAX_ITERATIONS) {
+ return [TOO_MANY_BUCKETS_PREVIEW_EXCEPTION];
+ }
+
+ const slicedRequests = [...Array(slices)].map((_, i) => {
+ return previewMetricThresholdAlert(
+ {
+ ...basePreviewParams,
+ end: Math.min(end, start + slicedLookback * (i + 1) * 1000),
+ overrideLookbackIntervalInSeconds: slicedLookback,
+ },
+ iterations + slices,
+ numberOfGroups
+ );
+ });
+ const results = await Promise.all(slicedRequests);
+ const zippedResult = zip(...results).map((result) =>
+ result
+ // `undefined` values occur if there is no data at all in a certain slice, and that slice
+ // returns an empty array. This is different from an error or no data state,
+ // so filter these results out entirely and only regard the resultA portion
+ .filter((value) => typeof value !== 'undefined')
+ .reduce((a, b) => {
+ if (typeof a !== 'number') return a;
+ if (typeof b !== 'number') return b;
+ return a + b;
+ })
+ );
+ return zippedResult;
+ } else throw e;
+ }
+};
diff --git a/x-pack/plugins/painless_lab/server/lib/index.ts b/x-pack/plugins/infra/server/routes/alerting/index.ts
similarity index 84%
rename from x-pack/plugins/painless_lab/server/lib/index.ts
rename to x-pack/plugins/infra/server/routes/alerting/index.ts
index a9a3c61472d8c..4ba2f56360f8a 100644
--- a/x-pack/plugins/painless_lab/server/lib/index.ts
+++ b/x-pack/plugins/infra/server/routes/alerting/index.ts
@@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export { isEsError } from './is_es_error';
+export * from './preview';
diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts
new file mode 100644
index 0000000000000..f4eed041481f6
--- /dev/null
+++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ METRIC_THRESHOLD_ALERT_TYPE_ID,
+ METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
+ INFRA_ALERT_PREVIEW_PATH,
+ TOO_MANY_BUCKETS_PREVIEW_EXCEPTION,
+ alertPreviewRequestParamsRT,
+ alertPreviewSuccessResponsePayloadRT,
+ MetricThresholdAlertPreviewRequestParams,
+} from '../../../common/alerting/metrics';
+import { createValidationFunction } from '../../../common/runtime_types';
+import { previewMetricThresholdAlert } from '../../lib/alerting/metric_threshold/preview_metric_threshold_alert';
+import { InfraBackendLibs } from '../../lib/infra_types';
+
+export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) => {
+ const { callWithRequest } = framework;
+ framework.registerRoute(
+ {
+ method: 'post',
+ path: INFRA_ALERT_PREVIEW_PATH,
+ validate: {
+ body: createValidationFunction(alertPreviewRequestParamsRT),
+ },
+ },
+ framework.router.handleLegacyErrors(async (requestContext, request, response) => {
+ const { criteria, filterQuery, lookback, sourceId, alertType, alertInterval } = request.body;
+
+ const callCluster = (endpoint: string, opts: Record) => {
+ return callWithRequest(requestContext, endpoint, opts);
+ };
+
+ const source = await sources.getSourceConfiguration(
+ requestContext.core.savedObjects.client,
+ sourceId || 'default'
+ );
+
+ try {
+ switch (alertType) {
+ case METRIC_THRESHOLD_ALERT_TYPE_ID: {
+ const { groupBy } = request.body as MetricThresholdAlertPreviewRequestParams;
+ const previewResult = await previewMetricThresholdAlert({
+ callCluster,
+ params: { criteria, filterQuery, groupBy },
+ lookback,
+ config: source.configuration,
+ alertInterval,
+ });
+
+ const numberOfGroups = previewResult.length;
+ const resultTotals = previewResult.reduce(
+ (totals, groupResult) => {
+ if (groupResult === TOO_MANY_BUCKETS_PREVIEW_EXCEPTION)
+ return { ...totals, tooManyBuckets: totals.tooManyBuckets + 1 };
+ if (groupResult === null) return { ...totals, noData: totals.noData + 1 };
+ if (isNaN(groupResult)) return { ...totals, error: totals.error + 1 };
+ return { ...totals, fired: totals.fired + groupResult };
+ },
+ {
+ fired: 0,
+ noData: 0,
+ error: 0,
+ tooManyBuckets: 0,
+ }
+ );
+
+ return response.ok({
+ body: alertPreviewSuccessResponsePayloadRT.encode({
+ numberOfGroups,
+ resultTotals,
+ }),
+ });
+ }
+ case METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID: {
+ // TODO: Add inventory preview functionality
+ return response.ok({});
+ }
+ default:
+ throw new Error('Unknown alert type');
+ }
+ } catch (error) {
+ return response.customError({
+ statusCode: error.statusCode ?? 500,
+ body: {
+ message: error.message ?? 'An unexpected error occurred',
+ },
+ });
+ }
+ })
+ );
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
index dc61da685c88d..e126a8b871d45 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/components/actions_menu.tsx
@@ -6,22 +6,32 @@
import React, { memo, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiContextMenuItem, EuiPortal } from '@elastic/eui';
-import { useCapabilities, useLink } from '../../../hooks';
+import { AgentConfig } from '../../../types';
+import { useCapabilities } from '../../../hooks';
import { ContextMenuActions } from '../../../components';
+import { AgentEnrollmentFlyout } from '../../fleet/components';
import { ConfigYamlFlyout } from './config_yaml_flyout';
-export const AgentConfigActionMenu = memo<{ configId: string; fullButton?: boolean }>(
- ({ configId, fullButton = false }) => {
- const { getHref } = useLink();
+export const AgentConfigActionMenu = memo<{ config: AgentConfig; fullButton?: boolean }>(
+ ({ config, fullButton = false }) => {
const hasWriteCapabilities = useCapabilities().write;
const [isYamlFlyoutOpen, setIsYamlFlyoutOpen] = useState(false);
+ const [isEnrollmentFlyoutOpen, setIsEnrollmentFlyoutOpen] = useState(false);
return (
<>
{isYamlFlyoutOpen ? (
- setIsYamlFlyoutOpen(false)} />
+ setIsYamlFlyoutOpen(false)} />
) : null}
+ {isEnrollmentFlyoutOpen && (
+
+ setIsEnrollmentFlyoutOpen(false)}
+ />
+
+ )}
setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
- key="viewConfig"
+ disabled={!hasWriteCapabilities}
+ icon="plusInCircle"
+ onClick={() => setIsEnrollmentFlyoutOpen(true)}
+ key="enrollAgents"
>
,
setIsYamlFlyoutOpen(!isYamlFlyoutOpen)}
+ key="viewConfig"
>
,
]}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
index 6fab78951038f..c74958078ca94 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/details_page/index.tsx
@@ -147,7 +147,9 @@ export const AgentConfigDetailsPage: React.FunctionComponent = () => {
},
{ isDivider: true },
{
- content: agentConfig && ,
+ content: agentConfig && (
+
+ ),
},
].map((item, index) => (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
index 0d43d8856c2fb..487c1c070bb3b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/list_page/index.tsx
@@ -189,7 +189,7 @@ export const AgentConfigListPage: React.FunctionComponent<{}> = () => {
}),
actions: [
{
- render: (config: AgentConfig) => ,
+ render: (config: AgentConfig) => ,
},
],
},
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx
index cc12ea19fbecf..60cbc31081302 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/list_layout.tsx
@@ -112,7 +112,7 @@ export const ListLayout: React.FunctionComponent<{}> = ({ children }) => {
setIsEnrollmentFlyoutOpen(true)}>
diff --git a/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts b/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts
deleted file mode 100644
index 4137293cf39c0..0000000000000
--- a/x-pack/plugins/ingest_pipelines/server/lib/is_es_error.ts
+++ /dev/null
@@ -1,13 +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 * as legacyElasticsearch from 'elasticsearch';
-
-const esErrorsParent = legacyElasticsearch.errors._Abstract;
-
-export function isEsError(err: Error) {
- return err instanceof esErrorsParent;
-}
diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts
index 8cec03c49d439..7a78bf608b8e1 100644
--- a/x-pack/plugins/ingest_pipelines/server/plugin.ts
+++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts
@@ -11,7 +11,7 @@ import { PLUGIN_ID, PLUGIN_MIN_LICENSE_TYPE } from '../common/constants';
import { License } from './services';
import { ApiRoutes } from './routes';
-import { isEsError } from './lib';
+import { isEsError } from './shared_imports';
import { Dependencies } from './types';
export class IngestPipelinesPlugin implements Plugin {
diff --git a/x-pack/plugins/ingest_pipelines/server/shared_imports.ts b/x-pack/plugins/ingest_pipelines/server/shared_imports.ts
new file mode 100644
index 0000000000000..454beda5394c7
--- /dev/null
+++ b/x-pack/plugins/ingest_pipelines/server/shared_imports.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts
index 70094859dff3f..261317daa26d9 100644
--- a/x-pack/plugins/ingest_pipelines/server/types.ts
+++ b/x-pack/plugins/ingest_pipelines/server/types.ts
@@ -8,7 +8,7 @@ import { IRouter } from 'src/core/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server';
import { License } from './services';
-import { isEsError } from './lib';
+import { isEsError } from './shared_imports';
export interface Dependencies {
security: SecurityPluginSetup;
diff --git a/x-pack/plugins/license_management/server/lib/is_es_error.ts b/x-pack/plugins/license_management/server/lib/is_es_error.ts
deleted file mode 100644
index 4137293cf39c0..0000000000000
--- a/x-pack/plugins/license_management/server/lib/is_es_error.ts
+++ /dev/null
@@ -1,13 +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 * as legacyElasticsearch from 'elasticsearch';
-
-const esErrorsParent = legacyElasticsearch.errors._Abstract;
-
-export function isEsError(err: Error) {
- return err instanceof esErrorsParent;
-}
diff --git a/x-pack/plugins/license_management/server/plugin.ts b/x-pack/plugins/license_management/server/plugin.ts
index 9546f5b1ef88a..7b1887e438024 100644
--- a/x-pack/plugins/license_management/server/plugin.ts
+++ b/x-pack/plugins/license_management/server/plugin.ts
@@ -7,7 +7,7 @@
import { Plugin, CoreSetup } from 'kibana/server';
import { ApiRoutes } from './routes';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
import { Dependencies } from './types';
export class LicenseManagementServerPlugin implements Plugin {
diff --git a/x-pack/plugins/license_management/server/shared_imports.ts b/x-pack/plugins/license_management/server/shared_imports.ts
new file mode 100644
index 0000000000000..454beda5394c7
--- /dev/null
+++ b/x-pack/plugins/license_management/server/shared_imports.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { isEsError } from '../../../../src/plugins/es_ui_shared/server';
diff --git a/x-pack/plugins/license_management/server/types.ts b/x-pack/plugins/license_management/server/types.ts
index 37f4781ba1e02..11b1ed696874b 100644
--- a/x-pack/plugins/license_management/server/types.ts
+++ b/x-pack/plugins/license_management/server/types.ts
@@ -7,7 +7,7 @@ import { ScopedClusterClient, IRouter } from 'kibana/server';
import { LicensingPluginSetup } from '../../licensing/server';
import { SecurityPluginSetup } from '../../security/server';
-import { isEsError } from './lib/is_es_error';
+import { isEsError } from './shared_imports';
export interface Dependencies {
licensing: LicensingPluginSetup;
diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts
index bd91995b6efc3..cd0f10bb7f577 100644
--- a/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_server_info.test.ts
@@ -10,6 +10,7 @@ import {
isCloud,
getNewJobDefaults,
getNewJobLimits,
+ extractDeploymentId,
} from './ml_server_info';
import mockMlInfoResponse from './__mocks__/ml_info_response.json';
@@ -20,7 +21,7 @@ jest.mock('./ml_api_service', () => ({
}));
describe('ml_server_info initial state', () => {
- it('server info not loaded ', () => {
+ it('should fail to get server info ', () => {
expect(isCloud()).toBe(false);
expect(getCloudDeploymentId()).toBe(null);
});
@@ -33,14 +34,14 @@ describe('ml_server_info', () => {
});
describe('cloud information', () => {
- it('can get could deployment id', () => {
+ it('should get could deployment id', () => {
expect(isCloud()).toBe(true);
expect(getCloudDeploymentId()).toBe('85d666f3350c469e8c3242d76a7f459c');
});
});
describe('defaults', () => {
- it('can get defaults', async (done) => {
+ it('should get defaults', async (done) => {
const defaults = getNewJobDefaults();
expect(defaults.anomaly_detectors.model_memory_limit).toBe('128mb');
@@ -52,11 +53,37 @@ describe('ml_server_info', () => {
});
describe('limits', () => {
- it('can get limits', async (done) => {
+ it('should get limits', async (done) => {
const limits = getNewJobLimits();
expect(limits.max_model_memory_limit).toBe('128mb');
done();
});
});
+
+ describe('cloud extract deployment ID', () => {
+ const cloudIdWithDeploymentName =
+ 'cloud_message_test:ZXUtd2VzdC0yLmF3cy5jbG91ZC5lcy5pbyQ4NWQ2NjZmMzM1MGM0NjllOGMzMjQyZDc2YTdmNDU5YyQxNmI1ZDM2ZGE1Mzk0YjlkYjIyZWJlNDk1OWY1OGQzMg==';
+
+ const cloudIdWithOutDeploymentName =
+ ':ZXUtd2VzdC0yLmF3cy5jbG91ZC5lcy5pbyQ4NWQ2NjZmMzM1MGM0NjllOGMzMjQyZDc2YTdmNDU5YyQxNmI1ZDM2ZGE1Mzk0YjlkYjIyZWJlNDk1OWY1OGQzMg==';
+
+ const badCloudId = 'cloud_message_test:this_is_not_a_base64_string';
+
+ it('should extract cloud ID when deployment name is present', () => {
+ expect(extractDeploymentId(cloudIdWithDeploymentName)).toBe(
+ '85d666f3350c469e8c3242d76a7f459c'
+ );
+ });
+
+ it('should extract cloud ID when deployment name is not present', () => {
+ expect(extractDeploymentId(cloudIdWithOutDeploymentName)).toBe(
+ '85d666f3350c469e8c3242d76a7f459c'
+ );
+ });
+
+ it('should fail to extract cloud ID', () => {
+ expect(extractDeploymentId(badCloudId)).toBe(null);
+ });
+ });
});
diff --git a/x-pack/plugins/ml/public/application/services/ml_server_info.ts b/x-pack/plugins/ml/public/application/services/ml_server_info.ts
index 8ab955b479108..d1f92df01f061 100644
--- a/x-pack/plugins/ml/public/application/services/ml_server_info.ts
+++ b/x-pack/plugins/ml/public/application/services/ml_server_info.ts
@@ -53,10 +53,11 @@ export function isCloud(): boolean {
}
export function getCloudDeploymentId(): string | null {
- if (cloudInfo.cloudId === null) {
- return null;
- }
- const tempCloudId = cloudInfo.cloudId.replace(/^.+:/, '');
+ return cloudInfo.cloudId === null ? null : extractDeploymentId(cloudInfo.cloudId);
+}
+
+export function extractDeploymentId(cloudId: string) {
+ const tempCloudId = cloudId.replace(/^(.+)?:/, '');
try {
const matches = atob(tempCloudId).match(/^.+\$(.+)(?=\$)/);
return matches !== null && matches.length === 2 ? matches[1] : null;
diff --git a/x-pack/plugins/monitoring/kibana.json b/x-pack/plugins/monitoring/kibana.json
index 4ed693464712d..c3000218aa125 100644
--- a/x-pack/plugins/monitoring/kibana.json
+++ b/x-pack/plugins/monitoring/kibana.json
@@ -4,7 +4,7 @@
"kibanaVersion": "kibana",
"configPath": ["monitoring"],
"requiredPlugins": ["licensing", "features", "data", "navigation", "kibanaLegacy"],
- "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home"],
+ "optionalPlugins": ["alerts", "actions", "infra", "telemetryCollectionManager", "usageCollection", "home", "cloud"],
"server": true,
"ui": true
}
diff --git a/x-pack/plugins/monitoring/public/angular/app_modules.ts b/x-pack/plugins/monitoring/public/angular/app_modules.ts
index 96b122801085f..726d4be4924d7 100644
--- a/x-pack/plugins/monitoring/public/angular/app_modules.ts
+++ b/x-pack/plugins/monitoring/public/angular/app_modules.ts
@@ -29,8 +29,6 @@ import { extractIp } from '../lib/extract_ip';
// @ts-ignore
import { PrivateProvider } from './providers/private';
// @ts-ignore
-import { KbnUrlProvider } from './providers/url';
-// @ts-ignore
import { breadcrumbsProvider } from '../services/breadcrumbs';
// @ts-ignore
import { monitoringClustersProvider } from '../services/clusters';
@@ -67,7 +65,6 @@ export const localAppModule = ({
createLocalPrivateModule();
createLocalStorage();
createLocalConfigModule(core);
- createLocalKbnUrlModule();
createLocalStateModule(query);
createLocalTopNavModule(navigation);
createHrefModule(core);
@@ -80,7 +77,6 @@ export const localAppModule = ({
...thirdPartyAngularDependencies,
'monitoring/I18n',
'monitoring/Private',
- 'monitoring/KbnUrl',
'monitoring/Storage',
'monitoring/Config',
'monitoring/State',
@@ -126,14 +122,6 @@ function createLocalStateModule(query: any) {
});
}
-function createLocalKbnUrlModule() {
- angular
- .module('monitoring/KbnUrl', ['monitoring/Private', 'ngRoute'])
- .service('kbnUrl', function (Private: IPrivate) {
- return Private(KbnUrlProvider);
- });
-}
-
function createMonitoringAppServices() {
angular
.module('monitoring/services', ['monitoring/Private'])
diff --git a/x-pack/plugins/monitoring/public/angular/providers/url.js b/x-pack/plugins/monitoring/public/angular/providers/url.js
deleted file mode 100644
index 0c984a71c9f2c..0000000000000
--- a/x-pack/plugins/monitoring/public/angular/providers/url.js
+++ /dev/null
@@ -1,217 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import _ from 'lodash';
-
-export function KbnUrlProvider($injector, $location, $rootScope, $parse) {
- /**
- * the `kbnUrl` service was created to smooth over some of the
- * inconsistent behavior that occurs when modifying the url via
- * the `$location` api. In general it is recommended that you use
- * the `kbnUrl` service any time you want to modify the url.
- *
- * "features" that `kbnUrl` does it's best to guarantee, which
- * are not guaranteed with the `$location` service:
- * - calling `kbnUrl.change()` with a url that resolves to the current
- * route will force a full transition (rather than just updating the
- * properties of the $route object)
- *
- * Additional features of `kbnUrl`
- * - parameterized urls
- * - easily include an app state with the url
- *
- * @type {KbnUrl}
- */
- const self = this;
-
- /**
- * Navigate to a url
- *
- * @param {String} url - the new url, can be a template. See #eval
- * @param {Object} [paramObj] - optional set of parameters for the url template
- * @return {undefined}
- */
- self.change = function (url, paramObj, appState) {
- self._changeLocation('url', url, paramObj, false, appState);
- };
-
- /**
- * Same as #change except only changes the url's path,
- * leaving the search string and such intact
- *
- * @param {String} path - the new path, can be a template. See #eval
- * @param {Object} [paramObj] - optional set of parameters for the path template
- * @return {undefined}
- */
- self.changePath = function (path, paramObj) {
- self._changeLocation('path', path, paramObj);
- };
-
- /**
- * Same as #change except that it removes the current url from history
- *
- * @param {String} url - the new url, can be a template. See #eval
- * @param {Object} [paramObj] - optional set of parameters for the url template
- * @return {undefined}
- */
- self.redirect = function (url, paramObj, appState) {
- self._changeLocation('url', url, paramObj, true, appState);
- };
-
- /**
- * Same as #redirect except only changes the url's path,
- * leaving the search string and such intact
- *
- * @param {String} path - the new path, can be a template. See #eval
- * @param {Object} [paramObj] - optional set of parameters for the path template
- * @return {undefined}
- */
- self.redirectPath = function (path, paramObj) {
- self._changeLocation('path', path, paramObj, true);
- };
-
- /**
- * Evaluate a url template. templates can contain double-curly wrapped
- * expressions that are evaluated in the context of the paramObj
- *
- * @param {String} template - the url template to evaluate
- * @param {Object} [paramObj] - the variables to expose to the template
- * @return {String} - the evaluated result
- * @throws {Error} If any of the expressions can't be parsed.
- */
- self.eval = function (template, paramObj) {
- paramObj = paramObj || {};
-
- return template.replace(/\{\{([^\}]+)\}\}/g, function (match, expr) {
- // remove filters
- const key = expr.split('|')[0].trim();
-
- // verify that the expression can be evaluated
- const p = $parse(key)(paramObj);
-
- // if evaluation can't be made, throw
- if (_.isUndefined(p)) {
- throw new Error(`Replacement failed, unresolved expression: ${expr}`);
- }
-
- return encodeURIComponent($parse(expr)(paramObj));
- });
- };
-
- /**
- * convert an object's route to an href, compatible with
- * window.location.href= and
- *
- * @param {Object} obj - any object that list's it's routes at obj.routes{}
- * @param {string} route - the route name
- * @return {string} - the computed href
- */
- self.getRouteHref = function (obj, route) {
- return '#' + self.getRouteUrl(obj, route);
- };
-
- /**
- * convert an object's route to a url, compatible with url.change() or $location.url()
- *
- * @param {Object} obj - any object that list's it's routes at obj.routes{}
- * @param {string} route - the route name
- * @return {string} - the computed url
- */
- self.getRouteUrl = function (obj, route) {
- const template = obj && obj.routes && obj.routes[route];
- if (template) return self.eval(template, obj);
- };
-
- /**
- * Similar to getRouteUrl, supports objects which list their routes,
- * and redirects to the named route. See #redirect
- *
- * @param {Object} obj - any object that list's it's routes at obj.routes{}
- * @param {string} route - the route name
- * @return {undefined}
- */
- self.redirectToRoute = function (obj, route) {
- self.redirect(self.getRouteUrl(obj, route));
- };
-
- /**
- * Similar to getRouteUrl, supports objects which list their routes,
- * and changes the url to the named route. See #change
- *
- * @param {Object} obj - any object that list's it's routes at obj.routes{}
- * @param {string} route - the route name
- * @return {undefined}
- */
- self.changeToRoute = function (obj, route) {
- self.change(self.getRouteUrl(obj, route));
- };
-
- /**
- * Removes the given parameter from the url. Does so without modifying the browser
- * history.
- * @param param
- */
- self.removeParam = function (param) {
- $location.search(param, null).replace();
- };
-
- /////
- // private api
- /////
- let reloading;
-
- self._changeLocation = function (type, url, paramObj, replace, appState) {
- const prev = {
- path: $location.path(),
- search: $location.search(),
- };
-
- url = self.eval(url, paramObj);
- $location[type](url);
- if (replace) $location.replace();
-
- if (appState) {
- $location.search(appState.getQueryParamName(), appState.toQueryParam());
- }
-
- const next = {
- path: $location.path(),
- search: $location.search(),
- };
-
- if ($injector.has('$route')) {
- const $route = $injector.get('$route');
-
- if (self._shouldForceReload(next, prev, $route)) {
- reloading = $rootScope.$on('$locationChangeSuccess', function () {
- // call the "unlisten" function returned by $on
- reloading();
- reloading = false;
-
- $route.reload();
- });
- }
- }
- };
-
- // determine if the router will automatically reload the route
- self._shouldForceReload = function (next, prev, $route) {
- if (reloading) return false;
-
- const route = $route.current && $route.current.$$route;
- if (!route) return false;
-
- // for the purposes of determining whether the router will
- // automatically be reloading, '' and '/' are equal
- const nextPath = next.path || '/';
- const prevPath = prev.path || '/';
- if (nextPath !== prevPath) return false;
-
- const reloadOnSearch = route.reloadOnSearch;
- const searchSame = _.isEqual(next.search, prev.search);
- return (reloadOnSearch && searchSame) || !reloadOnSearch;
- };
-}
diff --git a/x-pack/plugins/monitoring/public/components/alerts/alerts.js b/x-pack/plugins/monitoring/public/components/alerts/alerts.js
index 0ac67228db359..b3fc70e9ffd7d 100644
--- a/x-pack/plugins/monitoring/public/components/alerts/alerts.js
+++ b/x-pack/plugins/monitoring/public/components/alerts/alerts.js
@@ -29,7 +29,7 @@ const linkToCategories = {
[ALERT_TYPE_LICENSE_EXPIRATION]: 'License expiration',
[ALERT_TYPE_CLUSTER_STATE]: 'Cluster state',
};
-const getColumns = (kbnUrl, scope, timezone) => [
+const getColumns = (timezone) => [
{
name: i18n.translate('xpack.monitoring.alerts.statusColumnTitle', {
defaultMessage: 'Status',
@@ -109,11 +109,6 @@ const getColumns = (kbnUrl, scope, timezone) => [
suffix={alert.suffix}
message={message}
metadata={alert.metadata}
- changeUrl={(target) => {
- scope.$evalAsync(() => {
- kbnUrl.changePath(target);
- });
- }}
/>
);
},
@@ -155,7 +150,7 @@ const getColumns = (kbnUrl, scope, timezone) => [
},
];
-export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange }) => {
+export const Alerts = ({ alerts, sorting, pagination, onTableChange }) => {
const alertsFlattened = alerts.map((alert) => ({
...alert,
status: get(alert, 'metadata.severity', get(alert, 'severity', 0)),
@@ -169,7 +164,7 @@ export const Alerts = ({ alerts, angular, sorting, pagination, onTableChange })
{
if (metadata && metadata.link) {
if (metadata.link.startsWith('https')) {
@@ -22,10 +23,8 @@ export function FormattedAlert({ prefix, suffix, message, metadata, changeUrl })
);
}
- const goToLink = () => changeUrl(`/${metadata.link}`);
-
return (
-
+
{message}
);
diff --git a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js
index d47af51ee159f..b90e7b52f4962 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/listing/listing.js
@@ -231,12 +231,12 @@ const getColumns = (
];
};
-const changeCluster = (scope, globalState, kbnUrl, clusterUuid, ccs) => {
+const changeCluster = (scope, globalState, clusterUuid, ccs) => {
scope.$evalAsync(() => {
globalState.cluster_uuid = clusterUuid;
globalState.ccs = ccs;
globalState.save();
- kbnUrl.redirect('/overview');
+ window.history.replaceState(null, null, '#/overview');
});
};
@@ -399,12 +399,7 @@ export class Listing extends Component {
render() {
const { angular, clusters, sorting, pagination, onTableChange } = this.props;
- const _changeCluster = partial(
- changeCluster,
- angular.scope,
- angular.globalState,
- angular.kbnUrl
- );
+ const _changeCluster = partial(changeCluster, angular.scope, angular.globalState);
const _handleClickIncompatibleLicense = partial(handleClickIncompatibleLicense, angular.scope);
const _handleClickInvalidLicense = partial(handleClickInvalidLicense, angular.scope);
const hasStandaloneCluster = !!clusters.find(
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js
index 5ad0f8c0597f2..2dc76aa7e4496 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/alerts_panel.js
@@ -17,6 +17,7 @@ import {
import { formatDateTimeLocal } from '../../../../common/formatting';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
+import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
import {
EuiFlexGroup,
@@ -60,15 +61,13 @@ function replaceTokens(alert) {
return text;
}
-export function AlertsPanel({ alerts, changeUrl }) {
- const goToAlerts = () => changeUrl('/alerts');
-
+export function AlertsPanel({ alerts }) {
if (!alerts || !alerts.length) {
// no-op
return null;
}
- // enclosed component for accessing changeUrl
+ // enclosed component for accessing
function TopAlertItem({ item, index }) {
const severityIcon = mapSeverity(item.metadata.severity);
@@ -101,7 +100,6 @@ export function AlertsPanel({ alerts, changeUrl }) {
suffix={item.suffix}
message={item.message}
metadata={item.metadata}
- changeUrl={changeUrl}
/>
@@ -183,7 +181,11 @@ export function AlertsPanel({ alerts, changeUrl }) {
-
+
props.changeUrl('apm');
- const goToInstances = () => props.changeUrl('apm/instances');
-
+ const goToInstances = () => getSafeForExternalLink('#/apm/instances');
const setupModeData = get(setupMode.data, 'apm');
const setupModeTooltip =
setupMode && setupMode.enabled ? (
) : null;
@@ -64,7 +63,7 @@ export function ApmPanel(props) {
props.changeUrl('beats');
- const goToInstances = () => props.changeUrl('beats/beats');
-
const setupModeData = get(setupMode.data, 'beats');
const setupModeTooltip =
setupMode && setupMode.enabled ? (
) : null;
@@ -77,7 +75,7 @@ export function BeatsPanel(props) {
props.changeUrl('elasticsearch');
- const goToNodes = () => props.changeUrl('elasticsearch/nodes');
- const goToIndices = () => props.changeUrl('elasticsearch/indices');
+ const goToElasticsearch = () => getSafeForExternalLink('#/elasticsearch');
+ const goToNodes = () => getSafeForExternalLink('#/elasticsearch/nodes');
+ const goToIndices = () => getSafeForExternalLink('#/elasticsearch/indices');
const { primaries, replicas } = calculateShards(get(props, 'cluster_stats.indices.shards', {}));
@@ -162,7 +162,7 @@ export function ElasticsearchPanel(props) {
) : null;
@@ -215,7 +215,7 @@ export function ElasticsearchPanel(props) {
-
+
+
{!isFromStandaloneCluster ? (
@@ -48,32 +48,19 @@ export function Overview(props) {
{...props.cluster.elasticsearch}
version={props.cluster.version}
ml={props.cluster.ml}
- changeUrl={props.changeUrl}
license={props.cluster.license}
setupMode={props.setupMode}
showLicenseExpiration={props.showLicenseExpiration}
/>
-
+
) : null}
-
+
-
+
-
+
);
diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
index 541c240b3c35a..8bf2bc472b8fd 100644
--- a/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
+++ b/x-pack/plugins/monitoring/public/components/cluster/overview/kibana_panel.js
@@ -29,6 +29,7 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { SetupModeTooltip } from '../../setup_mode/tooltip';
import { KIBANA_SYSTEM_ID } from '../../../../common/constants';
+import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
export function KibanaPanel(props) {
const setupMode = props.setupMode;
@@ -40,8 +41,8 @@ export function KibanaPanel(props) {
const statusIndicator = ;
- const goToKibana = () => props.changeUrl('kibana');
- const goToInstances = () => props.changeUrl('kibana/instances');
+ const goToKibana = () => getSafeForExternalLink('#/kibana');
+ const goToInstances = () => getSafeForExternalLink('#/kibana/instances');
const setupModeData = get(setupMode.data, 'kibana');
const setupModeTooltip =
@@ -49,7 +50,7 @@ export function KibanaPanel(props) {
) : null;
@@ -70,7 +71,7 @@ export function KibanaPanel(props) {
props.changeUrl('logstash');
- const goToNodes = () => props.changeUrl('logstash/nodes');
- const goToPipelines = () => props.changeUrl('logstash/pipelines');
+ const goToLogstash = () => getSafeForExternalLink('#/logstash');
+ const goToNodes = () => getSafeForExternalLink('#/logstash/nodes');
+ const goToPipelines = () => getSafeForExternalLink('#/logstash/pipelines');
const setupModeData = get(setupMode.data, 'logstash');
const setupModeTooltip =
@@ -51,7 +52,7 @@ export function LogstashPanel(props) {
) : null;
@@ -71,7 +72,7 @@ export function LogstashPanel(props) {
{
+export const Index = ({ scope, indexSummary, metrics, clusterUuid, indexUuid, logs, ...props }) => {
const metricsToShow = [
metrics.index_mem,
metrics.index_size,
@@ -59,7 +50,7 @@ export const Index = ({
-
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
index e8b3ef7e680f8..418661ff322e4 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.js
@@ -21,16 +21,7 @@ import { MonitoringTimeseriesContainer } from '../../chart';
import { ShardAllocation } from '../shard_allocation/shard_allocation';
import { FormattedMessage } from '@kbn/i18n/react';
-export const Node = ({
- nodeSummary,
- metrics,
- logs,
- nodeId,
- clusterUuid,
- scope,
- kbnUrl,
- ...props
-}) => {
+export const Node = ({ nodeSummary, metrics, logs, nodeId, clusterUuid, scope, ...props }) => {
const metricsToShow = [
metrics.node_jvm_mem,
metrics.node_mem,
@@ -71,7 +62,7 @@ export const Node = ({
-
+
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js
index ac3daf25e0478..e292381fdf9a1 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/assigned.js
@@ -8,8 +8,18 @@ import { get, sortBy } from 'lodash';
import React from 'react';
import { Shard } from './shard';
import { calculateClass } from '../lib/calculate_class';
-import { generateQueryAndLink } from '../lib/generate_query_and_link';
import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiKeyboardAccessible } from '@elastic/eui';
+import { getSafeForExternalLink } from '../../../../lib/get_safe_for_external_link';
+
+const generateQueryAndLink = (data) => {
+ let type = 'indices';
+ let ident = data.name;
+ if (data.type === 'node') {
+ type = 'nodes';
+ ident = data.id;
+ }
+ return getSafeForExternalLink(`#/elasticsearch/${type}/${ident}`);
+};
function sortByName(item) {
if (item.type === 'node') {
@@ -43,17 +53,13 @@ export class Assigned extends React.Component {
}
}
- const changeUrl = () => {
- this.props.changeUrl(generateQueryAndLink(data));
- };
-
// TODO: redesign for shard allocation, possibly giving shard display the
// ability to use the euiLink CSS class (blue link text instead of white link text)
// Disabling eslint because EuiKeyboardAccessible does it for us
/* eslint-disable jsx-a11y/click-events-have-key-events */
const name = (
-
+
{data.name}
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js
index ccd5c266bcfd5..23bf87e19df73 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/cluster_view.js
@@ -19,8 +19,6 @@ export class ClusterView extends React.Component {
constructor(props) {
super(props);
- const scope = props.scope;
- const kbnChangePath = props.kbnUrl.changePath;
this.state = {
labels: props.scope.labels || [],
@@ -28,9 +26,6 @@ export class ClusterView extends React.Component {
shardStats: props.scope.pageData.shardStats,
showSystemIndices: props.showSystemIndices,
toggleShowSystemIndices: props.toggleShowSystemIndices,
- angularChangeUrl: (url) => {
- scope.$evalAsync(() => kbnChangePath(url));
- },
};
}
@@ -71,7 +66,6 @@ export class ClusterView extends React.Component {
rows={this.state.showing}
cols={this.state.labels.length}
shardStats={this.state.shardStats}
- changeUrl={this.state.angularChangeUrl}
/>
);
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js
index d2df988ed2893..378a9cb996291 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/components/table_body.js
@@ -22,11 +22,7 @@ const ShardRow = (props) => {
return (
{unassigned}
-
+
);
};
@@ -40,14 +36,7 @@ export class TableBody extends React.Component {
);
createRow = (data, index) => {
- return (
-
- );
+ return ;
};
render() {
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js
deleted file mode 100644
index 04802e96cdf4b..0000000000000
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/lib/generate_query_and_link.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-export function generateQueryAndLink(data) {
- let type = 'indices';
- let ident = data.name;
- if (data.type === 'node') {
- type = 'nodes';
- ident = data.id;
- }
- return '/elasticsearch/' + type + '/' + ident;
-}
diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js
index af3b55bf92146..781dca7122fca 100644
--- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js
+++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/shard_allocation.js
@@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n';
import { ClusterView } from './components/cluster_view';
import './shard_allocation.scss';
-export const ShardAllocation = ({ scope, kbnUrl, type, shardStats }) => {
+export const ShardAllocation = ({ scope, type, shardStats }) => {
const types = [
{
label: i18n.translate('xpack.monitoring.elasticsearch.shardAllocation.primaryLabel', {
@@ -79,7 +79,6 @@ export const ShardAllocation = ({ scope, kbnUrl, type, shardStats }) => {
{
export class KibanaInstances extends PureComponent {
render() {
- const { clusterStatus, angular, setupMode, sorting, pagination, onTableChange } = this.props;
+ const { clusterStatus, setupMode, sorting, pagination, onTableChange } = this.props;
let setupModeCallOut = null;
// Merge the instances data with the setup data if enabled
@@ -262,7 +262,7 @@ export class KibanaInstances extends PureComponent {
- {
- scope.$evalAsync(() => {
- kbnUrl.changePath(`/logstash/node/${node.logstash.uuid}`);
- });
- }}
- >
+
{name}
diff --git a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js
index 525918f7c99ad..e8baee6408b22 100644
--- a/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js
+++ b/x-pack/plugins/monitoring/public/components/logstash/listing/listing.test.js
@@ -54,10 +54,6 @@ describe('Listing', () => {
it('should render with expected props', () => {
const props = {
data: expectedData,
- angular: {
- scope: null,
- kbnUrl: null,
- },
sorting: {
sort: 'asc',
},
@@ -74,10 +70,6 @@ describe('Listing', () => {
const { os, process, logstash, jvm, events, ...rest } = item; // eslint-disable-line no-unused-vars
return rest;
}),
- angular: {
- scope: null,
- kbnUrl: null,
- },
sorting: {
sort: 'asc',
},
diff --git a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
index 9a04cf0c13005..1b22bc6823bb8 100644
--- a/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
+++ b/x-pack/plugins/monitoring/public/components/logstash/pipeline_listing/pipeline_listing.js
@@ -24,6 +24,7 @@ import { Sparkline } from '../../../components/sparkline';
import { EuiMonitoringSSPTable } from '../../table';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link';
export class PipelineListing extends Component {
tooltipXValueFormatter(xValue, dateFormat) {
@@ -36,7 +37,6 @@ export class PipelineListing extends Component {
getColumns() {
const { onBrush, dateFormat } = this.props;
- const { kbnUrl, scope } = this.props.angular;
return [
{
@@ -46,14 +46,7 @@ export class PipelineListing extends Component {
field: 'id',
sortable: true,
render: (id) => (
- {
- scope.$evalAsync(() => {
- kbnUrl.changePath(`/logstash/pipelines/${id}`);
- });
- }}
- >
+
{id}
),
diff --git a/x-pack/plugins/monitoring/public/components/no_data/no_data.js b/x-pack/plugins/monitoring/public/components/no_data/no_data.js
index 77329b6299376..bb6b4fa853636 100644
--- a/x-pack/plugins/monitoring/public/components/no_data/no_data.js
+++ b/x-pack/plugins/monitoring/public/components/no_data/no_data.js
@@ -27,6 +27,7 @@ import { toggleSetupMode } from '../../lib/setup_mode';
import { CheckingSettings } from './checking_settings';
import { ReasonFound, WeTried } from './reasons';
import { CheckerErrors } from './checker_errors';
+import { getSafeForExternalLink } from '../../lib/get_safe_for_external_link';
function NoDataMessage(props) {
const { isLoading, reason, checkMessage } = props;
@@ -49,7 +50,7 @@ export function NoData(props) {
async function startSetup() {
setIsLoading(true);
await toggleSetupMode(true);
- props.changePath('/elasticsearch/nodes');
+ window.location.hash = getSafeForExternalLink('#/elasticsearch/nodes');
}
if (useInternalCollection) {
@@ -167,7 +168,6 @@ export function NoData(props) {
}
NoData.propTypes = {
- changePath: PropTypes.func,
isLoading: PropTypes.bool.isRequired,
reason: PropTypes.object,
checkMessage: PropTypes.string,
diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap
index 49ce0e84fdacc..aafba6791f4a0 100644
--- a/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap
+++ b/x-pack/plugins/monitoring/public/components/setup_mode/__snapshots__/tooltip.test.js.snap
@@ -11,8 +11,8 @@ exports[`setupMode SetupModeTooltip allInternalCollection should render for apm
>
Self monitoring
@@ -32,8 +32,8 @@ exports[`setupMode SetupModeTooltip allInternalCollection should render for beat
>
Self monitoring
@@ -53,8 +53,8 @@ exports[`setupMode SetupModeTooltip allInternalCollection should render for elas
>
Self monitoring
@@ -74,8 +74,8 @@ exports[`setupMode SetupModeTooltip allInternalCollection should render for kiba
>
Self monitoring
@@ -95,8 +95,8 @@ exports[`setupMode SetupModeTooltip allInternalCollection should render for logs
>
Self monitoring
@@ -116,8 +116,8 @@ exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for a
>
Metricbeat monitoring
@@ -137,8 +137,8 @@ exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for b
>
Metricbeat monitoring
@@ -158,8 +158,8 @@ exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for e
>
Metricbeat monitoring
@@ -179,8 +179,8 @@ exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for k
>
Metricbeat monitoring
@@ -200,8 +200,8 @@ exports[`setupMode SetupModeTooltip allMonitoredByMetricbeat should render for l
>
Metricbeat monitoring
@@ -221,8 +221,8 @@ exports[`setupMode SetupModeTooltip internalCollectionOn should render for apm 1
>
Self monitoring is on
@@ -242,8 +242,8 @@ exports[`setupMode SetupModeTooltip internalCollectionOn should render for beats
>
Self monitoring is on
@@ -263,8 +263,8 @@ exports[`setupMode SetupModeTooltip internalCollectionOn should render for elast
>
|