From 403b5d2fd51475aeb63a5156225592ec369e635a Mon Sep 17 00:00:00 2001
From: Maryam Saeidi
Date: Tue, 20 Jun 2023 12:06:21 +0200
Subject: [PATCH 01/14] [AO] Make spaces plugin optional (#159980)
## Summary
Based on this
[comment](https://github.com/elastic/kibana/pull/158657#discussion_r1233750595),
[spaces](https://github.com/elastic/kibana/issues/149687) should be an
optional plugin.
---
x-pack/plugins/observability/kibana.jsonc | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc
index a69ffbd0234f6..5c08133f15a95 100644
--- a/x-pack/plugins/observability/kibana.jsonc
+++ b/x-pack/plugins/observability/kibana.jsonc
@@ -25,12 +25,11 @@
"triggersActionsUi",
"security",
"share",
- "spaces",
"unifiedSearch",
"visualizations"
],
- "optionalPlugins": ["discover", "home", "licensing", "usageCollection", "cloud"],
- "requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat"],
+ "optionalPlugins": ["discover", "home", "licensing", "usageCollection", "cloud", "spaces"],
+ "requiredBundles": ["data", "kibanaReact", "kibanaUtils", "unifiedSearch", "cloudChat", "spaces"],
"extraPublicDirs": ["common"]
}
}
From 97dc2ecba127e997e5229b499d83a0a38072c043 Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 20 Jun 2023 06:13:29 -0400
Subject: [PATCH 02/14] [EBT] Add page url to browser-side context (#159916)
## Summary
Part of https://github.com/elastic/kibana/issues/149249
Add a new EBT context providing the `page_url` field to events.
`page_url` is based on the current url's `pathname` and `hash`
exclusively (no domain, port, query param...)
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../application_service.test.tsx | 82 +++++++++++++++++++
.../src/application_service.test.mocks.ts | 12 +++
.../src/application_service.test.ts | 34 ++++++++
.../src/application_service.tsx | 18 +++-
...egister_analytics_context_provider.test.ts | 38 +++++++++
.../register_analytics_context_provider.ts | 26 ++++++
.../src/utils/get_location_observable.test.ts | 65 +++++++++++++++
.../src/utils/get_location_observable.ts | 35 ++++++++
.../src/utils/index.ts | 1 +
.../tsconfig.json | 2 +
.../src/core_system.ts | 2 +-
.../core_context_providers.ts | 5 ++
12 files changed, 318 insertions(+), 2 deletions(-)
create mode 100644 packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.test.ts
create mode 100644 packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.ts
create mode 100644 packages/core/application/core-application-browser-internal/src/utils/get_location_observable.test.ts
create mode 100644 packages/core/application/core-application-browser-internal/src/utils/get_location_observable.ts
diff --git a/packages/core/application/core-application-browser-internal/integration_tests/application_service.test.tsx b/packages/core/application/core-application-browser-internal/integration_tests/application_service.test.tsx
index a4f117d5c883b..95dc6d8f8b181 100644
--- a/packages/core/application/core-application-browser-internal/integration_tests/application_service.test.tsx
+++ b/packages/core/application/core-application-browser-internal/integration_tests/application_service.test.tsx
@@ -12,6 +12,7 @@ import { act } from 'react-dom/test-utils';
import { createMemoryHistory, MemoryHistory } from 'history';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
+import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import type { AppMountParameters, AppUpdater } from '@kbn/core-application-browser';
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
@@ -38,11 +39,13 @@ describe('ApplicationService', () => {
beforeEach(() => {
history = createMemoryHistory();
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ const analytics = analyticsServiceMock.createAnalyticsServiceSetup();
http.post.mockResolvedValue({ navLinks: {} });
setupDeps = {
http,
+ analytics,
history: history as any,
};
startDeps = {
@@ -87,6 +90,45 @@ describe('ApplicationService', () => {
expect(await currentAppId$.pipe(take(1)).toPromise()).toEqual('app1');
});
+
+ it('updates the page_url analytics context', async () => {
+ const { register } = service.setup(setupDeps);
+
+ const context$ = setupDeps.analytics.registerContextProvider.mock.calls[0][0]
+ .context$ as Observable<{
+ page_url: string;
+ }>;
+ const locations: string[] = [];
+ context$.subscribe((context) => locations.push(context.page_url));
+
+ register(Symbol(), {
+ id: 'app1',
+ title: 'App1',
+ mount: async () => () => undefined,
+ });
+ register(Symbol(), {
+ id: 'app2',
+ title: 'App2',
+ mount: async () => () => undefined,
+ });
+
+ const { getComponent } = await service.start(startDeps);
+ update = createRenderer(getComponent());
+
+ await navigate('/app/app1/bar?hello=dolly');
+ await flushPromises();
+ await navigate('/app/app2#/foo');
+ await flushPromises();
+ await navigate('/app/app2#/another-path');
+ await flushPromises();
+
+ expect(locations).toEqual([
+ '/',
+ '/app/app1/bar',
+ '/app/app2#/foo',
+ '/app/app2#/another-path',
+ ]);
+ });
});
describe('using navigateToApp', () => {
@@ -127,6 +169,46 @@ describe('ApplicationService', () => {
expect(currentAppIds).toEqual(['app1']);
});
+ it('updates the page_url analytics context', async () => {
+ const { register } = service.setup(setupDeps);
+
+ const context$ = setupDeps.analytics.registerContextProvider.mock.calls[0][0]
+ .context$ as Observable<{
+ page_url: string;
+ }>;
+ const locations: string[] = [];
+ context$.subscribe((context) => locations.push(context.page_url));
+
+ register(Symbol(), {
+ id: 'app1',
+ title: 'App1',
+ mount: async () => () => undefined,
+ });
+ register(Symbol(), {
+ id: 'app2',
+ title: 'App2',
+ mount: async () => () => undefined,
+ });
+
+ const { navigateToApp, getComponent } = await service.start(startDeps);
+ update = createRenderer(getComponent());
+
+ await act(async () => {
+ await navigateToApp('app1');
+ update();
+ });
+ await act(async () => {
+ await navigateToApp('app2', { path: '/nested' });
+ update();
+ });
+ await act(async () => {
+ await navigateToApp('app2', { path: '/another-path' });
+ update();
+ });
+
+ expect(locations).toEqual(['/', '/app/app1', '/app/app2/nested', '/app/app2/another-path']);
+ });
+
it('replaces the current history entry when the `replace` option is true', async () => {
const { register } = service.setup(setupDeps);
diff --git a/packages/core/application/core-application-browser-internal/src/application_service.test.mocks.ts b/packages/core/application/core-application-browser-internal/src/application_service.test.mocks.ts
index 7197e7308def6..a41c27f348f3a 100644
--- a/packages/core/application/core-application-browser-internal/src/application_service.test.mocks.ts
+++ b/packages/core/application/core-application-browser-internal/src/application_service.test.mocks.ts
@@ -26,11 +26,23 @@ jest.doMock('history', () => ({
}));
export const parseAppUrlMock = jest.fn();
+export const getLocationObservableMock = jest.fn();
jest.doMock('./utils', () => {
const original = jest.requireActual('./utils');
return {
...original,
parseAppUrl: parseAppUrlMock,
+ getLocationObservable: getLocationObservableMock,
+ };
+});
+
+export const registerAnalyticsContextProviderMock = jest.fn();
+jest.doMock('./register_analytics_context_provider', () => {
+ const original = jest.requireActual('./register_analytics_context_provider');
+
+ return {
+ ...original,
+ registerAnalyticsContextProvider: registerAnalyticsContextProviderMock,
};
});
diff --git a/packages/core/application/core-application-browser-internal/src/application_service.test.ts b/packages/core/application/core-application-browser-internal/src/application_service.test.ts
index 65e5867db83a0..09e7ed5a385a4 100644
--- a/packages/core/application/core-application-browser-internal/src/application_service.test.ts
+++ b/packages/core/application/core-application-browser-internal/src/application_service.test.ts
@@ -10,17 +10,21 @@ import {
MockCapabilitiesService,
MockHistory,
parseAppUrlMock,
+ getLocationObservableMock,
+ registerAnalyticsContextProviderMock,
} from './application_service.test.mocks';
import { createElement } from 'react';
import { BehaviorSubject, firstValueFrom, Subject } from 'rxjs';
import { bufferCount, takeUntil } from 'rxjs/operators';
import { mount, shallow } from 'enzyme';
+import { createBrowserHistory } from 'history';
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
+import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
import { MockLifecycle } from './test_helpers/test_types';
import { ApplicationService } from './application_service';
import {
@@ -48,9 +52,12 @@ let service: ApplicationService;
describe('#setup()', () => {
beforeEach(() => {
+ jest.clearAllMocks();
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
+ const analytics = analyticsServiceMock.createAnalyticsServiceSetup();
setupDeps = {
http,
+ analytics,
redirectTo: jest.fn(),
};
startDeps = {
@@ -469,13 +476,38 @@ describe('#setup()', () => {
]);
});
});
+
+ describe('analytics context provider', () => {
+ it('calls getLocationObservable with the correct parameters', () => {
+ const history = createBrowserHistory();
+ service.setup({ ...setupDeps, history });
+
+ expect(getLocationObservableMock).toHaveBeenCalledTimes(1);
+ expect(getLocationObservableMock).toHaveBeenCalledWith(window.location, history);
+ });
+
+ it('calls registerAnalyticsContextProvider with the correct parameters', () => {
+ const location$ = new Subject();
+ getLocationObservableMock.mockReturnValue(location$);
+
+ service.setup(setupDeps);
+
+ expect(registerAnalyticsContextProviderMock).toHaveBeenCalledTimes(1);
+ expect(registerAnalyticsContextProviderMock).toHaveBeenCalledWith({
+ analytics: setupDeps.analytics,
+ location$,
+ });
+ });
+ });
});
describe('#start()', () => {
beforeEach(() => {
const http = httpServiceMock.createSetupContract({ basePath: '/base-path' });
+ const analytics = analyticsServiceMock.createAnalyticsServiceSetup();
setupDeps = {
http,
+ analytics,
redirectTo: jest.fn(),
};
startDeps = {
@@ -1185,8 +1217,10 @@ describe('#stop()', () => {
MockHistory.push.mockReset();
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
+ const analytics = analyticsServiceMock.createAnalyticsServiceSetup();
setupDeps = {
http,
+ analytics,
};
startDeps = {
http,
diff --git a/packages/core/application/core-application-browser-internal/src/application_service.tsx b/packages/core/application/core-application-browser-internal/src/application_service.tsx
index 5808410b07fe2..0c8207ca1d2f6 100644
--- a/packages/core/application/core-application-browser-internal/src/application_service.tsx
+++ b/packages/core/application/core-application-browser-internal/src/application_service.tsx
@@ -17,6 +17,7 @@ import type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
import type { Capabilities } from '@kbn/core-capabilities-common';
import type { MountPoint } from '@kbn/core-mount-utils-browser';
import type { OverlayStart } from '@kbn/core-overlays-browser';
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import type {
App,
AppDeepLink,
@@ -35,10 +36,18 @@ import type { InternalApplicationSetup, InternalApplicationStart, Mounter } from
import { getLeaveAction, isConfirmAction } from './application_leave';
import { getUserConfirmationHandler } from './navigation_confirm';
-import { appendAppPath, parseAppUrl, relativeToAbsolute, getAppInfo } from './utils';
+import {
+ appendAppPath,
+ parseAppUrl,
+ relativeToAbsolute,
+ getAppInfo,
+ getLocationObservable,
+} from './utils';
+import { registerAnalyticsContextProvider } from './register_analytics_context_provider';
export interface SetupDeps {
http: HttpSetup;
+ analytics: AnalyticsServiceSetup;
history?: History;
/** Used to redirect to external urls */
redirectTo?: (path: string) => void;
@@ -111,6 +120,7 @@ export class ApplicationService {
public setup({
http: { basePath },
+ analytics,
redirectTo = (path: string) => {
window.location.assign(path);
},
@@ -126,6 +136,12 @@ export class ApplicationService {
}),
});
+ const location$ = getLocationObservable(window.location, this.history);
+ registerAnalyticsContextProvider({
+ analytics,
+ location$,
+ });
+
this.navigate = (url, state, replace) => {
// basePath not needed here because `history` is configured with basename
return replace ? this.history!.replace(url, state) : this.history!.push(url, state);
diff --git a/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.test.ts b/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.test.ts
new file mode 100644
index 0000000000000..075095db9cfa8
--- /dev/null
+++ b/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.test.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { firstValueFrom, ReplaySubject, Subject } from 'rxjs';
+import { registerAnalyticsContextProvider } from './register_analytics_context_provider';
+import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
+
+describe('registerAnalyticsContextProvider', () => {
+ let analytics: ReturnType;
+ let location$: Subject;
+
+ beforeEach(() => {
+ analytics = analyticsServiceMock.createAnalyticsServiceSetup();
+ location$ = new ReplaySubject(1);
+ registerAnalyticsContextProvider({ analytics, location$ });
+ });
+
+ test('should register the analytics context provider', () => {
+ expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1);
+ expect(analytics.registerContextProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'page url',
+ })
+ );
+ });
+
+ test('emits a context value when location$ emits', async () => {
+ location$.next('/some_url');
+ await expect(
+ firstValueFrom(analytics.registerContextProvider.mock.calls[0][0].context$)
+ ).resolves.toEqual({ page_url: '/some_url' });
+ });
+});
diff --git a/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.ts b/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.ts
new file mode 100644
index 0000000000000..9c79b0e15f070
--- /dev/null
+++ b/packages/core/application/core-application-browser-internal/src/register_analytics_context_provider.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
+import { type Observable, map } from 'rxjs';
+
+export function registerAnalyticsContextProvider({
+ analytics,
+ location$,
+}: {
+ analytics: AnalyticsServiceSetup;
+ location$: Observable;
+}) {
+ analytics.registerContextProvider({
+ name: 'page url',
+ context$: location$.pipe(map((location) => ({ page_url: location }))),
+ schema: {
+ page_url: { type: 'text', _meta: { description: 'The page url' } },
+ },
+ });
+}
diff --git a/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.test.ts b/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.test.ts
new file mode 100644
index 0000000000000..3567929e6b688
--- /dev/null
+++ b/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.test.ts
@@ -0,0 +1,65 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { createBrowserHistory, type History } from 'history';
+import { firstValueFrom } from 'rxjs';
+import { getLocationObservable } from './get_location_observable';
+
+const nextTick = () => new Promise((resolve) => window.setTimeout(resolve, 1));
+
+describe('getLocationObservable', () => {
+ let history: History;
+
+ beforeEach(() => {
+ history = createBrowserHistory();
+ });
+
+ it('emits with the initial location', async () => {
+ const location$ = getLocationObservable({ pathname: '/foo', hash: '' }, history);
+ expect(await firstValueFrom(location$)).toEqual('/foo');
+ });
+
+ it('emits when the location changes', async () => {
+ const location$ = getLocationObservable({ pathname: '/foo', hash: '' }, history);
+ const locations: string[] = [];
+ location$.subscribe((location) => locations.push(location));
+
+ history.push({ pathname: '/bar' });
+ history.push({ pathname: '/dolly' });
+
+ await nextTick();
+
+ expect(locations).toEqual(['/foo', '/bar', '/dolly']);
+ });
+
+ it('emits only once for a given url', async () => {
+ const location$ = getLocationObservable({ pathname: '/foo', hash: '' }, history);
+ const locations: string[] = [];
+ location$.subscribe((location) => locations.push(location));
+
+ history.push({ pathname: '/bar' });
+ history.push({ pathname: '/bar' });
+ history.push({ pathname: '/foo' });
+
+ await nextTick();
+
+ expect(locations).toEqual(['/foo', '/bar', '/foo']);
+ });
+
+ it('includes the hash when present', async () => {
+ const location$ = getLocationObservable({ pathname: '/foo', hash: '#/index' }, history);
+ const locations: string[] = [];
+ location$.subscribe((location) => locations.push(location));
+
+ history.push({ pathname: '/bar', hash: '#/home' });
+
+ await nextTick();
+
+ expect(locations).toEqual(['/foo#/index', '/bar#/home']);
+ });
+});
diff --git a/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.ts b/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.ts
new file mode 100644
index 0000000000000..3e1957de38b63
--- /dev/null
+++ b/packages/core/application/core-application-browser-internal/src/utils/get_location_observable.ts
@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Observable, Subject, startWith, shareReplay, distinctUntilChanged } from 'rxjs';
+import type { History } from 'history';
+
+// interface compatible for both window.location and history.location...
+export interface Location {
+ pathname: string;
+ hash: string;
+}
+
+export const getLocationObservable = (
+ initialLocation: Location,
+ history: History
+): Observable => {
+ const subject = new Subject();
+ history.listen((location) => {
+ subject.next(locationToUrl(location));
+ });
+ return subject.pipe(
+ startWith(locationToUrl(initialLocation)),
+ distinctUntilChanged(),
+ shareReplay(1)
+ );
+};
+
+const locationToUrl = (location: Location) => {
+ return `${location.pathname}${location.hash}`;
+};
diff --git a/packages/core/application/core-application-browser-internal/src/utils/index.ts b/packages/core/application/core-application-browser-internal/src/utils/index.ts
index e88b1f7a8a6fc..f22146584f70a 100644
--- a/packages/core/application/core-application-browser-internal/src/utils/index.ts
+++ b/packages/core/application/core-application-browser-internal/src/utils/index.ts
@@ -11,3 +11,4 @@ export { getAppInfo } from './get_app_info';
export { parseAppUrl } from './parse_app_url';
export { relativeToAbsolute } from './relative_to_absolute';
export { removeSlashes } from './remove_slashes';
+export { getLocationObservable } from './get_location_observable';
diff --git a/packages/core/application/core-application-browser-internal/tsconfig.json b/packages/core/application/core-application-browser-internal/tsconfig.json
index 8f54fa8aa6ae1..cc07927f15ed2 100644
--- a/packages/core/application/core-application-browser-internal/tsconfig.json
+++ b/packages/core/application/core-application-browser-internal/tsconfig.json
@@ -33,6 +33,8 @@
"@kbn/test-jest-helpers",
"@kbn/core-custom-branding-browser",
"@kbn/core-custom-branding-browser-mocks",
+ "@kbn/core-analytics-browser-mocks",
+ "@kbn/core-analytics-browser",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/root/core-root-browser-internal/src/core_system.ts b/packages/core/root/core-root-browser-internal/src/core_system.ts
index c4213e5efdb58..b4a8b6d2d2815 100644
--- a/packages/core/root/core-root-browser-internal/src/core_system.ts
+++ b/packages/core/root/core-root-browser-internal/src/core_system.ts
@@ -241,7 +241,7 @@ export class CoreSystem {
const notifications = this.notifications.setup({ uiSettings });
const customBranding = this.customBranding.setup({ injectedMetadata });
- const application = this.application.setup({ http });
+ const application = this.application.setup({ http, analytics });
this.coreApp.setup({ application, http, injectedMetadata, notifications });
const core: InternalCoreSetup = {
diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
index 79e43f0df086f..3a7c075abd613 100644
--- a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
+++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
@@ -98,5 +98,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(event.context).to.have.property('viewport_height');
expect(event.context.viewport_height).to.be.a('number');
});
+
+ it('should have the properties provided by the "page url" context provider', () => {
+ expect(event.context).to.have.property('page_url');
+ expect(event.context.page_url).to.be.a('string');
+ });
});
}
From 8d83f64383ddbfe6ada155a161e6cb274bd6c540 Mon Sep 17 00:00:00 2001
From: Shahzad
Date: Tue, 20 Jun 2023 12:24:06 +0200
Subject: [PATCH 03/14] [Synthetics] Add TLS Certificate expiry alert (#159697)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Abdul Wahab Zahid
---
api_docs/deprecations_by_plugin.mdx | 2 +-
.../current_mappings.json | 29 ++--
.../group2/check_registered_types.test.ts | 2 +-
.../common/constants/monitor_defaults.ts | 2 +-
.../common/constants/synthetics/rest_api.ts | 2 +-
.../common/constants/synthetics_alerts.ts | 16 +-
.../common/constants/uptime_alerts.ts | 12 --
.../plugins/synthetics/common/field_names.ts | 11 +-
.../common/requests/get_certs_request_body.ts | 11 ++
.../common/rules/alert_actions.test.ts | 61 +++++++
.../synthetics/common/rules/alert_actions.ts | 14 ++
.../common/rules/synthetics/translations.ts | 52 ++++++
.../synthetics/common/runtime_types/certs.ts | 5 +
.../project_monitor_read_only.journey.ts | 3 +
.../components/alerts/alert_tls.tsx | 84 +++++++++
.../components/alerts/hooks/translations.ts | 6 +
.../alerts/hooks/use_synthetics_alert.ts | 17 +-
.../components/alerts/query_bar.tsx | 102 +++++++++++
.../components/alerts/tls_rule_ui.tsx | 47 +++++
.../alerts/toggle_alert_flyout_button.tsx | 30 +++-
.../certificates/certificates_list.tsx | 4 +
.../monitor_add_edit/form/field_config.tsx | 25 +++
.../monitor_add_edit/form/form_config.tsx | 3 +
.../components/monitor_add_edit/types.ts | 2 +
.../apps/synthetics/lib/alert_types/index.ts | 6 +-
.../alert_types/lazy_wrapper/tls_alert.tsx | 36 ++++
.../lazy_wrapper/validate_tls_alert.ts | 27 +++
.../apps/synthetics/lib/alert_types/tls.tsx | 57 +++++++
.../synthetics/state/alert_rules/actions.ts | 7 +-
.../apps/synthetics/state/alert_rules/api.ts | 2 +-
.../apps/synthetics/state/ui/actions.ts | 10 +-
.../public/apps/synthetics/state/ui/index.ts | 15 +-
.../apps/synthetics/state/ui/selectors.ts | 17 --
.../__mocks__/synthetics_store.mock.ts | 2 +-
.../certificates/certificates_list.tsx | 4 +
.../alert_rules/tls_rule/message_utils.ts | 70 ++++++++
.../server/alert_rules/tls_rule/tls_rule.ts | 161 ++++++++++++++++++
.../tls_rule/tls_rule_executor.test.ts | 82 +++++++++
.../alert_rules/tls_rule/tls_rule_executor.ts | 147 ++++++++++++++++
.../server/alert_rules/tls_rule/types.ts | 23 +++
.../server/alert_rules/translations.ts | 88 ++++++++++
.../lib/requests/get_certs.test.ts | 7 +
.../migrations/monitors/8.9.0.test.ts | 88 ++++++++++
.../migrations/monitors/8.9.0.ts | 52 ++++++
.../migrations/monitors/index.ts | 2 +
.../lib/saved_objects/synthetics_monitor.ts | 8 +
.../synthetics/server/queries/get_certs.ts | 37 ++++
.../default_alerts/enable_default_alert.ts | 19 ++-
.../default_alerts/status_alert_service.ts | 1 -
.../default_alerts/tls_alert_service.ts | 131 ++++++++++++++
.../default_alerts/update_default_alert.ts | 7 +-
x-pack/plugins/synthetics/server/server.ts | 11 ++
.../normalizers/common_fields.test.ts | 6 +
.../normalizers/common_fields.ts | 37 ++--
.../normalizers/http_monitor.ts | 2 +-
.../group4/check_registered_rule_types.ts | 1 +
.../apis/synthetics/add_monitor_project.ts | 23 ++-
.../check_registered_task_types.ts | 1 +
58 files changed, 1635 insertions(+), 94 deletions(-)
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/alert_tls.tsx
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/tls_rule_ui.tsx
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/tls_alert.tsx
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/validate_tls_alert.ts
create mode 100644 x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx
create mode 100644 x-pack/plugins/synthetics/server/alert_rules/tls_rule/message_utils.ts
create mode 100644 x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts
create mode 100644 x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts
create mode 100644 x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts
create mode 100644 x-pack/plugins/synthetics/server/alert_rules/tls_rule/types.ts
create mode 100644 x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.test.ts
create mode 100644 x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.ts
create mode 100644 x-pack/plugins/synthetics/server/queries/get_certs.ts
create mode 100644 x-pack/plugins/synthetics/server/routes/default_alerts/tls_alert_service.ts
diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx
index d5cba68bc1777..d0ba182370c29 100644
--- a/api_docs/deprecations_by_plugin.mdx
+++ b/api_docs/deprecations_by_plugin.mdx
@@ -1211,7 +1211,7 @@ migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/
| Deprecated API | Reference location(s) | Remove By |
| ---------------|-----------|-----------|
-| | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/common.ts#:~:text=alertFactory), [status_check.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/status_check.ts#:~:text=alertFactory), [tls.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls.ts#:~:text=alertFactory), [tls_legacy.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls_legacy.ts#:~:text=alertFactory), [duration_anomaly.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/duration_anomaly.ts#:~:text=alertFactory), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/common.ts#:~:text=alertFactory), [monitor_status_rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts#:~:text=alertFactory) | - |
+| | [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/common.ts#:~:text=alertFactory), [status_check.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/status_check.ts#:~:text=alertFactory), [tls.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls.ts#:~:text=alertFactory), [tls_legacy.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/tls_legacy.ts#:~:text=alertFactory), [duration_anomaly.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/legacy_uptime/lib/alerts/duration_anomaly.ts#:~:text=alertFactory), [common.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/common.ts#:~:text=alertFactory), [tls_rule.ts](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/server/alert_rules/status_rule/tls_rule.ts#:~:text=alertFactory) | - |
| | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title), [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |
| | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title), [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |
| | [filter_group.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/filter_group/filter_group.tsx#:~:text=title), [filters_expression_select.tsx](https://github.com/elastic/kibana/tree/main/x-pack/plugins/synthetics/public/legacy_uptime/components/overview/alerts/monitor_expressions/filters_expression_select.tsx#:~:text=title) | - |
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index e0a24046196d6..1e0ac8548906e 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -931,17 +931,6 @@
}
}
},
- "event-annotation-group": {
- "dynamic": false,
- "properties": {
- "title": {
- "type": "text"
- },
- "description": {
- "type": "text"
- }
- }
- },
"visualization": {
"dynamic": false,
"properties": {
@@ -959,6 +948,17 @@
}
}
},
+ "event-annotation-group": {
+ "dynamic": false,
+ "properties": {
+ "title": {
+ "type": "text"
+ },
+ "description": {
+ "type": "text"
+ }
+ }
+ },
"dashboard": {
"properties": {
"description": {
@@ -2377,6 +2377,13 @@
"type": "boolean"
}
}
+ },
+ "tls": {
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
}
}
},
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
index 58ec8e1597cba..cd7ef7ea94031 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
@@ -140,7 +140,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"slo": "2048ab6791df2e1ae0936f29c20765cb8d2fcfaa",
"space": "8de4ec513e9bbc6b2f1d635161d850be7747d38e",
"spaces-usage-stats": "3abca98713c52af8b30300e386c7779b3025a20e",
- "synthetics-monitor": "ca7c0710c0607e44b2c52e5a41086b8b4a214f63",
+ "synthetics-monitor": "33ddc4b8979f378edf58bcc7ba13c5c5b572f42d",
"synthetics-param": "3ebb744e5571de678b1312d5c418c8188002cf5e",
"synthetics-privates-locations": "9cfbd6d1d2e2c559bf96dd6fbc27ff0c47719dd3",
"tag": "e2544392fe6563e215bb677abc8b01c2601ef2dc",
diff --git a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts
index b8f307f12428c..36f9fbf467dc0 100644
--- a/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts
+++ b/x-pack/plugins/synthetics/common/constants/monitor_defaults.ts
@@ -132,7 +132,7 @@ export const DEFAULT_COMMON_FIELDS: CommonFields = {
[ConfigKey.MONITOR_TYPE]: DataStream.HTTP,
[ConfigKey.FORM_MONITOR_TYPE]: FormMonitorType.MULTISTEP,
[ConfigKey.ENABLED]: true,
- [ConfigKey.ALERT_CONFIG]: { status: { enabled: true } },
+ [ConfigKey.ALERT_CONFIG]: { status: { enabled: true }, tls: { enabled: true } },
[ConfigKey.SCHEDULE]: {
number: '3',
unit: ScheduleUnit.MINUTES,
diff --git a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts
index 62f16b75fdb96..d83cecdf567bc 100644
--- a/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts
+++ b/x-pack/plugins/synthetics/common/constants/synthetics/rest_api.ts
@@ -8,7 +8,7 @@
export enum SYNTHETICS_API_URLS {
// Service end points
INDEX_TEMPLATES = '/internal/synthetics/service/index_templates',
- SERVICE_LOCATIONS = '/internal/synthetics/service/locations',
+ SERVICE_LOCATIONS = '/internal/uptime/service/locations',
SYNTHETICS_MONITORS = '/internal/synthetics/service/monitors',
SYNTHETICS_MONITOR_INSPECT = '/internal/synthetics/service/monitor/inspect',
GET_SYNTHETICS_MONITOR = '/internal/synthetics/service/monitor/{monitorId}',
diff --git a/x-pack/plugins/synthetics/common/constants/synthetics_alerts.ts b/x-pack/plugins/synthetics/common/constants/synthetics_alerts.ts
index 7f6d306f8be88..620c5c88033a7 100644
--- a/x-pack/plugins/synthetics/common/constants/synthetics_alerts.ts
+++ b/x-pack/plugins/synthetics/common/constants/synthetics_alerts.ts
@@ -9,7 +9,8 @@ import { ActionGroup } from '@kbn/alerting-plugin/common';
import { i18n } from '@kbn/i18n';
export type MonitorStatusActionGroup =
- ActionGroup<'xpack.synthetics.alerts.actionGroups.monitorStatus'>;
+ | ActionGroup<'xpack.synthetics.alerts.actionGroups.monitorStatus'>
+ | ActionGroup<'xpack.synthetics.alerts.actionGroups.tls'>;
export const MONITOR_STATUS: MonitorStatusActionGroup = {
id: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
@@ -18,18 +19,29 @@ export const MONITOR_STATUS: MonitorStatusActionGroup = {
}),
};
+export const TLS_CERTIFICATE: MonitorStatusActionGroup = {
+ id: 'xpack.synthetics.alerts.actionGroups.tls',
+ name: i18n.translate('xpack.synthetics.alertRules.actionGroups.tls', {
+ defaultMessage: 'Synthetics TLS certificate',
+ }),
+};
+
export const ACTION_GROUP_DEFINITIONS: {
MONITOR_STATUS: MonitorStatusActionGroup;
+ TLS_CERTIFICATE: MonitorStatusActionGroup;
} = {
MONITOR_STATUS,
+ TLS_CERTIFICATE,
};
export const SYNTHETICS_STATUS_RULE = 'xpack.synthetics.alerts.monitorStatus';
+export const SYNTHETICS_TLS_RULE = 'xpack.synthetics.alerts.tls';
export const SYNTHETICS_ALERT_RULE_TYPES = {
MONITOR_STATUS: SYNTHETICS_STATUS_RULE,
+ TLS: SYNTHETICS_TLS_RULE,
};
-export const SYNTHETICS_RULE_TYPES = [SYNTHETICS_STATUS_RULE];
+export const SYNTHETICS_RULE_TYPES = [SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE];
export const SYNTHETICS_RULE_TYPES_ALERT_CONTEXT = 'observability.uptime';
diff --git a/x-pack/plugins/synthetics/common/constants/uptime_alerts.ts b/x-pack/plugins/synthetics/common/constants/uptime_alerts.ts
index a265d13fb71d8..71f6bd1b183fb 100644
--- a/x-pack/plugins/synthetics/common/constants/uptime_alerts.ts
+++ b/x-pack/plugins/synthetics/common/constants/uptime_alerts.ts
@@ -34,18 +34,6 @@ export const DURATION_ANOMALY: DurationAnomalyActionGroup = {
name: 'Uptime Duration Anomaly',
};
-export const ACTION_GROUP_DEFINITIONS: {
- MONITOR_STATUS: MonitorStatusActionGroup;
- TLS_LEGACY: TLSLegacyActionGroup;
- TLS: TLSActionGroup;
- DURATION_ANOMALY: DurationAnomalyActionGroup;
-} = {
- MONITOR_STATUS,
- TLS_LEGACY,
- TLS,
- DURATION_ANOMALY,
-};
-
export const CLIENT_ALERT_TYPES = {
MONITOR_STATUS: 'xpack.uptime.alerts.monitorStatus',
TLS_LEGACY: 'xpack.uptime.alerts.tls',
diff --git a/x-pack/plugins/synthetics/common/field_names.ts b/x-pack/plugins/synthetics/common/field_names.ts
index 3a2054089d41d..0407fad341d8a 100644
--- a/x-pack/plugins/synthetics/common/field_names.ts
+++ b/x-pack/plugins/synthetics/common/field_names.ts
@@ -6,16 +6,17 @@
*/
export const AGENT_NAME = 'agent.name';
-
export const MONITOR_ID = 'monitor.id';
export const MONITOR_NAME = 'monitor.name';
export const MONITOR_TYPE = 'monitor.type';
-
export const URL_FULL = 'url.full';
export const URL_PORT = 'url.port';
-
export const OBSERVER_GEO_NAME = 'observer.geo.name';
-
export const ERROR_MESSAGE = 'error.message';
+export const STATE_ID = 'monitor.state.id';
-export const STATE_ID = 'montior.state.id';
+export const CERT_COMMON_NAME = 'tls.server.x509.subject.common_name';
+export const CERT_ISSUER_NAME = 'tls.server.x509.issuer.common_name';
+export const CERT_VALID_NOT_AFTER = 'tls.server.x509.not_after';
+export const CERT_VALID_NOT_BEFORE = 'tls.server.x509.not_before';
+export const CERT_HASH_SHA256 = 'tls.server.hash.sha256';
diff --git a/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts b/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts
index 72ed6346df188..7a57f8ab13d41 100644
--- a/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts
+++ b/x-pack/plugins/synthetics/common/requests/get_certs_request_body.ts
@@ -31,6 +31,7 @@ function absoluteDate(relativeDate: string) {
}
export const getCertsRequestBody = ({
+ monitorIds,
pageIndex,
search,
notValidBefore,
@@ -79,6 +80,9 @@ export const getCertsRequestBody = ({
: {}),
filter: [
...(filters ? [filters] : []),
+ ...(monitorIds && monitorIds.length > 0
+ ? [{ terms: { 'monitor.id': monitorIds } }]
+ : []),
{
exists: {
field: 'tls.server.hash.sha256',
@@ -129,6 +133,9 @@ export const getCertsRequestBody = ({
_source: [
'monitor.id',
'monitor.name',
+ 'monitor.type',
+ 'url.full',
+ 'observer.geo.name',
'tls.server.x509.issuer.common_name',
'tls.server.x509.subject.common_name',
'tls.server.hash.sha1',
@@ -193,6 +200,10 @@ export const processCertsResult = (result: CertificatesResults): CertResult => {
not_after: notAfter,
not_before: notBefore,
common_name: commonName,
+ monitorName: ping?.monitor?.name,
+ monitorUrl: ping?.url?.full,
+ monitorType: ping?.monitor?.type,
+ locationName: ping?.observer?.geo?.name,
};
});
const total = result.aggregations?.total?.value ?? 0;
diff --git a/x-pack/plugins/synthetics/common/rules/alert_actions.test.ts b/x-pack/plugins/synthetics/common/rules/alert_actions.test.ts
index e8514303059d6..3315f5059ef86 100644
--- a/x-pack/plugins/synthetics/common/rules/alert_actions.test.ts
+++ b/x-pack/plugins/synthetics/common/rules/alert_actions.test.ts
@@ -211,6 +211,7 @@ describe('Legacy Alert Actions factory', () => {
defaultSubjectMessage: MonitorStatusTranslations.defaultSubjectMessage,
defaultRecoverySubjectMessage: MonitorStatusTranslations.defaultRecoverySubjectMessage,
},
+ isLegacy: true,
});
expect(resp).toEqual([
{
@@ -243,6 +244,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
actionTypeId: '.pagerduty',
group: 'xpack.uptime.alerts.actionGroups.monitorStatus',
params: {
@@ -264,6 +270,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -274,6 +285,11 @@ describe('Alert Actions factory', () => {
},
},
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -291,6 +307,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
actionTypeId: '.index',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@@ -312,6 +333,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -329,6 +355,11 @@ describe('Alert Actions factory', () => {
},
},
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -352,6 +383,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
actionTypeId: '.pagerduty',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@@ -374,6 +410,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -384,6 +425,11 @@ describe('Alert Actions factory', () => {
},
},
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -401,6 +447,11 @@ describe('Alert Actions factory', () => {
groupId: SYNTHETICS_MONITOR_STATUS.id,
defaultActions: [
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
actionTypeId: '.email',
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
params: {
@@ -426,6 +477,11 @@ describe('Alert Actions factory', () => {
});
expect(resp).toEqual([
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'recovered',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
@@ -444,6 +500,11 @@ describe('Alert Actions factory', () => {
},
},
{
+ frequency: {
+ notifyWhen: 'onActionGroupChange',
+ summary: false,
+ throttle: null,
+ },
group: 'xpack.synthetics.alerts.actionGroups.monitorStatus',
id: 'f2a3b195-ed76-499a-805d-82d24d4eeba9',
params: {
diff --git a/x-pack/plugins/synthetics/common/rules/alert_actions.ts b/x-pack/plugins/synthetics/common/rules/alert_actions.ts
index db128ec6a26f5..70a2de4d7cf59 100644
--- a/x-pack/plugins/synthetics/common/rules/alert_actions.ts
+++ b/x-pack/plugins/synthetics/common/rules/alert_actions.ts
@@ -58,6 +58,13 @@ export function populateAlertActions({
id: aId.id,
group: groupId,
params: {},
+ frequency: !isLegacy
+ ? {
+ notifyWhen: 'onActionGroupChange',
+ throttle: null,
+ summary: false,
+ }
+ : undefined,
};
const recoveredAction: RuleAction = {
@@ -66,6 +73,13 @@ export function populateAlertActions({
params: {
message: translations.defaultRecoveryMessage,
},
+ frequency: !isLegacy
+ ? {
+ notifyWhen: 'onActionGroupChange',
+ throttle: null,
+ summary: false,
+ }
+ : undefined,
};
switch (aId.actionTypeId) {
diff --git a/x-pack/plugins/synthetics/common/rules/synthetics/translations.ts b/x-pack/plugins/synthetics/common/rules/synthetics/translations.ts
index b288ff25fc045..4ac78b9620efe 100644
--- a/x-pack/plugins/synthetics/common/rules/synthetics/translations.ts
+++ b/x-pack/plugins/synthetics/common/rules/synthetics/translations.ts
@@ -72,3 +72,55 @@ export const SyntheticsMonitorStatusTranslations = {
defaultMessage: 'Alert when a monitor is down.',
}),
};
+
+export const TlsTranslations = {
+ defaultActionMessage: i18n.translate('xpack.synthetics.rules.tls.defaultActionMessage', {
+ defaultMessage: `Detected TLS certificate {commonName} is {status} - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
+ values: {
+ commonName: '{{context.commonName}}',
+ issuer: '{{context.issuer}}',
+ summary: '{{context.summary}}',
+ status: '{{context.status}}',
+ monitorName: '{{context.monitorName}}',
+ monitorUrl: '{{{context.monitorUrl}}}',
+ monitorType: '{{context.monitorType}}',
+ locationName: '{{context.locationName}}',
+ },
+ }),
+ defaultRecoveryMessage: i18n.translate('xpack.synthetics.rules.tls.defaultRecoveryMessage', {
+ defaultMessage: `Alert for TLS certificate {commonName} from issuer {issuer} has recovered - Elastic Synthetics\n\nDetails:\n\n- Summary: {summary}\n- Common name: {commonName}\n- Issuer: {issuer}\n- Monitor: {monitorName} \n- Monitor URL: {monitorUrl} \n- Monitor type: {monitorType} \n- From: {locationName}`,
+ values: {
+ commonName: '{{context.commonName}}',
+ issuer: '{{context.issuer}}',
+ summary: '{{context.summary}}',
+ monitorName: '{{context.monitorName}}',
+ monitorUrl: '{{{context.monitorUrl}}}',
+ monitorType: '{{context.monitorType}}',
+ locationName: '{{context.locationName}}',
+ },
+ }),
+ name: i18n.translate('xpack.synthetics.rules.tls.clientName', {
+ defaultMessage: 'Synthetics TLS',
+ }),
+ description: i18n.translate('xpack.synthetics.rules.tls.description', {
+ defaultMessage: 'Alert when the TLS certificate of a Synthetics monitor is about to expire.',
+ }),
+ defaultSubjectMessage: i18n.translate(
+ 'xpack.synthetics.alerts.syntheticsMonitorTLS.defaultSubjectMessage',
+ {
+ defaultMessage: 'Alert triggered for certificate {commonName} - Elastic Synthetics',
+ values: {
+ commonName: '{{context.commonName}}',
+ },
+ }
+ ),
+ defaultRecoverySubjectMessage: i18n.translate(
+ 'xpack.synthetics.alerts.syntheticsMonitorTLS.defaultRecoverySubjectMessage',
+ {
+ defaultMessage: 'Alert has resolved for certificate {commonName} - Elastic Synthetics',
+ values: {
+ commonName: '{{context.commonName}}',
+ },
+ }
+ ),
+};
diff --git a/x-pack/plugins/synthetics/common/runtime_types/certs.ts b/x-pack/plugins/synthetics/common/runtime_types/certs.ts
index f7de2280d4b65..bb854930f8e4d 100644
--- a/x-pack/plugins/synthetics/common/runtime_types/certs.ts
+++ b/x-pack/plugins/synthetics/common/runtime_types/certs.ts
@@ -21,6 +21,7 @@ export const GetCertsParamsType = t.intersection([
direction: t.string,
size: t.number,
filters: t.unknown,
+ monitorIds: t.array(t.string),
}),
]);
@@ -44,6 +45,10 @@ export const CertType = t.intersection([
common_name: t.string,
issuer: t.string,
sha1: t.string,
+ monitorName: t.string,
+ monitorType: t.string,
+ monitorUrl: t.string,
+ locationName: t.string,
}),
]);
diff --git a/x-pack/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts b/x-pack/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts
index e3cf2d4eee4ea..37da92bf3277e 100644
--- a/x-pack/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts
+++ b/x-pack/plugins/synthetics/e2e/synthetics/journeys/project_monitor_read_only.journey.ts
@@ -94,6 +94,9 @@ journey('ProjectMonitorReadOnly', async ({ page, params }) => {
status: {
enabled: !(originalMonitorConfiguration?.alert?.status?.enabled as boolean),
},
+ tls: {
+ enabled: originalMonitorConfiguration?.alert?.tls?.enabled as boolean,
+ },
},
enabled: !originalMonitorConfiguration?.enabled,
});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/alert_tls.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/alert_tls.tsx
new file mode 100644
index 0000000000000..6c3c8619a13ec
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/alert_tls.tsx
@@ -0,0 +1,84 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { EuiExpression, EuiFlexItem, EuiFlexGroup, EuiSpacer } from '@elastic/eui';
+import React from 'react';
+import { ValueExpression } from '@kbn/triggers-actions-ui-plugin/public';
+import { i18n } from '@kbn/i18n';
+
+interface Props {
+ ageThreshold: number;
+ expirationThreshold: number;
+ setAgeThreshold: (value: number) => void;
+ setExpirationThreshold: (value: number) => void;
+}
+
+export const AlertTlsComponent: React.FC = ({
+ ageThreshold,
+ expirationThreshold,
+ setAgeThreshold,
+ setExpirationThreshold,
+}) => (
+ <>
+
+
+
+
+
+
+ {
+ setExpirationThreshold(val);
+ }}
+ description={TlsTranslations.expirationDescription}
+ errors={[]}
+ />
+
+
+ {
+ setAgeThreshold(val);
+ }}
+ description={TlsTranslations.ageDescription}
+ errors={[]}
+ />
+
+
+
+ >
+);
+
+export const TlsTranslations = {
+ criteriaAriaLabel: i18n.translate('xpack.synthetics.rules.tls.criteriaExpression.ariaLabel', {
+ defaultMessage:
+ 'An expression displaying the criteria for the monitors that are being watched by this alert',
+ }),
+ criteriaDescription: i18n.translate(
+ 'xpack.synthetics.alerts.tls.criteriaExpression.description',
+ {
+ defaultMessage: 'when',
+ description:
+ 'The context of this `when` is in the conditional sense, like "when there are three cookies, eat them all".',
+ }
+ ),
+ criteriaValue: i18n.translate('xpack.synthetics.tls.criteriaExpression.value', {
+ defaultMessage: 'matching monitor',
+ }),
+ expirationDescription: i18n.translate('xpack.synthetics.tls.expirationExpression.description', {
+ defaultMessage: 'has a certificate expiring within days: ',
+ }),
+ ageDescription: i18n.translate('xpack.synthetics.tls.ageExpression.description', {
+ defaultMessage: 'or older than days: ',
+ }),
+};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
index 31b336c81b748..2901b67820485 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/translations.ts
@@ -18,9 +18,15 @@ export const ToggleFlyoutTranslations = {
toggleMonitorStatusAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.ariaLabel', {
defaultMessage: 'Open add rule flyout',
}),
+ toggleTlsAriaLabel: i18n.translate('xpack.synthetics.toggleAlertFlyout.tls.ariaLabel', {
+ defaultMessage: 'Open add tls rule flyout',
+ }),
toggleMonitorStatusContent: i18n.translate('xpack.synthetics.toggleAlertButton.content', {
defaultMessage: 'Monitor status rule',
}),
+ toggleTlsContent: i18n.translate('xpack.synthetics.toggleTlsAlertButton.label.content', {
+ defaultMessage: 'TLS certificate rule',
+ }),
navigateToAlertingUIAriaLabel: i18n.translate('xpack.synthetics.app.navigateToAlertingUi', {
defaultMessage: 'Leave Synthetics and go to Alerting Management page',
}),
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts
index 120597c62636d..de661f9d33d09 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/hooks/use_synthetics_alert.ts
@@ -6,18 +6,20 @@
*/
import { useFetcher } from '@kbn/observability-shared-plugin/public';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useMemo, useState } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
-import { setAlertFlyoutVisible } from '../../../state';
+import { SYNTHETICS_TLS_RULE } from '../../../../../../common/constants/synthetics_alerts';
+import { selectAlertFlyoutVisibility, setAlertFlyoutVisible } from '../../../state';
import { enableDefaultAlertingAPI } from '../../../state/alert_rules/api';
import { ClientPluginsStart } from '../../../../../plugin';
export const useSyntheticsAlert = (isOpen: boolean) => {
const dispatch = useDispatch();
- const [alert, setAlert] = useState(null);
+ const [defaultRules, setAlert] = useState<{ statusRule: Rule; tlsRule: Rule } | null>(null);
+ const alertFlyoutVisible = useSelector(selectAlertFlyoutVisibility);
const { data, loading } = useFetcher(() => {
if (isOpen) {
@@ -34,15 +36,16 @@ export const useSyntheticsAlert = (isOpen: boolean) => {
const { triggersActionsUi } = useKibana().services;
const EditAlertFlyout = useMemo(() => {
- if (!alert) {
+ if (!defaultRules) {
return null;
}
return triggersActionsUi.getEditRuleFlyout({
- onClose: () => dispatch(setAlertFlyoutVisible(false)),
+ onClose: () => dispatch(setAlertFlyoutVisible(null)),
hideInterval: true,
- initialRule: alert,
+ initialRule:
+ alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules.tlsRule : defaultRules.statusRule,
});
- }, [alert, dispatch, triggersActionsUi]);
+ }, [defaultRules, dispatch, triggersActionsUi, alertFlyoutVisible]);
return useMemo(() => ({ loading, EditAlertFlyout }), [EditAlertFlyout, loading]);
};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx
new file mode 100644
index 0000000000000..dd9aada9b8d20
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/query_bar.tsx
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { EuiFlexItem } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { QueryStringInput } from '@kbn/unified-search-plugin/public';
+import { useKibana } from '@kbn/kibana-react-plugin/public';
+import { useFetcher } from '@kbn/observability-shared-plugin/public';
+import { SYNTHETICS_INDEX_PATTERN } from '../../../../../common/constants';
+import { ClientPluginsStart } from '../../../../plugin';
+
+interface Props {
+ query: string;
+ onChange: (query: string) => void;
+}
+export const isValidKuery = (query: string) => {
+ if (query === '') {
+ return true;
+ }
+ const listOfOperators = [':', '>=', '=>', '>', '<'];
+ for (let i = 0; i < listOfOperators.length; i++) {
+ const operator = listOfOperators[i];
+ const qParts = query.trim().split(operator);
+ if (query.includes(operator) && qParts.length > 1 && qParts[1]) {
+ return true;
+ }
+ }
+ return false;
+};
+
+export const AlertQueryBar = ({ query = '', onChange }: Props) => {
+ const { services } = useKibana();
+
+ const {
+ appName,
+ notifications,
+ http,
+ docLinks,
+ uiSettings,
+ data,
+ dataViews,
+ unifiedSearch,
+ storage,
+ usageCollection,
+ } = services;
+
+ const [inputVal, setInputVal] = useState(query);
+
+ const { data: dataView } = useFetcher(async () => {
+ return await dataViews.create({ title: SYNTHETICS_INDEX_PATTERN });
+ }, []);
+
+ useEffect(() => {
+ onChange(query);
+ setInputVal(query);
+ }, [onChange, query]);
+
+ return (
+
+ {
+ setInputVal(queryN?.query as string);
+ if (isValidKuery(queryN?.query as string)) {
+ // we want to submit when user clears or paste a complete kuery
+ onChange(queryN.query as string);
+ }
+ }}
+ onSubmit={(queryN) => {
+ if (queryN) onChange(queryN.query as string);
+ }}
+ query={{ query: inputVal, language: 'kuery' }}
+ dataTestSubj="xpack.synthetics.alerts.monitorStatus.filterBar"
+ autoSubmit={true}
+ disableLanguageSwitcher={true}
+ isInvalid={!!(inputVal && !query)}
+ placeholder={i18n.translate('xpack.synthetics.alerts.searchPlaceholder.kql', {
+ defaultMessage: 'Filter using kql syntax',
+ })}
+ appName={appName}
+ deps={{
+ unifiedSearch,
+ data,
+ dataViews,
+ storage,
+ notifications,
+ http,
+ docLinks,
+ uiSettings,
+ usageCollection,
+ }}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/tls_rule_ui.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/tls_rule_ui.tsx
new file mode 100644
index 0000000000000..a4e653bb1ddb0
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/tls_rule_ui.tsx
@@ -0,0 +1,47 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { useDispatch, useSelector } from 'react-redux';
+import React, { useEffect } from 'react';
+import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
+import { AlertTlsComponent } from './alert_tls';
+import { getDynamicSettings } from '../../state/settings/api';
+import { selectDynamicSettings } from '../../state/settings';
+import { TLSParams } from '../../../../../common/runtime_types/alerts/tls';
+import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../../common/constants';
+
+export const TLSRuleComponent: React.FC<{
+ ruleParams: RuleTypeParamsExpressionProps['ruleParams'];
+ setRuleParams: RuleTypeParamsExpressionProps['setRuleParams'];
+}> = ({ ruleParams, setRuleParams }) => {
+ const dispatch = useDispatch();
+
+ const { settings } = useSelector(selectDynamicSettings);
+
+ useEffect(() => {
+ if (typeof settings === 'undefined') {
+ dispatch(getDynamicSettings());
+ }
+ }, [dispatch, settings]);
+
+ return (
+ setRuleParams('certAgeThreshold', Number(value))}
+ setExpirationThreshold={(value) => setRuleParams('certExpirationThreshold', Number(value))}
+ />
+ );
+};
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
index 96c00d08843b5..89ee3fd6faab0 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx
@@ -19,6 +19,10 @@ import {
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import {
+ SYNTHETICS_STATUS_RULE,
+ SYNTHETICS_TLS_RULE,
+} from '../../../../../common/constants/synthetics_alerts';
import { ManageRulesLink } from '../common/links/manage_rules_link';
import { ClientPluginsStart } from '../../../../plugin';
import { ToggleFlyoutTranslations } from './hooks/translations';
@@ -48,7 +52,29 @@ export const ToggleAlertFlyoutButton = () => {
),
onClick: () => {
- dispatch(setAlertFlyoutVisible(true));
+ dispatch(setAlertFlyoutVisible(SYNTHETICS_STATUS_RULE));
+ setIsOpen(false);
+ },
+ toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
+ disabled: !hasUptimeWrite || loading,
+ icon: 'bell',
+ };
+
+ const tlsAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
+ 'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
+ 'data-test-subj': 'xpack.synthetics.toggleAlertFlyout.tls',
+ name: (
+
+ {ToggleFlyoutTranslations.toggleTlsContent}
+ {loading && (
+
+
+
+ )}
+
+ ),
+ onClick: () => {
+ dispatch(setAlertFlyoutVisible(SYNTHETICS_TLS_RULE));
setIsOpen(false);
},
toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
@@ -66,7 +92,7 @@ export const ToggleAlertFlyoutButton = () => {
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
- items: [monitorStatusAlertContextMenuItem, managementContextItem],
+ items: [monitorStatusAlertContextMenuItem, tlsAlertContextMenuItem, managementContextItem],
},
];
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/certificates/certificates_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/certificates/certificates_list.tsx
index f85f16eea0a69..1832d9a8796d9 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/certificates/certificates_list.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/certificates/certificates_list.tsx
@@ -21,6 +21,10 @@ interface Page {
}
export type CertFields =
+ | 'monitorName'
+ | 'locationName'
+ | 'monitorType'
+ | 'monitorUrl'
| 'sha256'
| 'sha1'
| 'issuer'
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx
index 49148bb7cb4bf..17d540c05ca9c 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/field_config.tsx
@@ -500,6 +500,31 @@ export const FIELD = (readOnly?: boolean): FieldMap => ({
// isDisabled: readOnly,
}),
},
+ [AlertConfigKey.TLS_ENABLED]: {
+ fieldKey: AlertConfigKey.TLS_ENABLED,
+ component: Switch,
+ label: i18n.translate('xpack.synthetics.monitorConfig.enabledAlerting.tls.label', {
+ defaultMessage: 'Enable TLS alerts',
+ }),
+ controlled: true,
+ props: ({ isEdit, setValue, field }): EuiSwitchProps => ({
+ id: 'syntheticsMonitorConfigIsTlsAlertEnabled',
+ label: isEdit
+ ? i18n.translate('xpack.synthetics.monitorConfig.edit.alertTlsEnabled.label', {
+ defaultMessage: 'Disabling will stop tls alerting on this monitor.',
+ })
+ : i18n.translate('xpack.synthetics.monitorConfig.create.alertTlsEnabled.label', {
+ defaultMessage: 'Enable tls alerts on this monitor.',
+ }),
+ checked: field?.value || false,
+ onChange: (event) => {
+ setValue(AlertConfigKey.TLS_ENABLED, !!event.target.checked);
+ },
+ 'data-test-subj': 'syntheticsAlertStatusSwitch',
+ // alert config is an allowed field for read only
+ // isDisabled: readOnly,
+ }),
+ },
[ConfigKey.TAGS]: {
fieldKey: ConfigKey.TAGS,
component: FormattedComboBox,
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
index 8e74162ecef77..ffd8f1c3cfccd 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/form_config.tsx
@@ -199,6 +199,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
+ FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [
DEFAULT_DATA_OPTIONS(readOnly),
@@ -218,6 +219,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
+ FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [
DEFAULT_DATA_OPTIONS(readOnly),
@@ -285,6 +287,7 @@ export const FORM_CONFIG = (readOnly: boolean): FieldConfig => ({
FIELD(readOnly)[ConfigKey.TIMEOUT],
FIELD(readOnly)[ConfigKey.ENABLED],
FIELD(readOnly)[AlertConfigKey.STATUS_ENABLED],
+ FIELD(readOnly)[AlertConfigKey.TLS_ENABLED],
],
advanced: [DEFAULT_DATA_OPTIONS(readOnly), ICMP_ADVANCED(readOnly).requestConfig],
},
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts
index 3f81386a8cbdd..e63d65b73fdfb 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/types.ts
@@ -45,6 +45,7 @@ export type FormConfig = MonitorFields & {
['schedule.number']: string;
['source.inline']: string;
[AlertConfigKey.STATUS_ENABLED]: boolean;
+ [AlertConfigKey.TLS_ENABLED]: boolean;
[ConfigKey.LOCATIONS]: FormLocation[];
/* Dot notation keys must have a type configuration both for their flattened and nested
@@ -127,6 +128,7 @@ export interface FieldMap {
[ConfigKey.SCREENSHOTS]: FieldMeta;
[ConfigKey.ENABLED]: FieldMeta;
[AlertConfigKey.STATUS_ENABLED]: FieldMeta;
+ [AlertConfigKey.TLS_ENABLED]: FieldMeta;
[ConfigKey.NAMESPACE]: FieldMeta;
[ConfigKey.TIMEOUT]: FieldMeta;
[ConfigKey.MAX_REDIRECTS]: FieldMeta;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/index.ts
index 464ff13077872..0ae1a1da2729b 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/index.ts
@@ -7,6 +7,7 @@
import { CoreStart } from '@kbn/core/public';
import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
+import { initTlsAlertType } from './tls';
import { ClientPluginsStart } from '../../../../plugin';
import { initMonitorStatusAlertType } from './monitor_status';
@@ -15,4 +16,7 @@ export type AlertTypeInitializer =
plugins: ClientPluginsStart;
}) => TAlertTypeModel;
-export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [initMonitorStatusAlertType];
+export const syntheticsAlertTypeInitializers: AlertTypeInitializer[] = [
+ initMonitorStatusAlertType,
+ initTlsAlertType,
+];
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/tls_alert.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/tls_alert.tsx
new file mode 100644
index 0000000000000..1f04e9388c63f
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/tls_alert.tsx
@@ -0,0 +1,36 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { Provider as ReduxProvider } from 'react-redux';
+import { CoreStart } from '@kbn/core/public';
+import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
+import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
+import { TLSRuleComponent } from '../../../components/alerts/tls_rule_ui';
+import { ClientPluginsStart } from '../../../../../plugin';
+import { TLSParams } from '../../../../../../common/runtime_types/alerts/tls';
+import { kibanaService } from '../../../../../utils/kibana_service';
+import { store } from '../../../state';
+
+interface Props {
+ core: CoreStart;
+ plugins: ClientPluginsStart;
+ ruleParams: RuleTypeParamsExpressionProps['ruleParams'];
+ setRuleParams: RuleTypeParamsExpressionProps['setRuleParams'];
+}
+
+// eslint-disable-next-line import/no-default-export
+export default function TLSAlert({ core, plugins, ruleParams, setRuleParams }: Props) {
+ kibanaService.core = core;
+ return (
+
+
+
+
+
+ );
+}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/validate_tls_alert.ts b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/validate_tls_alert.ts
new file mode 100644
index 0000000000000..68f635b843693
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/lazy_wrapper/validate_tls_alert.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { PathReporter } from 'io-ts/lib/PathReporter';
+import { isRight } from 'fp-ts/lib/Either';
+import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
+import { TLSParamsType } from '../../../../../../common/runtime_types/alerts/tls';
+
+export function validateTLSAlertParams(ruleParams: any): ValidationResult {
+ const errors: Record = {};
+ const decoded = TLSParamsType.decode(ruleParams);
+
+ if (!isRight(decoded)) {
+ return {
+ errors: {
+ typeCheckFailure: 'Provided parameters do not conform to the expected type.',
+ typeCheckParsingMessage: PathReporter.report(decoded),
+ },
+ };
+ }
+
+ return { errors };
+}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx
new file mode 100644
index 0000000000000..c1ab8d88b4612
--- /dev/null
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/lib/alert_types/tls.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import { ALERT_REASON } from '@kbn/rule-data-utils';
+import { ObservabilityRuleTypeModel } from '@kbn/observability-plugin/public';
+import type { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
+import { ValidationResult } from '@kbn/triggers-actions-ui-plugin/public';
+import { TlsTranslations } from '../../../../../common/rules/synthetics/translations';
+import { CERTIFICATES_ROUTE } from '../../../../../common/constants/ui';
+import { SYNTHETICS_ALERT_RULE_TYPES } from '../../../../../common/constants/synthetics_alerts';
+import type { TLSParams } from '../../../../../common/runtime_types/alerts/tls';
+import { AlertTypeInitializer } from '.';
+
+let validateFunc: (ruleParams: any) => ValidationResult;
+
+const { defaultActionMessage, defaultRecoveryMessage, description } = TlsTranslations;
+const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
+export const initTlsAlertType: AlertTypeInitializer = ({
+ core,
+ plugins,
+}): ObservabilityRuleTypeModel => ({
+ id: SYNTHETICS_ALERT_RULE_TYPES.TLS,
+ iconClass: 'uptimeApp',
+ documentationUrl(docLinks) {
+ return `${docLinks.links.observability.tlsCertificate}`;
+ },
+ ruleParamsExpression: (params: RuleTypeParamsExpressionProps) => (
+
+ ),
+ description,
+ validate: (ruleParams: any) => {
+ if (!validateFunc) {
+ (async function loadValidate() {
+ const { validateTLSAlertParams } = await import('./lazy_wrapper/validate_tls_alert');
+ validateFunc = validateTLSAlertParams;
+ })();
+ }
+ return validateFunc ? validateFunc(ruleParams) : ({} as ValidationResult);
+ },
+ defaultActionMessage,
+ defaultRecoveryMessage,
+ requiresAppContext: false,
+ format: ({ fields }) => ({
+ reason: fields[ALERT_REASON] || '',
+ link: `/app/synthetics${CERTIFICATES_ROUTE}`,
+ }),
+});
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/actions.ts
index f25669f08285f..b168024f34c8c 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/actions.ts
@@ -8,9 +8,10 @@
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { createAsyncAction } from '../utils/actions';
-export const enableDefaultAlertingAction = createAsyncAction(
- 'enableDefaultAlertingAction'
-);
+export const enableDefaultAlertingAction = createAsyncAction<
+ void,
+ { statusRule: Rule; tlsRule: Rule }
+>('enableDefaultAlertingAction');
export const updateDefaultAlertingAction = createAsyncAction(
'updateDefaultAlertingAction'
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/api.ts
index 62f1a975d7cf0..7eb9921bb9aeb 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/api.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/alert_rules/api.ts
@@ -9,7 +9,7 @@ import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { apiService } from '../../../../utils/api_service';
-export async function enableDefaultAlertingAPI(): Promise {
+export async function enableDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
return apiService.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts
index 92f261e29dde5..e3738f3737cf0 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/actions.ts
@@ -6,15 +6,19 @@
*/
import { createAction } from '@reduxjs/toolkit';
+import {
+ SYNTHETICS_STATUS_RULE,
+ SYNTHETICS_TLS_RULE,
+} from '../../../../../common/constants/synthetics_alerts';
export interface PopoverState {
id: string;
open: boolean;
}
-export const setAlertFlyoutVisible = createAction('[UI] TOGGLE ALERT FLYOUT');
-
-export const setAlertFlyoutType = createAction('[UI] SET ALERT FLYOUT TYPE');
+export const setAlertFlyoutVisible = createAction<
+ typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE | null
+>('[UI] TOGGLE ALERT FLYOUT');
export const setBasePath = createAction('[UI] SET BASE PATH');
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts
index 9a8a41308d4a9..6c6ef93bbf3a7 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/index.ts
@@ -7,13 +7,16 @@
import { createReducer } from '@reduxjs/toolkit';
+import {
+ SYNTHETICS_STATUS_RULE,
+ SYNTHETICS_TLS_RULE,
+} from '../../../../../common/constants/synthetics_alerts';
import { CLIENT_DEFAULTS_SYNTHETICS } from '../../../../../common/constants/synthetics/client_defaults';
import {
PopoverState,
toggleIntegrationsPopover,
setBasePath,
setEsKueryString,
- setAlertFlyoutType,
setAlertFlyoutVisible,
setSearchTextAction,
setSelectedMonitorId,
@@ -23,8 +26,7 @@ import {
const { AUTOREFRESH_INTERVAL_SECONDS, AUTOREFRESH_IS_PAUSED } = CLIENT_DEFAULTS_SYNTHETICS;
export interface UiState {
- alertFlyoutVisible: boolean;
- alertFlyoutType?: string;
+ alertFlyoutVisible: typeof SYNTHETICS_TLS_RULE | typeof SYNTHETICS_STATUS_RULE | null;
basePath: string;
esKuery: string;
searchText: string;
@@ -35,7 +37,7 @@ export interface UiState {
}
const initialState: UiState = {
- alertFlyoutVisible: false,
+ alertFlyoutVisible: null,
basePath: '',
esKuery: '',
searchText: '',
@@ -51,10 +53,7 @@ export const uiReducer = createReducer(initialState, (builder) => {
state.integrationsPopoverOpen = action.payload;
})
.addCase(setAlertFlyoutVisible, (state, action) => {
- state.alertFlyoutVisible = action.payload ?? !state.alertFlyoutVisible;
- })
- .addCase(setAlertFlyoutType, (state, action) => {
- state.alertFlyoutType = action.payload;
+ state.alertFlyoutVisible = action.payload;
})
.addCase(setBasePath, (state, action) => {
state.basePath = action.payload;
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts
index 657980c3f6431..4e365d8343555 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/ui/selectors.ts
@@ -9,29 +9,12 @@ import { createSelector } from 'reselect';
import type { SyntheticsAppState } from '../root_reducer';
const uiStateSelector = (appState: SyntheticsAppState) => appState.ui;
-export const selectBasePath = createSelector(uiStateSelector, ({ basePath }) => basePath);
-
-export const selectIsIntegrationsPopupOpen = createSelector(
- uiStateSelector,
- ({ integrationsPopoverOpen }) => integrationsPopoverOpen
-);
export const selectAlertFlyoutVisibility = createSelector(
uiStateSelector,
({ alertFlyoutVisible }) => alertFlyoutVisible
);
-export const selectAlertFlyoutType = createSelector(
- uiStateSelector,
- ({ alertFlyoutType }) => alertFlyoutType
-);
-
-export const selectEsKuery = createSelector(uiStateSelector, ({ esKuery }) => esKuery);
-
-export const selectSearchText = createSelector(uiStateSelector, ({ searchText }) => searchText);
-
-export const selectMonitorId = createSelector(uiStateSelector, ({ monitorId }) => monitorId);
-
export const selectRefreshPaused = createSelector(
uiStateSelector,
({ refreshPaused }) => refreshPaused
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
index c97d99782ec02..a5c656d253fa3 100644
--- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
+++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/synthetics_store.mock.ts
@@ -23,7 +23,7 @@ import {
*/
export const mockState: SyntheticsAppState = {
ui: {
- alertFlyoutVisible: false,
+ alertFlyoutVisible: null,
basePath: 'yyz',
esKuery: '',
integrationsPopoverOpen: null,
diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/certificates/certificates_list.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/certificates/certificates_list.tsx
index 381ee9cd5b792..0c083db058f97 100644
--- a/x-pack/plugins/synthetics/public/legacy_uptime/components/certificates/certificates_list.tsx
+++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/certificates/certificates_list.tsx
@@ -21,6 +21,10 @@ interface Page {
}
export type CertFields =
+ | 'monitorName'
+ | 'locationName'
+ | 'monitorType'
+ | 'monitorUrl'
| 'sha256'
| 'sha1'
| 'issuer'
diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/message_utils.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/message_utils.ts
new file mode 100644
index 0000000000000..6f87c5242ec79
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/message_utils.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import moment from 'moment/moment';
+import { tlsTranslations } from '../translations';
+import { Cert } from '../../../common/runtime_types';
+interface TLSContent {
+ summary: string;
+ status?: string;
+}
+
+const getValidBefore = ({ not_before: date }: Cert): TLSContent => {
+ if (!date) return { summary: 'Error, missing `certificate_not_valid_before` date.' };
+ const relativeDate = moment().diff(date, 'days');
+ const formattedDate = moment(date).format('MMM D, YYYY z');
+ return relativeDate >= 0
+ ? {
+ summary: tlsTranslations.validBeforeExpiredString(formattedDate, relativeDate),
+ status: tlsTranslations.agingLabel,
+ }
+ : {
+ summary: tlsTranslations.validBeforeExpiringString(formattedDate, Math.abs(relativeDate)),
+ status: tlsTranslations.invalidLabel,
+ };
+};
+const getValidAfter = ({ not_after: date }: Cert): TLSContent => {
+ if (!date) return { summary: 'Error, missing `certificate_not_valid_after` date.' };
+ const relativeDate = moment().diff(date, 'days');
+ const formattedDate = moment(date).format('MMM D, YYYY z');
+ return relativeDate >= 0
+ ? {
+ summary: tlsTranslations.validAfterExpiredString(formattedDate, relativeDate),
+ status: tlsTranslations.expiredLabel,
+ }
+ : {
+ summary: tlsTranslations.validAfterExpiringString(formattedDate, Math.abs(relativeDate)),
+ status: tlsTranslations.expiringLabel,
+ };
+};
+const mapCertsToSummaryString = (
+ cert: Cert,
+ certLimitMessage: (cert: Cert) => TLSContent
+): TLSContent => certLimitMessage(cert);
+export const getCertSummary = (cert: Cert, expirationThreshold: number, ageThreshold: number) => {
+ const isExpiring = new Date(cert.not_after ?? '').valueOf() < expirationThreshold;
+ const isAging = new Date(cert.not_before ?? '').valueOf() < ageThreshold;
+ let content: TLSContent | null = null;
+
+ if (isExpiring) {
+ content = mapCertsToSummaryString(cert, getValidAfter);
+ } else if (isAging) {
+ content = mapCertsToSummaryString(cert, getValidBefore);
+ }
+
+ const { summary = '', status = '' } = content || {};
+ return {
+ summary,
+ status,
+ commonName: cert.common_name ?? '',
+ issuer: cert.issuer ?? '',
+ monitorName: cert.monitorName,
+ monitorType: cert.monitorType,
+ locationName: cert.locationName,
+ monitorUrl: cert.monitorUrl,
+ };
+};
diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts
new file mode 100644
index 0000000000000..fd31b5e8140ff
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts
@@ -0,0 +1,161 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common';
+import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server';
+import { asyncForEach } from '@kbn/std';
+import { ALERT_REASON, ALERT_UUID } from '@kbn/rule-data-utils';
+import {
+ alertsLocatorID,
+ AlertsLocatorParams,
+ getAlertUrl,
+} from '@kbn/observability-plugin/common';
+import { LocatorPublic } from '@kbn/share-plugin/common';
+import { schema } from '@kbn/config-schema';
+import { TlsTranslations } from '../../../common/rules/synthetics/translations';
+import {
+ CERT_COMMON_NAME,
+ CERT_HASH_SHA256,
+ CERT_ISSUER_NAME,
+ CERT_VALID_NOT_AFTER,
+ CERT_VALID_NOT_BEFORE,
+} from '../../../common/field_names';
+import { getCertSummary } from './message_utils';
+import { SyntheticsCommonState } from '../../../common/runtime_types/alert_rules/common';
+import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
+import { TLSRuleExecutor } from './tls_rule_executor';
+import {
+ SYNTHETICS_ALERT_RULE_TYPES,
+ TLS_CERTIFICATE,
+} from '../../../common/constants/synthetics_alerts';
+import { updateState } from '../common';
+import { getActionVariables } from '../action_variables';
+import { ALERT_DETAILS_URL } from '../../legacy_uptime/lib/alerts/action_variables';
+import { UMServerLibs } from '../../legacy_uptime/uptime_server';
+import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
+import {
+ generateAlertMessage,
+ setRecoveredAlertsContext,
+ UptimeRuleTypeAlertDefinition,
+} from '../../legacy_uptime/lib/alerts/common';
+
+export type ActionGroupIds = ActionGroupIdsOf;
+
+export const registerSyntheticsTLSCheckRule = (
+ server: UptimeServerSetup,
+ libs: UMServerLibs,
+ plugins: UptimeCorePluginsSetup,
+ syntheticsMonitorClient: SyntheticsMonitorClient,
+ ruleDataClient: IRuleDataClient
+) => {
+ const createLifecycleRuleType = createLifecycleRuleTypeFactory({
+ ruleDataClient,
+ logger: server.logger,
+ });
+
+ return createLifecycleRuleType({
+ id: SYNTHETICS_ALERT_RULE_TYPES.TLS,
+ producer: 'uptime',
+ name: TLS_CERTIFICATE.name,
+ validate: {
+ params: schema.object({
+ search: schema.maybe(schema.string()),
+ certExpirationThreshold: schema.maybe(schema.number()),
+ certAgeThreshold: schema.maybe(schema.number()),
+ }),
+ },
+ defaultActionGroupId: TLS_CERTIFICATE.id,
+ actionGroups: [TLS_CERTIFICATE],
+ actionVariables: getActionVariables({ plugins }),
+ isExportable: true,
+ minimumLicenseRequired: 'basic',
+ doesSetRecoveryContext: true,
+ async executor({ state, params, services, spaceId, previousStartedAt, startedAt }) {
+ const ruleState = state as SyntheticsCommonState;
+
+ const { basePath, share } = server;
+ const alertsLocator: LocatorPublic | undefined =
+ share.url.locators.get(alertsLocatorID);
+
+ const {
+ alertFactory,
+ getAlertUuid,
+ savedObjectsClient,
+ scopedClusterClient,
+ alertWithLifecycle,
+ getAlertStartedDate,
+ } = services;
+
+ const tlsRule = new TLSRuleExecutor(
+ previousStartedAt,
+ params,
+ savedObjectsClient,
+ scopedClusterClient.asCurrentUser,
+ server,
+ syntheticsMonitorClient
+ );
+
+ const { foundCerts, certs, absoluteExpirationThreshold, absoluteAgeThreshold } =
+ await tlsRule.getExpiredCertificates();
+
+ if (foundCerts) {
+ await asyncForEach(certs, async (cert) => {
+ const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
+
+ if (!summary.summary || !summary.status) {
+ return;
+ }
+
+ const alertId = `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`;
+ const alertUuid = getAlertUuid(alertId);
+ const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
+
+ const alertInstance = alertWithLifecycle({
+ id: alertId,
+ fields: {
+ [CERT_COMMON_NAME]: cert.common_name,
+ [CERT_ISSUER_NAME]: cert.issuer,
+ [CERT_VALID_NOT_AFTER]: cert.not_after,
+ [CERT_VALID_NOT_BEFORE]: cert.not_before,
+ [CERT_HASH_SHA256]: cert.sha256,
+ [ALERT_UUID]: alertUuid,
+ [ALERT_REASON]: generateAlertMessage(TlsTranslations.defaultActionMessage, summary),
+ },
+ });
+
+ alertInstance.replaceState({
+ ...updateState(ruleState, foundCerts),
+ ...summary,
+ });
+
+ alertInstance.scheduleActions(TLS_CERTIFICATE.id, {
+ [ALERT_DETAILS_URL]: await getAlertUrl(
+ alertUuid,
+ spaceId,
+ indexedStartedAt,
+ alertsLocator,
+ basePath.publicBaseUrl
+ ),
+ ...summary,
+ });
+ });
+ }
+
+ await setRecoveredAlertsContext({
+ alertFactory,
+ basePath,
+ defaultStartedAt: startedAt.toISOString(),
+ getAlertStartedDate,
+ getAlertUuid,
+ spaceId,
+ alertsLocator,
+ });
+
+ return { state: updateState(ruleState, foundCerts) };
+ },
+ alerts: UptimeRuleTypeAlertDefinition,
+ });
+};
diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.ts
new file mode 100644
index 0000000000000..a1377e19dbd29
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.test.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
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import moment from 'moment';
+import { loggerMock } from '@kbn/logging-mocks';
+import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
+import { TLSRuleExecutor } from './tls_rule_executor';
+import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
+import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
+import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
+import { SyntheticsService } from '../../synthetics_service/synthetics_service';
+import * as monitorUtils from '../../saved_objects/synthetics_monitor/get_all_monitors';
+import * as locationsUtils from '../../synthetics_service/get_all_locations';
+import type { PublicLocation } from '../../../common/runtime_types';
+
+describe('tlsRuleExecutor', () => {
+ const mockEsClient = elasticsearchClientMock.createElasticsearchClient();
+ const logger = loggerMock.create();
+ const soClient = savedObjectsClientMock.create();
+ jest.spyOn(locationsUtils, 'getAllLocations').mockResolvedValue({
+ // @ts-ignore
+ publicLocations: [
+ {
+ id: 'us_central_qa',
+ label: 'US Central QA',
+ },
+ {
+ id: 'us_central_dev',
+ label: 'US Central DEV',
+ },
+ ] as unknown as PublicLocation,
+ privateLocations: [],
+ });
+
+ const serverMock: UptimeServerSetup = {
+ logger,
+ uptimeEsClient: mockEsClient,
+ authSavedObjectsClient: soClient,
+ config: {
+ service: {
+ username: 'dev',
+ password: '12345',
+ manifestUrl: 'http://localhost:8080/api/manifest',
+ },
+ },
+ spaces: {
+ spacesService: {
+ getSpaceId: jest.fn().mockReturnValue('test-space'),
+ },
+ },
+ encryptedSavedObjects: mockEncryptedSO(),
+ } as unknown as UptimeServerSetup;
+
+ const syntheticsService = new SyntheticsService(serverMock);
+
+ const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
+
+ it('should only query enabled monitors', async () => {
+ const spy = jest.spyOn(monitorUtils, 'getAllMonitors').mockResolvedValue([]);
+ const tlsRule = new TLSRuleExecutor(
+ moment().toDate(),
+ {},
+ soClient,
+ mockEsClient,
+ serverMock,
+ monitorClient
+ );
+
+ const { certs } = await tlsRule.getExpiredCertificates();
+
+ expect(certs).toEqual([]);
+
+ expect(spy).toHaveBeenCalledWith({
+ filter: 'synthetics-monitor.attributes.alert.tls.enabled: true',
+ soClient,
+ });
+ });
+});
diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts
new file mode 100644
index 0000000000000..5949aed727740
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule_executor.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import {
+ SavedObjectsClientContract,
+ SavedObjectsFindResult,
+} from '@kbn/core-saved-objects-api-server';
+import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
+import moment from 'moment';
+import { getSyntheticsCerts } from '../../queries/get_certs';
+import { TLSParams } from '../../../common/runtime_types/alerts/tls';
+import { savedObjectsAdapter } from '../../legacy_uptime/lib/saved_objects';
+import { DYNAMIC_SETTINGS_DEFAULTS, SYNTHETICS_INDEX_PATTERN } from '../../../common/constants';
+import {
+ getAllMonitors,
+ processMonitors,
+} from '../../saved_objects/synthetics_monitor/get_all_monitors';
+import { UptimeEsClient } from '../../legacy_uptime/lib/lib';
+import { CertResult, EncryptedSyntheticsMonitor } from '../../../common/runtime_types';
+import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
+import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
+import { monitorAttributes } from '../../../common/types/saved_objects';
+import { AlertConfigKey } from '../../../common/constants/monitor_management';
+import { formatFilterString } from '../../legacy_uptime/lib/alerts/status_check';
+
+export class TLSRuleExecutor {
+ previousStartedAt: Date | null;
+ params: TLSParams;
+ esClient: UptimeEsClient;
+ soClient: SavedObjectsClientContract;
+ server: UptimeServerSetup;
+ syntheticsMonitorClient: SyntheticsMonitorClient;
+ monitors: Array> = [];
+
+ constructor(
+ previousStartedAt: Date | null,
+ p: TLSParams,
+ soClient: SavedObjectsClientContract,
+ scopedClient: ElasticsearchClient,
+ server: UptimeServerSetup,
+ syntheticsMonitorClient: SyntheticsMonitorClient
+ ) {
+ this.previousStartedAt = previousStartedAt;
+ this.params = p;
+ this.soClient = soClient;
+ this.esClient = new UptimeEsClient(this.soClient, scopedClient, {
+ heartbeatIndices: SYNTHETICS_INDEX_PATTERN,
+ });
+ this.server = server;
+ this.syntheticsMonitorClient = syntheticsMonitorClient;
+ }
+
+ async getMonitors() {
+ this.monitors = await getAllMonitors({
+ soClient: this.soClient,
+ filter: `${monitorAttributes}.${AlertConfigKey.TLS_ENABLED}: true`,
+ });
+
+ const {
+ allIds,
+ enabledMonitorQueryIds,
+ listOfLocations,
+ monitorLocationMap,
+ projectMonitorsCount,
+ monitorQueryIdToConfigIdMap,
+ } = await processMonitors(
+ this.monitors,
+ this.server,
+ this.soClient,
+ this.syntheticsMonitorClient
+ );
+
+ return {
+ enabledMonitorQueryIds,
+ listOfLocations,
+ allIds,
+ monitorLocationMap,
+ projectMonitorsCount,
+ monitorQueryIdToConfigIdMap,
+ };
+ }
+
+ async getExpiredCertificates() {
+ const { enabledMonitorQueryIds } = await this.getMonitors();
+
+ const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(this.soClient);
+
+ const expiryThreshold =
+ this.params.certExpirationThreshold ??
+ dynamicSettings?.certExpirationThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold;
+
+ const ageThreshold =
+ this.params.certAgeThreshold ??
+ dynamicSettings?.certAgeThreshold ??
+ DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold;
+
+ const absoluteExpirationThreshold = moment().add(expiryThreshold, 'd').valueOf();
+ const absoluteAgeThreshold = moment().subtract(ageThreshold, 'd').valueOf();
+
+ if (enabledMonitorQueryIds.length === 0) {
+ return {
+ certs: [],
+ total: 0,
+ foundCerts: false,
+ expiryThreshold,
+ ageThreshold,
+ absoluteExpirationThreshold,
+ absoluteAgeThreshold,
+ };
+ }
+
+ let filters: QueryDslQueryContainer | undefined;
+
+ if (this.params.search) {
+ filters = await formatFilterString(this.esClient, undefined, this.params.search);
+ }
+
+ const { certs, total }: CertResult = await getSyntheticsCerts({
+ uptimeEsClient: this.esClient,
+ pageIndex: 0,
+ size: 1000,
+ notValidAfter: `now+${expiryThreshold}d`,
+ notValidBefore: `now-${ageThreshold}d`,
+ sortBy: 'common_name',
+ direction: 'desc',
+ filters,
+ monitorIds: enabledMonitorQueryIds,
+ });
+
+ const foundCerts = total > 0;
+
+ return {
+ foundCerts,
+ certs,
+ total,
+ expiryThreshold,
+ ageThreshold,
+ absoluteExpirationThreshold,
+ absoluteAgeThreshold,
+ };
+ }
+}
diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/types.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/types.ts
new file mode 100644
index 0000000000000..9c8cc0b9a6e36
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/types.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+export interface MonitorSummaryStatusRule {
+ reason: string;
+ status: string;
+ configId: string;
+ hostName: string;
+ monitorId: string;
+ checkedAt: string;
+ monitorUrl: string;
+ locationId: string;
+ monitorType: string;
+ monitorName: string;
+ locationName: string;
+ lastErrorMessage: string;
+ stateId: string | null;
+ monitorUrlLabel: string;
+}
diff --git a/x-pack/plugins/synthetics/server/alert_rules/translations.ts b/x-pack/plugins/synthetics/server/alert_rules/translations.ts
index 99bc5143e66a6..f501a8bf1c5c7 100644
--- a/x-pack/plugins/synthetics/server/alert_rules/translations.ts
+++ b/x-pack/plugins/synthetics/server/alert_rules/translations.ts
@@ -164,3 +164,91 @@ export const commonStateTranslations = [
),
},
];
+
+export const tlsTranslations = {
+ actionVariables: [
+ {
+ name: 'count',
+ description: i18n.translate('xpack.synthetics.rules.tls.actionVariables.state.count', {
+ defaultMessage: 'The number of certs detected by the alert executor',
+ }),
+ },
+ {
+ name: 'expiringCount',
+ description: i18n.translate(
+ 'xpack.synthetics.rules.tls.actionVariables.state.expiringCount',
+ {
+ defaultMessage: 'The number of expiring certs detected by the alert.',
+ }
+ ),
+ },
+ {
+ name: 'expiringCommonNameAndDate',
+ description: i18n.translate(
+ 'xpack.synthetics.rules.tls.actionVariables.state.expiringCommonNameAndDate',
+ {
+ defaultMessage: 'The common names and expiration date/time of the detected certs',
+ }
+ ),
+ },
+ {
+ name: 'agingCount',
+ description: i18n.translate('xpack.synthetics.rules.tls.actionVariables.state.agingCount', {
+ defaultMessage: 'The number of detected certs that are becoming too old.',
+ }),
+ },
+ {
+ name: 'agingCommonNameAndDate',
+ description: i18n.translate(
+ 'xpack.synthetics.rules.tls.actionVariables.state.agingCommonNameAndDate',
+ {
+ defaultMessage: 'The common names and expiration date/time of the detected certs.',
+ }
+ ),
+ },
+ ],
+ validAfterExpiredString: (date: string, relativeDate: number) =>
+ i18n.translate('xpack.synthetics.rules.tls.validAfterExpiredString', {
+ defaultMessage: `Expired on {date}, {relativeDate} days ago.`,
+ values: {
+ date,
+ relativeDate,
+ },
+ }),
+ validAfterExpiringString: (date: string, relativeDate: number) =>
+ i18n.translate('xpack.synthetics.rules.tls.validAfterExpiringString', {
+ defaultMessage: `Expires on {date} in {relativeDate} days.`,
+ values: {
+ date,
+ relativeDate,
+ },
+ }),
+ validBeforeExpiredString: (date: string, relativeDate: number) =>
+ i18n.translate('xpack.synthetics.rules.tls.validBeforeExpiredString', {
+ defaultMessage: 'valid since {date}, {relativeDate} days ago.',
+ values: {
+ date,
+ relativeDate,
+ },
+ }),
+ validBeforeExpiringString: (date: string, relativeDate: number) =>
+ i18n.translate('xpack.synthetics.rules.tls.validBeforeExpiringString', {
+ defaultMessage: 'invalid until {date}, {relativeDate} days from now.',
+ values: {
+ date,
+ relativeDate,
+ },
+ }),
+ expiredLabel: i18n.translate('xpack.synthetics.rules.tls.expiredLabel', {
+ defaultMessage: 'expired',
+ }),
+ expiringLabel: i18n.translate('xpack.synthetics.rules.tls.expiringLabel', {
+ defaultMessage: 'expiring',
+ }),
+ agingLabel: i18n.translate('xpack.synthetics.rules.tls.agingLabel', {
+ defaultMessage: 'becoming too old',
+ }),
+ invalidLabel: i18n.translate('xpack.synthetics.rules.tls.invalidLabel', {
+ defaultMessage: 'invalid',
+ }),
+};
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_certs.test.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_certs.test.ts
index e6874dbfd79a8..f9523c09e43a1 100644
--- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_certs.test.ts
+++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/requests/get_certs.test.ts
@@ -112,6 +112,10 @@ describe('getCerts', () => {
Object {
"common_name": "r2.shared.global.fastly.net",
"issuer": "GlobalSign CloudSSL CA - SHA256 - G3",
+ "locationName": undefined,
+ "monitorName": "Real World Test",
+ "monitorType": undefined,
+ "monitorUrl": "https://fullurl.com",
"monitors": Array [
Object {
"configId": undefined,
@@ -137,6 +141,9 @@ describe('getCerts', () => {
"_source": Array [
"monitor.id",
"monitor.name",
+ "monitor.type",
+ "url.full",
+ "observer.geo.name",
"tls.server.x509.issuer.common_name",
"tls.server.x509.subject.common_name",
"tls.server.hash.sha1",
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.test.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.test.ts
new file mode 100644
index 0000000000000..3c23c008824a4
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.test.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
+import { migrationMocks } from '@kbn/core/server/mocks';
+import { ConfigKey } from '../../../../../../common/runtime_types';
+import { browserUI } from './test_fixtures/8.7.0';
+import { httpUI as httpUI850 } from './test_fixtures/8.5.0';
+import { migration890 } from './8.9.0';
+
+const context = migrationMocks.createContext();
+const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
+
+describe('Monitor migrations v8.8.0 -> v8.9.0', () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ encryptedSavedObjectsSetup.createMigration.mockImplementation(({ migration }) => migration);
+ });
+
+ describe('alerting config', () => {
+ it('sets alerting config when it is not defined', () => {
+ expect(httpUI850.attributes[ConfigKey.ALERT_CONFIG]).toBeUndefined();
+ const actual = migration890(encryptedSavedObjectsSetup)(httpUI850, context);
+ expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
+ status: {
+ enabled: true,
+ },
+ tls: {
+ enabled: true,
+ },
+ });
+ });
+
+ it('uses existing alerting config when it is defined', () => {
+ const testMonitor = {
+ ...browserUI,
+ attributes: {
+ ...browserUI.attributes,
+ [ConfigKey.ALERT_CONFIG]: {
+ status: {
+ enabled: false,
+ },
+ tls: {
+ enabled: true,
+ },
+ },
+ },
+ };
+ expect(testMonitor.attributes[ConfigKey.ALERT_CONFIG]).toBeTruthy();
+ const actual = migration890(encryptedSavedObjectsSetup)(testMonitor, context);
+ expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
+ status: {
+ enabled: false,
+ },
+ tls: {
+ enabled: true,
+ },
+ });
+ });
+
+ it('uses existing alerting config when it already exists', () => {
+ const testMonitor = {
+ ...browserUI,
+ attributes: {
+ ...browserUI.attributes,
+ [ConfigKey.ALERT_CONFIG]: {
+ status: {
+ enabled: false,
+ },
+ },
+ },
+ };
+ expect(testMonitor.attributes[ConfigKey.ALERT_CONFIG]).toBeTruthy();
+ const actual = migration890(encryptedSavedObjectsSetup)(testMonitor, context);
+ expect(actual.attributes[ConfigKey.ALERT_CONFIG]).toEqual({
+ status: {
+ enabled: false,
+ },
+ tls: {
+ enabled: true,
+ },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.ts
new file mode 100644
index 0000000000000..d18c6473cc729
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/8.9.0.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
+import { SavedObjectUnsanitizedDoc } from '@kbn/core/server';
+import { ConfigKey, SyntheticsMonitorWithSecrets } from '../../../../../../common/runtime_types';
+import { SYNTHETICS_MONITOR_ENCRYPTED_TYPE } from '../../synthetics_monitor';
+
+export const migration890 = (encryptedSavedObjects: EncryptedSavedObjectsPluginSetup) => {
+ return encryptedSavedObjects.createMigration<
+ SyntheticsMonitorWithSecrets,
+ SyntheticsMonitorWithSecrets
+ >({
+ isMigrationNeededPredicate: function shouldBeMigrated(
+ doc
+ ): doc is SavedObjectUnsanitizedDoc {
+ return true;
+ },
+ migration: (
+ doc: SavedObjectUnsanitizedDoc
+ ): SavedObjectUnsanitizedDoc => {
+ let migrated = doc;
+ migrated = {
+ ...migrated,
+ attributes: {
+ ...migrated.attributes,
+ [ConfigKey.ALERT_CONFIG]: {
+ status: {
+ enabled: true,
+ },
+ tls: {
+ enabled: true,
+ },
+ ...(migrated.attributes[ConfigKey.ALERT_CONFIG] ?? {}),
+ },
+ // when any action to change a project monitor configuration is taken
+ // outside the synthetics agent cli, we should set the config hash back
+ // to an empty string so that the project monitors configuration
+ // will be updated on next push
+ [ConfigKey.CONFIG_HASH]: '',
+ },
+ };
+
+ return migrated;
+ },
+ inputType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
+ migratedType: SYNTHETICS_MONITOR_ENCRYPTED_TYPE,
+ });
+};
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/index.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/index.ts
index b200e7b09b389..6e20a0231e0bc 100644
--- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/index.ts
+++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/migrations/monitors/index.ts
@@ -5,10 +5,12 @@
* 2.0.
*/
+import { migration890 } from './8.9.0';
import { migration860 } from './8.6.0';
import { migration880 } from './8.8.0';
export const monitorMigrations = {
'8.6.0': migration860,
'8.8.0': migration880,
+ '8.9.0': migration890,
};
diff --git a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts
index cec7d8600a380..8b57943b3b7ce 100644
--- a/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts
+++ b/x-pack/plugins/synthetics/server/legacy_uptime/lib/saved_objects/synthetics_monitor.ts
@@ -60,6 +60,7 @@ export const getSyntheticsMonitorSavedObjectType = (
migrations: {
'8.6.0': monitorMigrations['8.6.0'](encryptedSavedObjects),
'8.8.0': monitorMigrations['8.8.0'](encryptedSavedObjects),
+ '8.9.0': monitorMigrations['8.9.0'](encryptedSavedObjects),
},
mappings: {
dynamic: false,
@@ -167,6 +168,13 @@ export const getSyntheticsMonitorSavedObjectType = (
},
},
},
+ tls: {
+ properties: {
+ enabled: {
+ type: 'boolean',
+ },
+ },
+ },
},
},
throttling: {
diff --git a/x-pack/plugins/synthetics/server/queries/get_certs.ts b/x-pack/plugins/synthetics/server/queries/get_certs.ts
new file mode 100644
index 0000000000000..0f3ea6d2a4294
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/queries/get_certs.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { CertResult, GetCertsParams, Ping } from '../../common/runtime_types';
+import {
+ getCertsRequestBody,
+ processCertsResult,
+} from '../../common/requests/get_certs_request_body';
+import { UptimeEsClient } from '../legacy_uptime/lib/lib';
+
+export const getSyntheticsCerts = async (
+ requestParams: GetCertsParams & { uptimeEsClient: UptimeEsClient }
+): Promise => {
+ const result = await getCertsResults(requestParams);
+
+ return processCertsResult(result);
+};
+
+const getCertsResults = async (
+ requestParams: GetCertsParams & { uptimeEsClient: UptimeEsClient }
+) => {
+ const { uptimeEsClient } = requestParams;
+
+ const searchBody = getCertsRequestBody(requestParams);
+
+ const request = { body: searchBody };
+
+ const { body: result } = await uptimeEsClient.search({
+ body: searchBody,
+ });
+
+ return result;
+};
diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts
index eb14a6dea8a65..f483ffa186b38 100644
--- a/x-pack/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts
+++ b/x-pack/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { TLSAlertService } from './tls_alert_service';
import { StatusAlertService } from './status_alert_service';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@@ -16,7 +17,23 @@ export const enableDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => (
writeAccess: true,
handler: async ({ context, server, savedObjectsClient }): Promise => {
const statusAlertService = new StatusAlertService(context, server, savedObjectsClient);
+ const tlsAlertService = new TLSAlertService(context, server, savedObjectsClient);
- return await statusAlertService.createDefaultAlertIfNotExist();
+ const [statusRule, tlsRule] = await Promise.allSettled([
+ statusAlertService.createDefaultAlertIfNotExist(),
+ tlsAlertService.createDefaultAlertIfNotExist(),
+ ]);
+
+ if (statusRule.status === 'rejected') {
+ throw statusRule.reason;
+ }
+ if (tlsRule.status === 'rejected') {
+ throw tlsRule.reason;
+ }
+
+ return {
+ statusRule: statusRule.status === 'fulfilled' ? statusRule.value : null,
+ tlsRule: tlsRule.status === 'fulfilled' ? tlsRule.value : null,
+ };
},
});
diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/status_alert_service.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/status_alert_service.ts
index e175325d51f4c..c6da83c73681f 100644
--- a/x-pack/plugins/synthetics/server/routes/default_alerts/status_alert_service.ts
+++ b/x-pack/plugins/synthetics/server/routes/default_alerts/status_alert_service.ts
@@ -66,7 +66,6 @@ export class StatusAlertService {
consumer: 'uptime',
alertTypeId: SYNTHETICS_ALERT_RULE_TYPES.MONITOR_STATUS,
schedule: { interval: '1m' },
- notifyWhen: 'onActionGroupChange',
tags: ['SYNTHETICS_DEFAULT_ALERT'],
name: `Synthetics internal alert`,
enabled: true,
diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/tls_alert_service.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/tls_alert_service.ts
new file mode 100644
index 0000000000000..62c400281f28b
--- /dev/null
+++ b/x-pack/plugins/synthetics/server/routes/default_alerts/tls_alert_service.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
+import { FindActionResult } from '@kbn/actions-plugin/server';
+import { TLSParams } from '../../../common/runtime_types/alerts/tls';
+import { savedObjectsAdapter } from '../../legacy_uptime/lib/saved_objects';
+import { UptimeServerSetup } from '../../legacy_uptime/lib/adapters';
+import { populateAlertActions } from '../../../common/rules/alert_actions';
+import { TlsTranslations } from '../../../common/rules/synthetics/translations';
+import { UptimeRequestHandlerContext } from '../../types';
+import {
+ ACTION_GROUP_DEFINITIONS,
+ SYNTHETICS_ALERT_RULE_TYPES,
+} from '../../../common/constants/synthetics_alerts';
+
+// uuid based on the string 'uptime-tls-default-alert'
+const TLS_DEFAULT_ALERT_ID = '7a532181-ff1d-4317-9367-7ca789133920';
+
+export class TLSAlertService {
+ context: UptimeRequestHandlerContext;
+ soClient: SavedObjectsClientContract;
+ server: UptimeServerSetup;
+
+ constructor(
+ context: UptimeRequestHandlerContext,
+ server: UptimeServerSetup,
+ soClient: SavedObjectsClientContract
+ ) {
+ this.context = context;
+ this.server = server;
+ this.soClient = soClient;
+ }
+
+ async getExistingAlert() {
+ const rulesClient = (await this.context.alerting)?.getRulesClient();
+ try {
+ const alert = await rulesClient.get({ id: TLS_DEFAULT_ALERT_ID });
+ return { ...alert, ruleTypeId: alert.alertTypeId };
+ } catch (e) {
+ return null;
+ }
+ }
+ async createDefaultAlertIfNotExist() {
+ const alert = await this.getExistingAlert();
+ if (alert) {
+ return alert;
+ }
+
+ const actions = await this.getAlertActions();
+
+ const rulesClient = (await this.context.alerting)?.getRulesClient();
+ const newAlert = await rulesClient.create({
+ data: {
+ actions,
+ params: {},
+ consumer: 'uptime',
+ alertTypeId: SYNTHETICS_ALERT_RULE_TYPES.TLS,
+ schedule: { interval: '1m' },
+ tags: ['SYNTHETICS_TLS_DEFAULT_ALERT'],
+ name: `Synthetics internal TLS alert`,
+ enabled: true,
+ throttle: null,
+ },
+ options: {
+ id: TLS_DEFAULT_ALERT_ID,
+ },
+ });
+ return { ...newAlert, ruleTypeId: newAlert.alertTypeId };
+ }
+
+ async updateDefaultAlert() {
+ const rulesClient = (await this.context.alerting)?.getRulesClient();
+
+ const alert = await this.getExistingAlert();
+ if (alert) {
+ const actions = await this.getAlertActions();
+ const updatedAlert = await rulesClient.update({
+ id: alert.id,
+ data: {
+ actions,
+ name: alert.name,
+ tags: alert.tags,
+ schedule: alert.schedule,
+ params: alert.params,
+ notifyWhen: alert.notifyWhen,
+ },
+ });
+ return { ...updatedAlert, ruleTypeId: updatedAlert.alertTypeId };
+ }
+
+ return await this.createDefaultAlertIfNotExist();
+ }
+
+ async getAlertActions() {
+ const { actionConnectors, settings } = await this.getActionConnectors();
+
+ const defaultActions = (actionConnectors ?? []).filter((act) =>
+ settings?.defaultConnectors?.includes(act.id)
+ );
+
+ return populateAlertActions({
+ groupId: ACTION_GROUP_DEFINITIONS.TLS_CERTIFICATE.id,
+ defaultActions,
+ defaultEmail: settings?.defaultEmail!,
+ translations: {
+ defaultActionMessage: TlsTranslations.defaultActionMessage,
+ defaultRecoveryMessage: TlsTranslations.defaultRecoveryMessage,
+ defaultSubjectMessage: TlsTranslations.defaultSubjectMessage,
+ defaultRecoverySubjectMessage: TlsTranslations.defaultRecoverySubjectMessage,
+ },
+ });
+ }
+
+ async getActionConnectors() {
+ const actionsClient = (await this.context.actions)?.getActionsClient();
+
+ const settings = await savedObjectsAdapter.getUptimeDynamicSettings(this.soClient);
+ let actionConnectors: FindActionResult[] = [];
+ try {
+ actionConnectors = await actionsClient.getAll();
+ } catch (e) {
+ this.server.logger.error(e);
+ }
+ return { actionConnectors, settings };
+ }
+}
diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts
index 8945b4b4455db..4435a4e962daf 100644
--- a/x-pack/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts
+++ b/x-pack/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts
@@ -5,6 +5,7 @@
* 2.0.
*/
+import { TLSAlertService } from './tls_alert_service';
import { StatusAlertService } from './status_alert_service';
import { SyntheticsRestApiRouteFactory } from '../../legacy_uptime/routes';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
@@ -16,7 +17,11 @@ export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => (
writeAccess: true,
handler: async ({ context, server, savedObjectsClient }): Promise => {
const statusAlertService = new StatusAlertService(context, server, savedObjectsClient);
+ const tlsAlertService = new TLSAlertService(context, server, savedObjectsClient);
- return await statusAlertService.updateDefaultAlert();
+ return Promise.allSettled([
+ statusAlertService.updateDefaultAlert(),
+ tlsAlertService.updateDefaultAlert(),
+ ]);
},
});
diff --git a/x-pack/plugins/synthetics/server/server.ts b/x-pack/plugins/synthetics/server/server.ts
index 49652f1ccc27a..e7a3031056f49 100644
--- a/x-pack/plugins/synthetics/server/server.ts
+++ b/x-pack/plugins/synthetics/server/server.ts
@@ -6,6 +6,7 @@
*/
import { Subject } from 'rxjs';
import { IRuleDataClient } from '@kbn/rule-registry-plugin/server';
+import { registerSyntheticsTLSCheckRule } from './alert_rules/tls_rule/tls_rule';
import { registerSyntheticsStatusCheckRule } from './alert_rules/status_rule/monitor_status_rule';
import { UptimeRequestHandlerContext } from './types';
import { createSyntheticsRouteWithAuth } from './routes/create_route_with_auth';
@@ -73,6 +74,16 @@ export const initSyntheticsServer = (
registerType(statusAlert);
+ const tlsRule = registerSyntheticsTLSCheckRule(
+ server,
+ libs,
+ plugins,
+ syntheticsMonitorClient,
+ ruleDataClient
+ );
+
+ registerType(tlsRule);
+
syntheticsAppStreamingApiRoutes.forEach((route) => {
const { method, streamHandler, path, options } = syntheticsRouteWrapper(
createSyntheticsRouteWithAuth(libs, route),
diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts
index 60f2089243043..f86c3097e809c 100644
--- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.test.ts
@@ -108,6 +108,9 @@ describe('getNormalizeCommonFields', () => {
status: {
enabled: statusEnabled ?? true,
},
+ tls: {
+ enabled: true,
+ },
},
custom_heartbeat_id: 'test-id-test-projectId-test-namespace',
enabled: true,
@@ -169,6 +172,9 @@ describe('getNormalizeCommonFields', () => {
status: {
enabled: true,
},
+ tls: {
+ enabled: true,
+ },
},
custom_heartbeat_id: 'test-id-test-projectId-test-namespace',
enabled: true,
diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts
index 4f44dcb3283be..bbe233a66e763 100644
--- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/common_fields.ts
@@ -88,22 +88,35 @@ export const getNormalizeCommonFields = ({
? JSON.stringify(monitor.params)
: defaultFields[ConfigKey.PARAMS],
// picking out keys specifically, so users can't add arbitrary fields
- [ConfigKey.ALERT_CONFIG]: monitor.alert
- ? {
- ...defaultFields[ConfigKey.ALERT_CONFIG],
- status: {
- ...defaultFields[ConfigKey.ALERT_CONFIG]?.status,
- enabled:
- monitor.alert?.status?.enabled ??
- defaultFields[ConfigKey.ALERT_CONFIG]?.status?.enabled ??
- true,
- },
- }
- : defaultFields[ConfigKey.ALERT_CONFIG],
+ [ConfigKey.ALERT_CONFIG]: getAlertConfig(monitor),
};
return { normalizedFields, errors };
};
+const getAlertConfig = (monitor: ProjectMonitor) => {
+ const defaultFields = DEFAULT_COMMON_FIELDS;
+
+ return monitor.alert
+ ? {
+ ...defaultFields[ConfigKey.ALERT_CONFIG],
+ status: {
+ ...defaultFields[ConfigKey.ALERT_CONFIG]?.status,
+ enabled:
+ monitor.alert?.status?.enabled ??
+ defaultFields[ConfigKey.ALERT_CONFIG]?.status?.enabled ??
+ true,
+ },
+ tls: {
+ ...defaultFields[ConfigKey.ALERT_CONFIG]?.tls,
+ enabled:
+ monitor.alert?.tls?.enabled ??
+ defaultFields[ConfigKey.ALERT_CONFIG]?.tls?.enabled ??
+ true,
+ },
+ }
+ : defaultFields[ConfigKey.ALERT_CONFIG];
+};
+
export const getCustomHeartbeatId = (
monitor: NormalizedProjectProps['monitor'],
projectId: string,
diff --git a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts
index 0045cc711f9bc..892fbd53617a1 100644
--- a/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts
+++ b/x-pack/plugins/synthetics/server/synthetics_service/project_monitor/normalizers/http_monitor.ts
@@ -46,7 +46,7 @@ export const getNormalizeHTTPFields = ({
version,
});
- // Add common erros to errors arary
+ // Add common errors to errors array
errors.push(...commonErrors);
/* Check if monitor has multiple urls */
diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts
index 101cf69fc6034..cb74808e3d63b 100644
--- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts
+++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group4/check_registered_rule_types.ts
@@ -31,6 +31,7 @@ export default function createRegisteredRuleTypeTests({ getService }: FtrProvide
'xpack.ml.anomaly_detection_alert',
'xpack.ml.anomaly_detection_jobs_health',
'xpack.synthetics.alerts.monitorStatus',
+ 'xpack.synthetics.alerts.tls',
'xpack.uptime.alerts.monitorStatus',
'xpack.uptime.alerts.tlsCertificate',
'xpack.uptime.alerts.durationAnomaly',
diff --git a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
index bf0b46022f83e..439bd1b986a11 100644
--- a/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
+++ b/x-pack/test/api_integration/apis/synthetics/add_monitor_project.ts
@@ -176,6 +176,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
+ tls: {
+ enabled: true,
+ },
},
'filter_journeys.match': 'check if title is present',
'filter_journeys.tags': [],
@@ -358,6 +361,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
+ tls: {
+ enabled: true,
+ },
},
form_monitor_type: 'http',
journey_id: journeyId,
@@ -474,6 +480,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
+ tls: {
+ enabled: true,
+ },
},
form_monitor_type: 'tcp',
journey_id: journeyId,
@@ -580,6 +589,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: true,
},
+ tls: {
+ enabled: true,
+ },
},
form_monitor_type: 'icmp',
journey_id: journeyId,
@@ -1920,7 +1932,13 @@ export default function ({ getService }: FtrProviderContext) {
it('project monitors - handles alert config without adding arbitrary fields', async () => {
const project = `test-project-${uuidv4()}`;
const testAlert = {
- status: { enabled: false, doesnotexit: true },
+ status: {
+ enabled: false,
+ doesnotexit: true,
+ tls: {
+ enabled: true,
+ },
+ },
};
try {
await supertest
@@ -1953,6 +1971,9 @@ export default function ({ getService }: FtrProviderContext) {
status: {
enabled: testAlert.status.enabled,
},
+ tls: {
+ enabled: true,
+ },
});
} finally {
await deleteMonitor(httpProjectMonitors.monitors[1].id, project);
diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
index 48d15ce6d8c8c..fb2fb52dbbd52 100644
--- a/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
+++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/check_registered_task_types.ts
@@ -105,6 +105,7 @@ export default function ({ getService }: FtrProviderContext) {
'alerting:xpack.ml.anomaly_detection_alert',
'alerting:xpack.ml.anomaly_detection_jobs_health',
'alerting:xpack.synthetics.alerts.monitorStatus',
+ 'alerting:xpack.synthetics.alerts.tls',
'alerting:xpack.uptime.alerts.durationAnomaly',
'alerting:xpack.uptime.alerts.monitorStatus',
'alerting:xpack.uptime.alerts.tls',
From 06a00d6b596d839ef084a5489237c6927279b02b Mon Sep 17 00:00:00 2001
From: Yngrid Coello
Date: Tue, 20 Jun 2023 13:23:47 +0200
Subject: [PATCH 04/14] [Logs onboarding] design feedback (#159790)
closes https://github.com/elastic/kibana/issues/159655.
- [x] Make sure inputs in configure logs step occupy the whole width
- [x] Fix copy button in apiKey callout, currently is cropped
- [x] Add space bellow action buttons (back and continue)
Apart from the tasks above, we are hiding `Give feedback` button from
plugin root page, since the form is dedicated to logs onboarding.
https://github.com/elastic/kibana/assets/1313018/80872e8a-f239-4584-9b15-7ec8cdd32d5d
---
.../app/custom_logs/wizard/api_key_banner.tsx | 6 ++-
.../app/custom_logs/wizard/configure_logs.tsx | 2 +-
.../app/header_action_menu/index.tsx | 41 +++++++++++--------
.../public/components/shared/step_panel.tsx | 5 ++-
4 files changed, 35 insertions(+), 19 deletions(-)
diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx
index f7e73161fa8b3..2254105f979ff 100644
--- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/api_key_banner.tsx
@@ -94,7 +94,11 @@ export function ApiKeyBanner({
iconType="copyClipboard"
onClick={copy}
color="success"
- style={{ backgroundColor: 'transparent' }}
+ css={{
+ '> svg.euiIcon': {
+ borderRadius: '0 !important',
+ },
+ }}
aria-label={i18n.translate(
'xpack.observability_onboarding.apiKeyBanner.field.copyButton',
{
diff --git a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx
index e86710f71c5c8..317b615c7226d 100644
--- a/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/app/custom_logs/wizard/configure_logs.tsx
@@ -258,7 +258,7 @@ export function ConfigureLogs() {
direction="column"
gutterSize="xs"
>
-
+
- {i18n.translate('xpack.observability_onboarding.header.feedback', {
- defaultMessage: 'Give feedback',
- })}
-
- );
+ const location = useLocation();
+ const normalizedPathname = location.pathname.replace(/\/$/, '');
+
+ const isRootPage = normalizedPathname === '';
+
+ if (!isRootPage) {
+ return (
+
+ {i18n.translate('xpack.observability_onboarding.header.feedback', {
+ defaultMessage: 'Give feedback',
+ })}
+
+ );
+ }
+
+ return <>>;
}
diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx
index b38eaedb0df8f..f5b53cc6b93eb 100644
--- a/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx
+++ b/x-pack/plugins/observability_onboarding/public/components/shared/step_panel.tsx
@@ -13,6 +13,7 @@ import {
EuiPanelProps,
EuiSpacer,
EuiTitle,
+ useEuiTheme,
} from '@elastic/eui';
interface StepPanelProps {
@@ -57,8 +58,10 @@ interface StepPanelFooterProps {
}
export function StepPanelFooter(props: StepPanelFooterProps) {
const { items = [], children } = props;
+ const { euiTheme } = useEuiTheme();
+
return (
-
+
{children}
{items && (
From 27df64c2bc266f2c861719dcc2891de03a12dc3d Mon Sep 17 00:00:00 2001
From: Pierre Gayvallet
Date: Tue, 20 Jun 2023 07:31:25 -0400
Subject: [PATCH 05/14] [EBT] Add page title to browser-side context (#159936)
## Summary
Part of https://github.com/elastic/kibana/issues/149249
Add a new EBT context providing the page_title field to events.
---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../src/chrome_service.test.mocks.ts | 14 ++
.../src/chrome_service.test.tsx | 22 ++-
.../src/chrome_service.tsx | 13 +-
.../src/doc_title/doc_title_service.test.ts | 135 ++++++++++++++----
.../src/doc_title/doc_title_service.ts | 29 +++-
...egister_analytics_context_provider.test.ts | 39 +++++
.../register_analytics_context_provider.ts | 33 +++++
.../tsconfig.json | 2 +
.../src/chrome_service.mock.ts | 1 +
.../src/core_system.test.ts | 5 +
.../src/core_system.ts | 1 +
.../core_context_providers.ts | 5 +
12 files changed, 265 insertions(+), 34 deletions(-)
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.mocks.ts
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.test.ts
create mode 100644 packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.ts
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.mocks.ts b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.mocks.ts
new file mode 100644
index 0000000000000..6ac3e812413c4
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.mocks.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export const registerAnalyticsContextProviderMock = jest.fn();
+jest.doMock('./register_analytics_context_provider', () => {
+ return {
+ registerAnalyticsContextProvider: registerAnalyticsContextProviderMock,
+ };
+});
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx
index 0e18b5b72c367..f680a1b5d6ba9 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.test.tsx
@@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
+import { registerAnalyticsContextProviderMock } from './chrome_service.test.mocks';
import { shallow, mount } from 'enzyme';
import React from 'react';
import * as Rx from 'rxjs';
@@ -18,6 +19,7 @@ import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks';
+import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
import { getAppInfo } from '@kbn/core-application-browser-internal';
import { findTestSubject } from '@kbn/test-jest-helpers';
import { ChromeService } from './chrome_service';
@@ -84,15 +86,19 @@ async function start({
startDeps.injectedMetadata.getCspConfig.mockReturnValue(cspConfigMock);
}
+ await service.setup({ analytics: analyticsServiceMock.createAnalyticsServiceSetup() });
+ const chromeStart = await service.start(startDeps);
+
return {
service,
startDeps,
- chrome: await service.start(startDeps),
+ chrome: chromeStart,
};
}
beforeEach(() => {
store.clear();
+ registerAnalyticsContextProviderMock.mockReset();
window.history.pushState(undefined, '', '#/home?a=b');
});
@@ -100,6 +106,20 @@ afterAll(() => {
(window as any).localStorage = originalLocalStorage;
});
+describe('setup', () => {
+ it('calls registerAnalyticsContextProvider with the correct parameters', async () => {
+ const service = new ChromeService(defaultStartTestOptions({}));
+ const analytics = analyticsServiceMock.createAnalyticsServiceSetup();
+ await service.setup({ analytics });
+
+ expect(registerAnalyticsContextProviderMock).toHaveBeenCalledTimes(1);
+ expect(registerAnalyticsContextProviderMock).toHaveBeenCalledWith(
+ analytics,
+ expect.any(Object)
+ );
+ });
+});
+
describe('start', () => {
it('adds legacy browser warning if browserSupportsCsp is disabled and warnLegacyBrowsers is enabled', async () => {
const { startDeps } = await start({
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
index ede90c7098b5f..56fdcf3142bef 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
+++ b/packages/core/chrome/core-chrome-browser-internal/src/chrome_service.tsx
@@ -14,6 +14,7 @@ import { parse } from 'url';
import { EuiLink } from '@elastic/eui';
import useObservable from 'react-use/lib/useObservable';
import type { InternalInjectedMetadataStart } from '@kbn/core-injected-metadata-browser-internal';
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
import type { DocLinksStart } from '@kbn/core-doc-links-browser';
import type { HttpStart } from '@kbn/core-http-browser';
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
@@ -40,6 +41,7 @@ import { NavLinksService } from './nav_links';
import { ProjectNavigationService } from './project_navigation';
import { RecentlyAccessedService } from './recently_accessed';
import { Header, ProjectHeader, ProjectSideNavigation } from './ui';
+import { registerAnalyticsContextProvider } from './register_analytics_context_provider';
import type { InternalChromeStart } from './types';
const IS_LOCKED_KEY = 'core.chrome.isLocked';
@@ -50,6 +52,10 @@ interface ConstructorParams {
kibanaVersion: string;
}
+export interface SetupDeps {
+ analytics: AnalyticsServiceSetup;
+}
+
export interface StartDeps {
application: InternalApplicationStart;
docLinks: DocLinksStart;
@@ -104,6 +110,11 @@ export class ChromeService {
);
}
+ public setup({ analytics }: SetupDeps) {
+ const docTitle = this.docTitle.setup({ document: window.document });
+ registerAnalyticsContextProvider(analytics, docTitle.title$);
+ }
+
public async start({
application,
docLinks,
@@ -155,7 +166,7 @@ export class ChromeService {
const navLinks = this.navLinks.start({ application, http });
const projectNavigation = this.projectNavigation.start({ application, navLinks });
const recentlyAccessed = await this.recentlyAccessed.start({ http });
- const docTitle = this.docTitle.start({ document: window.document });
+ const docTitle = this.docTitle.start();
const { customBranding$ } = customBranding;
// erase chrome fields from a previous app while switching to a next app
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.test.ts
index f728321fa6dcf..145f0f1a59627 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.test.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.test.ts
@@ -10,47 +10,128 @@ import { DocTitleService } from './doc_title_service';
describe('DocTitleService', () => {
const defaultTitle = 'KibanaTest';
- const document = { title: '' };
+ let document: { title: string };
const getStart = (title: string = defaultTitle) => {
document.title = title;
- return new DocTitleService().start({ document });
+ const docTitle = new DocTitleService();
+ docTitle.setup({ document });
+ return docTitle.start();
};
beforeEach(() => {
- document.title = defaultTitle;
+ document = { title: defaultTitle };
});
- describe('#change()', () => {
- it('changes the title of the document', async () => {
- getStart().change('TitleA');
- expect(document.title).toEqual('TitleA - KibanaTest');
+ describe('#setup', () => {
+ describe('title$', () => {
+ it('emits with the initial title', () => {
+ document.title = 'Kibana';
+ const docTitle = new DocTitleService();
+ const { title$ } = docTitle.setup({ document });
+ docTitle.start();
+
+ const titles: string[] = [];
+ title$.subscribe((title) => {
+ titles.push(title);
+ });
+
+ expect(titles).toEqual(['Kibana']);
+ });
+
+ it('emits when the title changes', () => {
+ document.title = 'Kibana';
+ const docTitle = new DocTitleService();
+ const { title$ } = docTitle.setup({ document });
+ const { change } = docTitle.start();
+
+ const titles: string[] = [];
+ title$.subscribe((title) => {
+ titles.push(title);
+ });
+
+ change('title 2');
+ change('title 3');
+
+ expect(titles).toEqual(['Kibana', 'title 2 - Kibana', 'title 3 - Kibana']);
+ });
+
+ it('emits when the title is reset', () => {
+ document.title = 'Kibana';
+ const docTitle = new DocTitleService();
+ const { title$ } = docTitle.setup({ document });
+ const { change, reset } = docTitle.start();
+
+ const titles: string[] = [];
+ title$.subscribe((title) => {
+ titles.push(title);
+ });
+
+ change('title 2');
+ reset();
+
+ expect(titles).toEqual(['Kibana', 'title 2 - Kibana', 'Kibana']);
+ });
+
+ it('only emits on unique titles', () => {
+ document.title = 'Kibana';
+ const docTitle = new DocTitleService();
+ const { title$ } = docTitle.setup({ document });
+ const { change } = docTitle.start();
+
+ const titles: string[] = [];
+ title$.subscribe((title) => {
+ titles.push(title);
+ });
+
+ change('title 2');
+ change('title 2');
+ change('title 3');
+
+ expect(titles).toEqual(['Kibana', 'title 2 - Kibana', 'title 3 - Kibana']);
+ });
});
+ });
- it('appends the baseTitle to the title', async () => {
- const start = getStart('BaseTitle');
- start.change('TitleA');
- expect(document.title).toEqual('TitleA - BaseTitle');
- start.change('TitleB');
- expect(document.title).toEqual('TitleB - BaseTitle');
+ describe('#start', () => {
+ it('throws if called before #setup', () => {
+ const docTitle = new DocTitleService();
+ expect(() => docTitle.start()).toThrowErrorMatchingInlineSnapshot(
+ `"DocTitleService#setup must be called before DocTitleService#start"`
+ );
});
- it('accepts string arrays as input', async () => {
- const start = getStart();
- start.change(['partA', 'partB']);
- expect(document.title).toEqual(`partA - partB - ${defaultTitle}`);
- start.change(['partA', 'partB', 'partC']);
- expect(document.title).toEqual(`partA - partB - partC - ${defaultTitle}`);
+ describe('#change()', () => {
+ it('changes the title of the document', async () => {
+ getStart().change('TitleA');
+ expect(document.title).toEqual('TitleA - KibanaTest');
+ });
+
+ it('appends the baseTitle to the title', async () => {
+ const start = getStart('BaseTitle');
+ start.change('TitleA');
+ expect(document.title).toEqual('TitleA - BaseTitle');
+ start.change('TitleB');
+ expect(document.title).toEqual('TitleB - BaseTitle');
+ });
+
+ it('accepts string arrays as input', async () => {
+ const start = getStart();
+ start.change(['partA', 'partB']);
+ expect(document.title).toEqual(`partA - partB - ${defaultTitle}`);
+ start.change(['partA', 'partB', 'partC']);
+ expect(document.title).toEqual(`partA - partB - partC - ${defaultTitle}`);
+ });
});
- });
- describe('#reset()', () => {
- it('resets the title to the initial value', async () => {
- const start = getStart('InitialTitle');
- start.change('TitleA');
- expect(document.title).toEqual('TitleA - InitialTitle');
- start.reset();
- expect(document.title).toEqual('InitialTitle');
+ describe('#reset()', () => {
+ it('resets the title to the initial value', async () => {
+ const start = getStart('InitialTitle');
+ start.change('TitleA');
+ expect(document.title).toEqual('TitleA - InitialTitle');
+ start.reset();
+ expect(document.title).toEqual('InitialTitle');
+ });
});
});
});
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.ts
index ee884b59d8122..04a118a842d54 100644
--- a/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.ts
+++ b/packages/core/chrome/core-chrome-browser-internal/src/doc_title/doc_title_service.ts
@@ -7,9 +7,14 @@
*/
import { compact, flattenDeep, isString } from 'lodash';
+import { Observable, ReplaySubject, distinctUntilChanged } from 'rxjs';
import type { ChromeDocTitle } from '@kbn/core-chrome-browser';
-interface StartDeps {
+export interface InternalChromeDocTitleSetup {
+ title$: Observable;
+}
+
+interface SetupDeps {
document: { title: string };
}
@@ -18,12 +23,24 @@ const titleSeparator = ' - ';
/** @internal */
export class DocTitleService {
- private document = { title: '' };
- private baseTitle = '';
+ private document?: { title: string };
+ private baseTitle?: string;
+ private titleSubject = new ReplaySubject(1);
- public start({ document }: StartDeps): ChromeDocTitle {
+ public setup({ document }: SetupDeps): InternalChromeDocTitleSetup {
this.document = document;
this.baseTitle = document.title;
+ this.titleSubject.next(this.baseTitle);
+
+ return {
+ title$: this.titleSubject.asObservable().pipe(distinctUntilChanged()),
+ };
+ }
+
+ public start(): ChromeDocTitle {
+ if (this.document === undefined || this.baseTitle === undefined) {
+ throw new Error('DocTitleService#setup must be called before DocTitleService#start');
+ }
return {
change: (title: string | string[]) => {
@@ -36,7 +53,9 @@ export class DocTitleService {
}
private applyTitle(title: string | string[]) {
- this.document.title = this.render(title);
+ const rendered = this.render(title);
+ this.document!.title = rendered;
+ this.titleSubject.next(rendered);
}
private render(title: string | string[]) {
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.test.ts
new file mode 100644
index 0000000000000..85caf49f12f0a
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.test.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { firstValueFrom, of, ReplaySubject } from 'rxjs';
+import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
+import { registerAnalyticsContextProvider } from './register_analytics_context_provider';
+
+describe('registerAnalyticsContextProvider', () => {
+ let analytics: ReturnType;
+
+ beforeEach(() => {
+ analytics = analyticsServiceMock.createAnalyticsServiceSetup();
+ });
+
+ test('it registers the context provider', async () => {
+ registerAnalyticsContextProvider(analytics, of('some title'));
+ expect(analytics.registerContextProvider).toHaveBeenCalledTimes(1);
+ expect(analytics.registerContextProvider).toHaveBeenCalledWith(
+ expect.objectContaining({
+ name: 'page title',
+ })
+ );
+ });
+
+ test('emits a context value when location$ emits', async () => {
+ const title$ = new ReplaySubject(1);
+ registerAnalyticsContextProvider(analytics, title$);
+ title$.next('kibana title');
+
+ await expect(
+ firstValueFrom(analytics.registerContextProvider.mock.calls[0][0].context$)
+ ).resolves.toEqual({ page_title: 'kibana title' });
+ });
+});
diff --git a/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.ts b/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.ts
new file mode 100644
index 0000000000000..be811577d3977
--- /dev/null
+++ b/packages/core/chrome/core-chrome-browser-internal/src/register_analytics_context_provider.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { type Observable, map } from 'rxjs';
+import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
+
+/**
+ * Registers the Analytics context provider to enrich events with the page title.
+ * @param analytics Analytics service.
+ * @param pageTitle$ Observable emitting the page title.
+ * @private
+ */
+export function registerAnalyticsContextProvider(
+ analytics: AnalyticsServiceSetup,
+ pageTitle$: Observable
+) {
+ analytics.registerContextProvider({
+ name: 'page title',
+ context$: pageTitle$.pipe(
+ map((pageTitle) => ({
+ page_title: pageTitle,
+ }))
+ ),
+ schema: {
+ page_title: { type: 'text', _meta: { description: 'The page title' } },
+ },
+ });
+}
diff --git a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
index cd27209bef12c..687c2a2d9eace 100644
--- a/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
+++ b/packages/core/chrome/core-chrome-browser-internal/tsconfig.json
@@ -39,6 +39,8 @@
"@kbn/core-custom-branding-browser-mocks",
"@kbn/core-custom-branding-browser",
"@kbn/core-custom-branding-common",
+ "@kbn/core-analytics-browser-mocks",
+ "@kbn/core-analytics-browser",
],
"exclude": [
"target/**/*",
diff --git a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
index 68554c646cd99..a2e511a68b90f 100644
--- a/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
+++ b/packages/core/chrome/core-chrome-browser-mocks/src/chrome_service.mock.ts
@@ -87,6 +87,7 @@ const createStartContractMock = () => {
type ChromeServiceContract = PublicMethodsOf;
const createMock = () => {
const mocked: jest.Mocked = {
+ setup: jest.fn(),
start: jest.fn(),
stop: jest.fn(),
};
diff --git a/packages/core/root/core-root-browser-internal/src/core_system.test.ts b/packages/core/root/core-root-browser-internal/src/core_system.test.ts
index 0e4c0eb1340d6..d77ccaedb5279 100644
--- a/packages/core/root/core-root-browser-internal/src/core_system.test.ts
+++ b/packages/core/root/core-root-browser-internal/src/core_system.test.ts
@@ -311,6 +311,11 @@ describe('#setup()', () => {
await setupCore();
expect(MockThemeService.setup).toHaveBeenCalledTimes(1);
});
+
+ it('calls chrome#setup()', async () => {
+ await setupCore();
+ expect(MockChromeService.setup).toHaveBeenCalledTimes(1);
+ });
});
describe('#start()', () => {
diff --git a/packages/core/root/core-root-browser-internal/src/core_system.ts b/packages/core/root/core-root-browser-internal/src/core_system.ts
index b4a8b6d2d2815..cf9a172479b31 100644
--- a/packages/core/root/core-root-browser-internal/src/core_system.ts
+++ b/packages/core/root/core-root-browser-internal/src/core_system.ts
@@ -236,6 +236,7 @@ export class CoreSystem {
fatalErrors: this.fatalErrorsSetup,
executionContext,
});
+ this.chrome.setup({ analytics });
const uiSettings = this.uiSettings.setup({ http, injectedMetadata });
const settings = this.settings.setup({ http, injectedMetadata });
const notifications = this.notifications.setup({ uiSettings });
diff --git a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
index 3a7c075abd613..19a0bff55153d 100644
--- a/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
+++ b/test/analytics/tests/instrumented_events/from_the_browser/core_context_providers.ts
@@ -99,6 +99,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(event.context.viewport_height).to.be.a('number');
});
+ it('should have the properties provided by the "page title" context provider', async () => {
+ expect(event.context).to.have.property('page_title');
+ expect(event.context.page_title).to.be.a('string');
+ });
+
it('should have the properties provided by the "page url" context provider', () => {
expect(event.context).to.have.property('page_url');
expect(event.context.page_url).to.be.a('string');
From 3a34e3593d51e19ecf265a7c16d61fdc9d7b0692 Mon Sep 17 00:00:00 2001
From: Kevin Delemme
Date: Tue, 20 Jun 2023 07:43:17 -0400
Subject: [PATCH 06/14] feat(slo): Support for calendar aligned time window
(#159949)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Resolves https://github.com/elastic/kibana/issues/159948
## 📝 Summary
This PR updates the SLO form to support the calendar aligned time
windows for both create and edit flow.
I've also moved the budgeting method selector down, so when selecting
"timeslices", the timeslices related inputs are shown next to it on the
same line.
| Screenshot | Screenshot |
|--------|--------|
|

|

|
---
.../kbn-slo-schema/src/rest_specs/slo.ts | 3 +
.../kbn-slo-schema/src/schema/time_window.ts | 5 +
.../slo_edit/components/slo_edit_form.tsx | 7 +-
.../slo_edit_form_objective_section.tsx | 118 ++++++++++++++----
...edit_form_objective_section_timeslices.tsx | 10 +-
.../public/pages/slo_edit/constants.ts | 42 +++++--
.../translations/translations/fr-FR.json | 3 -
.../translations/translations/ja-JP.json | 3 -
.../translations/translations/zh-CN.json | 3 -
9 files changed, 142 insertions(+), 52 deletions(-)
diff --git a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
index 9d37c36c94534..cbab659f07c82 100644
--- a/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
+++ b/x-pack/packages/kbn-slo-schema/src/rest_specs/slo.ts
@@ -22,6 +22,7 @@ import {
summarySchema,
tagsSchema,
timeWindowSchema,
+ timeWindowTypeSchema,
} from '../schema';
const createSLOParamsSchema = t.type({
@@ -166,6 +167,7 @@ type GetPreviewDataParams = t.TypeOf;
type BudgetingMethod = t.TypeOf;
+type TimeWindow = t.TypeOf;
type Indicator = t.OutputOf;
type MetricCustomIndicator = t.OutputOf;
@@ -211,4 +213,5 @@ export type {
Indicator,
MetricCustomIndicator,
KQLCustomIndicator,
+ TimeWindow,
};
diff --git a/x-pack/packages/kbn-slo-schema/src/schema/time_window.ts b/x-pack/packages/kbn-slo-schema/src/schema/time_window.ts
index ebbb957d723b6..b495f7e545314 100644
--- a/x-pack/packages/kbn-slo-schema/src/schema/time_window.ts
+++ b/x-pack/packages/kbn-slo-schema/src/schema/time_window.ts
@@ -20,6 +20,10 @@ const calendarAlignedTimeWindowSchema = t.type({
type: calendarAlignedTimeWindowTypeSchema,
});
+const timeWindowTypeSchema = t.union([
+ rollingTimeWindowTypeSchema,
+ calendarAlignedTimeWindowTypeSchema,
+]);
const timeWindowSchema = t.union([rollingTimeWindowSchema, calendarAlignedTimeWindowSchema]);
export {
@@ -28,4 +32,5 @@ export {
calendarAlignedTimeWindowSchema,
calendarAlignedTimeWindowTypeSchema,
timeWindowSchema,
+ timeWindowTypeSchema,
};
diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx
index a4f24da9345e3..1d1f8878de20e 100644
--- a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx
+++ b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form.tsx
@@ -272,17 +272,16 @@ export function SloEditForm({ slo }: Props) {
})}
- navigateToUrl(basePath.prepend(paths.observability.slos))}
>
{i18n.translate('xpack.observability.slo.sloEdit.cancelButton', {
defaultMessage: 'Cancel',
})}
-
+
();
+ const { control, watch, getFieldState, resetField } = useFormContext();
const budgetingSelect = useGeneratedHtmlId({ prefix: 'budgetingSelect' });
+ const timeWindowTypeSelect = useGeneratedHtmlId({ prefix: 'timeWindowTypeSelect' });
const timeWindowSelect = useGeneratedHtmlId({ prefix: 'timeWindowSelect' });
+ const timeWindowType = watch('timeWindow.type');
+
+ useEffect(() => {
+ resetField('timeWindow.duration', {
+ defaultValue:
+ timeWindowType === 'calendarAligned'
+ ? CALENDARALIGNED_TIMEWINDOW_OPTIONS[1].value
+ : ROLLING_TIMEWINDOW_OPTIONS[1].value,
+ });
+ }, [timeWindowType, resetField]);
return (
- {i18n.translate('xpack.observability.slo.sloEdit.budgetingMethod.label', {
- defaultMessage: 'Budgeting method',
+ {i18n.translate('xpack.observability.slo.sloEdit.timeWindowType.label', {
+ defaultMessage: 'Time window',
})}{' '}
(
)}
/>
-
- {i18n.translate('xpack.observability.slo.sloEdit.timeWindow.label', {
- defaultMessage: 'Time window',
+ {i18n.translate('xpack.observability.slo.sloEdit.timeWindowDuration.label', {
+ defaultMessage: 'Duration',
})}{' '}
@@ -96,6 +113,7 @@ export function SloEditFormObjectiveSection() {
(
)}
/>
+
+
+
+
+
+
+ {i18n.translate('xpack.observability.slo.sloEdit.budgetingMethod.label', {
+ defaultMessage: 'Budgeting method',
+ })}{' '}
+
+
+ }
+ >
+ (
+
+ )}
+ />
+
+
+
+ {watch('budgetingMethod') === 'timeslices' ? (
+
+ ) : null}
+
+
+
+
+
-
- {watch('budgetingMethod') === 'timeslices' ? (
- <>
-
-
- >
- ) : null}
);
}
diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form_objective_section_timeslices.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form_objective_section_timeslices.tsx
index 5cd05e75f4825..f5c0dd068e460 100644
--- a/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form_objective_section_timeslices.tsx
+++ b/x-pack/plugins/observability/public/pages/slo_edit/components/slo_edit_form_objective_section_timeslices.tsx
@@ -5,17 +5,17 @@
* 2.0.
*/
-import React from 'react';
-import { EuiFieldNumber, EuiFlexGrid, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
+import { EuiFieldNumber, EuiFlexItem, EuiFormRow, EuiIconTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { Controller, useFormContext } from 'react-hook-form';
import type { CreateSLOInput } from '@kbn/slo-schema';
+import React from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
export function SloEditFormObjectiveSectionTimeslices() {
const { control, getFieldState } = useFormContext();
return (
-
+ <>
-
+ >
);
}
diff --git a/x-pack/plugins/observability/public/pages/slo_edit/constants.ts b/x-pack/plugins/observability/public/pages/slo_edit/constants.ts
index 3fd9e0681783d..8430babc45a5b 100644
--- a/x-pack/plugins/observability/public/pages/slo_edit/constants.ts
+++ b/x-pack/plugins/observability/public/pages/slo_edit/constants.ts
@@ -6,7 +6,7 @@
*/
import { i18n } from '@kbn/i18n';
-import { BudgetingMethod, CreateSLOInput } from '@kbn/slo-schema';
+import { BudgetingMethod, CreateSLOInput, TimeWindow } from '@kbn/slo-schema';
import {
BUDGETING_METHOD_OCCURRENCES,
BUDGETING_METHOD_TIMESLICES,
@@ -49,9 +49,39 @@ export const BUDGETING_METHOD_OPTIONS: Array<{ value: BudgetingMethod; text: str
},
];
-export const TIMEWINDOW_OPTIONS = [90, 30, 7].map((number) => ({
+export const TIMEWINDOW_TYPE_OPTIONS: Array<{ value: TimeWindow; text: string }> = [
+ {
+ value: 'rolling',
+ text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.rolling', {
+ defaultMessage: 'Rolling',
+ }),
+ },
+ {
+ value: 'calendarAligned',
+ text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.calendarAligned', {
+ defaultMessage: 'Calendar aligned',
+ }),
+ },
+];
+
+export const CALENDARALIGNED_TIMEWINDOW_OPTIONS = [
+ {
+ value: '1w',
+ text: i18n.translate('xpack.observability.slo.sloEdit.calendarTimeWindow.weekly', {
+ defaultMessage: 'Weekly',
+ }),
+ },
+ {
+ value: '1M',
+ text: i18n.translate('xpack.observability.slo.sloEdit.calendarTimeWindow.monthly', {
+ defaultMessage: 'Monthly',
+ }),
+ },
+];
+
+export const ROLLING_TIMEWINDOW_OPTIONS = [90, 30, 7].map((number) => ({
value: `${number}d`,
- text: i18n.translate('xpack.observability.slo.sloEdit.timeWindow.days', {
+ text: i18n.translate('xpack.observability.slo.sloEdit.rollingTimeWindow.days', {
defaultMessage: '{number} days',
values: { number },
}),
@@ -71,8 +101,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES: CreateSLOInput = {
},
},
timeWindow: {
- duration:
- TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
+ duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
type: 'rolling',
},
tags: [],
@@ -96,8 +125,7 @@ export const SLO_EDIT_FORM_DEFAULT_VALUES_CUSTOM_METRIC: CreateSLOInput = {
},
},
timeWindow: {
- duration:
- TIMEWINDOW_OPTIONS[TIMEWINDOW_OPTIONS.findIndex((option) => option.value === '30d')].value,
+ duration: ROLLING_TIMEWINDOW_OPTIONS[1].value,
type: 'rolling',
},
tags: [],
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 62a79c1cff886..3e8a5818e1766 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -26635,7 +26635,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{value} (l'objectif est {objective})",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} en cours",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "Dernière(s) {duration}",
- "xpack.observability.slo.sloEdit.timeWindow.days": "{number} jours",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " et inférieur à {bad}",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd} %)",
@@ -26998,8 +26997,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "La cible d'intervalle de temps individuel utilisée pour déterminer si l'intervalle est bon ou mauvais.",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "Fenêtre d'intervalle de temps (en minutes)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "La taille de la fenêtre d'intervalle de temps utilisée pour évaluer les données.",
- "xpack.observability.slo.sloEdit.timeWindow.label": "Fenêtre temporelle",
- "xpack.observability.slo.sloEdit.timeWindow.tooltip": "La durée de la fenêtre temporelle glissante utilisée pour calculer le SLO.",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "Créer un nouveau SLO",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "Créer un SLO",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "Pour commencer, créez votre premier SLO.",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index fbda4b3dfe09c..c46500f6890e8 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -26617,7 +26617,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{objective}(目的は{value})",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration}ローリング",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "過去{duration}",
- "xpack.observability.slo.sloEdit.timeWindow.days": "{number}日",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " {bad}未満",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)",
@@ -26980,8 +26979,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "スライスが良好か問題があるかどうかを判断するために使用される、個別のタイムスライス目標。",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "タイムスライス期間(分)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "データを評価するために使用されるタイムスライス期間サイズ。",
- "xpack.observability.slo.sloEdit.timeWindow.label": "時間枠",
- "xpack.observability.slo.sloEdit.timeWindow.tooltip": "SLOを計算するために使用されるローリング時間枠期間。",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "新規SLOを作成",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "SLOの作成",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "開始するには、まずSLOを作成します。",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 306f183b72169..ae12d1043483b 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -26615,7 +26615,6 @@
"xpack.observability.slo.sloDetails.overview.observedValueSubtitle": "{value}(目标为 {objective})",
"xpack.observability.slo.sloDetails.overview.rollingTimeWindow": "{duration} 滚动",
"xpack.observability.slo.sloDetails.sliHistoryChartPanel.duration": "过去 {duration}",
- "xpack.observability.slo.sloEdit.timeWindow.days": "{number} 天",
"xpack.observability.transactionRateLabel": "{value} tpm",
"xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}",
"xpack.observability.ux.coreVitals.paletteLegend.rankPercentage": "{labelsInd} ({ranksInd}%)",
@@ -26978,8 +26977,6 @@
"xpack.observability.slo.sloEdit.timeSliceTarget.tooltip": "用于确定切片是良好还是不良的单个时间片目标。",
"xpack.observability.slo.sloEdit.timesliceWindow.label": "时间片窗口(分钟)",
"xpack.observability.slo.sloEdit.timesliceWindow.tooltip": "用于评估接收的数据的时间片窗口大小。",
- "xpack.observability.slo.sloEdit.timeWindow.label": "时间窗口",
- "xpack.observability.slo.sloEdit.timeWindow.tooltip": "用于在其间计算 SLO 的滚动时间窗口持续时间。",
"xpack.observability.slo.sloList.pageHeader.createNewButtonLabel": "创建新 SLO",
"xpack.observability.slo.sloList.welcomePrompt.buttonLabel": "创建 SLO",
"xpack.observability.slo.sloList.welcomePrompt.getStartedMessage": "要开始使用,请创建您的首个 SLO。",
From 12a2203d1030bfa738b3e09dd6ca127de6e6a664 Mon Sep 17 00:00:00 2001
From: Kevin Delemme
Date: Tue, 20 Jun 2023 07:43:53 -0400
Subject: [PATCH 07/14] chore(slo): improve index selection (#159849)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 📝 Summary
This PR changes the index selection component behaviours. We are
fetching the Data Views (limit to 10 at a time). The user can select one
of them, or search for other Data Views and index pattern that matches
at least on index.
Therefore the user can choose any Data Views matching a search, or the
index pattern derived from the search, if it matches at least one index.
| Screenshot |
| -- |
|

|
|

|
|

|
## 🧪 Testing
1. **Create 11 data views or more**
Easiest method is to use curl with a POST on `/api/data_views/data_view`
with a random payload:
```
{
"data_view": {
"title": "{% uuid 'v4' %}-log*",
"name": "{% uuid 'v4' %} "
}
}
```
2. **Go to the SLO form and select custom KQL, then search for a data
view or another index pattern**
---
.../__storybook_mocks__/use_fetch_indices.ts | 11 +-
.../public/hooks/use_fetch_data_views.ts | 17 ++-
.../public/hooks/use_fetch_indices.ts | 43 ++++---
.../custom_common/index_selection.tsx | 110 +++++++++---------
.../custom_kql_indicator_type_form.tsx | 12 +-
.../public/pages/slo_edit/slo_edit.test.tsx | 27 +++--
.../translations/translations/fr-FR.json | 1 -
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
9 files changed, 110 insertions(+), 113 deletions(-)
diff --git a/x-pack/plugins/observability/public/hooks/__storybook_mocks__/use_fetch_indices.ts b/x-pack/plugins/observability/public/hooks/__storybook_mocks__/use_fetch_indices.ts
index fdedecfef7243..a709dea0600f8 100644
--- a/x-pack/plugins/observability/public/hooks/__storybook_mocks__/use_fetch_indices.ts
+++ b/x-pack/plugins/observability/public/hooks/__storybook_mocks__/use_fetch_indices.ts
@@ -12,18 +12,13 @@ export const useFetchIndices = (): UseFetchIndicesResponse => {
isLoading: false,
isError: false,
isSuccess: true,
- indices: [
+ data: [
...Array(10)
.fill(0)
- .map((_, i) => ({
- name: `.index-${i}`,
- })),
+ .map((_, i) => `.index-${i}`),
...Array(10)
.fill(0)
- .map((_, i) => ({
- name: `.some-other-index-${i}`,
- })),
+ .map((_, i) => `.some-other-index-${i}`),
] as Index[],
- refetch: function () {} as UseFetchIndicesResponse['refetch'],
};
};
diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts b/x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts
index 976d6426d4ce3..1da32948e2e50 100644
--- a/x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts
+++ b/x-pack/plugins/observability/public/hooks/use_fetch_data_views.ts
@@ -18,34 +18,31 @@ export interface UseFetchDataViewsResponse {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
- dataViews: DataView[] | undefined;
+ data: DataView[] | undefined;
refetch: (
options?: (RefetchOptions & RefetchQueryFilters) | undefined
) => Promise>;
}
-interface FetchDataViewParams {
+interface Params {
name?: string;
size?: number;
}
-export function useFetchDataViews({
- name = '',
- size = 10,
-}: FetchDataViewParams): UseFetchDataViewsResponse {
+export function useFetchDataViews({ name = '', size = 10 }: Params): UseFetchDataViewsResponse {
const { dataViews } = useKibana().services;
+ const search = name.endsWith('*') ? name : `${name}*`;
const { isLoading, isError, isSuccess, data, refetch } = useQuery({
- queryKey: ['fetchDataViews', name],
+ queryKey: ['fetchDataViews', search],
queryFn: async () => {
try {
- const response = await dataViews.find(`${name}*`, size);
- return response;
+ return await dataViews.find(search, size);
} catch (error) {
throw new Error(`Something went wrong. Error: ${error}`);
}
},
});
- return { isLoading, isError, isSuccess, dataViews: data, refetch };
+ return { isLoading, isError, isSuccess, data, refetch };
}
diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_indices.ts b/x-pack/plugins/observability/public/hooks/use_fetch_indices.ts
index 88c7b1d5561c1..2d622a702a839 100644
--- a/x-pack/plugins/observability/public/hooks/use_fetch_indices.ts
+++ b/x-pack/plugins/observability/public/hooks/use_fetch_indices.ts
@@ -5,41 +5,46 @@
* 2.0.
*/
-import {
- QueryObserverResult,
- RefetchOptions,
- RefetchQueryFilters,
- useQuery,
-} from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
import { useKibana } from '../utils/kibana_react';
+export type Index = string;
+
export interface UseFetchIndicesResponse {
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
- indices: Index[] | undefined;
- refetch: (
- options?: (RefetchOptions & RefetchQueryFilters) | undefined
- ) => Promise>;
+ data: Index[] | undefined;
+}
+
+interface Params {
+ search?: string;
}
-export interface Index {
- name: string;
+
+interface ResolveIndexReponse {
+ indices: Array<{ name: string }>;
}
-export function useFetchIndices(): UseFetchIndicesResponse {
+export function useFetchIndices({ search }: Params): UseFetchIndicesResponse {
const { http } = useKibana().services;
- const { isLoading, isError, isSuccess, data, refetch } = useQuery({
- queryKey: ['fetchIndices'],
- queryFn: async ({ signal }) => {
+ const { isLoading, isError, isSuccess, data } = useQuery({
+ queryKey: ['fetchIndices', search],
+ queryFn: async () => {
+ const searchPattern = search?.endsWith('*') ? search : `${search}*`;
try {
- const response = await http.get(`/api/index_management/indices`, { signal });
- return response;
+ const response = await http.get(
+ `/internal/index-pattern-management/resolve_index/${searchPattern}`
+ );
+ return response.indices.map((index) => index.name);
} catch (error) {
throw new Error(`Something went wrong. Error: ${error}`);
}
},
+ retry: false,
+ enabled: Boolean(search),
+ refetchOnWindowFocus: false,
});
- return { isLoading, isError, isSuccess, indices: data, refetch };
+ return { isLoading, isError, isSuccess, data };
}
diff --git a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_common/index_selection.tsx b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_common/index_selection.tsx
index 691761bd8a94b..8d914991bf926 100644
--- a/x-pack/plugins/observability/public/pages/slo_edit/components/custom_common/index_selection.tsx
+++ b/x-pack/plugins/observability/public/pages/slo_edit/components/custom_common/index_selection.tsx
@@ -5,16 +5,15 @@
* 2.0.
*/
-import React, { useEffect, useMemo, useState } from 'react';
-import { Controller, useFormContext } from 'react-hook-form';
import { EuiComboBox, EuiComboBoxOptionOption, EuiFormRow } from '@elastic/eui';
+import { DataView } from '@kbn/data-views-plugin/public';
import { i18n } from '@kbn/i18n';
import { CreateSLOInput } from '@kbn/slo-schema';
-import { DataView } from '@kbn/data-views-plugin/public';
import { debounce } from 'lodash';
-
+import React, { useEffect, useMemo, useState } from 'react';
+import { Controller, useFormContext } from 'react-hook-form';
import { useFetchDataViews } from '../../../../hooks/use_fetch_data_views';
-import { useFetchIndices, Index } from '../../../../hooks/use_fetch_indices';
+import { useFetchIndices } from '../../../../hooks/use_fetch_indices';
interface Option {
label: string;
@@ -23,55 +22,62 @@ interface Option {
export function IndexSelection() {
const { control, getFieldState } = useFormContext();
- const { isLoading: isIndicesLoading, indices = [] } = useFetchIndices();
+
const [searchValue, setSearchValue] = useState('');
- const { isLoading: isDataViewsLoading, dataViews = [] } = useFetchDataViews({
+ const [dataViewOptions, setDataViewOptions] = useState
-
-
-
- ),
- sourcedata: (
-
-
-
- ),
- }}
- />
- }
- iconType="iInCircle"
- />
>
}
actions={[
@@ -118,37 +71,19 @@ export const AnalyticsEmptyPrompt: FC = () => {
onClick={navigateToSourceSelection}
isDisabled={disabled}
color="primary"
- iconType="plusInCircle"
- fill
data-test-subj="mlAnalyticsCreateFirstButton"
>
{i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptButtonText', {
- defaultMessage: 'Create job',
+ defaultMessage: 'Create data frame analytics job',
})}
,
+
+
+ ,
]}
- footer={
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
data-test-subj="mlNoDataFrameAnalyticsFound"
/>
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts
index 613f9034e2ff4..949c8a47deb32 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.test.ts
@@ -67,7 +67,7 @@ describe('get_analytics', () => {
// act and assert
expect(getAnalyticsJobsStats(mockResponse)).toEqual({
total: {
- label: 'Total analytics jobs',
+ label: 'Total',
value: 2,
show: true,
},
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
index 170a90b1fcba0..9a3ea0c9bef90 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts
@@ -47,7 +47,7 @@ export function getInitialAnalyticsStats(): AnalyticStatsBarStats {
return {
total: {
label: i18n.translate('xpack.ml.overview.statsBar.totalAnalyticsLabel', {
- defaultMessage: 'Total analytics jobs',
+ defaultMessage: 'Total',
}),
value: 0,
show: true,
@@ -97,12 +97,18 @@ export function getAnalyticsJobsStats(
);
resultStats.failed.show = resultStats.failed.value > 0;
resultStats.total.value = analyticsStats.count;
+
+ if (resultStats.total.value === 0) {
+ resultStats.started.show = false;
+ resultStats.stopped.show = false;
+ }
+
return resultStats;
}
export const getAnalyticsFactory = (
setAnalytics: React.Dispatch>,
- setAnalyticsStats: React.Dispatch>,
+ setAnalyticsStats: (update: AnalyticStatsBarStats | undefined) => void,
setErrorMessage: React.Dispatch<
React.SetStateAction
>,
diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/anomaly_detection_empty_state/anomaly_detection_empty_state.tsx b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/anomaly_detection_empty_state/anomaly_detection_empty_state.tsx
index b8dee1a7e6f60..171320dd7b781 100644
--- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/anomaly_detection_empty_state/anomaly_detection_empty_state.tsx
+++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/anomaly_detection_empty_state/anomaly_detection_empty_state.tsx
@@ -7,15 +7,7 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
-import {
- EuiButton,
- EuiEmptyPrompt,
- EuiFlexGroup,
- EuiFlexItem,
- EuiImage,
- EuiLink,
- EuiTitle,
-} from '@elastic/eui';
+import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink } from '@elastic/eui';
import adImage from './anomaly_detection_kibana.png';
import { ML_PAGES } from '../../../../../../common/constants/locator';
import { useMlKibana, useMlLocator, useNavigateToPath } from '../../../../contexts/kibana';
@@ -47,12 +39,11 @@ export const AnomalyDetectionEmptyState: FC = () => {
hasBorder={false}
hasShadow={false}
icon={}
- color="subdued"
title={
}
@@ -66,43 +57,25 @@ export const AnomalyDetectionEmptyState: FC = () => {
>
}
- actions={
+ actions={[
-
- }
- footer={
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
+ ,
+
+
+ ,
+ ]}
data-test-subj="mlAnomalyDetectionEmptyState"
/>
);
diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx
index a615e40c9e3ea..a8e5848aaef1a 100644
--- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx
+++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx
@@ -200,15 +200,20 @@ export const NodesList: FC = ({ compactView = false }) => {
return (
-
-
- {nodesStats && (
-
-
-
- )}
-
-
+ {nodesStats && !compactView ? (
+ <>
+
+
+ {nodesStats && (
+
+
+
+ )}
+
+
+ >
+ ) : null}
+
allowNeutralSort={false}
diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
index ea981a25d7ecb..c44c391a2fcfd 100644
--- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
+++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts
@@ -43,3 +43,7 @@ export function lazyMlNodesAvailable() {
export function permissionToViewMlNodeCount() {
return userHasPermissionToViewMlNodeCount;
}
+
+export function getMlNodesCount(): number {
+ return mlNodeCount;
+}
diff --git a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
index b53860d9a3be6..41e9732461bb3 100644
--- a/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/analytics_panel/analytics_panel.tsx
@@ -5,28 +5,32 @@
* 2.0.
*/
-import React, { FC, useEffect, useState } from 'react';
-import {
- EuiButton,
- EuiCallOut,
- EuiFlexGroup,
- EuiFlexItem,
- EuiLoadingSpinner,
- EuiPanel,
- EuiSpacer,
- EuiText,
-} from '@elastic/eui';
+import React, { FC, useCallback, useEffect, useState } from 'react';
+import { EuiCallOut, EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
+import { useStorage } from '@kbn/ml-local-storage';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { type AnalyticStatsBarStats } from '../../../components/stats_bar';
+import {
+ OverviewStatsBar,
+ type StatEntry,
+} from '../../../components/collapsible_panel/collapsible_panel';
+import {
+ ML_OVERVIEW_PANELS,
+ MlStorageKey,
+ TMlStorageMapped,
+} from '../../../../../common/types/storage';
import { AnalyticsTable } from './table';
import { getAnalyticsFactory } from '../../../data_frame_analytics/pages/analytics_management/services/analytics_service';
import { DataFrameAnalyticsListRow } from '../../../data_frame_analytics/pages/analytics_management/components/analytics_list/common';
-import { AnalyticStatsBarStats, StatsBar } from '../../../components/stats_bar';
import { useMlLink } from '../../../contexts/kibana';
import { ML_PAGES } from '../../../../../common/constants/locator';
import { useRefresh } from '../../../routing/use_refresh';
import type { GetDataFrameAnalyticsStatsResponseError } from '../../../services/ml_api_service/data_frame_analytics';
import { AnalyticsEmptyPrompt } from '../../../data_frame_analytics/pages/analytics_management/components/empty_prompt';
+import { overviewPanelDefaultState } from '../../overview_page';
+import { CollapsiblePanel } from '../../../components/collapsible_panel';
interface Props {
setLazyJobCount: React.Dispatch>;
@@ -35,9 +39,7 @@ export const AnalyticsPanel: FC = ({ setLazyJobCount }) => {
const refresh = useRefresh();
const [analytics, setAnalytics] = useState([]);
- const [analyticsStats, setAnalyticsStats] = useState(
- undefined
- );
+ const [analyticsStats, setAnalyticsStats] = useState(undefined);
const [errorMessage, setErrorMessage] = useState();
const [isInitialized, setIsInitialized] = useState(false);
@@ -45,9 +47,24 @@ export const AnalyticsPanel: FC = ({ setLazyJobCount }) => {
page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
});
+ const [panelsState, setPanelsState] = useStorage<
+ MlStorageKey,
+ TMlStorageMapped
+ >(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
+
+ const setAnalyticsStatsCustom = useCallback((stats: AnalyticStatsBarStats | undefined) => {
+ if (!stats) return;
+
+ const result = Object.entries(stats)
+ .filter(([k, v]) => v.show)
+ .map(([k, v]) => v);
+
+ setAnalyticsStats(result);
+ }, []);
+
const getAnalytics = getAnalyticsFactory(
setAnalytics,
- setAnalyticsStats,
+ setAnalyticsStatsCustom,
setErrorMessage,
setIsInitialized,
setLazyJobCount,
@@ -78,58 +95,40 @@ export const AnalyticsPanel: FC = ({ setLazyJobCount }) => {
const noDFAJobs = errorMessage === undefined && isInitialized === true && analytics.length === 0;
return (
- <>
- {noDFAJobs ? (
-
- ) : (
-
- {typeof errorMessage !== 'undefined' ? errorDisplay : null}
- {isInitialized === false && (
-
- )}
-
- {isInitialized === true && analytics.length > 0 && (
- <>
-
-
-
-
- {i18n.translate('xpack.ml.overview.analyticsList.PanelTitle', {
- defaultMessage: 'Analytics',
- })}
-
-
-
-
-
- {analyticsStats !== undefined ? (
-
-
-
- ) : null}
-
-
- {i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
- defaultMessage: 'Manage jobs',
- })}
-
-
-
-
-
-
-
- >
- )}
-
- )}
- >
+ {
+ setPanelsState({ ...panelsState, dfaJobs: update });
+ }}
+ header={
+
+ }
+ headerItems={[
+ ...(analyticsStats
+ ? [
+ ,
+ ]
+ : []),
+
+ {i18n.translate('xpack.ml.overview.analyticsList.manageJobsButtonText', {
+ defaultMessage: 'Manage jobs',
+ })}
+ ,
+ ]}
+ >
+ {noDFAJobs ? : null}
+
+ {typeof errorMessage !== 'undefined' ? errorDisplay : null}
+
+ {isInitialized === false && }
+
+ {isInitialized === true && analytics.length > 0 ? : null}
+
);
};
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
index 1773531cb9aa2..30aa0f6d22dfb 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/anomaly_detection_panel.tsx
@@ -6,10 +6,20 @@
*/
import React, { FC, Fragment, useEffect, useState } from 'react';
-import { EuiCallOut, EuiLoadingSpinner, EuiPanel } from '@elastic/eui';
+import { EuiCallOut, EuiLink, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-import { zipObject } from 'lodash';
-import { useMlKibana } from '../../../contexts/kibana';
+import { zipObject, groupBy } from 'lodash';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { useStorage } from '@kbn/ml-local-storage';
+import {
+ ML_OVERVIEW_PANELS,
+ MlStorageKey,
+ TMlStorageMapped,
+} from '../../../../../common/types/storage';
+import { ML_PAGES } from '../../../../../common/constants/locator';
+import { OverviewStatsBar } from '../../../components/collapsible_panel/collapsible_panel';
+import { CollapsiblePanel } from '../../../components/collapsible_panel';
+import { useMlKibana, useMlLink } from '../../../contexts/kibana';
import { AnomalyDetectionTable } from './table';
import { ml } from '../../../services/ml_api_service';
import { getGroupsFromJobs, getStatsBarData } from './utils';
@@ -19,8 +29,8 @@ import { useRefresh } from '../../../routing/use_refresh';
import { useToastNotificationService } from '../../../services/toast_notification_service';
import { AnomalyTimelineService } from '../../../services/anomaly_timeline_service';
import type { OverallSwimlaneData } from '../../../explorer/explorer_utils';
-import { JobStatsBarStats } from '../../../components/stats_bar';
import { AnomalyDetectionEmptyState } from '../../../jobs/jobs_list/components/anomaly_detection_empty_state';
+import { overviewPanelDefaultState } from '../../overview_page';
export type GroupsDictionary = Dictionary;
@@ -50,10 +60,21 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa
const refresh = useRefresh();
+ const [panelsState, setPanelsState] = useStorage<
+ MlStorageKey,
+ TMlStorageMapped
+ >(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
+
+ const manageJobsLink = useMlLink({
+ page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
+ });
+
const [isLoading, setIsLoading] = useState(false);
const [groups, setGroups] = useState({});
const [groupsCount, setGroupsCount] = useState(0);
- const [statsBarData, setStatsBarData] = useState();
+ const [statsBarData, setStatsBarData] = useState>();
+ const [restStatsBarData, setRestStatsBarData] =
+ useState>();
const [errorMessage, setErrorMessage] = useState();
const loadJobs = async () => {
@@ -71,9 +92,20 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa
});
const { groups: jobsGroups, count } = getGroupsFromJobs(jobsSummaryList);
const stats = getStatsBarData(jobsSummaryList);
+
+ const statGroups = groupBy(
+ Object.entries(stats)
+ .filter(([k, v]) => v.show)
+ .map(([k, v]) => v),
+ 'group'
+ );
+
setIsLoading(false);
setErrorMessage(undefined);
- setStatsBarData(stats);
+
+ setStatsBarData(statGroups[0]);
+ setRestStatsBarData(statGroups[1]);
+
setGroupsCount(count);
setGroups(jobsGroups);
loadOverallSwimLanes(jobsGroups);
@@ -138,30 +170,52 @@ export const AnomalyDetectionPanel: FC = ({ anomalyTimelineService, setLa
);
- const panelClass = isLoading ? 'mlOverviewPanel__isLoading' : 'mlOverviewPanel';
-
const noAdJobs =
!errorMessage &&
isLoading === false &&
typeof errorMessage === 'undefined' &&
groupsCount === 0;
- if (noAdJobs) {
- return ;
- }
-
return (
-
+ {
+ setPanelsState({ ...panelsState, adJobs: update });
+ }}
+ header={
+
+ }
+ headerItems={[
+ ...(statsBarData
+ ? []
+ : []),
+ ...(restStatsBarData
+ ? [
+ ,
+ ]
+ : []),
+
+ {i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
+ defaultMessage: 'Manage jobs',
+ })}
+ ,
+ ]}
+ >
+ {noAdJobs ? : null}
+
{typeof errorMessage !== 'undefined' && errorDisplay}
- {isLoading && }
+
+ {isLoading ? : null}
{isLoading === false && typeof errorMessage === 'undefined' && groupsCount > 0 ? (
-
+
) : null}
-
+
);
};
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
index 016261be7997e..de4a05c638ce2 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/table.tsx
@@ -9,13 +9,8 @@ import React, { FC, useState } from 'react';
import {
Direction,
EuiBasicTableColumn,
- EuiButton,
- EuiFlexGroup,
- EuiFlexItem,
EuiIcon,
EuiInMemoryTable,
- EuiSpacer,
- EuiText,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@@ -24,13 +19,10 @@ import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { formatHumanReadableDateTime } from '@kbn/ml-date-utils';
import { useGroupActions } from './actions';
import { Group, GroupsDictionary } from './anomaly_detection_panel';
-import { JobStatsBarStats, StatsBar } from '../../../components/stats_bar';
import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge';
import { toLocaleString } from '../../../util/string_utils';
import { SwimlaneContainer } from '../../../explorer/swimlane_container';
import { useTimeBuckets } from '../../../components/custom_hooks/use_time_buckets';
-import { ML_PAGES } from '../../../../../common/constants/locator';
-import { useMlLink } from '../../../contexts/kibana';
export enum AnomalyDetectionListColumns {
id = 'id',
@@ -44,11 +36,10 @@ export enum AnomalyDetectionListColumns {
interface Props {
items: GroupsDictionary;
- statsBarData: JobStatsBarStats;
chartsService: ChartsPluginStart;
}
-export const AnomalyDetectionTable: FC = ({ items, statsBarData, chartsService }) => {
+export const AnomalyDetectionTable: FC = ({ items, chartsService }) => {
const groupsList = Object.values(items);
const [pageIndex, setPageIndex] = useState(0);
const [pageSize, setPageSize] = useState(10);
@@ -58,10 +49,6 @@ export const AnomalyDetectionTable: FC = ({ items, statsBarData, chartsSe
const timeBuckets = useTimeBuckets();
- const manageJobsLink = useMlLink({
- page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
- });
-
const columns: Array> = [
{
field: AnomalyDetectionListColumns.id,
@@ -195,47 +182,19 @@ export const AnomalyDetectionTable: FC = ({ items, statsBarData, chartsSe
};
return (
- <>
-
-
-
-
- {i18n.translate('xpack.ml.overview.anomalyDetection.panelTitle', {
- defaultMessage: 'Anomaly Detection',
- })}
-
-
-
-
-
-
-
-
-
-
- {i18n.translate('xpack.ml.overview.anomalyDetection.manageJobsButtonText', {
- defaultMessage: 'Manage jobs',
- })}
-
-
-
-
-
-
-
- allowNeutralSort={false}
- className="mlAnomalyDetectionTable"
- columns={columns}
- hasActions={true}
- isExpandable={false}
- isSelectable={false}
- items={groupsList}
- itemId={AnomalyDetectionListColumns.id}
- onTableChange={onTableChange}
- pagination={pagination}
- sorting={sorting}
- data-test-subj="mlOverviewTableAnomalyDetection"
- />
- >
+
+ allowNeutralSort={false}
+ className="mlAnomalyDetectionTable"
+ columns={columns}
+ hasActions={true}
+ isExpandable={false}
+ isSelectable={false}
+ items={groupsList}
+ itemId={AnomalyDetectionListColumns.id}
+ onTableChange={onTableChange}
+ pagination={pagination}
+ sorting={sorting}
+ data-test-subj="mlOverviewTableAnomalyDetection"
+ />
);
};
diff --git a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts
index fa2e83151b80f..a5a864e139eae 100644
--- a/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts
+++ b/x-pack/plugins/ml/public/application/overview/components/anomaly_detection_panel/utils.ts
@@ -76,40 +76,45 @@ export function getGroupsFromJobs(jobs: MlSummaryJobs): {
export function getStatsBarData(jobsList: any) {
const jobStats = {
- activeNodes: {
- label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', {
- defaultMessage: 'Active ML nodes',
- }),
- value: 0,
- show: true,
- },
total: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.totalJobsLabel', {
- defaultMessage: 'Total jobs',
+ defaultMessage: 'Total',
}),
value: 0,
show: true,
+ group: 0,
},
open: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.openJobsLabel', {
- defaultMessage: 'Open jobs',
+ defaultMessage: 'Open',
}),
value: 0,
show: true,
+ group: 0,
},
closed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.closedJobsLabel', {
- defaultMessage: 'Closed jobs',
+ defaultMessage: 'Closed',
}),
value: 0,
show: true,
+ group: 0,
},
failed: {
label: i18n.translate('xpack.ml.overviewJobsList.statsBar.failedJobsLabel', {
- defaultMessage: 'Failed jobs',
+ defaultMessage: 'Failed',
}),
value: 0,
show: false,
+ group: 0,
+ },
+ activeNodes: {
+ label: i18n.translate('xpack.ml.overviewJobsList.statsBar.activeMLNodesLabel', {
+ defaultMessage: 'Active ML nodes',
+ }),
+ value: 0,
+ show: true,
+ group: 1,
},
activeDatafeeds: {
label: i18n.translate('xpack.ml.jobsList.statsBar.activeDatafeedsLabel', {
@@ -117,6 +122,7 @@ export function getStatsBarData(jobsList: any) {
}),
value: 0,
show: true,
+ group: 1,
},
};
@@ -158,5 +164,13 @@ export function getStatsBarData(jobsList: any) {
jobStats.activeNodes.value = Object.keys(mlNodes).length;
+ if (jobStats.total.value === 0) {
+ for (const [statKey, val] of Object.entries(jobStats)) {
+ if (statKey !== 'total') {
+ val.show = false;
+ }
+ }
+ }
+
return jobStats;
}
diff --git a/x-pack/plugins/ml/public/application/overview/overview_page.tsx b/x-pack/plugins/ml/public/application/overview/overview_page.tsx
index 4f7244c37f298..6772125bb9532 100644
--- a/x-pack/plugins/ml/public/application/overview/overview_page.tsx
+++ b/x-pack/plugins/ml/public/application/overview/overview_page.tsx
@@ -6,9 +6,15 @@
*/
import React, { FC, useState } from 'react';
-import { EuiPanel, EuiSpacer } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import { EuiLink, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { mlTimefilterRefresh$, useTimefilter } from '@kbn/ml-date-picker';
+import { useStorage } from '@kbn/ml-local-storage';
+import { OverviewStatsBar } from '../components/collapsible_panel/collapsible_panel';
+import { ML_PAGES } from '../../../common/constants/locator';
+import { ML_OVERVIEW_PANELS, MlStorageKey, TMlStorageMapped } from '../../../common/types/storage';
+import { CollapsiblePanel } from '../components/collapsible_panel';
import { usePermissionCheck } from '../capabilities/check_capabilities';
import { mlNodesAvailable } from '../ml_nodes_check';
import { OverviewContent } from './components/content';
@@ -17,11 +23,18 @@ import { JobsAwaitingNodeWarning } from '../components/jobs_awaiting_node_warnin
import { SavedObjectsWarning } from '../components/saved_objects_warning';
import { UpgradeWarning } from '../components/upgrade';
import { HelpMenu } from '../components/help_menu';
-import { useMlKibana } from '../contexts/kibana';
+import { useMlKibana, useMlLink } from '../contexts/kibana';
import { NodesList } from '../memory_usage/nodes_overview';
import { MlPageHeader } from '../components/page_header';
import { PageTitle } from '../components/page_title';
import { useIsServerless } from '../contexts/kibana/use_is_serverless';
+import { getMlNodesCount } from '../ml_nodes_check/check_ml_nodes';
+
+export const overviewPanelDefaultState = Object.freeze({
+ nodes: true,
+ adJobs: true,
+ dfaJobs: true,
+});
export const OverviewPage: FC = () => {
const serverless = useIsServerless();
@@ -33,11 +46,20 @@ export const OverviewPage: FC = () => {
} = useMlKibana();
const helpLink = docLinks.links.ml.guide;
+ const viewNodesLink = useMlLink({
+ page: ML_PAGES.MEMORY_USAGE,
+ });
+
const timefilter = useTimefilter({ timeRangeSelector: true, autoRefreshSelector: true });
const [adLazyJobCount, setAdLazyJobCount] = useState(0);
const [dfaLazyJobCount, setDfaLazyJobCount] = useState(0);
+ const [panelsState, setPanelsState] = useStorage<
+ MlStorageKey,
+ TMlStorageMapped
+ >(ML_OVERVIEW_PANELS, overviewPanelDefaultState);
+
return (
@@ -63,9 +85,36 @@ export const OverviewPage: FC = () => {
{canViewMlNodes && serverless === false ? (
<>
-
+ {
+ setPanelsState({ ...panelsState, nodes: update });
+ }}
+ header={
+
+ }
+ headerItems={[
+ ,
+
+ {i18n.translate('xpack.ml.overview.nodesPanel.viewNodeLink', {
+ defaultMessage: 'View nodes',
+ })}
+ ,
+ ]}
+ >
-
+
>
) : null}
diff --git a/x-pack/plugins/ml/public/application/routing/use_resolver.tsx b/x-pack/plugins/ml/public/application/routing/use_resolver.tsx
index eb3586c15c05f..5c67ed9769426 100644
--- a/x-pack/plugins/ml/public/application/routing/use_resolver.tsx
+++ b/x-pack/plugins/ml/public/application/routing/use_resolver.tsx
@@ -35,7 +35,6 @@ export const useRouteResolver = (
): {
context: RouteResolverContext;
results: ResolverResults;
- component?: React.Component;
} => {
const requiredCapabilitiesRef = useRef(requiredCapabilities);
const customResolversRef = useRef(customResolvers);
diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 84582899a6d4b..493218761bd72 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -22521,7 +22521,6 @@
"xpack.ml.notifications.newNotificationsMessage": "Il y a eu {newNotificationsCount, plural, one {# notification} many {# notifications} other {# notifications}} depuis {sinceDate}. Actualisez la page pour afficher les mises à jour.",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "Il y a eu {count, plural, one {# notification} many {# notifications} other {# notifications}} avec un niveau d'avertissement ou d'erreur depuis {lastCheckedAt}",
"xpack.ml.notificationsIndicator.unreadLabel": "Vous avez des notifications non lues depuis {lastCheckedAt}",
- "xpack.ml.overview.analyticsList.emptyPromptHelperText": "Avant de créer une tâche d'analyse du cadre de données, utilisez des {transforms} pour créer une {sourcedata}.",
"xpack.ml.previewAlert.otherValuesLabel": "et {count, plural, one {# autre} many {# autres} other {# autres}}",
"xpack.ml.previewAlert.previewMessage": "{alertsCount, plural, one {# anomalie a été trouvée} many {# anomalies ont été trouvées} other {# anomalies ont été trouvées}} au cours des dernières {interval}.",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message} Veuillez contacter votre administrateur.",
@@ -22898,7 +22897,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "couloir d'anomalie ajouté",
"xpack.ml.changePointDetection.pageHeader": "Modifier la détection du point",
"xpack.ml.chrome.help.appName": "Machine Learning",
- "xpack.ml.common.learnMoreQuestion": "Envie d'en savoir plus ?",
"xpack.ml.common.readDocumentationLink": "Lire la documentation",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "Bleu",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "Vert – Rouge",
@@ -24525,7 +24523,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "Aucune anomalie n'a été trouvée",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "Résultat introuvable",
"xpack.ml.overview.anomalyDetection.overallScore": "Score général",
- "xpack.ml.overview.anomalyDetection.panelTitle": "Détection des anomalies",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "Afficher les tâches",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "Afficher dans l’Explorateur d'anomalies",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "Actions",
@@ -24539,8 +24536,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "Valeurs typiques dans les résultats d'enregistrement des anomalies.",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "Afficher les tâches",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "Afficher dans l’Explorateur d'anomalies",
- "xpack.ml.overview.gettingStartedSectionSourceData": "ensemble de données source centré sur les entités",
- "xpack.ml.overview.gettingStartedSectionTransforms": "transformations",
"xpack.ml.overview.notificationsLabel": "Notifications",
"xpack.ml.overview.overviewLabel": "Aperçu",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "Échoué",
diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json
index 3eece16c9098d..e4f5c1867869e 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -22512,7 +22512,6 @@
"xpack.ml.notifications.newNotificationsMessage": "{sinceDate}以降に{newNotificationsCount, plural, other {#件の通知があります}}。更新を表示するには、ページを更新してください。",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "{lastCheckedAt}以降にエラーまたは警告レベルの{count, plural, other {#件の通知があります}}",
"xpack.ml.notificationsIndicator.unreadLabel": "{lastCheckedAt}以降に未読の通知があります",
- "xpack.ml.overview.analyticsList.emptyPromptHelperText": "データフレーム分析ジョブを構築する前に、{transforms}を使用して{sourcedata}を作成してください。",
"xpack.ml.previewAlert.otherValuesLabel": "および{count, plural, other {#個のその他}}",
"xpack.ml.previewAlert.previewMessage": "過去{interval}に{alertsCount, plural, other {#個の異常}}が見つかりました。",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message} 管理者にお問い合わせください。",
@@ -22884,7 +22883,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "追加された異常スイムレーン",
"xpack.ml.changePointDetection.pageHeader": "変化点検出",
"xpack.ml.chrome.help.appName": "機械学習",
- "xpack.ml.common.learnMoreQuestion": "詳細について",
"xpack.ml.common.readDocumentationLink": "ドキュメンテーションを表示",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "青",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "緑 - 赤",
@@ -24511,7 +24509,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "異常値が見つかりませんでした",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "結果が見つかりませんでした",
"xpack.ml.overview.anomalyDetection.overallScore": "全体スコア",
- "xpack.ml.overview.anomalyDetection.panelTitle": "異常検知",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "ジョブを表示",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "異常エクスプローラーで表示",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "アクション",
@@ -24525,8 +24522,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "異常レコード結果の標準的な値。",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "ジョブを表示",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "異常エクスプローラーで表示",
- "xpack.ml.overview.gettingStartedSectionSourceData": "エンティティ中心のソースデータセット",
- "xpack.ml.overview.gettingStartedSectionTransforms": "トランスフォーム",
"xpack.ml.overview.notificationsLabel": "通知",
"xpack.ml.overview.overviewLabel": "概要",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "失敗",
diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json
index 41d6320f76e0a..ecd9de2def365 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -22511,7 +22511,6 @@
"xpack.ml.notifications.newNotificationsMessage": "自 {sinceDate}以来有 {newNotificationsCount, plural, other {# 个通知}}。刷新页面以查看更新。",
"xpack.ml.notificationsIndicator.errorsAndWarningLabel": "自 {lastCheckedAt}以来有 {count, plural, other {# 个通知}}包含错误或警告级别",
"xpack.ml.notificationsIndicator.unreadLabel": "自 {lastCheckedAt}以来您有未计通知",
- "xpack.ml.overview.analyticsList.emptyPromptHelperText": "构建数据帧分析作业之前,请使用 {transforms} 构造一个 {sourcedata}。",
"xpack.ml.previewAlert.otherValuesLabel": "和{count, plural, other {另外 # 个}}",
"xpack.ml.previewAlert.previewMessage": "在过去 {interval}找到 {alertsCount, plural, other {# 个异常}}。",
"xpack.ml.privilege.pleaseContactAdministratorTooltip": "{message}请联系您的管理员。",
@@ -22883,7 +22882,6 @@
"xpack.ml.cases.anomalySwimLane.embeddableAddedEvent": "已添加异常泳道",
"xpack.ml.changePointDetection.pageHeader": "更改点检测",
"xpack.ml.chrome.help.appName": "Machine Learning",
- "xpack.ml.common.learnMoreQuestion": "希望了解详情?",
"xpack.ml.common.readDocumentationLink": "阅读文档",
"xpack.ml.components.colorRangeLegend.blueColorRangeLabel": "蓝",
"xpack.ml.components.colorRangeLegend.greenRedColorRangeLabel": "绿 - 红",
@@ -24510,7 +24508,6 @@
"xpack.ml.overview.anomalyDetection.noAnomaliesFoundMessage": "找不到异常",
"xpack.ml.overview.anomalyDetection.noResultsFoundMessage": "找不到结果",
"xpack.ml.overview.anomalyDetection.overallScore": "总分",
- "xpack.ml.overview.anomalyDetection.panelTitle": "异常检测",
"xpack.ml.overview.anomalyDetection.resultActions.openInJobManagementText": "查看作业",
"xpack.ml.overview.anomalyDetection.resultActions.openJobsInAnomalyExplorerText": "在 Anomaly Explorer 中查看",
"xpack.ml.overview.anomalyDetection.tableActionLabel": "操作",
@@ -24524,8 +24521,6 @@
"xpack.ml.overview.anomalyDetection.tableTypicalTooltip": "异常记录结果中的典型值。",
"xpack.ml.overview.anomalyDetection.viewJobsActionName": "查看作业",
"xpack.ml.overview.anomalyDetection.viewResultsActionName": "在 Anomaly Explorer 中查看",
- "xpack.ml.overview.gettingStartedSectionSourceData": "实体中心型源数据集",
- "xpack.ml.overview.gettingStartedSectionTransforms": "转换",
"xpack.ml.overview.notificationsLabel": "通知",
"xpack.ml.overview.overviewLabel": "概览",
"xpack.ml.overview.statsBar.failedAnalyticsLabel": "失败",
From 3a34200afdec9389adda86eb6f395f507d5cd17b Mon Sep 17 00:00:00 2001
From: mohamedhamed-ahmed
Date: Tue, 20 Jun 2023 13:16:39 +0100
Subject: [PATCH 09/14] [Infra UI] Add 'host.name' field view for 'Logs' tab
(#159561)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
closes #159560
## 📝 Summary
This PR adds an inline view for the logs tab in the Hosts view, where
the `host.name` column is statically added to the columns definition.
## ✅ Testing
1. Navigate to Stream page
2. Change the settings to have any Date Source and columns
3. Navigate to the logs tab in the Hosts view
4. Verify that the columns applied in step 2 aren't showing in the logs,
and that `host.name` is showing.
5. Click the open in logs link and make sure that the settings show the
host.name and notes an inline view is being used.
https://github.com/elastic/kibana/assets/11225826/a19f7969-31b3-40af-9e07-784631d6292f
---
.../tabs/logs/logs_link_to_stream.tsx | 5 +-
.../components/tabs/logs/logs_tab_content.tsx | 65 +++++++++++++++++--
.../test/functional/apps/infra/hosts_view.ts | 8 +++
.../page_objects/infra_hosts_view.ts | 7 ++
4 files changed, 80 insertions(+), 5 deletions(-)
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx
index f5c2101317f01..ac3981026ea8e 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_link_to_stream.tsx
@@ -8,15 +8,17 @@ import React from 'react';
import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app';
import { EuiButtonEmpty } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
+import { LogViewReference } from '../../../../../../../common/log_views';
import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
interface LogsLinkToStreamProps {
startTime: number;
endTime: number;
query: string;
+ logView: LogViewReference;
}
-export const LogsLinkToStream = ({ startTime, endTime, query }: LogsLinkToStreamProps) => {
+export const LogsLinkToStream = ({ startTime, endTime, query, logView }: LogsLinkToStreamProps) => {
const { services } = useKibanaContextForPlugin();
const { locators } = services;
@@ -30,6 +32,7 @@ export const LogsLinkToStream = ({ startTime, endTime, query }: LogsLinkToStream
endTime,
},
filter: query,
+ logView,
})}
data-test-subj="hostsView-logs-link-to-stream-button"
iconType="popout"
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
index db93a9a4617cc..55a8410618b28 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/tabs/logs/logs_tab_content.tsx
@@ -5,9 +5,15 @@
* 2.0.
*/
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
+import { DEFAULT_LOG_VIEW } from '../../../../../../../common/log_views';
+import type {
+ LogIndexReference,
+ LogViewReference,
+} from '../../../../../../../common/log_views/types';
+import { useKibanaContextForPlugin } from '../../../../../../hooks/use_kibana';
import { InfraLoadingPanel } from '../../../../../../components/loading';
import { LogStream } from '../../../../../../components/log_stream';
import { useHostsViewContext } from '../../../hooks/use_hosts_view';
@@ -23,11 +29,57 @@ export const LogsTabContent = () => {
const { from, to } = useMemo(() => getDateRangeAsTimestamp(), [getDateRangeAsTimestamp]);
const { hostNodes, loading } = useHostsViewContext();
+ const [logViewIndices, setLogViewIndices] = useState();
+
+ const {
+ services: {
+ logViews: { client },
+ },
+ } = useKibanaContextForPlugin();
+
+ useEffect(() => {
+ const getLogView = async () => {
+ const { attributes } = await client.getLogView(DEFAULT_LOG_VIEW);
+ setLogViewIndices(attributes.logIndices);
+ };
+ getLogView();
+ }, [client, setLogViewIndices]);
+
const hostsFilterQuery = useMemo(
() => createHostsFilter(hostNodes.map((p) => p.name)),
[hostNodes]
);
+ const logView: LogViewReference = useMemo(() => {
+ return {
+ type: 'log-view-inline',
+ id: 'hosts-logs-view',
+ attributes: {
+ name: 'Hosts Logs View',
+ description: 'Default view for hosts logs tab',
+ logIndices: logViewIndices!,
+ logColumns: [
+ {
+ timestampColumn: {
+ id: '5e7f964a-be8a-40d8-88d2-fbcfbdca0e2f',
+ },
+ },
+ {
+ fieldColumn: {
+ id: 'eb9777a8-fcd3-420e-ba7d-172fff6da7a2',
+ field: 'host.name',
+ },
+ },
+ {
+ messageColumn: {
+ id: 'b645d6da-824b-4723-9a2a-e8cece1645c0',
+ },
+ },
+ ],
+ },
+ };
+ }, [logViewIndices]);
+
const logsLinkToStreamQuery = useMemo(() => {
const hostsFilterQueryParam = createHostsFilterQueryParam(hostNodes.map((p) => p.name));
@@ -38,7 +90,7 @@ export const LogsTabContent = () => {
return filterQuery.query || hostsFilterQueryParam;
}, [filterQuery.query, hostNodes]);
- if (loading) {
+ if (loading || !logViewIndices) {
return (
@@ -64,14 +116,19 @@ export const LogsTabContent = () => {
-
+
{
it('should load the Logs tab section when clicking on it', async () => {
await testSubjects.existOrFail('hostsView-logs');
});
+
+ it('should load the Logs tab with the right columns', async () => {
+ await retry.try(async () => {
+ const columnLabels = await pageObjects.infraHostsView.getLogsTableColumnHeaders();
+
+ expect(columnLabels).to.eql(['Timestamp', 'host.name', 'Message']);
+ });
+ });
});
describe('Alerts Tab', () => {
diff --git a/x-pack/test/functional/page_objects/infra_hosts_view.ts b/x-pack/test/functional/page_objects/infra_hosts_view.ts
index b9219b53fda2d..c2982ae77f211 100644
--- a/x-pack/test/functional/page_objects/infra_hosts_view.ts
+++ b/x-pack/test/functional/page_objects/infra_hosts_view.ts
@@ -214,6 +214,13 @@ export function InfraHostsViewProvider({ getService }: FtrProviderContext) {
return container.findAllByCssSelector('[data-test-subj*=streamEntry]');
},
+ async getLogsTableColumnHeaders() {
+ const columnHeaderElements: WebElementWrapper[] = await testSubjects.findAll(
+ '~logColumnHeader'
+ );
+ return await Promise.all(columnHeaderElements.map((element) => element.getVisibleText()));
+ },
+
// Alerts Tab
getAlertsTab() {
return testSubjects.find('hostsView-tabs-alerts');
From ee6f0f773f507f937d9e415359148d8a7c17dc8b Mon Sep 17 00:00:00 2001
From: Carlos Crespo
Date: Tue, 20 Jun 2023 14:20:18 +0200
Subject: [PATCH 10/14] [Infrastructure UI] Propagate Kibana version to hosts
view feedback form (#159210)
closes:
[#1053](https://github.com/elastic/obs-infraobs-team/issues/1053)
## Summary
This PR changes the Hosts View feedback button to pass also the current
Kibana version.
The URL is no longer a shortened one, and that's because it wasn't
forwarding query parameters.
### How to test
- Start a local Kibana
- Navigate to `Infrastructure > Hosts`
- Click on "Tell us what you think" button
- On the forms, the question "What version of Elastic are you using?"
should be filled automatically with the Kibana version
---------
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/pages/metrics/hosts/index.tsx | 19 +++++++++++++++++--
x-pack/plugins/infra/public/plugin.ts | 11 +++++++++--
x-pack/plugins/infra/public/types.ts | 1 +
3 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
index 053de350a8a0d..35d0ca77ce23b 100644
--- a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
+++ b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx
@@ -12,6 +12,7 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public';
import { FormattedMessage } from '@kbn/i18n-react';
import { css } from '@emotion/react';
import { i18n } from '@kbn/i18n';
+import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { SourceErrorPage } from '../../../components/source_error_page';
import { SourceLoadingPage } from '../../../components/source_loading_page';
import { useSourceContext } from '../../../containers/metrics_source';
@@ -25,10 +26,24 @@ import { HostContainer } from './components/hosts_container';
import { BetaBadge } from '../../../components/beta_badge';
import { NoRemoteCluster } from '../../../components/empty_states';
-const HOSTS_FEEDBACK_LINK = 'https://ela.st/host-feedback';
+const HOSTS_FEEDBACK_LINK =
+ 'https://docs.google.com/forms/d/e/1FAIpQLScRHG8TIVb1Oq8ZhD4aks3P1TmgiM58TY123QpDCcBz83YC6w/viewform';
+const KIBANA_VERSION_QUERY_PARAM = 'entry.548460210';
+
+const getHostFeedbackURL = (kibanaVersion?: string) => {
+ const url = new URL(HOSTS_FEEDBACK_LINK);
+ if (kibanaVersion) {
+ url.searchParams.append(KIBANA_VERSION_QUERY_PARAM, kibanaVersion);
+ }
+
+ return url.href;
+};
export const HostsPage = () => {
const { isLoading, loadSourceFailureMessage, loadSource, source } = useSourceContext();
+ const {
+ services: { kibanaVersion },
+ } = useKibanaContextForPlugin();
useTrackPageview({ app: 'infra_metrics', path: 'hosts' });
useTrackPageview({ app: 'infra_metrics', path: 'hosts', delay: 15000 });
@@ -83,7 +98,7 @@ export const HostsPage = () => {
rightSideItems: [
(() => ({}));
constructor(context: PluginInitializerContext) {
@@ -74,6 +75,7 @@ export class Plugin implements InfraClientPluginClass {
this.metricsExplorerViews = new MetricsExplorerViewsService();
this.telemetry = new TelemetryService();
this.appTarget = this.config.logs.app_target;
+ this.kibanaVersion = context.env.packageInfo.version;
}
setup(core: InfraClientCoreSetup, pluginsSetup: InfraClientSetupDeps) {
@@ -286,10 +288,15 @@ export class Plugin implements InfraClientPluginClass {
deepLinks: infraDeepLinks,
mount: async (params: AppMountParameters) => {
// mount callback should not use setup dependencies, get start dependencies instead
- const [coreStart, pluginsStart, pluginStart] = await core.getStartServices();
+ const [coreStart, plugins, pluginStart] = await core.getStartServices();
const { renderApp } = await import('./apps/metrics_app');
- return renderApp(coreStart, pluginsStart, pluginStart, params);
+ return renderApp(
+ coreStart,
+ { ...plugins, kibanaVersion: this.kibanaVersion },
+ pluginStart,
+ params
+ );
},
});
diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts
index fb1d2d4ab2a91..15d4f4ea5fb6e 100644
--- a/x-pack/plugins/infra/public/types.ts
+++ b/x-pack/plugins/infra/public/types.ts
@@ -93,6 +93,7 @@ export interface InfraClientStartDeps {
dataViews: DataViewsPublicPluginStart;
discover: DiscoverStart;
embeddable?: EmbeddableStart;
+ kibanaVersion?: string;
lens: LensPublicStart;
ml: MlPluginStart;
observability: ObservabilityPublicStart;
From 66e87e63e98f8f092a22a304bc6149588891181a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gerg=C5=91=20=C3=81brah=C3=A1m?=
Date: Tue, 20 Jun 2023 14:54:26 +0200
Subject: [PATCH 11/14] [Fleet][Agent Tampering] Remove unused `created_at`
field from uninstall token SO mapping (#159985)
## Summary
`created_at` field was added to the mapping for the uninstall token
Saved Object, but it's not used and causes trouble.
~There is a discussion whether to remove it from the mapping or not,
before the recently added mapping itself is released with v8.9.0, so I
prepared this PR to merge in case we want to remove it.~ The discussion
ended with the decision to remove the field, so the aim is to merge this
PR.
---
packages/kbn-check-mappings-update-cli/current_mappings.json | 3 ---
.../migrations/group2/check_registered_types.test.ts | 2 +-
x-pack/plugins/fleet/server/saved_objects/index.ts | 1 -
.../server/services/security/uninstall_token_service/index.ts | 2 +-
4 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json
index 1e0ac8548906e..525cbba463d2d 100644
--- a/packages/kbn-check-mappings-update-cli/current_mappings.json
+++ b/packages/kbn-check-mappings-update-cli/current_mappings.json
@@ -1898,9 +1898,6 @@
"fleet-uninstall-tokens": {
"dynamic": false,
"properties": {
- "created_at": {
- "type": "date"
- },
"policy_id": {
"type": "keyword"
},
diff --git a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
index cd7ef7ea94031..9859886cfd6d5 100644
--- a/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
+++ b/src/core/server/integration_tests/saved_objects/migrations/group2/check_registered_types.test.ts
@@ -96,7 +96,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f",
"fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e",
"fleet-proxy": "6cb688f0d2dd856400c1dbc998b28704ff70363d",
- "fleet-uninstall-tokens": "d25a8aedb522d2b839ab0950160777528122070f",
+ "fleet-uninstall-tokens": "ed8aa37e3cdd69e4360709e64944bb81cae0c025",
"graph-workspace": "5cc6bb1455b078fd848c37324672163f09b5e376",
"guided-onboarding-guide-state": "d338972ed887ac480c09a1a7fbf582d6a3827c91",
"guided-onboarding-plugin-state": "bc109e5ef46ca594fdc179eda15f3095ca0a37a4",
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index c606994edf185..48a997a180365 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -410,7 +410,6 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
mappings: {
dynamic: false,
properties: {
- created_at: { type: 'date' },
policy_id: { type: 'keyword' },
token_plain: { type: 'keyword' },
},
diff --git a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts
index 013f77f3005f3..40f0768161368 100644
--- a/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts
+++ b/x-pack/plugins/fleet/server/services/security/uninstall_token_service/index.ts
@@ -152,7 +152,7 @@ export class UninstallTokenService implements UninstallTokenServiceInterface {
latest: {
top_hits: {
size: 1,
- sort: [{ [`${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.created_at`]: { order: 'desc' } }],
+ sort: [{ created_at: { order: 'desc' } }],
},
},
},
From c0e43dbf2834ea526ef43cd5d366e19434cd1cdc Mon Sep 17 00:00:00 2001
From: Stratoula Kalafateli
Date: Tue, 20 Jun 2023 16:02:48 +0300
Subject: [PATCH 12/14] [Lens][Visualize] Removes wrong padding on the
dashboard (#159992)
## Summary
Closes https://github.com/elastic/kibana/issues/159942
If the height of a partition chart exceeds 1000px paddings are added,
reducing the chart size.
This is caused due to this piece of code
https://github.com/elastic/kibana/pull/122420
This was added for the aggbased editor to reduce a bit the pie size
(otherwise it was taking the full container size and the pie was huge)
Although we want this, we don't want this to be applied in dashboards or
lens editor. This PR is fixing this by adding the paddings only on the
agg based editor level
In agg based editor
Dashboard with very tall treemap, no paddings
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
src/plugins/chart_expressions/common/index.ts | 7 +-
.../chart_expressions/common/utils.test.ts | 76 ++++++++++++++++++-
src/plugins/chart_expressions/common/utils.ts | 25 ++++++
.../partition_vis_component.test.tsx | 1 +
.../components/partition_vis_component.tsx | 23 ++++--
.../partition_vis_renderer.tsx | 9 ++-
.../public/utils/get_partition_theme.test.ts | 23 ++++--
.../public/utils/get_partition_theme.ts | 8 +-
8 files changed, 155 insertions(+), 17 deletions(-)
diff --git a/src/plugins/chart_expressions/common/index.ts b/src/plugins/chart_expressions/common/index.ts
index 4373260657909..0983b1ed28d4d 100644
--- a/src/plugins/chart_expressions/common/index.ts
+++ b/src/plugins/chart_expressions/common/index.ts
@@ -6,5 +6,10 @@
* Side Public License, v 1.
*/
-export { extractContainerType, extractVisualizationType, getOverridesFor } from './utils';
+export {
+ extractContainerType,
+ extractVisualizationType,
+ getOverridesFor,
+ isOnAggBasedEditor,
+} from './utils';
export type { Simplify, MakeOverridesSerializable } from './types';
diff --git a/src/plugins/chart_expressions/common/utils.test.ts b/src/plugins/chart_expressions/common/utils.test.ts
index 2ed71e9a17b92..48519c9f6f1a9 100644
--- a/src/plugins/chart_expressions/common/utils.test.ts
+++ b/src/plugins/chart_expressions/common/utils.test.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { getOverridesFor } from './utils';
+import { getOverridesFor, isOnAggBasedEditor } from './utils';
describe('Overrides utilities', () => {
describe('getOverridesFor', () => {
@@ -31,3 +31,77 @@ describe('Overrides utilities', () => {
});
});
});
+
+describe('isOnAggBasedEditor', () => {
+ it('should return false if is on dashboard', () => {
+ const context = {
+ type: 'dashboard',
+ description: 'test',
+ child: {
+ type: 'lens',
+ name: 'lnsPie',
+ id: 'd8bb29a7-13a4-43fa-a162-d7705050bb6c',
+ description: 'test',
+ url: '/gdu/app/lens#/edit_by_value',
+ },
+ };
+ expect(isOnAggBasedEditor(context)).toEqual(false);
+ });
+
+ it('should return false if is on editor but lens', () => {
+ const context = {
+ type: 'application',
+ description: 'test',
+ child: {
+ type: 'lens',
+ name: 'lnsPie',
+ id: 'd8bb29a7-13a4-43fa-a162-d7705050bb6c',
+ description: 'test',
+ url: '/gdu/app/lens#/edit_by_value',
+ },
+ };
+ expect(isOnAggBasedEditor(context)).toEqual(false);
+ });
+
+ it('should return false if is on dashboard but agg_based', () => {
+ const context = {
+ type: 'dashboard',
+ description: 'test',
+ child: {
+ type: 'agg_based',
+ name: 'pie',
+ id: 'd8bb29a7-13a4-43fa-a162-d7705050bb6c',
+ description: 'test',
+ url: '',
+ },
+ };
+ expect(isOnAggBasedEditor(context)).toEqual(false);
+ });
+
+ it('should return true if is on editor but agg_based', () => {
+ const context = {
+ type: 'application',
+ description: 'test',
+ child: {
+ type: 'agg_based',
+ name: 'pie',
+ id: 'd8bb29a7-13a4-43fa-a162-d7705050bb6c',
+ description: 'test',
+ url: '',
+ },
+ };
+ expect(isOnAggBasedEditor(context)).toEqual(true);
+ });
+
+ it('should return false if child is missing', () => {
+ const context = {
+ type: 'application',
+ description: 'test',
+ };
+ expect(isOnAggBasedEditor(context)).toEqual(false);
+ });
+
+ it('should return false if context is missing', () => {
+ expect(isOnAggBasedEditor()).toEqual(false);
+ });
+});
diff --git a/src/plugins/chart_expressions/common/utils.ts b/src/plugins/chart_expressions/common/utils.ts
index 2966532c44117..db2e564efc4b3 100644
--- a/src/plugins/chart_expressions/common/utils.ts
+++ b/src/plugins/chart_expressions/common/utils.ts
@@ -20,6 +20,31 @@ export const extractContainerType = (context?: KibanaExecutionContext): string |
}
};
+/* Function to identify if the pie is rendered inside the aggBased editor
+ Context comes with this format
+ {
+ type: 'dashboard', // application for lens, agg based charts
+ description: 'test',
+ child: {
+ type: 'lens', // agg_based for legacy editor
+ name: 'pie',
+ id: 'id',
+ description: 'test',
+ url: '',
+ },
+ }; */
+export const isOnAggBasedEditor = (context?: KibanaExecutionContext): boolean => {
+ if (context) {
+ return Boolean(
+ context.type &&
+ context.type === 'application' &&
+ context.child &&
+ context.child.type === 'agg_based'
+ );
+ }
+ return false;
+};
+
export const extractVisualizationType = (context?: KibanaExecutionContext): string | undefined => {
if (context) {
const recursiveGet = (item: KibanaExecutionContext): KibanaExecutionContext | undefined => {
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
index 5eb48cfab6cd5..c935ce847e40e 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.test.tsx
@@ -83,6 +83,7 @@ describe('PartitionVisComponent', function () {
data: dataPluginMock.createStartContract(),
fieldFormats: fieldFormatsServiceMock.createStartContract(),
},
+ hasOpenedOnAggBasedEditor: false,
};
});
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
index 94590ff164555..4ce300a7d9bb9 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/components/partition_vis_component.tsx
@@ -93,6 +93,7 @@ export type PartitionVisComponentProps = Omit<
palettesRegistry: PaletteRegistry;
services: Pick;
columnCellValueActions: ColumnCellValueActions;
+ hasOpenedOnAggBasedEditor: boolean;
};
const PartitionVisComponent = (props: PartitionVisComponentProps) => {
@@ -105,6 +106,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
syncColors,
interactive,
overrides,
+ hasOpenedOnAggBasedEditor,
} = props;
const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]);
const chartTheme = props.chartsThemeService.useChartsTheme();
@@ -148,7 +150,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const [showLegend, setShowLegend] = useState(() => showLegendDefault());
const showToggleLegendElement = props.uiState !== undefined;
-
+ const [chartIsLoaded, setChartIsLoaded] = useState(false);
const [containerDimensions, setContainerDimensions] = useState<
undefined | PieContainerDimensions
>();
@@ -156,12 +158,14 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const parentRef = useRef(null);
useEffect(() => {
- if (parentRef && parentRef.current) {
+ // chart should be loaded to compute the dimensions
+ // otherwise the height is set to 0
+ if (parentRef && parentRef.current && chartIsLoaded) {
const parentHeight = parentRef.current!.getBoundingClientRect().height;
const parentWidth = parentRef.current!.getBoundingClientRect().width;
setContainerDimensions({ width: parentWidth, height: parentHeight });
}
- }, [parentRef]);
+ }, [chartIsLoaded, parentRef]);
useEffect(() => {
const legendShow = showLegendDefault();
@@ -172,6 +176,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
(isRendered: boolean = true) => {
if (isRendered) {
props.renderComplete();
+ setChartIsLoaded(true);
}
},
[props]
@@ -363,8 +368,16 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
) as Partial;
const themeOverrides = useMemo(
- () => getPartitionTheme(visType, visParams, chartTheme, containerDimensions, rescaleFactor),
- [visType, visParams, chartTheme, containerDimensions, rescaleFactor]
+ () =>
+ getPartitionTheme(
+ visType,
+ visParams,
+ chartTheme,
+ containerDimensions,
+ rescaleFactor,
+ hasOpenedOnAggBasedEditor
+ ),
+ [visType, visParams, chartTheme, containerDimensions, rescaleFactor, hasOpenedOnAggBasedEditor]
);
const fixedViewPort = document.getElementById('app-fixed-viewport');
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
index 056ba6b7136ce..2379096796639 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/expression_renderers/partition_vis_renderer.tsx
@@ -21,7 +21,11 @@ import { withSuspense } from '@kbn/presentation-util-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { METRIC_TYPE } from '@kbn/analytics';
import { getColumnByAccessor } from '@kbn/visualizations-plugin/common/utils';
-import { extractContainerType, extractVisualizationType } from '@kbn/chart-expressions-common';
+import {
+ extractContainerType,
+ extractVisualizationType,
+ isOnAggBasedEditor,
+} from '@kbn/chart-expressions-common';
import { VisTypePieDependencies } from '../plugin';
import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants';
import { CellValueAction, GetCompatibleCellValueActions } from '../types';
@@ -110,6 +114,8 @@ export const getPartitionVisRenderer: (
plugins.charts.palettes.getPalettes(),
]);
+ const hasOpenedOnAggBasedEditor = isOnAggBasedEditor(handlers.getExecutionContext());
+
render(
@@ -128,6 +134,7 @@ export const getPartitionVisRenderer: (
syncColors={syncColors}
columnCellValueActions={columnCellValueActions}
overrides={overrides}
+ hasOpenedOnAggBasedEditor={hasOpenedOnAggBasedEditor}
/>
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts
index 345d6ce068d0c..31d025ac0310f 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.test.ts
@@ -144,9 +144,16 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition
});
});
- it('should return adjusted padding settings if dimensions are specified', () => {
+ it('should return adjusted padding settings if dimensions are specified and is on aggBased editor', () => {
const specifiedDimensions = { width: 2000, height: 2000 };
- const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions);
+ const theme = getPartitionTheme(
+ chartType,
+ visParams,
+ chartTheme,
+ specifiedDimensions,
+ undefined,
+ true
+ );
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
@@ -233,7 +240,6 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
- chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: rescaleFactor,
@@ -263,7 +269,6 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
- chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, vParams),
outerSizeRatio: 0.5,
@@ -285,7 +290,6 @@ const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: Partition
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
- chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, vParams),
linkLabel: linkLabelWithEnoughSpace(vParams),
@@ -420,7 +424,14 @@ const runTreemapMosaicTestSuites = (chartType: ChartTypes, visParams: PartitionV
it('should return fullfilled padding settings if dimensions are specified', () => {
const specifiedDimensions = { width: 2000, height: 2000 };
- const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions);
+ const theme = getPartitionTheme(
+ chartType,
+ visParams,
+ chartTheme,
+ specifiedDimensions,
+ undefined,
+ true
+ );
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts
index edb1aaea64aad..3714cac911829 100644
--- a/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts
+++ b/src/plugins/chart_expressions/expression_partition_vis/public/utils/get_partition_theme.ts
@@ -26,7 +26,8 @@ type GetThemeFn = (
visParams: PartitionVisParams,
chartTheme: RecursivePartial,
dimensions?: PieContainerDimensions,
- rescaleFactor?: number
+ rescaleFactor?: number,
+ hasOpenedOnAggBasedEditor?: boolean
) => PartialTheme;
type GetPieDonutWaffleThemeFn = (
@@ -118,12 +119,13 @@ export const getPartitionTheme: GetThemeFn = (
visParams,
chartTheme,
dimensions,
- rescaleFactor = 1
+ rescaleFactor = 1,
+ hasOpenedOnAggBasedEditor
) => {
// On small multiples we want the labels to only appear inside
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
const paddingProps: PartialTheme | null =
- dimensions && !isSplitChart
+ dimensions && !isSplitChart && hasOpenedOnAggBasedEditor
? {
chartPaddings: {
top: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height,
From bd0e09504c60bdf4c68b89cc8459f48fb30ccede Mon Sep 17 00:00:00 2001
From: Nicolas Chaulet
Date: Tue, 20 Jun 2023 09:11:56 -0400
Subject: [PATCH 13/14] [Fleet] Fix navigation from multi page layout (#159967)
---
.../hooks/navigation.tsx | 57 ++++++++++++-------
.../single_page_layout/hooks/form.tsx | 4 +-
.../single_page_layout/index.test.tsx | 14 +++--
.../single_page_layout/index.tsx | 17 +++---
x-pack/plugins/fleet/tsconfig.json | 1 +
5 files changed, 59 insertions(+), 34 deletions(-)
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
index b908cb4908ade..097323962bc0c 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/hooks/navigation.tsx
@@ -4,9 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
+
import { useCallback, useMemo, useEffect, useRef } from 'react';
-import { useHistory } from 'react-router-dom';
+import type { ApplicationStart } from '@kbn/core-application-browser';
+import { PLUGIN_ID } from '../../../../constants';
import { useStartServices, useLink, useIntraAppState } from '../../../../hooks';
import type {
CreatePackagePolicyRouteState,
@@ -67,7 +69,6 @@ export const useOnSaveNavigate = (params: UseOnSaveNavigateParams) => {
const routeState = useIntraAppState();
const doOnSaveNavigation = useRef(true);
const { getPath } = useLink();
- const history = useHistory();
const {
application: { navigateToApp },
@@ -81,32 +82,46 @@ export const useOnSaveNavigate = (params: UseOnSaveNavigateParams) => {
}, []);
const onSaveNavigate = useCallback(
- (policy?: PackagePolicy, paramsToApply: OnSaveQueryParamKeys[] = []) => {
+ (policy: PackagePolicy, paramsToApply: OnSaveQueryParamKeys[] = []) => {
if (!doOnSaveNavigation.current) {
return;
}
-
const packagePolicyPath = getPath('policy_details', { policyId: packagePolicy.policy_id });
- if (routeState?.onSaveNavigateTo && policy) {
- const [appId, options] = routeState.onSaveNavigateTo;
- if (options?.path) {
- const pathWithQueryString = appendOnSaveQueryParamsToPath({
- // In cases where we want to navigate back to a new/existing policy, we need to override the initial `path`
- // value and navigate to the actual agent policy instead
- path: queryParamsPolicyId ? packagePolicyPath : options.path,
- policy,
- mappingOptions: routeState.onSaveQueryParams,
- paramsToApply,
- });
- navigateToApp(appId, { ...options, path: pathWithQueryString });
- } else {
- navigateToApp(...routeState.onSaveNavigateTo);
- }
+
+ const [onSaveNavigateTo, onSaveQueryParams]: [
+ Parameters,
+ CreatePackagePolicyRouteState['onSaveQueryParams']
+ ] = routeState?.onSaveNavigateTo
+ ? [routeState.onSaveNavigateTo, routeState?.onSaveQueryParams]
+ : [
+ [
+ PLUGIN_ID,
+ {
+ path: packagePolicyPath,
+ },
+ ],
+ {
+ showAddAgentHelp: true,
+ openEnrollmentFlyout: true,
+ },
+ ];
+
+ const [appId, options] = onSaveNavigateTo;
+ if (options?.path) {
+ const pathWithQueryString = appendOnSaveQueryParamsToPath({
+ // In cases where we want to navigate back to a new/existing policy, we need to override the initial `path`
+ // value and navigate to the actual agent policy instead
+ path: queryParamsPolicyId ? packagePolicyPath : options.path,
+ policy,
+ mappingOptions: onSaveQueryParams,
+ paramsToApply,
+ });
+ navigateToApp(appId, { ...options, path: pathWithQueryString });
} else {
- history.push(packagePolicyPath);
+ navigateToApp(...onSaveNavigateTo);
}
},
- [packagePolicy.policy_id, getPath, navigateToApp, history, routeState, queryParamsPolicyId]
+ [packagePolicy.policy_id, getPath, navigateToApp, routeState, queryParamsPolicyId]
);
return onSaveNavigate;
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
index cc01cadccb788..83b476b774913 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx
@@ -233,10 +233,10 @@ export function useOnSubmit({
queryParamsPolicyId,
});
- const navigateAddAgent = (policy?: PackagePolicy) =>
+ const navigateAddAgent = (policy: PackagePolicy) =>
onSaveNavigate(policy, ['openEnrollmentFlyout']);
- const navigateAddAgentHelp = (policy?: PackagePolicy) =>
+ const navigateAddAgentHelp = (policy: PackagePolicy) =>
onSaveNavigate(policy, ['showAddAgentHelp']);
const onSubmit = useCallback(
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx
index e2f2ec5e908f7..458f4fab53384 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx
@@ -5,7 +5,6 @@
* 2.0.
*/
-import { useHistory } from 'react-router-dom';
import { Route } from '@kbn/shared-ux-router';
import React from 'react';
import { fireEvent, act, waitFor } from '@testing-library/react';
@@ -127,6 +126,8 @@ describe('when on the package policy create page', () => {
mockApiCalls(testRenderer.startServices.http);
testRenderer.mountHistory.push(createPageUrlPath);
+ jest.mocked(useStartServices().application.navigateToApp).mockReset();
+
mockPackageInfo = {
data: {
item: {
@@ -339,12 +340,15 @@ describe('when on the package policy create page', () => {
test('should navigate to save navigate path with query param if set', async () => {
const routeState = {
onSaveNavigateTo: [PLUGIN_ID, { path: '/save/url/here' }],
+ onSaveQueryParams: {
+ openEnrollmentFlyout: true,
+ },
};
const queryParamsPolicyId = 'agent-policy-1';
await setupSaveNavigate(routeState, queryParamsPolicyId);
expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, {
- path: '/policies/agent-policy-1',
+ path: '/policies/agent-policy-1?openEnrollmentFlyout=true',
});
});
@@ -357,10 +361,12 @@ describe('when on the package policy create page', () => {
expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID);
});
- test('should set history if no routeState', async () => {
+ test('should navigate to agent policy if no route state is set', async () => {
await setupSaveNavigate({});
- expect(useHistory().push).toHaveBeenCalledWith('/policies/agent-policy-1');
+ expect(useStartServices().application.navigateToApp).toHaveBeenCalledWith(PLUGIN_ID, {
+ path: '/policies/agent-policy-1?openEnrollmentFlyout=true',
+ });
});
});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
index 81c1c518ccd4f..fd62addd08391 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx
@@ -402,13 +402,16 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
onCancel={() => setFormState('VALID')}
/>
)}
- {formState === 'SUBMITTED_NO_AGENTS' && agentPolicy && packageInfo && (
- navigateAddAgent(savedPackagePolicy)}
- onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
- />
- )}
+ {formState === 'SUBMITTED_NO_AGENTS' &&
+ agentPolicy &&
+ packageInfo &&
+ savedPackagePolicy && (
+ navigateAddAgent(savedPackagePolicy)}
+ onCancel={() => navigateAddAgentHelp(savedPackagePolicy)}
+ />
+ )}
{packageInfo && (
Date: Tue, 20 Jun 2023 15:16:26 +0200
Subject: [PATCH 14/14] [Enterprise Search] Prefix target index for access
control syncs with "search-acl-filter-" (#159996)
## Summary
Closes https://github.com/elastic/enterprise-search-team/issues/4979
### Checklist
Delete any items that are not applicable to this PR.
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
---
.../plugins/enterprise_search/server/index.ts | 1 +
.../server/lib/connectors/start_sync.test.ts | 10 +++++++---
.../server/lib/connectors/start_sync.ts | 18 ++++++++++++++----
3 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts
index 692d17d9952bc..bb4e1e2e0bf0e 100644
--- a/x-pack/plugins/enterprise_search/server/index.ts
+++ b/x-pack/plugins/enterprise_search/server/index.ts
@@ -52,3 +52,4 @@ export const CURRENT_CONNECTORS_INDEX = '.elastic-connectors-v1';
export const CONNECTORS_JOBS_INDEX = '.elastic-connectors-sync-jobs';
export const CONNECTORS_VERSION = 1;
export const CRAWLERS_INDEX = '.ent-search-actastic-crawler2_configurations_v2';
+export const CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX = 'search-acl-filter-';
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
index 6c69273e2e240..ca7ae9fc76a66 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.test.ts
@@ -7,7 +7,11 @@
import { IScopedClusterClient } from '@kbn/core/server';
-import { CONNECTORS_INDEX, CONNECTORS_JOBS_INDEX } from '../..';
+import {
+ CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX,
+ CONNECTORS_INDEX,
+ CONNECTORS_JOBS_INDEX,
+} from '../..';
import { SyncJobType, SyncStatus, TriggerMethod } from '../../../common/types/connectors';
import { ErrorCode } from '../../../common/types/error_codes';
@@ -311,7 +315,7 @@ describe('startSync lib function', () => {
created_at: null,
custom_scheduling: {},
error: null,
- index_name: 'index_name',
+ index_name: 'search-index_name',
language: null,
last_access_control_sync_status: null,
last_seen: null,
@@ -345,7 +349,7 @@ describe('startSync lib function', () => {
configuration: {},
filtering: null,
id: 'connectorId',
- index_name: 'index_name',
+ index_name: `${CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX}index_name`,
language: null,
pipeline: null,
service_type: null,
diff --git a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
index ff9e0419e69b0..8d2ac4715e8df 100644
--- a/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
+++ b/x-pack/plugins/enterprise_search/server/lib/connectors/start_sync.ts
@@ -7,18 +7,22 @@
import { IScopedClusterClient } from '@kbn/core/server';
-import { CONNECTORS_INDEX, CONNECTORS_JOBS_INDEX } from '../..';
+import {
+ CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX,
+ CONNECTORS_INDEX,
+ CONNECTORS_JOBS_INDEX,
+} from '../..';
import { isConfigEntry } from '../../../common/connectors/is_category_entry';
import { ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE } from '../../../common/constants';
import {
- ConnectorSyncConfiguration,
ConnectorDocument,
+ ConnectorSyncConfiguration,
ConnectorSyncJobDocument,
+ SyncJobType,
SyncStatus,
TriggerMethod,
- SyncJobType,
} from '../../../common/types/connectors';
import { ErrorCode } from '../../../common/types/error_codes';
@@ -63,6 +67,12 @@ export const startConnectorSync = async (
});
}
+ const indexNameWithoutSearchPrefix = index_name.replace('search-', '');
+ const targetIndexName =
+ jobType === SyncJobType.ACCESS_CONTROL
+ ? `${CONNECTORS_ACCESS_CONTROL_INDEX_PREFIX}${indexNameWithoutSearchPrefix}`
+ : index_name;
+
return await client.asCurrentUser.index({
document: {
cancelation_requested_at: null,
@@ -72,7 +82,7 @@ export const startConnectorSync = async (
configuration,
filtering: filtering ? filtering[0]?.active ?? null : null,
id: connectorId,
- index_name,
+ index_name: targetIndexName,
language,
pipeline: pipeline ?? null,
service_type,