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/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/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 9a37afdb78e16..ed619f50ec63b 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, http }); 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 845c3aa3e8eb6..76192aff162c7 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 @@ -88,6 +88,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 c4213e5efdb58..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,12 +236,13 @@ 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 }); 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/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index e0a24046196d6..525cbba463d2d 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": { @@ -1898,9 +1898,6 @@ "fleet-uninstall-tokens": { "dynamic": false, "properties": { - "created_at": { - "type": "date" - }, "policy_id": { "type": "keyword" }, @@ -2377,6 +2374,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..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", @@ -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/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, 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..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 @@ -98,5 +98,15 @@ 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 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'); + }); }); } 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/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, 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 && ( ({ 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' } }], }, }, }, diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json index 499af010d076f..dc858ff5029a8 100644 --- a/x-pack/plugins/fleet/tsconfig.json +++ b/x-pack/plugins/fleet/tsconfig.json @@ -98,5 +98,6 @@ "@kbn/safer-lodash-set", "@kbn/shared-ux-file-types", "@kbn/core-http-router-server-mocks", + "@kbn/core-application-browser", ] } 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 = () => { - + { + 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; diff --git a/x-pack/plugins/ml/common/types/storage.ts b/x-pack/plugins/ml/common/types/storage.ts index cb80b17bda583..a74bbea0e3aff 100644 --- a/x-pack/plugins/ml/common/types/storage.ts +++ b/x-pack/plugins/ml/common/types/storage.ts @@ -14,6 +14,7 @@ export const ML_GETTING_STARTED_CALLOUT_DISMISSED = 'ml.gettingStarted.isDismiss export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference'; export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels'; export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt'; +export const ML_OVERVIEW_PANELS = 'ml.overviewPanels'; export type PartitionFieldConfig = | { @@ -52,6 +53,12 @@ export interface AnomalyExplorerPanelsState { mainPage: { size: number }; } +export interface OverviewPanelsState { + nodes: boolean; + adJobs: boolean; + dfaJobs: boolean; +} + export interface MlStorageRecord { [key: string]: unknown; [ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig; @@ -60,6 +67,7 @@ export interface MlStorageRecord { [ML_FROZEN_TIER_PREFERENCE]: FrozenTierPreference; [ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined; [ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined; + [ML_OVERVIEW_PANELS]: OverviewPanelsState; } export type MlStorage = Partial | null; @@ -78,6 +86,8 @@ export type TMlStorageMapped = T extends typeof ML_ENTIT ? AnomalyExplorerPanelsState | undefined : T extends typeof ML_NOTIFICATIONS_LAST_CHECKED_AT ? number | undefined + : T extends typeof ML_OVERVIEW_PANELS + ? OverviewPanelsState | undefined : null; export const ML_STORAGE_KEYS = [ @@ -87,4 +97,5 @@ export const ML_STORAGE_KEYS = [ ML_FROZEN_TIER_PREFERENCE, ML_ANOMALY_EXPLORER_PANELS, ML_NOTIFICATIONS_LAST_CHECKED_AT, + ML_OVERVIEW_PANELS, ] as const; diff --git a/x-pack/plugins/ml/public/application/components/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/ml/public/application/components/collapsible_panel/collapsible_panel.tsx new file mode 100644 index 0000000000000..53ed046423c2f --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/collapsible_panel/collapsible_panel.tsx @@ -0,0 +1,137 @@ +/* + * 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 { + EuiBadge, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiSplitPanel, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import React, { type FC } from 'react'; +import { css } from '@emotion/react/dist/emotion-react.cjs'; +import { useCurrentThemeVars } from '../../contexts/kibana'; + +export interface CollapsiblePanelProps { + isOpen: boolean; + onToggle: (isOpen: boolean) => void; + + header: React.ReactElement; + headerItems?: React.ReactElement[]; +} + +export const CollapsiblePanel: FC = ({ + isOpen, + onToggle, + children, + header, + headerItems, +}) => { + const { euiTheme } = useCurrentThemeVars(); + + return ( + + + + + + + { + onToggle(!isOpen); + }} + /> + + + +

{header}

+
+
+
+
+ {headerItems ? ( + + + {headerItems.map((item, i) => { + return ( + +
+ {item} +
+
+ ); + })} +
+
+ ) : null} +
+
+ {isOpen ? ( + + {children} + + ) : null} +
+ ); +}; + +export interface StatEntry { + label: string; + value: number; + 'data-test-subj'?: string; +} + +export interface OverviewStatsBarProps { + inputStats: StatEntry[]; + dataTestSub?: string; +} + +export const OverviewStatsBar: FC = ({ inputStats, dataTestSub }) => { + return ( + + {inputStats.map(({ value, label, 'data-test-subj': dataTestSubjValue }) => { + return ( + + + + {label}: + + + {value} + + + + ); + })} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/collapsible_panel/index.ts b/x-pack/plugins/ml/public/application/components/collapsible_panel/index.ts new file mode 100644 index 0000000000000..d45a251f69ca9 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/collapsible_panel/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { CollapsiblePanel } from './collapsible_panel'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/empty_prompt/empty_prompt.tsx index 23ce92dce1b91..14745812e3045 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/empty_prompt/empty_prompt.tsx @@ -6,16 +6,7 @@ */ import React, { FC } from 'react'; -import { - EuiButton, - EuiCallOut, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiImage, - EuiLink, - EuiTitle, -} from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt, EuiImage, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import dfaImage from './data_frame_analytics_kibana.png'; @@ -26,10 +17,7 @@ import { usePermissionCheck } from '../../../../../capabilities/check_capabiliti export const AnalyticsEmptyPrompt: FC = () => { const { - services: { - docLinks, - http: { basePath }, - }, + services: { docLinks }, } = useMlKibana(); const [canCreateDataFrameAnalytics, canStartStopDataFrameAnalytics] = usePermissionCheck([ @@ -40,7 +28,6 @@ export const AnalyticsEmptyPrompt: FC = () => { const disabled = !mlNodesAvailable() || !canCreateDataFrameAnalytics || !canStartStopDataFrameAnalytics; - const transformsLink = `${basePath.get()}/app/management/data/transform`; const navigateToPath = useNavigateToPath(); const navigateToSourceSelection = async () => { @@ -57,16 +44,15 @@ export const AnalyticsEmptyPrompt: FC = () => { size="fullWidth" src={dfaImage} alt={i18n.translate('xpack.ml.dataFrame.analyticsList.emptyPromptTitle', { - defaultMessage: 'Create your first data frame analytics job', + defaultMessage: 'Analyze your data with data frame analytics', })} /> } - color="subdued" title={

} @@ -78,39 +64,6 @@ export const AnalyticsEmptyPrompt: FC = () => { defaultMessage="Train outlier detection, regression, or classification machine learning models using data frame analytics." />

- - - - ), - 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/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"] } } 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([]); + const [indexPatternOption, setIndexPatternOption] = useState