diff --git a/packages/deeplinks/observability/constants.ts b/packages/deeplinks/observability/constants.ts index a28c9e4aaff21..9f2a28fcef971 100644 --- a/packages/deeplinks/observability/constants.ts +++ b/packages/deeplinks/observability/constants.ts @@ -8,7 +8,7 @@ export const LOGS_APP_ID = 'logs'; -export const OBSERVABILITY_LOG_EXPLORER = 'observability-log-explorer'; +export const OBSERVABILITY_LOG_EXPLORER_APP_ID = 'observability-log-explorer'; export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview'; diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 844d68fdb27cc..831976bcc37ea 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -7,16 +7,16 @@ */ import { + APM_APP_ID, LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, - OBSERVABILITY_OVERVIEW_APP_ID, METRICS_APP_ID, - APM_APP_ID, + OBSERVABILITY_LOG_EXPLORER_APP_ID, OBSERVABILITY_ONBOARDING_APP_ID, + OBSERVABILITY_OVERVIEW_APP_ID, } from './constants'; type LogsApp = typeof LOGS_APP_ID; -type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER; +type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER_APP_ID; type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID; type MetricsApp = typeof METRICS_APP_ID; type ApmApp = typeof APM_APP_ID; diff --git a/packages/deeplinks/observability/index.ts b/packages/deeplinks/observability/index.ts index 81dac13f6b6a0..3d816db884765 100644 --- a/packages/deeplinks/observability/index.ts +++ b/packages/deeplinks/observability/index.ts @@ -7,12 +7,10 @@ */ export { - OBSERVABILITY_ONBOARDING_APP_ID, LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, + OBSERVABILITY_LOG_EXPLORER_APP_ID, + OBSERVABILITY_ONBOARDING_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, } from './constants'; - export type { AppId, DeepLinkId } from './deep_links'; - export * from './locators'; diff --git a/packages/deeplinks/observability/locators/log_explorer.ts b/packages/deeplinks/observability/locators/log_explorer.ts index 752ae3d79bee8..d20c36e73917d 100644 --- a/packages/deeplinks/observability/locators/log_explorer.ts +++ b/packages/deeplinks/observability/locators/log_explorer.ts @@ -14,6 +14,17 @@ export type RefreshInterval = { value: number; }; +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type FilterControls = { + namespace?: ListFilterControl; +}; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type ListFilterControl = { + mode: 'include'; + values: string[]; +}; + export const LOG_EXPLORER_LOCATOR_ID = 'LOG_EXPLORER_LOCATOR'; export interface LogExplorerNavigationParams extends SerializableRecord { @@ -34,13 +45,13 @@ export interface LogExplorerNavigationParams extends SerializableRecord { */ columns?: string[]; /** - * Array of the used sorting [[field,direction],...] + * Optionally apply free-form filters. */ - sort?: string[][]; + filters?: Filter[]; /** - * Optionally apply filters. + * Optionally apply curated filter controls */ - filters?: Filter[]; + filterControls?: FilterControls; } export interface LogExplorerLocatorParams extends LogExplorerNavigationParams { diff --git a/packages/kbn-xstate-utils/src/dev_tools.ts b/packages/kbn-xstate-utils/src/dev_tools.ts index fa16b808b3aec..15b5779677a5b 100644 --- a/packages/kbn-xstate-utils/src/dev_tools.ts +++ b/packages/kbn-xstate-utils/src/dev_tools.ts @@ -6,4 +6,52 @@ * Side Public License, v 1. */ +import { + isArray, + isBoolean, + isDate, + isNil, + isNumber, + isPlainObject, + isString, + mapValues, +} from 'lodash'; + export const isDevMode = () => process.env.NODE_ENV !== 'production'; + +export const getDevToolsOptions = (): boolean | object => + isDevMode() + ? { + actionSanitizer: sanitizeAction, + stateSanitizer: sanitizeState, + } + : false; + +const redactComplexValues = (value: unknown): unknown => { + if (isString(value) || isNumber(value) || isBoolean(value) || isDate(value) || isNil(value)) { + return value; + } + + if (isArray(value)) { + if (value.length > 100) { + return '[redacted large array]'; + } + return value.map(redactComplexValues); + } + + if ((isPlainObject as (v: unknown) => v is object)(value)) { + if (Object.keys(value).length > 100) { + return '[redacted large object]'; + } + return mapValues(value, (innerValue: unknown) => redactComplexValues(innerValue)); + } + + return `[redacted complex value of type ${typeof value}]`; +}; + +const sanitizeAction = redactComplexValues; + +const sanitizeState = (state: Record) => ({ + value: state.value, + context: redactComplexValues(state.context), +}); diff --git a/packages/kbn-xstate-utils/src/index.ts b/packages/kbn-xstate-utils/src/index.ts index 2cf5853db6e08..02cd8a2b176b1 100644 --- a/packages/kbn-xstate-utils/src/index.ts +++ b/packages/kbn-xstate-utils/src/index.ts @@ -7,6 +7,6 @@ */ export * from './actions'; +export * from './dev_tools'; export * from './notification_channel'; export * from './types'; -export * from './dev_tools'; diff --git a/src/plugins/discover/public/application/main/discover_main_route.tsx b/src/plugins/discover/public/application/main/discover_main_route.tsx index 1bb24661da0cb..2a2575a773524 100644 --- a/src/plugins/discover/public/application/main/discover_main_route.tsx +++ b/src/plugins/discover/public/application/main/discover_main_route.tsx @@ -9,7 +9,11 @@ import React, { useEffect, useState, memo, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import type { DataView } from '@kbn/data-views-plugin/public'; -import { redirectWhenMissing, SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; +import { + type IKbnUrlStateStorage, + redirectWhenMissing, + SavedObjectNotFound, +} from '@kbn/kibana-utils-plugin/public'; import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { AnalyticsNoDataPageKibanaProvider, @@ -46,6 +50,7 @@ interface DiscoverLandingParams { export interface MainRouteProps { customizationCallbacks: CustomizationCallback[]; + stateStorageContainer?: IKbnUrlStateStorage; isDev: boolean; customizationContext: DiscoverCustomizationContext; } @@ -53,6 +58,7 @@ export interface MainRouteProps { export function DiscoverMainRoute({ customizationCallbacks, customizationContext, + stateStorageContainer, }: MainRouteProps) { const history = useHistory(); const services = useDiscoverServices(); @@ -70,6 +76,7 @@ export function DiscoverMainRoute({ history, services, customizationContext, + stateStorageContainer, }) ); const { customizationService, isInitialized: isCustomizationServiceInitialized } = diff --git a/src/plugins/discover/public/application/main/services/discover_state.test.ts b/src/plugins/discover/public/application/main/services/discover_state.test.ts index a14a7d3b797c6..2cf1e2c71880d 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.test.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.test.ts @@ -11,7 +11,7 @@ import { DiscoverStateContainer, createSearchSessionRestorationDataProvider, } from './discover_state'; -import { createBrowserHistory, History } from 'history'; +import { createBrowserHistory, createMemoryHistory, History } from 'history'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public'; import { @@ -27,6 +27,7 @@ import { waitFor } from '@testing-library/react'; import { DiscoverCustomizationContext, FetchStatus } from '../../types'; import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex'; import { copySavedSearch } from './discover_saved_search_container'; +import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; const startSync = (appState: DiscoverAppStateContainer) => { const { start, stop } = appState.syncState(); @@ -151,6 +152,68 @@ describe('Test discover state', () => { expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!t,value:5000))'); }); }); + +describe('Test discover state with overridden state storage', () => { + let stopSync = () => {}; + let history: History; + let stateStorage: IKbnUrlStateStorage; + let state: DiscoverStateContainer; + + beforeEach(async () => { + jest.useFakeTimers(); + history = createMemoryHistory({ + initialEntries: [ + { + pathname: '/', + hash: `?_a=()`, + }, + ], + }); + stateStorage = createKbnUrlStateStorage({ + history, + useHash: false, + useHashQuery: true, + }); + state = getDiscoverStateContainer({ + services: discoverServiceMock, + history, + customizationContext, + stateStorageContainer: stateStorage, + }); + state.savedSearchState.set(savedSearchMock); + state.appState.update({}, true); + stopSync = startSync(state.appState); + }); + + afterEach(() => { + stopSync(); + stopSync = () => {}; + jest.useRealTimers(); + }); + + test('setting app state and syncing to URL', async () => { + state.appState.update({ index: 'modified' }); + + await jest.runAllTimersAsync(); + + expect(history.createHref(history.location)).toMatchInlineSnapshot( + `"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"` + ); + }); + + test('changing URL to be propagated to appState', async () => { + history.push('/#?_a=(index:modified)'); + + await jest.runAllTimersAsync(); + + expect(state.appState.getState()).toMatchInlineSnapshot(` + Object { + "index": "modified", + } + `); + }); +}); + describe('Test discover initial state sort handling', () => { test('Non-empty sort in URL should not be overwritten by saved search sort', async () => { const savedSearch = { diff --git a/src/plugins/discover/public/application/main/services/discover_state.ts b/src/plugins/discover/public/application/main/services/discover_state.ts index 5e4f8a30199ed..1dc58643ebdc0 100644 --- a/src/plugins/discover/public/application/main/services/discover_state.ts +++ b/src/plugins/discover/public/application/main/services/discover_state.ts @@ -71,6 +71,10 @@ interface DiscoverStateContainerParams { * Context object for customization related properties */ customizationContext: DiscoverCustomizationContext; + /** + * a custom url state storage + */ + stateStorageContainer?: IKbnUrlStateStorage; } export interface LoadParams { @@ -204,6 +208,7 @@ export function getDiscoverStateContainer({ history, services, customizationContext, + stateStorageContainer, }: DiscoverStateContainerParams): DiscoverStateContainer { const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage'); const toasts = services.core.notifications.toasts; @@ -211,12 +216,14 @@ export function getDiscoverStateContainer({ /** * state storage for state in the URL */ - const stateStorage = createKbnUrlStateStorage({ - useHash: storeInSessionStorage, - history, - useHashQuery: customizationContext.displayMode !== 'embedded', - ...(toasts && withNotifyOnErrors(toasts)), - }); + const stateStorage = + stateStorageContainer ?? + createKbnUrlStateStorage({ + useHash: storeInSessionStorage, + history, + useHashQuery: customizationContext.displayMode !== 'embedded', + ...(toasts && withNotifyOnErrors(toasts)), + }); /** * Search session logic diff --git a/src/plugins/discover/public/components/discover_container/discover_container.tsx b/src/plugins/discover/public/components/discover_container/discover_container.tsx index 43426c83c7730..51fdbd0d3432d 100644 --- a/src/plugins/discover/public/components/discover_container/discover_container.tsx +++ b/src/plugins/discover/public/components/discover_container/discover_container.tsx @@ -11,6 +11,7 @@ import type { ScopedHistory } from '@kbn/core/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import React, { useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; +import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import { DiscoverMainRoute } from '../../application/main'; import type { DiscoverServices } from '../../build_services'; import type { CustomizationCallback } from '../../customizations'; @@ -29,6 +30,7 @@ export interface DiscoverContainerInternalProps { getDiscoverServices: () => Promise; scopedHistory: ScopedHistory; customizationCallbacks: CustomizationCallback[]; + stateStorageContainer?: IKbnUrlStateStorage; isDev: boolean; isLoading?: boolean; } @@ -55,6 +57,7 @@ export const DiscoverContainerInternal = ({ customizationCallbacks, isDev, getDiscoverServices, + stateStorageContainer, isLoading = false, }: DiscoverContainerInternalProps) => { const [discoverServices, setDiscoverServices] = useState(); @@ -97,6 +100,7 @@ export const DiscoverContainerInternal = ({ diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts index a7b72cb863f71..f3d7a0e0d1f15 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.test.ts @@ -202,7 +202,7 @@ describe('kbn_url_storage', () => { await Promise.all([pr1, pr2, pr3]); expect(getCurrentUrl()).toBe('/3'); - expect(urlControls.getPendingUrl()).toBeUndefined(); + expect(urlControls.getPendingUrl()).toEqual(getCurrentUrl()); }); }); diff --git a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts index b81d3c1b81b63..9a4ed8e704b09 100644 --- a/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts +++ b/src/plugins/kibana_utils/public/state_management/url/kbn_url_storage.ts @@ -193,17 +193,17 @@ export const createKbnUrlControls = ( // runs scheduled url updates function flush(replace = shouldReplace) { - const nextUrl = getPendingUrl(); - - if (!nextUrl) return; + if (updateQueue.length === 0) { + return; + } + const nextUrl = getPendingUrl(); cleanUp(); const newUrl = updateUrl(nextUrl, replace); return newUrl; } function getPendingUrl() { - if (updateQueue.length === 0) return undefined; const resultUrl = updateQueue.reduce( (url, nextUpdate) => nextUpdate(url) ?? url, getCurrentUrl(history) diff --git a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts index 6ad69238608c6..d823a95da59ce 100644 --- a/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts +++ b/src/plugins/kibana_utils/public/state_sync/state_sync_state_storage/create_kbn_url_state_storage.ts @@ -116,7 +116,11 @@ export const createKbnUrlStateStorage = ( unlisten(); }; }).pipe( - map(() => getStateFromKbnUrl(key, undefined, { getFromHashQuery: useHashQuery })), + map(() => + getStateFromKbnUrl(key, history?.createHref(history.location), { + getFromHashQuery: useHashQuery, + }) + ), catchError((error) => { if (onGetErrorThrottled) onGetErrorThrottled(error); return of(null); diff --git a/x-pack/plugins/log_explorer/common/constants.ts b/x-pack/plugins/log_explorer/common/constants.ts index 5aeb491b5a9d2..a73f304a76a5f 100644 --- a/x-pack/plugins/log_explorer/common/constants.ts +++ b/x-pack/plugins/log_explorer/common/constants.ts @@ -32,8 +32,17 @@ export const DATA_GRID_COLUMN_WIDTH_SMALL = 240; export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320; // UI preferences -export const DATA_GRID_DEFAULT_COLUMNS = [SERVICE_NAME_FIELD, HOST_NAME_FIELD, MESSAGE_FIELD]; -export const DATA_GRID_COLUMNS_PREFERENCES = { - [HOST_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_MEDIUM }, - [SERVICE_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_SMALL }, -}; +export const DEFAULT_COLUMNS = [ + { + field: SERVICE_NAME_FIELD, + width: DATA_GRID_COLUMN_WIDTH_SMALL, + }, + { + field: HOST_NAME_FIELD, + width: DATA_GRID_COLUMN_WIDTH_MEDIUM, + }, + { + field: MESSAGE_FIELD, + }, +]; +export const DEFAULT_ROWS_PER_PAGE = 100; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts b/x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts similarity index 69% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts rename to x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts index 2a058e2dde017..865b225585486 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/defaults.ts +++ b/x-pack/plugins/log_explorer/common/control_panels/available_control_panels.ts @@ -5,18 +5,13 @@ * 2.0. */ -import { AllDatasetSelection } from '../../../../common/dataset_selection'; -import { ControlPanels, DefaultLogExplorerProfileState } from './types'; - -export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = { - datasetSelection: AllDatasetSelection.create(), -}; - -export const CONTROL_PANELS_URL_KEY = 'controlPanels'; +import { ControlPanels } from './types'; export const availableControlsPanels = { NAMESPACE: 'data_stream.namespace', -}; +} as const; + +export type AvailableControlPanels = typeof availableControlsPanels; export const controlPanelConfigs: ControlPanels = { [availableControlsPanels.NAMESPACE]: { diff --git a/x-pack/plugins/log_explorer/common/control_panels/index.ts b/x-pack/plugins/log_explorer/common/control_panels/index.ts new file mode 100644 index 0000000000000..6ea70a7fd630d --- /dev/null +++ b/x-pack/plugins/log_explorer/common/control_panels/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './available_control_panels'; +export * from './types'; diff --git a/x-pack/plugins/log_explorer/common/control_panels/types.ts b/x-pack/plugins/log_explorer/common/control_panels/types.ts new file mode 100644 index 0000000000000..22381932d34b6 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/control_panels/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import * as rt from 'io-ts'; + +const PanelRT = rt.type({ + order: rt.number, + width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]), + grow: rt.boolean, + type: rt.string, + explicitInput: rt.intersection([ + rt.type({ id: rt.string }), + rt.partial({ + dataViewId: rt.string, + exclude: rt.boolean, + existsSelected: rt.boolean, + fieldName: rt.string, + selectedOptions: rt.array(rt.string), + title: rt.union([rt.string, rt.undefined]), + }), + ]), +}); + +export const ControlPanelRT = rt.record(rt.string, PanelRT); + +export type ControlPanels = rt.TypeOf; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts index c505a07da7768..8b8ab4e1ea241 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/all_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy } from './types'; export class AllDatasetSelection implements DatasetSelectionStrategy { @@ -23,18 +22,13 @@ export class AllDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, - }); + }; } public static create() { diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts b/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts deleted file mode 100644 index d88d939858d0e..0000000000000 --- a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IndexPattern } from '@kbn/io-ts-utils'; -import { encodeDatasetSelection, decodeDatasetSelectionId } from './encoding'; -import { DatasetEncodingError } from './errors'; -import { DatasetSelectionPlain } from './types'; - -describe('DatasetSelection', () => { - const allDatasetSelectionPlain: DatasetSelectionPlain = { - selectionType: 'all', - }; - const encodedAllDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA'; - - const singleDatasetSelectionPlain: DatasetSelectionPlain = { - selectionType: 'single', - selection: { - name: 'azure', - version: '1.5.23', - dataset: { - name: 'logs-azure.activitylogs-*' as IndexPattern, - title: 'activitylogs', - }, - }, - }; - const encodedSingleDatasetSelection = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu0m8wrEEjTkAjBwCsHAEwBmcuvBQeKACqCADmSPJqUVUA=='; - - const invalidDatasetSelectionPlain = { - selectionType: 'single', - selection: { - dataset: { - // Missing mandatory `name` property - title: 'activitylogs', - }, - }, - }; - const invalidCompressedId = 'random'; - const invalidEncodedDatasetSelection = 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4T2QHMoBKIA=='; - - describe('#encodeDatasetSelection', () => { - test('should encode and compress a valid DatasetSelection plain object', () => { - // Encode AllDatasetSelection plain object - expect(encodeDatasetSelection(allDatasetSelectionPlain)).toEqual(encodedAllDatasetSelection); - // Encode SingleDatasetSelection plain object - expect(encodeDatasetSelection(singleDatasetSelectionPlain)).toEqual( - encodedSingleDatasetSelection - ); - }); - - test('should throw a DatasetEncodingError if the input is an invalid DatasetSelection plain object', () => { - const encodingRunner = () => - encodeDatasetSelection(invalidDatasetSelectionPlain as DatasetSelectionPlain); - - expect(encodingRunner).toThrow(DatasetEncodingError); - expect(encodingRunner).toThrow(/^The current dataset selection is invalid/); - }); - }); - - describe('#decodeDatasetSelectionId', () => { - test('should decode and decompress a valid encoded string', () => { - // Decode AllDatasetSelection plain object - expect(decodeDatasetSelectionId(encodedAllDatasetSelection)).toEqual( - allDatasetSelectionPlain - ); - // Decode SingleDatasetSelection plain object - expect(decodeDatasetSelectionId(encodedSingleDatasetSelection)).toEqual( - singleDatasetSelectionPlain - ); - }); - - test('should throw a DatasetEncodingError if the input is an invalid compressed id', () => { - expect(() => decodeDatasetSelectionId(invalidCompressedId)).toThrow( - new DatasetEncodingError('The stored id is not a valid compressed value.') - ); - }); - - test('should throw a DatasetEncodingError if the decompressed value is an invalid DatasetSelection plain object', () => { - const decodingRunner = () => decodeDatasetSelectionId(invalidEncodedDatasetSelection); - - expect(decodingRunner).toThrow(DatasetEncodingError); - expect(decodingRunner).toThrow(/^The current dataset selection is invalid/); - }); - }); - - test('encoding and decoding should restore the original DatasetSelection plain object', () => { - // Encode/Decode AllDatasetSelection plain object - expect(decodeDatasetSelectionId(encodeDatasetSelection(allDatasetSelectionPlain))).toEqual( - allDatasetSelectionPlain - ); - // Encode/Decode SingleDatasetSelection plain object - expect(decodeDatasetSelectionId(encodeDatasetSelection(singleDatasetSelectionPlain))).toEqual( - singleDatasetSelectionPlain - ); - }); -}); diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts b/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts deleted file mode 100644 index 83a7c5357fde5..0000000000000 --- a/x-pack/plugins/log_explorer/common/dataset_selection/encoding.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { decode, encode, RisonValue } from '@kbn/rison'; -import * as lz from 'lz-string'; -import { decodeOrThrow } from '../runtime_types'; -import { DatasetEncodingError } from './errors'; -import { DatasetSelectionPlain, datasetSelectionPlainRT } from './types'; - -export const encodeDatasetSelection = (datasetSelectionPlain: DatasetSelectionPlain) => { - const safeDatasetSelection = decodeOrThrow( - datasetSelectionPlainRT, - (message: string) => - new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`) - )(datasetSelectionPlain); - - return lz.compressToBase64(encode(safeDatasetSelection)); -}; - -export const decodeDatasetSelectionId = (datasetSelectionId: string): DatasetSelectionPlain => { - const risonDatasetSelection: RisonValue = lz.decompressFromBase64(datasetSelectionId); - - if (risonDatasetSelection === null || risonDatasetSelection === '') { - throw new DatasetEncodingError('The stored id is not a valid compressed value.'); - } - - const decodedDatasetSelection = decode(risonDatasetSelection); - - const datasetSelection = decodeOrThrow( - datasetSelectionPlainRT, - (message: string) => - new DatasetEncodingError(`The current dataset selection is invalid: ${message}"`) - )(decodedDatasetSelection); - - return datasetSelection; -}; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts b/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts index 43faebc618140..f881e90723e14 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/hydrate_dataset_selection.ts.ts @@ -13,11 +13,9 @@ import { UnresolvedDatasetSelection } from './unresolved_dataset_selection'; export const hydrateDatasetSelection = (datasetSelection: DatasetSelectionPlain) => { if (datasetSelection.selectionType === 'all') { return AllDatasetSelection.create(); - } - if (datasetSelection.selectionType === 'single') { + } else if (datasetSelection.selectionType === 'single') { return SingleDatasetSelection.fromSelection(datasetSelection.selection); - } - if (datasetSelection.selectionType === 'unresolved') { + } else { return UnresolvedDatasetSelection.fromSelection(datasetSelection.selection); } }; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts index f390f7a89f87c..26fd974d0f0ac 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/index.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/index.ts @@ -28,7 +28,6 @@ export const isDatasetSelection = (input: any): input is DatasetSelection => { export * from './all_dataset_selection'; export * from './single_dataset_selection'; export * from './unresolved_dataset_selection'; -export * from './encoding'; export * from './errors'; export * from './hydrate_dataset_selection.ts'; export * from './types'; diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts index 21c788579ed70..6667dd55f3abe 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/single_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types'; export class SingleDatasetSelection implements DatasetSelectionStrategy { @@ -29,16 +28,11 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, selection: { name: this.selection.name, @@ -46,7 +40,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy { version: this.selection.version, dataset: this.selection.dataset.toPlain(), }, - }); + }; } public static fromSelection(selection: SingleDatasetSelectionPayload) { diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/types.ts b/x-pack/plugins/log_explorer/common/dataset_selection/types.ts index 239bbc1108a29..db3638aff6331 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/types.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/types.ts @@ -62,7 +62,11 @@ export type UnresolvedDatasetSelectionPayload = rt.TypeOf< >; export type DatasetSelectionPlain = rt.TypeOf; +export type DataViewSpecWithId = DataViewSpec & { + id: string; +}; + export interface DatasetSelectionStrategy { - toDataviewSpec(): DataViewSpec; - toURLSelectionId(): string; + toDataviewSpec(): DataViewSpecWithId; + toPlainSelection(): DatasetSelectionPlain; } diff --git a/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts b/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts index acfd5180f0ed3..e534403fab617 100644 --- a/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/common/dataset_selection/unresolved_dataset_selection.ts @@ -6,7 +6,6 @@ */ import { Dataset } from '../datasets'; -import { encodeDatasetSelection } from './encoding'; import { DatasetSelectionStrategy, UnresolvedDatasetSelectionPayload } from './types'; export class UnresolvedDatasetSelection implements DatasetSelectionStrategy { @@ -25,22 +24,17 @@ export class UnresolvedDatasetSelection implements DatasetSelectionStrategy { } toDataviewSpec() { - const { name, title } = this.selection.dataset.toDataviewSpec(); - return { - id: this.toURLSelectionId(), - name, - title, - }; + return this.selection.dataset.toDataviewSpec(); } - toURLSelectionId() { - return encodeDatasetSelection({ + toPlainSelection() { + return { selectionType: this.selectionType, selection: { name: this.selection.name, dataset: this.selection.dataset.toPlain(), }, - }); + }; } public static fromSelection(selection: UnresolvedDatasetSelectionPayload) { diff --git a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts index 6c18f62350e41..18545b24754d8 100644 --- a/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts +++ b/x-pack/plugins/log_explorer/common/datasets/models/dataset.ts @@ -6,9 +6,9 @@ */ import { IconType } from '@elastic/eui'; -import { DataViewSpec } from '@kbn/data-views-plugin/common'; import { IndexPattern } from '@kbn/io-ts-utils'; import { TIMESTAMP_FIELD } from '../../constants'; +import { DataViewSpecWithId } from '../../dataset_selection'; import { DatasetId, DatasetType, IntegrationType } from '../types'; type IntegrationBase = Partial>; @@ -49,7 +49,7 @@ export class Dataset { return `${type}-${dataset}-*` as IndexPattern; } - toDataviewSpec(): DataViewSpec { + toDataviewSpec(): DataViewSpecWithId { // Invert the property because the API returns the index pattern as `name` and a readable name as `title` return { id: this.id, diff --git a/x-pack/plugins/log_explorer/common/display_options/index.ts b/x-pack/plugins/log_explorer/common/display_options/index.ts new file mode 100644 index 0000000000000..6cc0ccaa93a6d --- /dev/null +++ b/x-pack/plugins/log_explorer/common/display_options/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 * from './types'; diff --git a/x-pack/plugins/log_explorer/common/display_options/types.ts b/x-pack/plugins/log_explorer/common/display_options/types.ts new file mode 100644 index 0000000000000..b4c482d088a54 --- /dev/null +++ b/x-pack/plugins/log_explorer/common/display_options/types.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ChartDisplayOptions { + breakdownField: string | null; +} + +export type PartialChartDisplayOptions = Partial; + +export interface GridColumnDisplayOptions { + field: string; + width?: number; +} + +export interface GridRowsDisplayOptions { + rowHeight: number; + rowsPerPage: number; +} + +export type PartialGridRowsDisplayOptions = Partial; + +export interface GridDisplayOptions { + columns: GridColumnDisplayOptions[]; + rows: GridRowsDisplayOptions; +} + +export type PartialGridDisplayOptions = Partial< + Omit & { rows?: PartialGridRowsDisplayOptions } +>; + +export interface DisplayOptions { + grid: GridDisplayOptions; + chart: ChartDisplayOptions; +} + +export interface PartialDisplayOptions { + grid?: PartialGridDisplayOptions; + chart?: PartialChartDisplayOptions; +} diff --git a/x-pack/plugins/log_explorer/common/index.ts b/x-pack/plugins/log_explorer/common/index.ts index 989f981879ac0..5466a00ae0caa 100644 --- a/x-pack/plugins/log_explorer/common/index.ts +++ b/x-pack/plugins/log_explorer/common/index.ts @@ -5,4 +5,28 @@ * 2.0. */ -export { AllDatasetSelection, UnresolvedDatasetSelection } from './dataset_selection'; +export { + availableControlPanelFields, + availableControlsPanels, + controlPanelConfigs, + ControlPanelRT, +} from './control_panels'; +export type { AvailableControlPanels, ControlPanels } from './control_panels'; +export { + AllDatasetSelection, + datasetSelectionPlainRT, + hydrateDatasetSelection, + UnresolvedDatasetSelection, +} from './dataset_selection'; +export type { DatasetSelectionPlain } from './dataset_selection'; +export type { + ChartDisplayOptions, + DisplayOptions, + GridColumnDisplayOptions, + GridDisplayOptions, + GridRowsDisplayOptions, + PartialChartDisplayOptions, + PartialDisplayOptions, + PartialGridDisplayOptions, + PartialGridRowsDisplayOptions, +} from './display_options'; diff --git a/x-pack/plugins/log_explorer/kibana.jsonc b/x-pack/plugins/log_explorer/kibana.jsonc index 71781ca9cada3..9275751ca1898 100644 --- a/x-pack/plugins/log_explorer/kibana.jsonc +++ b/x-pack/plugins/log_explorer/kibana.jsonc @@ -12,16 +12,18 @@ "logExplorer" ], "requiredPlugins": [ + "controls", "data", "dataViews", "discover", + "embeddable", "fieldFormats", "fleet", "kibanaReact", "kibanaUtils", - "controls", - "embeddable", + "navigation", "share", + "unifiedSearch" ], "optionalPlugins": [], "requiredBundles": [], diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx index 8f1da5cbfc18d..8be2f2ec6b03b 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/flyout_detail.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { FlyoutProps, LogDocument } from './types'; +import { LogExplorerFlyoutContentProps, LogDocument } from './types'; import { useDocDetail } from './use_doc_detail'; import { FlyoutHeader } from './flyout_header'; import { FlyoutHighlights } from './flyout_highlights'; @@ -16,7 +16,7 @@ export function FlyoutDetail({ dataView, doc, actions, -}: Pick) { +}: Pick) { const parsedDoc = useDocDetail(doc as LogDocument, { dataView }); return ( diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts index 958cc1a3351f0..d88d0d1c9ad8a 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/types.ts @@ -5,55 +5,4 @@ * 2.0. */ -import type { DataView } from '@kbn/data-views-plugin/common'; -import type { FlyoutContentProps } from '@kbn/discover-plugin/public'; -import type { DataTableRecord } from '@kbn/discover-utils/types'; - -export interface FlyoutProps extends FlyoutContentProps { - dataView: DataView; - doc: LogDocument; -} - -export interface LogDocument extends DataTableRecord { - flattened: { - '@timestamp': string; - 'log.level'?: [string]; - message?: [string]; - - 'host.name'?: string; - 'service.name'?: string; - 'trace.id'?: string; - 'agent.name'?: string; - 'orchestrator.cluster.name'?: string; - 'orchestrator.resource.id'?: string; - 'cloud.provider'?: string; - 'cloud.region'?: string; - 'cloud.availability_zone'?: string; - 'cloud.project.id'?: string; - 'cloud.instance.id'?: string; - 'log.file.path'?: string; - 'data_stream.namespace': string; - 'data_stream.dataset': string; - }; -} - -export interface FlyoutDoc { - '@timestamp': string; - 'log.level'?: string; - message?: string; - - 'host.name'?: string; - 'service.name'?: string; - 'trace.id'?: string; - 'agent.name'?: string; - 'orchestrator.cluster.name'?: string; - 'orchestrator.resource.id'?: string; - 'cloud.provider'?: string; - 'cloud.region'?: string; - 'cloud.availability_zone'?: string; - 'cloud.project.id'?: string; - 'cloud.instance.id'?: string; - 'log.file.path'?: string; - 'data_stream.namespace': string; - 'data_stream.dataset': string; -} +export type { FlyoutDoc, LogDocument, LogExplorerFlyoutContentProps } from '../../controller'; diff --git a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts index b07802e5fb946..2a6baca186ae3 100644 --- a/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts +++ b/x-pack/plugins/log_explorer/public/components/flyout_detail/use_doc_detail.ts @@ -7,11 +7,11 @@ import { formatFieldValue } from '@kbn/discover-utils'; import * as constants from '../../../common/constants'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { FlyoutDoc, FlyoutProps, LogDocument } from './types'; +import { FlyoutDoc, LogExplorerFlyoutContentProps, LogDocument } from './types'; export function useDocDetail( doc: LogDocument, - { dataView }: Pick + { dataView }: Pick ): FlyoutDoc { const { services } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx index 57736dd4b96dd..638ca847f6549 100644 --- a/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx +++ b/x-pack/plugins/log_explorer/public/components/log_explorer/log_explorer.tsx @@ -5,100 +5,43 @@ * 2.0. */ +import type { ScopedHistory } from '@kbn/core-application-browser'; +import type { CoreStart } from '@kbn/core/public'; import React, { useMemo } from 'react'; -import { ScopedHistory } from '@kbn/core-application-browser'; -import { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DiscoverAppState } from '@kbn/discover-plugin/public'; -import type { BehaviorSubject } from 'rxjs'; -import { CoreStart } from '@kbn/core/public'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import { HIDE_ANNOUNCEMENTS } from '@kbn/discover-utils'; +import type { LogExplorerController } from '../../controller'; import { createLogExplorerProfileCustomizations } from '../../customizations/log_explorer_profile'; -import { createPropertyGetProxy } from '../../utils/proxies'; -import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile'; import { LogExplorerStartDeps } from '../../types'; -import { LogExplorerCustomizations } from './types'; export interface CreateLogExplorerArgs { core: CoreStart; plugins: LogExplorerStartDeps; } -export interface LogExplorerStateContainer { - appState?: DiscoverAppState; - logExplorerState?: Partial; -} - export interface LogExplorerProps { - customizations?: LogExplorerCustomizations; scopedHistory: ScopedHistory; - state$?: BehaviorSubject; + controller: LogExplorerController; } export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => { const { - data, discover: { DiscoverContainer }, } = plugins; - const overrideServices = { - data: createDataServiceProxy(data), - uiSettings: createUiSettingsServiceProxy(core.uiSettings), - }; - - return ({ customizations = {}, scopedHistory, state$ }: LogExplorerProps) => { + return ({ scopedHistory, controller }: LogExplorerProps) => { const logExplorerCustomizations = useMemo( - () => [createLogExplorerProfileCustomizations({ core, customizations, plugins, state$ })], - [customizations, state$] + () => [createLogExplorerProfileCustomizations({ controller, core, plugins })], + [controller] ); + const { urlStateStorage, ...overrideServices } = controller.discoverServices; + return ( ); }; }; - -/** - * Create proxy for the data service, in which session service enablement calls - * are no-ops. - */ -const createDataServiceProxy = (data: DataPublicPluginStart) => { - const noOpEnableStorage = () => {}; - - const sessionServiceProxy = createPropertyGetProxy(data.search.session, { - enableStorage: () => noOpEnableStorage, - }); - - const searchServiceProxy = createPropertyGetProxy(data.search, { - session: () => sessionServiceProxy, - }); - - return createPropertyGetProxy(data, { - search: () => searchServiceProxy, - }); -}; -/** - * Create proxy for the uiSettings service, in which settings preferences are overwritten - * with custom values - */ -const createUiSettingsServiceProxy = (uiSettings: IUiSettingsClient) => { - const overrides: Record = { - [HIDE_ANNOUNCEMENTS]: true, - }; - - return createPropertyGetProxy(uiSettings, { - get: - () => - (key, ...args) => { - if (key in overrides) { - return overrides[key]; - } - - return uiSettings.get(key, ...args); - }, - }); -}; diff --git a/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts b/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts deleted file mode 100644 index 2b366cce7c55c..0000000000000 --- a/x-pack/plugins/log_explorer/public/components/log_explorer/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DataTableRecord } from '@kbn/discover-utils/types'; - -export type RenderPreviousContent = () => React.ReactNode; - -export interface LogExplorerFlyoutContentProps { - doc: DataTableRecord; -} - -export type FlyoutRenderContent = ( - renderPreviousContent: RenderPreviousContent, - props: LogExplorerFlyoutContentProps -) => React.ReactNode; - -export interface LogExplorerCustomizations { - flyout?: { - renderContent?: FlyoutRenderContent; - }; -} diff --git a/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts b/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts new file mode 100644 index 0000000000000..97f0cb4ff8b23 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/controller_customizations.ts @@ -0,0 +1,71 @@ +/* + * 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 { DataView } from '@kbn/data-views-plugin/common'; +import { FlyoutContentProps as DiscoverFlyoutContentProps } from '@kbn/discover-plugin/public'; +import type { DataTableRecord } from '@kbn/discover-utils/types'; + +export interface LogExplorerCustomizations { + flyout?: { + renderContent?: RenderContentCustomization; + }; +} + +export interface LogExplorerFlyoutContentProps extends DiscoverFlyoutContentProps { + dataView: DataView; + doc: LogDocument; +} + +export interface LogDocument extends DataTableRecord { + flattened: { + '@timestamp': string; + 'log.level'?: [string]; + message?: [string]; + + 'host.name'?: string; + 'service.name'?: string; + 'trace.id'?: string; + 'agent.name'?: string; + 'orchestrator.cluster.name'?: string; + 'orchestrator.resource.id'?: string; + 'cloud.provider'?: string; + 'cloud.region'?: string; + 'cloud.availability_zone'?: string; + 'cloud.project.id'?: string; + 'cloud.instance.id'?: string; + 'log.file.path'?: string; + 'data_stream.namespace': string; + 'data_stream.dataset': string; + }; +} + +export interface FlyoutDoc { + '@timestamp': string; + 'log.level'?: string; + message?: string; + + 'host.name'?: string; + 'service.name'?: string; + 'trace.id'?: string; + 'agent.name'?: string; + 'orchestrator.cluster.name'?: string; + 'orchestrator.resource.id'?: string; + 'cloud.provider'?: string; + 'cloud.region'?: string; + 'cloud.availability_zone'?: string; + 'cloud.project.id'?: string; + 'cloud.instance.id'?: string; + 'log.file.path'?: string; + 'data_stream.namespace': string; + 'data_stream.dataset': string; +} + +export type RenderContentCustomization = ( + renderPreviousContent: RenderPreviousContent +) => (props: Props) => React.ReactNode; + +export type RenderPreviousContent = (props: Props) => React.ReactNode; diff --git a/x-pack/plugins/log_explorer/public/controller/create_controller.ts b/x-pack/plugins/log_explorer/public/controller/create_controller.ts new file mode 100644 index 0000000000000..53260aeb97281 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/create_controller.ts @@ -0,0 +1,99 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; +import { getDevToolsOptions } from '@kbn/xstate-utils'; +import equal from 'fast-deep-equal'; +import { distinctUntilChanged, EMPTY, from, map, shareReplay } from 'rxjs'; +import { interpret } from 'xstate'; +import { DatasetsService } from '../services/datasets'; +import { createLogExplorerControllerStateMachine } from '../state_machines/log_explorer_controller'; +import { LogExplorerStartDeps } from '../types'; +import { LogExplorerCustomizations } from './controller_customizations'; +import { createDataServiceProxy } from './custom_data_service'; +import { createUiSettingsServiceProxy } from './custom_ui_settings_service'; +import { + createDiscoverMemoryHistory, + createMemoryUrlStateStorage, +} from './custom_url_state_storage'; +import { getContextFromPublicState, getPublicStateFromContext } from './public_state'; +import { + LogExplorerController, + LogExplorerDiscoverServices, + LogExplorerPublicStateUpdate, +} from './types'; + +interface Dependencies { + core: CoreStart; + plugins: LogExplorerStartDeps; +} + +type InitialState = LogExplorerPublicStateUpdate; + +export const createLogExplorerControllerFactory = + ({ core, plugins: { data } }: Dependencies) => + async ({ + customizations = {}, + initialState, + }: { + customizations?: LogExplorerCustomizations; + initialState?: InitialState; + }): Promise => { + const datasetsClient = new DatasetsService().start({ + http: core.http, + }).client; + + const customMemoryHistory = createDiscoverMemoryHistory(); + const customMemoryUrlStateStorage = createMemoryUrlStateStorage(customMemoryHistory); + const customUiSettings = createUiSettingsServiceProxy(core.uiSettings); + const customData = createDataServiceProxy({ + data, + http: core.http, + uiSettings: customUiSettings, + }); + const discoverServices: LogExplorerDiscoverServices = { + data: customData, + history: () => customMemoryHistory, + uiSettings: customUiSettings, + filterManager: customData.query.filterManager, + timefilter: customData.query.timefilter.timefilter, + urlStateStorage: customMemoryUrlStateStorage, + }; + + const initialContext = getContextFromPublicState(initialState ?? {}); + + const machine = createLogExplorerControllerStateMachine({ + datasetsClient, + initialContext, + query: discoverServices.data.query, + toasts: core.notifications.toasts, + }); + + const service = interpret(machine, { + devTools: getDevToolsOptions(), + }); + + const logExplorerState$ = from(service).pipe( + map(({ context }) => getPublicStateFromContext(context)), + distinctUntilChanged(equal), + shareReplay(1) + ); + + return { + actions: {}, + customizations, + datasetsClient, + discoverServices, + event$: EMPTY, + service, + state$: logExplorerState$, + stateMachine: machine, + }; + }; + +export type CreateLogExplorerControllerFactory = typeof createLogExplorerControllerFactory; +export type CreateLogExplorerController = ReturnType; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts b/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts new file mode 100644 index 0000000000000..790fbaa04df86 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_data_service.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpStart } from '@kbn/core-http-browser'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { DataPublicPluginStart, NowProvider, QueryService } from '@kbn/data-plugin/public'; +import { Storage } from '@kbn/kibana-utils-plugin/public'; +import { createPropertyGetProxy } from '../utils/proxies'; + +/** + * Create proxy for the data service, in which session service enablement calls + * are no-ops. + */ +export const createDataServiceProxy = ({ + data, + http, + uiSettings, +}: { + data: DataPublicPluginStart; + http: HttpStart; + uiSettings: IUiSettingsClient; +}) => { + /** + * search session + */ + const noOpEnableStorage = () => {}; + + const sessionServiceProxy = createPropertyGetProxy(data.search.session, { + enableStorage: () => noOpEnableStorage, + }); + + const searchServiceProxy = createPropertyGetProxy(data.search, { + session: () => sessionServiceProxy, + }); + + /** + * query + */ + const customStorage = new Storage(localStorage); + const customQueryService = new QueryService(); + customQueryService.setup({ + nowProvider: new NowProvider(), + storage: customStorage, + uiSettings, + }); + const customQuery = customQueryService.start({ + http, + storage: customStorage, + uiSettings, + }); + + /** + * combined + */ + return createPropertyGetProxy(data, { + query: () => customQuery, + search: () => searchServiceProxy, + }); +}; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.ts b/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.ts new file mode 100644 index 0000000000000..bd247b91ba1f5 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_ui_settings_service.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { HIDE_ANNOUNCEMENTS, MODIFY_COLUMNS_ON_SWITCH } from '@kbn/discover-utils'; +import { createPropertyGetProxy } from '../utils/proxies'; + +/** + * Create proxy for the uiSettings service, in which settings preferences are overwritten + * with custom values + */ +export const createUiSettingsServiceProxy = (uiSettings: IUiSettingsClient) => { + const overrides: Record = { + [HIDE_ANNOUNCEMENTS]: true, + [MODIFY_COLUMNS_ON_SWITCH]: false, + }; + + return createPropertyGetProxy(uiSettings, { + get: + () => + (key, ...args) => { + if (key in overrides) { + return overrides[key]; + } + + return uiSettings.get(key, ...args); + }, + }); +}; diff --git a/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts b/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts new file mode 100644 index 0000000000000..bddb162963376 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/custom_url_state_storage.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { createMemoryHistory } from 'history'; +import { LogExplorerDiscoverServices } from './types'; + +type DiscoverHistory = ReturnType; + +/** + * Create a MemoryHistory instance. It is initialized with an application state + * object, because Discover radically resets too much when the URL is "empty". + */ +export const createDiscoverMemoryHistory = (): DiscoverHistory => + createMemoryHistory({ + initialEntries: [{ search: `?_a=()` }], + }); + +/** + * Create a url state storage that's not connected to the real browser location + * to isolate the Discover component from these side-effects. + */ +export const createMemoryUrlStateStorage = (memoryHistory: DiscoverHistory) => + createKbnUrlStateStorage({ + history: memoryHistory, + useHash: false, + useHashQuery: false, + }); diff --git a/x-pack/plugins/log_explorer/public/controller/index.ts b/x-pack/plugins/log_explorer/public/controller/index.ts new file mode 100644 index 0000000000000..8c1ebbc752dbd --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/index.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './controller_customizations'; +export * from './create_controller'; +export * from './provider'; +export * from './types'; diff --git a/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts b/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts new file mode 100644 index 0000000000000..46104b3960940 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/lazy_create_controller.ts @@ -0,0 +1,15 @@ +/* + * 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 type { CreateLogExplorerControllerFactory } from './create_controller'; + +export const createLogExplorerControllerLazyFactory: CreateLogExplorerControllerFactory = + (dependencies) => async (args) => { + const { createLogExplorerControllerFactory } = await import('./create_controller'); + + return createLogExplorerControllerFactory(dependencies)(args); + }; diff --git a/x-pack/plugins/log_explorer/public/controller/provider.ts b/x-pack/plugins/log_explorer/public/controller/provider.ts new file mode 100644 index 0000000000000..a66b03f477cb1 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/provider.ts @@ -0,0 +1,15 @@ +/* + * 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 createContainer from 'constate'; +import type { LogExplorerController } from './types'; + +const useLogExplorerController = ({ controller }: { controller: LogExplorerController }) => + controller; + +export const [LogExplorerControllerProvider, useLogExplorerControllerContext] = + createContainer(useLogExplorerController); diff --git a/x-pack/plugins/log_explorer/public/controller/public_state.ts b/x-pack/plugins/log_explorer/public/controller/public_state.ts new file mode 100644 index 0000000000000..04c12160a8809 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/public_state.ts @@ -0,0 +1,134 @@ +/* + * 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 { + availableControlsPanels, + controlPanelConfigs, + ControlPanels, + hydrateDatasetSelection, +} from '../../common'; +import { + DEFAULT_CONTEXT, + LogExplorerControllerContext, +} from '../state_machines/log_explorer_controller'; +import { + LogExplorerPublicState, + LogExplorerPublicStateUpdate, + OptionsListControlOption, +} from './types'; + +export const getPublicStateFromContext = ( + context: LogExplorerControllerContext +): LogExplorerPublicState => { + return { + chart: context.chart, + datasetSelection: context.datasetSelection.toPlainSelection(), + grid: context.grid, + filters: context.filters, + query: context.query, + refreshInterval: context.refreshInterval, + time: context.time, + controls: getPublicControlsStateFromControlPanels(context.controlPanels), + }; +}; + +export const getContextFromPublicState = ( + publicState: LogExplorerPublicStateUpdate +): LogExplorerControllerContext => ({ + ...DEFAULT_CONTEXT, + chart: { + ...DEFAULT_CONTEXT.chart, + ...publicState.chart, + }, + controlPanels: getControlPanelsFromPublicControlsState(publicState.controls), + datasetSelection: + publicState.datasetSelection != null + ? hydrateDatasetSelection(publicState.datasetSelection) + : DEFAULT_CONTEXT.datasetSelection, + grid: { + ...DEFAULT_CONTEXT.grid, + ...publicState.grid, + rows: { + ...DEFAULT_CONTEXT.grid.rows, + ...publicState.grid?.rows, + }, + }, + filters: publicState.filters ?? DEFAULT_CONTEXT.filters, + query: publicState.query ?? DEFAULT_CONTEXT.query, + refreshInterval: publicState.refreshInterval ?? DEFAULT_CONTEXT.refreshInterval, + time: publicState.time ?? DEFAULT_CONTEXT.time, +}); + +const getPublicControlsStateFromControlPanels = ( + controlPanels: ControlPanels | undefined +): LogExplorerPublicState['controls'] => + controlPanels != null + ? { + ...(availableControlsPanels.NAMESPACE in controlPanels + ? { + [availableControlsPanels.NAMESPACE]: getOptionsListPublicControlStateFromControlPanel( + controlPanels[availableControlsPanels.NAMESPACE] + ), + } + : {}), + } + : {}; + +const getOptionsListPublicControlStateFromControlPanel = ( + optionsListControlPanel: ControlPanels[string] +): OptionsListControlOption => ({ + mode: optionsListControlPanel.explicitInput.exclude ? 'exclude' : 'include', + selection: optionsListControlPanel.explicitInput.existsSelected + ? { type: 'exists' } + : { + type: 'options', + selectedOptions: optionsListControlPanel.explicitInput.selectedOptions ?? [], + }, +}); + +const getControlPanelsFromPublicControlsState = ( + publicControlsState: LogExplorerPublicStateUpdate['controls'] +): ControlPanels => { + if (publicControlsState == null) { + return {}; + } + + const namespacePublicControlState = publicControlsState[availableControlsPanels.NAMESPACE]; + + return { + ...(namespacePublicControlState + ? { + [availableControlsPanels.NAMESPACE]: getControlPanelFromOptionsListPublicControlState( + availableControlsPanels.NAMESPACE, + namespacePublicControlState + ), + } + : {}), + }; +}; + +const getControlPanelFromOptionsListPublicControlState = ( + controlId: string, + publicControlState: OptionsListControlOption +): ControlPanels[string] => { + const defaultControlPanelConfig = controlPanelConfigs[controlId]; + + return { + ...defaultControlPanelConfig, + explicitInput: { + ...defaultControlPanelConfig.explicitInput, + exclude: publicControlState.mode === 'exclude', + ...(publicControlState.selection.type === 'exists' + ? { + existsSelected: true, + } + : { + selectedOptions: publicControlState.selection.selectedOptions, + }), + }, + }; +}; diff --git a/x-pack/plugins/log_explorer/public/controller/types.ts b/x-pack/plugins/log_explorer/public/controller/types.ts new file mode 100644 index 0000000000000..50eb259d38cb3 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/controller/types.ts @@ -0,0 +1,73 @@ +/* + * 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 { QueryState } from '@kbn/data-plugin/public'; +import { DiscoverContainerProps } from '@kbn/discover-plugin/public'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { Observable } from 'rxjs'; +import { + availableControlsPanels, + DatasetSelectionPlain, + DisplayOptions, + PartialDisplayOptions, +} from '../../common'; +import { IDatasetsClient } from '../services/datasets'; +import { + LogExplorerControllerStateMachine, + LogExplorerControllerStateService, +} from '../state_machines/log_explorer_controller'; +import { LogExplorerCustomizations } from './controller_customizations'; + +export interface LogExplorerController { + actions: {}; + customizations: LogExplorerCustomizations; + datasetsClient: IDatasetsClient; + discoverServices: LogExplorerDiscoverServices; + event$: Observable; + service: LogExplorerControllerStateService; + state$: Observable; + stateMachine: LogExplorerControllerStateMachine; +} + +export type LogExplorerDiscoverServices = Pick< + Required, + 'data' | 'filterManager' | 'timefilter' | 'uiSettings' | 'history' +> & { + urlStateStorage: IKbnUrlStateStorage; +}; + +export interface OptionsListControlOption { + mode: 'include' | 'exclude'; + selection: + | { + type: 'options'; + selectedOptions: string[]; + } + | { + type: 'exists'; + }; +} + +export interface ControlOptions { + [availableControlsPanels.NAMESPACE]?: OptionsListControlOption; +} + +// we might want to wrap this into an object that has a "state value" laster +export type LogExplorerPublicState = QueryState & + DisplayOptions & { + controls: ControlOptions; + datasetSelection: DatasetSelectionPlain; + }; + +export type LogExplorerPublicStateUpdate = QueryState & + PartialDisplayOptions & { + controls?: ControlOptions; + datasetSelection?: DatasetSelectionPlain; + }; + +// a placeholder for now +export type LogExplorerPublicEvent = never; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx index c315971fb23a5..e7b3dc6bbd431 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_filters.tsx @@ -10,21 +10,21 @@ import { Query } from '@kbn/es-query'; import { DataPublicPluginStart } from '@kbn/data-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; import { useControlPanels } from '../hooks/use_control_panels'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; const DATASET_FILTERS_CUSTOMIZATION_ID = 'datasetFiltersCustomization'; interface CustomDatasetFiltersProps { - logExplorerProfileStateService: LogExplorerProfileStateService; + logExplorerControllerStateService: LogExplorerControllerStateService; data: DataPublicPluginStart; } const CustomDatasetFilters = ({ - logExplorerProfileStateService, + logExplorerControllerStateService, data, }: CustomDatasetFiltersProps) => { const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels( - logExplorerProfileStateService, + logExplorerControllerStateService, data ); diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx index 5b9fc3223fddf..a93a893bb20e9 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_dataset_selector.tsx @@ -15,15 +15,15 @@ import { DataViewsProvider, useDataViewsContext } from '../hooks/use_data_views' import { useEsql } from '../hooks/use_esql'; import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations'; import { IDatasetsClient } from '../services/datasets'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; interface CustomDatasetSelectorProps { - logExplorerProfileStateService: LogExplorerProfileStateService; + logExplorerControllerStateService: LogExplorerControllerStateService; } -export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => { +export const CustomDatasetSelector = withProviders(({ logExplorerControllerStateService }) => { const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection( - logExplorerProfileStateService + logExplorerControllerStateService ); const { @@ -111,13 +111,13 @@ function withProviders(Component: React.FunctionComponent - + diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx index 2fd71f948146f..b94cc930cafec 100644 --- a/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/custom_flyout_content.tsx @@ -5,46 +5,41 @@ * 2.0. */ -import React, { useCallback } from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import { FlyoutDetail } from '../components/flyout_detail/flyout_detail'; -import { FlyoutProps } from '../components/flyout_detail'; -import { useLogExplorerCustomizationsContext } from '../hooks/use_log_explorer_customizations'; +import { LogExplorerFlyoutContentProps } from '../components/flyout_detail'; +import { useLogExplorerControllerContext } from '../controller'; -export const CustomFlyoutContent = ({ - actions, - dataView, - doc, - renderDefaultContent, -}: FlyoutProps) => { - const { flyout } = useLogExplorerCustomizationsContext(); +export const CustomFlyoutContent = (props: LogExplorerFlyoutContentProps) => { + const { + customizations: { flyout }, + } = useLogExplorerControllerContext(); - const renderPreviousContent = useCallback( - () => ( - <> - {/* Apply custom Log Explorer detail */} - - - - - ), - [actions, dataView, doc] + const renderCustomizedContent = useMemo( + () => flyout?.renderContent?.(renderContent) ?? renderContent, + [flyout] ); - const content = flyout?.renderContent - ? flyout?.renderContent(renderPreviousContent, { doc }) - : renderPreviousContent(); - return ( {/* Apply custom Log Explorer detail */} - {content} + {renderCustomizedContent(props)} {/* Restore default content */} - {renderDefaultContent()} + {props.renderDefaultContent()} ); }; +const renderContent = ({ actions, dataView, doc }: LogExplorerFlyoutContentProps) => ( + <> + {/* Apply custom Log Explorer detail */} + + + + +); + // eslint-disable-next-line import/no-default-export export default CustomFlyoutContent; diff --git a/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx b/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx new file mode 100644 index 0000000000000..1cc3d6bf95eec --- /dev/null +++ b/x-pack/plugins/log_explorer/public/customizations/custom_search_bar.tsx @@ -0,0 +1,41 @@ +/* + * 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 type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; + +export const createCustomSearchBar = ({ + navigation, + data, + unifiedSearch, +}: { + data: DataPublicPluginStart; + navigation: NavigationPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; +}) => { + const { + ui: { createTopNavWithCustomContext }, + } = navigation; + + const { + ui: { getCustomSearchBar }, + } = unifiedSearch; + + const CustomSearchBar = getCustomSearchBar(data); + + const customUnifiedSearch = { + ...unifiedSearch, + ui: { + ...unifiedSearch.ui, + SearchBar: CustomSearchBar, + AggregateQuerySearchBar: CustomSearchBar, + }, + }; + + return createTopNavWithCustomContext(customUnifiedSearch); +}; diff --git a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx index 89c585d8d201e..5712a72ed0e1c 100644 --- a/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx +++ b/x-pack/plugins/log_explorer/public/customizations/log_explorer_profile.tsx @@ -4,19 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; + import type { CoreStart } from '@kbn/core/public'; -import { CustomizationCallback, DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import type { CustomizationCallback } from '@kbn/discover-plugin/public'; import { i18n } from '@kbn/i18n'; +import React from 'react'; import useObservable from 'react-use/lib/useObservable'; -import { combineLatest, from, map, Subscription, type BehaviorSubject } from 'rxjs'; -import { LogExplorerStateContainer } from '../components/log_explorer'; -import { LogExplorerCustomizations } from '../components/log_explorer/types'; -import { LogExplorerCustomizationsProvider } from '../hooks/use_log_explorer_customizations'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; -import { LogExplorerStartDeps } from '../types'; +import { waitFor } from 'xstate/lib/waitFor'; +import type { LogExplorerController } from '../controller'; +import { LogExplorerControllerProvider } from '../controller/provider'; +import type { LogExplorerStartDeps } from '../types'; import { dynamic } from '../utils/dynamic'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; +import { createCustomSearchBar } from './custom_search_bar'; const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters')); const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector')); @@ -24,56 +24,31 @@ const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content')) export interface CreateLogExplorerProfileCustomizationsDeps { core: CoreStart; - customizations: LogExplorerCustomizations; plugins: LogExplorerStartDeps; - state$?: BehaviorSubject; + controller: LogExplorerController; } export const createLogExplorerProfileCustomizations = ({ core, - customizations: logExplorerCustomizations, plugins, - state$, + controller, }: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback => async ({ customizations, stateContainer }) => { - const { data, dataViews, discover } = plugins; - // Lazy load dependencies - const datasetServiceModuleLoadable = import('../services/datasets'); - const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile'); - - const [{ DatasetsService }, { initializeLogExplorerProfileStateService, waitForState }] = - await Promise.all([datasetServiceModuleLoadable, logExplorerMachineModuleLoadable]); - - const datasetsClient = new DatasetsService().start({ - http: core.http, - }).client; + const { discoverServices, service } = controller; + const pluginsWithOverrides = { + ...plugins, + ...discoverServices, + }; + const { data, dataViews, discover, navigation, unifiedSearch } = pluginsWithOverrides; - const logExplorerProfileStateService = initializeLogExplorerProfileStateService({ - datasetsClient, - stateContainer, - toasts: core.notifications.toasts, - }); + service.send('RECEIVED_STATE_CONTAINER', { discoverStateContainer: stateContainer }); /** * Wait for the machine to be fully initialized to set the restored selection * create the DataView and set it in the stateContainer from Discover */ - await waitForState(logExplorerProfileStateService, 'initialized'); - - /** - * Subscribe the state$ BehaviorSubject when the consumer app wants to react to state changes. - * It emits a combined state of: - * - log explorer state machine context - * - appState from the discover stateContainer - */ - let stateSubscription: Subscription; - if (state$) { - stateSubscription = createStateUpdater({ - logExplorerProfileStateService, - stateContainer, - }).subscribe(state$); - } + await waitFor(service, (state) => state.matches('initialized'), { timeout: 30000 }); /** * Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams @@ -87,20 +62,22 @@ export const createLogExplorerProfileCustomizations = return ( ); }, PrependFilterBar: () => ( - + ), + CustomSearchBar: createCustomSearchBar({ + data, + navigation, + unifiedSearch, + }), }); /** @@ -143,32 +120,13 @@ export const createLogExplorerProfileCustomizations = return ( - + - + ); }, }); - return () => { - if (stateSubscription) { - stateSubscription.unsubscribe(); - } - }; + return () => {}; }; - -const createStateUpdater = ({ - logExplorerProfileStateService, - stateContainer, -}: { - logExplorerProfileStateService: LogExplorerProfileStateService; - stateContainer: DiscoverStateContainer; -}) => { - return combineLatest([from(logExplorerProfileStateService), stateContainer.appState.state$]).pipe( - map(([logExplorerState, appState]) => ({ - logExplorerState: logExplorerState.context, - appState, - })) - ); -}; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx b/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx index c10cddabbe0b1..f38b902aaa045 100644 --- a/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx +++ b/x-pack/plugins/log_explorer/public/hooks/use_control_panels.tsx @@ -13,16 +13,16 @@ import { Query, TimeRange } from '@kbn/es-query'; import { useQuerySubscriber } from '@kbn/unified-field-list'; import { useSelector } from '@xstate/react'; import { useCallback } from 'react'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; export const useControlPanels = ( - logExplorerProfileStateService: LogExplorerProfileStateService, + logExplorerControllerStateService: LogExplorerControllerStateService, data: DataPublicPluginStart ) => { const { query, filters, fromDate, toDate } = useQuerySubscriber({ data }); const timeRange: TimeRange = { from: fromDate!, to: toDate! }; - const controlPanels = useSelector(logExplorerProfileStateService, (state) => { + const controlPanels = useSelector(logExplorerControllerStateService, (state) => { if (!('controlPanels' in state.context)) return; return state.context.controlPanels; }); @@ -45,12 +45,12 @@ export const useControlPanels = ( const setControlGroupAPI = useCallback( (controlGroupAPI: ControlGroupAPI) => { - logExplorerProfileStateService.send({ + logExplorerControllerStateService.send({ type: 'INITIALIZE_CONTROL_GROUP_API', controlGroupAPI, }); }, - [logExplorerProfileStateService] + [logExplorerControllerStateService] ); return { getInitialInput, setControlGroupAPI, query, filters, timeRange }; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts b/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts index 6bff4055f3635..1f611da05c8cc 100644 --- a/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts +++ b/x-pack/plugins/log_explorer/public/hooks/use_dataset_selection.ts @@ -8,20 +8,20 @@ import { useSelector } from '@xstate/react'; import { useCallback } from 'react'; import { DatasetSelectionChange } from '../../common/dataset_selection'; -import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile'; +import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller'; export const useDatasetSelection = ( - logExplorerProfileStateService: LogExplorerProfileStateService + logExplorerControllerStateService: LogExplorerControllerStateService ) => { - const datasetSelection = useSelector(logExplorerProfileStateService, (state) => { + const datasetSelection = useSelector(logExplorerControllerStateService, (state) => { return state.context.datasetSelection; }); const handleDatasetSelectionChange: DatasetSelectionChange = useCallback( (data) => { - logExplorerProfileStateService.send({ type: 'UPDATE_DATASET_SELECTION', data }); + logExplorerControllerStateService.send({ type: 'UPDATE_DATASET_SELECTION', data }); }, - [logExplorerProfileStateService] + [logExplorerControllerStateService] ); return { datasetSelection, handleDatasetSelectionChange }; diff --git a/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts b/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts deleted file mode 100644 index 0557e17761cb4..0000000000000 --- a/x-pack/plugins/log_explorer/public/hooks/use_log_explorer_customizations.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import createContainer from 'constate'; -import { LogExplorerCustomizations } from '../components/log_explorer/types'; - -interface UseLogExplorerCustomizationsDeps { - value: LogExplorerCustomizations; -} - -const useLogExplorerCustomizations = ({ value }: UseLogExplorerCustomizationsDeps) => value; - -export const [LogExplorerCustomizationsProvider, useLogExplorerCustomizationsContext] = - createContainer(useLogExplorerCustomizations); diff --git a/x-pack/plugins/log_explorer/public/index.ts b/x-pack/plugins/log_explorer/public/index.ts index 1ca7f37aa4c9b..005b5cca07a14 100644 --- a/x-pack/plugins/log_explorer/public/index.ts +++ b/x-pack/plugins/log_explorer/public/index.ts @@ -8,12 +8,20 @@ import type { PluginInitializerContext } from '@kbn/core/public'; import type { LogExplorerConfig } from '../common/plugin_config'; import { LogExplorerPlugin } from './plugin'; -export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; -export type { LogExplorerStateContainer } from './components/log_explorer'; export type { + CreateLogExplorerController, + LogExplorerController, LogExplorerCustomizations, LogExplorerFlyoutContentProps, -} from './components/log_explorer/types'; + LogExplorerPublicState, + LogExplorerPublicStateUpdate, +} from './controller'; +export type { LogExplorerControllerContext } from './state_machines/log_explorer_controller'; +export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types'; +export { + getDiscoverColumnsFromDisplayOptions, + getDiscoverGridFromDisplayOptions, +} from './utils/convert_discover_app_state'; export function plugin(context: PluginInitializerContext) { return new LogExplorerPlugin(context); diff --git a/x-pack/plugins/log_explorer/public/plugin.ts b/x-pack/plugins/log_explorer/public/plugin.ts index 3c637b6b06caf..8c527c28fb8b2 100644 --- a/x-pack/plugins/log_explorer/public/plugin.ts +++ b/x-pack/plugins/log_explorer/public/plugin.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; +import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators'; import { createLogExplorer } from './components/log_explorer'; -import { +import { createLogExplorerControllerLazyFactory } from './controller/lazy_create_controller'; +import type { LogExplorerPluginSetup, LogExplorerPluginStart, LogExplorerSetupDeps, @@ -48,8 +49,14 @@ export class LogExplorerPlugin implements Plugin => + async (context) => { + if (!('discoverStateContainer' in context)) return; + return context.controlPanels + ? constructControlPanelsWithDataViewId(context.discoverStateContainer, context.controlPanels) + : undefined; + }; + +export const subscribeControlGroup = + (): InvokeCreator => + (context) => + (send) => { + if (!('controlGroupAPI' in context)) return; + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + + const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe( + (newFilters) => { + discoverStateContainer.internalState.transitions.setCustomFilters(newFilters); + discoverStateContainer.actions.fetchData(); + } + ); + + const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => { + if (!deepEqual(panels, context.controlPanels)) { + send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels }); + } + }); + + return () => { + filtersSubscription.unsubscribe(); + inputSubscription.unsubscribe(); + }; + }; + +export const updateControlPanels = + (): InvokeCreator => + async (context, event) => { + if (!('controlGroupAPI' in context)) return; + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + + const newControlPanels = + ('controlPanels' in event && event.controlPanels) || context.controlPanels; + + if (!newControlPanels) return undefined; + + const controlPanelsWithId = constructControlPanelsWithDataViewId( + discoverStateContainer, + newControlPanels! + ); + + context.controlGroupAPI.updateInput({ panels: controlPanelsWithId }); + + return controlPanelsWithId; + }; + +const constructControlPanelsWithDataViewId = ( + stateContainer: DiscoverStateContainer, + newControlPanels: ControlPanels +) => { + const dataView = stateContainer.internalState.getState().dataView!; + + const validatedControlPanels = isValidState(newControlPanels) + ? newControlPanels + : getVisibleControlPanelsConfig(dataView); + + const controlsPanelsWithId = mergeDefaultPanelsWithControlPanels( + dataView, + validatedControlPanels! + ); + + return controlsPanelsWithId; +}; + +const isValidState = (state: ControlPanels | undefined | null): boolean => { + return Object.keys(state ?? {}).length > 0 && ControlPanelRT.is(state); +}; + +const getVisibleControlPanels = (dataView: DataView | undefined) => + availableControlPanelFields.filter( + (panelKey) => dataView?.fields.getByName(panelKey) !== undefined + ); + +export const getVisibleControlPanelsConfig = (dataView?: DataView) => { + return getVisibleControlPanels(dataView).reduce((panelsMap, panelKey) => { + const config = controlPanelConfigs[panelKey]; + + return { ...panelsMap, [panelKey]: config }; + }, {} as ControlPanels); +}; + +const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => { + return mapValues(controlPanels, (controlPanelConfig) => ({ + ...controlPanelConfig, + explicitInput: { ...controlPanelConfig.explicitInput, dataViewId }, + })); +}; + +const mergeDefaultPanelsWithControlPanels = (dataView: DataView, urlPanels: ControlPanels) => { + // Get default panel configs from existing fields in data view + const visiblePanels = getVisibleControlPanelsConfig(dataView); + + // Get list of panel which can be overridden to avoid merging additional config from url + const existingKeys = Object.keys(visiblePanels); + const controlPanelsToOverride = pick(urlPanels, existingKeys); + + // Merge default and existing configs and add dataView.id to each of them + return addDataViewIdToControlPanels( + { ...visiblePanels, ...controlPanelsToOverride }, + dataView.id + ); +}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts similarity index 64% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts index fa2b2992ee76b..76ac9c0c82f23 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/data_view_service.ts +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/data_view_service.ts @@ -5,23 +5,15 @@ * 2.0. */ -import { DiscoverStateContainer } from '@kbn/discover-plugin/public'; import { InvokeCreator } from 'xstate'; -import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types'; - -interface LogExplorerProfileDataViewStateDependencies { - stateContainer: DiscoverStateContainer; -} +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; export const createAndSetDataView = - ({ - stateContainer, - }: LogExplorerProfileDataViewStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => + (): InvokeCreator => async (context) => { - const dataView = await stateContainer.actions.createAndAppendAdHocDataView( + if (!('discoverStateContainer' in context)) return; + const { discoverStateContainer } = context; + const dataView = await discoverStateContainer.actions.createAndAppendAdHocDataView( context.datasetSelection.toDataviewSpec() ); /** @@ -32,5 +24,5 @@ export const createAndSetDataView = * to the existing one or the default logs-*. * We set explicitly the data view here to be used when restoring the data view on the initial load. */ - stateContainer.actions.setDataView(dataView); + discoverStateContainer.actions.setDataView(dataView); }; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts new file mode 100644 index 0000000000000..512523c24c8f7 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/discover_service.ts @@ -0,0 +1,83 @@ +/* + * 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 { isEmpty } from 'lodash'; +import { ActionFunction, actions, InvokeCallback } from 'xstate'; +import { + getChartDisplayOptionsFromDiscoverAppState, + getDiscoverAppStateFromContext, + getGridColumnDisplayOptionsFromDiscoverAppState, + getGridRowsDisplayOptionsFromDiscoverAppState, + getQueryStateFromDiscoverAppState, +} from '../../../../utils/convert_discover_app_state'; +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; + +export const subscribeToDiscoverState = + () => + ( + context: LogExplorerControllerContext + ): InvokeCallback => + (send, onEvent) => { + if (!('discoverStateContainer' in context)) { + throw new Error('Failed to subscribe to the Discover state: no state container in context.'); + } + + const { appState } = context.discoverStateContainer; + + const subscription = appState.state$.subscribe({ + next: (newAppState) => { + if (isEmpty(newAppState)) { + return; + } + + send({ + type: 'RECEIVE_DISCOVER_APP_STATE', + appState: newAppState, + }); + }, + }); + + return () => { + subscription.unsubscribe(); + }; + }; + +export const updateContextFromDiscoverAppState = actions.assign< + LogExplorerControllerContext, + LogExplorerControllerEvent +>((context, event) => { + if ('appState' in event && event.type === 'RECEIVE_DISCOVER_APP_STATE') { + return { + chart: { + ...context.chart, + ...getChartDisplayOptionsFromDiscoverAppState(event.appState), + }, + grid: { + columns: + getGridColumnDisplayOptionsFromDiscoverAppState(event.appState) ?? context.grid.columns, + rows: { + ...context.grid.rows, + ...getGridRowsDisplayOptionsFromDiscoverAppState(event.appState), + }, + }, + ...getQueryStateFromDiscoverAppState(event.appState), + }; + } + + return {}; +}); + +export const updateDiscoverAppStateFromContext: ActionFunction< + LogExplorerControllerContext, + LogExplorerControllerEvent +> = (context, _event) => { + if (!('discoverStateContainer' in context)) { + return; + } + + context.discoverStateContainer.appState.update(getDiscoverAppStateFromContext(context)); +}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts similarity index 79% rename from x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts rename to x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts index 6de3d24025802..a5aeac069fc32 100644 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/selection_service.ts +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/selection_service.ts @@ -6,21 +6,21 @@ */ import { InvokeCreator } from 'xstate'; -import { Dataset } from '../../../../common/datasets'; -import { SingleDatasetSelection } from '../../../../common/dataset_selection'; -import { IDatasetsClient } from '../../../services/datasets'; -import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types'; +import { Dataset } from '../../../../../common/datasets'; +import { SingleDatasetSelection } from '../../../../../common/dataset_selection'; +import { IDatasetsClient } from '../../../../services/datasets'; +import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; -interface LogExplorerProfileUrlStateDependencies { +interface LogExplorerControllerUrlStateDependencies { datasetsClient: IDatasetsClient; } export const validateSelection = ({ datasetsClient, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent + }: LogExplorerControllerUrlStateDependencies): InvokeCreator< + LogExplorerControllerContext, + LogExplorerControllerEvent > => (context) => async (send) => { diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts new file mode 100644 index 0000000000000..124a6778493b0 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/services/timefilter_service.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { QueryStart } from '@kbn/data-plugin/public'; +import { map, merge, Observable } from 'rxjs'; +import { ActionFunction, actions } from 'xstate'; +import type { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types'; + +export const subscribeToTimefilterService = + (query: QueryStart) => (): Observable => { + const { + timefilter: { timefilter }, + } = query; + + const time$ = timefilter.getTimeUpdate$().pipe( + map( + (): LogExplorerControllerEvent => ({ + type: 'RECEIVE_TIMEFILTER_TIME', + time: timefilter.getTime(), + }) + ) + ); + + const refreshInterval$ = timefilter.getRefreshIntervalUpdate$().pipe( + map( + (): LogExplorerControllerEvent => ({ + type: 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL', + refreshInterval: timefilter.getRefreshInterval(), + }) + ) + ); + + return merge(time$, refreshInterval$); + }; + +export const updateContextFromTimefilter = actions.assign< + LogExplorerControllerContext, + LogExplorerControllerEvent +>((context, event) => { + if (event.type === 'RECEIVE_TIMEFILTER_TIME' && 'time' in event) { + return { + time: event.time, + }; + } + + if (event.type === 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL' && 'refreshInterval' in event) { + return { + refreshInterval: event.refreshInterval, + }; + } + + return {}; +}); + +export const updateTimefilterFromContext = + (query: QueryStart): ActionFunction => + (context, _event) => { + if (context.time != null) { + query.timefilter.timefilter.setTime(context.time); + } + if (context.refreshInterval != null) { + query.timefilter.timefilter.setRefreshInterval(context.refreshInterval); + } + }; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts new file mode 100644 index 0000000000000..ef9c87ee7ae14 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/state_machine.ts @@ -0,0 +1,298 @@ +/* + * 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 { IToasts } from '@kbn/core/public'; +import { QueryStart } from '@kbn/data-plugin/public'; +import { actions, createMachine, interpret, InterpreterFrom, raise } from 'xstate'; +import { ControlPanelRT } from '../../../../common/control_panels'; +import { isDatasetSelection } from '../../../../common/dataset_selection'; +import { IDatasetsClient } from '../../../services/datasets'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + createCreateDataViewFailedNotifier, + createDatasetSelectionRestoreFailedNotifier, +} from './notifications'; +import { + initializeControlPanels, + subscribeControlGroup, + updateControlPanels, +} from './services/control_panels'; +import { createAndSetDataView } from './services/data_view_service'; +import { + subscribeToDiscoverState, + updateContextFromDiscoverAppState, + updateDiscoverAppStateFromContext, +} from './services/discover_service'; +import { validateSelection } from './services/selection_service'; +import { + subscribeToTimefilterService, + updateContextFromTimefilter, + updateTimefilterFromContext, +} from './services/timefilter_service'; +import { + LogExplorerControllerContext, + LogExplorerControllerEvent, + LogExplorerControllerTypeState, +} from './types'; + +export const createPureLogExplorerControllerStateMachine = ( + initialContext: LogExplorerControllerContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCBhVAOwBdDU88SA6AVwoEt2q2BDPNgL0gBiAEoZSGAJIA1DABEA+gGUAKgEEVGBaQDyAOXWS9GEQG0ADAF1EoHKlhsulGyCyIAtACYAbIwDsAIzmAJwAzH4ArAA0IACeiJ5+AByMwZ6hnokALD6e5kneAL6FMWiYuATEZJQ0dAyEjByOPHz8HFBy3FTc0mxgAO5CEJRgjRQAbqgA1qNl2PhEJOTUtPRMTVy8Au2d3b0DCByTAMZdbJQWlpcudg5OFC5uCAGejObentFxiBHBwYyeLIBX5JfLeX7BCLFUroeaVJY1Vb1MbNLZtCgdLo9PqDEi0Br4LoAMyIAFtGHMKotqis6utOC1thjdtiDkdUKd7pdrkgQLdms5eU9sowAqEkn8IuLIUDvN4YvEEFksqE3hFQaEglkperPNCQJSFlVlrU1g0Noz0VATasAArcChgPCwIYjMaTGYU2FU42Iunmhlo9o2uj2x3Ow4TDlnC5WHm2ewCh5Cjy+YLeJJ+cGfTMS96fBWIJJJAKMULa8wZbzmPyfbxZfWG+E003Ii1BjEhvBhp0uvFERiEqgkwjkpvUrttwOtYN+7sO3uRk4xijcqw3RP3R4JPyqgJZDM14sygJywsIXf-TV+LLBEIFXc5Rveo0I2lmlGbVrCMQSGRaORJCUXRZBEBQ1FtW1lHUTR4z5TdzmTUBhVCdNRX3QEQmSbUknPCJAn8IJawiTxgm1dIoRKA0X2bSd6VRb8IFEcQpFkBQAEUAFUTAATWgjQMDg-ktxTBBMIiRhiyzcUpSzSJz0fRhvACPws0rEsIgietn3KV8WyReivwESBGAgLFYDAKglCdMBjnuRhxi2MyuAxayGDsxChGQIDND0BQVB0bQAAk1D0ABxDAlCEhDBWQxAyN8W8wkCJIfDSIFzzlLJ0KyfIwmVAIklCUIdLhCc5ynBjjIgUzzMstzbPsxy+Gc9oGo8yghE4205AEhRevUJQMBUZQMGQcQVEkfRoruRDtwQB9RXBVCIn3cwQnBc8S1VcEVQ0yIPgCAJSp9N9W0My0TOc7gLKsmyOooBynLOVz7vuIQBrUIaRqG8bSEm-QFDEVQdDEBQADE1EkZBOLEGak3m8FS1COUAmCNS5Wvc80hSYtPFS4rvDvJJNJOvS6IDKrBBq67bva+y2AgBgup6vrPu+0a-oBvR4ZEuLnh8FJCaK6t8drcstrFJSIj24EDs8I6ydoiqLrRK66ru9yGaZsAPo0L7hs5iapr84GArByHodhwT115YS5tE4FwUYFUifMdJlQid2sklnaZfFOWtIV46qPHX130qozqdq7o6bexCWBwVrmSxfZBmGR13WmWYaPKiPVcYmObvq+PKET5PMT2HEl2jLk41thNZti1xEEK4qlKBPIUfVHJQmxtCIVS9MvewopQ9z8PzspqP1djkutYT5gk5eyvWVxQh8UHPBiTJL1dOV-Pp8ummNfpxfl5c1e05rzlELXaw7ZipCW+edMUi9kIwh77xNXPIJITeGkfKy0ayniVnnKen5j6MGOHOMKtAl6wBYOwac1UhBGEkJNNQ3kABaWhdAGBEDoZACgwpEO6uBW0kheYO35nmAEgI0afGVAHXC3xnjlgkrlSUwQmHuzHjCfeECDJHzVjVWB754GoEQY0HWet1AKGkJIDAAB1BQ3UBryBoc3J4LwDwAk1Ojcih5wh-ylNlIqljEj4wopRQRZVJ4iKgWImBcCEE4CQYzZmGi+oEJUEQkhtpQpjSig3eCTdn66JRn4VImRqx+DCLwzIAQzFkRdvuDMqk-D5FvHY6iQjHH+mcYXCRpopEyKXhXLsPZnSukzuyT0YczpOPbCUtx0iPHlxXtUhcEZ2S31jFcMJ9sdGt3RuYN4GRAShG-r-dhKkMzSzvFpXcRNEgh3sadfSRTWnVVcZI9xSDKndLnDUvsG8BxDhHGOCezSdmoOjqU1Y5TOnHMvj08MsAb4rnvhuCJ80VJpKSNqLIoJ+FpBvH-QIEl1rlhJumImaRKJUQoKgCAcAXBNO2WaP5CNRLuBvIREI4QviKgSf4CUwQSYkQPOkMi4DCkflYLs6muK+YvxyH-bwMT8Z-D5fyv4Aj8kOLuR+FlOxU44jZbQl+7hSye01AWdhyoJnYV+DM7UMzUoMtFZHS0s53xnOlaMhAXgJlZCzEHJK-KCgKX3GWFSt57zcvLHqceBTdUF2qsayJPwSKpA+JWBWvwgTanPKhV4CteEkTlCEHIwQdXYr1S42m89GoypGb6hA1ZVQo2UujasmMVJ4QIoENIGZgS5MSImimxS9mps1umsuzVGYrzPs3TN81Ah7hybC3aeQ-CZV+ACJhqUf5BoKDWlWojC4NvbY9LxYAfUAu1DyksCSSb1nMAeNhioso5TyuWIERUSrupFUmr10c52l0em8iVVcBjLsdqeGJwJULig3WRIq-dspVgjcCI8B4p2HzrY89piCn382rBM9MXdg1kX3KS1unxX0oyBO+9U6M8lYtrSykyTy6AvKOSgqmkBIMv3rCkWDQbgQIbDfMlGKQxTZlPGkfhwHIF4fEeBzpi7yO6KlP8UiylbzZLyPjP+R6lLdwVms9jZ6tm4YefhnjRyL4GtNEax+-zHZYQBH8EInxNLrsHfMt2bxKygt+HKP4nxijFCAA */ + createMachine< + LogExplorerControllerContext, + LogExplorerControllerEvent, + LogExplorerControllerTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'LogExplorerController', + initial: 'uninitialized', + states: { + uninitialized: { + on: { + RECEIVED_STATE_CONTAINER: { + target: 'initializingDataView', + actions: ['storeDiscoverStateContainer'], + }, + }, + }, + initializingDataView: { + invoke: { + src: 'createDataView', + onDone: { + target: 'initializingControlPanels', + actions: ['updateDiscoverAppStateFromContext', 'updateTimefilterFromContext'], + }, + onError: { + target: 'initialized', + actions: [ + 'notifyCreateDataViewFailed', + 'updateDiscoverAppStateFromContext', + 'updateTimefilterFromContext', + ], + }, + }, + }, + initializingControlPanels: { + invoke: { + src: 'initializeControlPanels', + onDone: { + target: 'initialized', + actions: ['storeControlPanels'], + }, + onError: { + target: 'initialized', + }, + }, + }, + initialized: { + type: 'parallel', + invoke: [ + { + src: 'discoverStateService', + id: 'discoverStateService', + }, + { + src: 'timefilterService', + id: 'timefilterService', + }, + ], + states: { + datasetSelection: { + initial: 'validatingSelection', + states: { + validatingSelection: { + invoke: { + src: 'validateSelection', + }, + on: { + LISTEN_TO_CHANGES: { + target: 'idle', + }, + UPDATE_DATASET_SELECTION: { + target: 'updatingDataView', + actions: ['storeDatasetSelection'], + }, + DATASET_SELECTION_RESTORE_FAILURE: { + target: 'updatingDataView', + actions: ['notifyDatasetSelectionRestoreFailed'], + }, + }, + }, + idle: { + on: { + UPDATE_DATASET_SELECTION: { + target: 'updatingDataView', + actions: ['storeDatasetSelection'], + }, + DATASET_SELECTION_RESTORE_FAILURE: { + target: 'updatingDataView', + actions: ['notifyDatasetSelectionRestoreFailed'], + }, + }, + }, + updatingDataView: { + invoke: { + src: 'createDataView', + onDone: { + target: 'idle', + actions: ['notifyDataViewUpdate'], + }, + onError: { + target: 'idle', + actions: ['notifyCreateDataViewFailed'], + }, + }, + }, + }, + }, + controlGroups: { + initial: 'uninitialized', + states: { + uninitialized: { + on: { + INITIALIZE_CONTROL_GROUP_API: { + target: 'idle', + cond: 'controlGroupAPIExists', + actions: ['storeControlGroupAPI'], + }, + }, + }, + idle: { + invoke: { + src: 'subscribeControlGroup', + }, + on: { + DATA_VIEW_UPDATED: { + target: 'updatingControlPanels', + }, + UPDATE_CONTROL_PANELS: { + target: 'updatingControlPanels', + }, + }, + }, + updatingControlPanels: { + invoke: { + src: 'updateControlPanels', + onDone: { + target: 'idle', + actions: ['storeControlPanels'], + }, + onError: { + target: 'idle', + }, + }, + }, + }, + }, + }, + on: { + RECEIVE_DISCOVER_APP_STATE: { + actions: ['updateContextFromDiscoverAppState'], + }, + RECEIVE_QUERY_STATE: { + actions: ['updateQueryStateFromQueryServiceState'], + }, + RECEIVE_TIMEFILTER_TIME: { + actions: ['updateContextFromTimefilter'], + }, + RECEIVE_TIMEFILTER_REFRESH_INTERVAL: { + actions: ['updateContextFromTimefilter'], + }, + }, + }, + }, + }, + { + actions: { + storeDatasetSelection: actions.assign((_context, event) => + 'data' in event && isDatasetSelection(event.data) + ? { + datasetSelection: event.data, + } + : {} + ), + storeDiscoverStateContainer: actions.assign((_context, event) => + 'discoverStateContainer' in event + ? { + discoverStateContainer: event.discoverStateContainer, + } + : {} + ), + storeControlGroupAPI: actions.assign((_context, event) => + 'controlGroupAPI' in event + ? { + controlGroupAPI: event.controlGroupAPI, + } + : {} + ), + storeControlPanels: actions.assign((_context, event) => + 'data' in event && ControlPanelRT.is(event.data) + ? { + controlPanels: event.data, + } + : {} + ), + notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'), + updateContextFromDiscoverAppState, + updateDiscoverAppStateFromContext, + updateContextFromTimefilter, + }, + guards: { + controlGroupAPIExists: (_context, event) => { + return 'controlGroupAPI' in event && event.controlGroupAPI != null; + }, + }, + } + ); + +export interface LogExplorerControllerStateMachineDependencies { + datasetsClient: IDatasetsClient; + initialContext?: LogExplorerControllerContext; + query: QueryStart; + toasts: IToasts; +} + +export const createLogExplorerControllerStateMachine = ({ + datasetsClient, + initialContext = DEFAULT_CONTEXT, + query, + toasts, +}: LogExplorerControllerStateMachineDependencies) => + createPureLogExplorerControllerStateMachine(initialContext).withConfig({ + actions: { + notifyCreateDataViewFailed: createCreateDataViewFailedNotifier(toasts), + notifyDatasetSelectionRestoreFailed: createDatasetSelectionRestoreFailedNotifier(toasts), + updateTimefilterFromContext: updateTimefilterFromContext(query), + }, + services: { + createDataView: createAndSetDataView(), + initializeControlPanels: initializeControlPanels(), + subscribeControlGroup: subscribeControlGroup(), + updateControlPanels: updateControlPanels(), + validateSelection: validateSelection({ datasetsClient }), + discoverStateService: subscribeToDiscoverState(), + timefilterService: subscribeToTimefilterService(query), + }, + }); + +export const initializeLogExplorerControllerStateService = ( + deps: LogExplorerControllerStateMachineDependencies +) => { + const machine = createLogExplorerControllerStateMachine(deps); + return interpret(machine).start(); +}; + +export type LogExplorerControllerStateService = InterpreterFrom< + typeof createLogExplorerControllerStateMachine +>; + +export type LogExplorerControllerStateMachine = ReturnType< + typeof createLogExplorerControllerStateMachine +>; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts new file mode 100644 index 0000000000000..73d87913a8150 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_controller/src/types.ts @@ -0,0 +1,166 @@ +/* + * 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 { ControlGroupAPI } from '@kbn/controls-plugin/public'; +import { QueryState, RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; +import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public'; +import { DoneInvokeEvent } from 'xstate'; +import { ControlPanels, DisplayOptions } from '../../../../common'; +import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection'; + +export interface WithDatasetSelection { + datasetSelection: DatasetSelection; +} + +export interface WithControlPanelGroupAPI { + controlGroupAPI: ControlGroupAPI; +} + +export interface WithControlPanels { + controlPanels?: ControlPanels; +} + +export type WithQueryState = QueryState; + +export type WithDisplayOptions = DisplayOptions; + +export interface WithDiscoverStateContainer { + discoverStateContainer: DiscoverStateContainer; +} + +export type DefaultLogExplorerControllerState = WithDatasetSelection & + WithQueryState & + WithDisplayOptions; + +export type LogExplorerControllerTypeState = + | { + value: 'uninitialized'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingDataView'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingControlPanels'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initializingStateContainer'; + context: WithDatasetSelection & WithControlPanels & WithQueryState & WithDisplayOptions; + } + | { + value: 'initialized'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.validatingSelection'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.idle'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.updatingDataView'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.datasetSelection.updatingStateContainer'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.uninitialized'; + context: WithDatasetSelection & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.idle'; + context: WithDatasetSelection & + WithControlPanelGroupAPI & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + } + | { + value: 'initialized.controlGroups.updatingControlPanels'; + context: WithDatasetSelection & + WithControlPanelGroupAPI & + WithControlPanels & + WithQueryState & + WithDisplayOptions & + WithDiscoverStateContainer; + }; + +export type LogExplorerControllerContext = LogExplorerControllerTypeState['context']; + +export type LogExplorerControllerStateValue = LogExplorerControllerTypeState['value']; + +export type LogExplorerControllerEvent = + | { + type: 'RECEIVED_STATE_CONTAINER'; + discoverStateContainer: DiscoverStateContainer; + } + | { + type: 'LISTEN_TO_CHANGES'; + } + | { + type: 'UPDATE_DATASET_SELECTION'; + data: DatasetSelection; + } + | { + type: 'DATASET_SELECTION_RESTORE_FAILURE'; + } + | { + type: 'INITIALIZE_CONTROL_GROUP_API'; + controlGroupAPI: ControlGroupAPI | undefined; + } + | { + type: 'UPDATE_CONTROL_PANELS'; + controlPanels: ControlPanels | null; + } + | { + type: 'RECEIVE_DISCOVER_APP_STATE'; + appState: DiscoverAppState; + } + | { + type: 'RECEIVE_TIMEFILTER_TIME'; + time: TimeRange; + } + | { + type: 'RECEIVE_TIMEFILTER_REFRESH_INTERVAL'; + refreshInterval: RefreshInterval; + } + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts deleted file mode 100644 index 4fa1673b5879d..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/state_machine.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IToasts } from '@kbn/core/public'; -import { DiscoverStateContainer } from '@kbn/discover-plugin/public'; -import { actions, createMachine, interpret, InterpreterFrom, raise } from 'xstate'; -import { IDatasetsClient } from '../../../services/datasets'; -import { isDatasetSelection } from '../../../../common/dataset_selection'; -import { createAndSetDataView } from './data_view_service'; -import { validateSelection } from './selection_service'; -import { DEFAULT_CONTEXT } from './defaults'; -import { - createCreateDataViewFailedNotifier, - createDatasetSelectionRestoreFailedNotifier, -} from './notifications'; -import { - ControlPanelRT, - LogExplorerProfileContext, - LogExplorerProfileEvent, - LogExplorerProfileTypeState, -} from './types'; -import { - initializeControlPanels, - initializeFromUrl, - listenUrlChange, - subscribeControlGroup, - updateControlPanels, - updateStateContainer, -} from './url_state_storage_service'; - -export const createPureLogExplorerProfileStateMachine = ( - initialContext: LogExplorerProfileContext -) => - /** @xstate-layout N4IgpgJg5mDOIC5QBkD2UCiAPADgG1QCcxCAFQ1AMwEs8wA6AVwDtrWAXagQz2oC9IAYgDaABgC6iUDlSxqnVMykgsiAIwAOAMz0NagEwBWUdoBsWgOxaAnEYA0IAJ7rt9UxesWD+2xf2WAXwCHNExcAmIyCho6ejZ5bl4+NigAMQoAWwBVQjxBCEUGNgA3VABrBlDsfCIScipaIo5E-hT01GzchBLUAGMuBWYxcWHlGTlB5VUEYw1dfQ1RQ30LC0NrbX0HZwQ1CwAWemtDLTV991FTPWWtIJD0aoi66Ma45p5W5jTMnLySCkI9HwA0oRAy9Cq4VqUQasXinA+yS+7U6eG6zFK-UGw1GSBA4wSiimiFm80Wy1W60220QenoxlEjP2J32alEmmsdxAkJqkXqMSaCURKQAIgMuAA1ahgADu+UKb1KFQhDyhfJecPeSVF4qlsvRmIG1EUOIkY1khKUeOmPkM9D8pkMFm0bMpFhpM3Obi0tqdxk0Fi5PKeMIFbyF2q+YvYkulcv+RCBeBBYJVYV5z1hgoRkag0dj+p6WONQwkuOkFsm1sQtvt+kdztOojdHquonoah91h9WkWS1MQdVGdDr3hLSRUAAwop2BQ8KQuMwwHhYPKl4rypUhyH+aOtZ8pzO5wulyuDX0jSay2a8QSq6BphZTNZ6Po1O+jMy++YPScLEdLi0UwDG7Z9jkHdMdw1bNxxSadmFnVB50XZdVwTQFgXYUFCHBYNoV3TUIwPeDEOQ09YHPYsrxGG8KwmEtiQQJ8XzfD9DC-RkfycRB9msdt9kuBYNA0Dxlg0Adgm5bd8Og8McwPABlGN2DAEiuDYEg1yaJUt0gmSszk2CviUgZVJndSl0ISjL1LGjJFvSsGOrBAtC0Q4tFEc5DHE-YPI0XjTA9NRDCdI4rhEvR9HrEKIMefSwzHYVjOUsyEIszT0KTFMcLTOL1QMxLcxMlS1I0qyixs017Loy1GNc9zPMdHy-ICoLDDZehzg0Wx3z4vtbkkvD8oS-cBAgegIHFWAwHYBTlzAXpBnoYoPkmzhjPmxaS0EZAAEkFIAFQwAA5AB9A6AHlTsnAAJABBY6AHEMAU8t8UcolnOE-9fLWZ9NEsAwgtc9ttG6p0fQsQCNFitVMxGoixomqaZrmugtsUZbVqNDb0cGQQslIEU7qO07iYOu6FIwA7Tqp5AMEnA7dou463rvJyH1pETOssQx-u0Lwtm43Yzjtbz1jZGwDg2Ab7j04a90RyBkZjabZs2paVt4NaUjRhb8fJynqdpjB6cZ5mzoAJRey7rdO1I7t25AsmttmPqtTmEG+nm-usAHBba9rOp67trGffz-Nh4cCJgxFlbWrg1b1jHmDiCA6AJomSYwMmSaNmm6YZpmWbd+jPs9s5PFff19jWZsDH2IWdk7MP7UdUx-GbdxTGbKOoIK0b45R9W8ZLNOM8NqmC9NouLdO63Douu2Hadl2MFL2rnMr-8jHZWvjEFxvgcZehTn0fZIr48WYcG6SFcI+SkYTpONbHxgcB1qNdTjLSN2VIb4aK0fkPVWqNX6Y3fp-PM39CwYgvNia81V3plw9ioGsbI1CvnfJ5SwtdTB4OBmoF83k+KmHcCcNyN85Z5UAQ-ccIDE5gNHhAj+ONoExj1PGQgAIspYVTAAkcdC47jWfkw-Wb9WHrXYQWGU1kEF2XNCgxib52RYLZL9PBBDhadncPQEwfF-BXGWKIBYfd4pAPoSI4eyclqQLYcVVKMYyq-x6P-O+tDY5JAYS-Zhqc7FSIcaVSyciSxVUUZvT2KjMGsRwQcJ8Wjm7sUwdYEOlgnxOk5LfeWHjDLCJVowke4iWFQMCeZZxmVMLYVwu4wRnj+DeLESnJgkjdYpSCSQEJ1EN73jQQgFR+h6TnGsL5TQQlz5BWCnMEwEsTGeSipk6hcNam5K8eNXoR4kKPQoO-WATBWCDwgIIXax1dpMzuntAAWjnScLMDqWwusgU6j17mE1OndUgu1ukc16bYQ4ngyHuDDs2ICgVtFrB+syICfse5QxsGY++dSkbrIQnOLZqAdnjzAIIQ2p0JS7QwAAdVOoTcmGARRfPLr098T5T6+T4s6RYWhvIen0J5O0TLgpkPWN3FY8KcmFXqWsjZeA0UYuoOnLFJLs7XVufcx5pAHqm1erRZBESqWdh0EsWu-kiG1zWEFDQJxdAbHaqIYZVxnQSUWdHWSAqkXCtFTgXZ-i4LCpPKhFxcC3HZOWXa5WyLSKOudS0r4JFjwoTPBVeRFLUHTF+UcJ8ZCPAwpBUFPi7kNjnGlm+c1fLfUHPoAG1F2ynXNKgWGpC7qVyCAqcmPhOUBExxWYKwtDqS3BvLW6iNFEo2hMQeEnpcbz4JoBcm4FZC01V0NWoHuYd8F9SCJJZgqAIBwGUI26CA7vnTAALSgp2Duu0KTj0npPVFPNTaWB+ogFuyl0wj7aI0AMwFBxOx8ydG+C9trRptB+LkW9saazOmNf5aZ+CfTMjai+PYngjCdgOJ5bQX6B6Ix1BwuMAHGLuFbPg0+6xTV6EuMyK1UkfVNrta6lFlbu2YecsYAZjciGms8qyVlhhfxRQ7F2KW58Fh+2QwjR+rTTLtMILRz22HtHtRYhyN87gpZMoExY4R4nensR0E6PQz5WTsm+h6Tw7ZvIwuZBSZ8stSM0PzUrKxoDCkp1U9MCW9JnQzuGRgvT2ilhTMNSk8+axOz7CU0I1Z+SfFFNTlrcV9jwGoPZnexA5hiEue0+5kSx9-xgxScJXiWqFkWaWeRgtoi7NLXFXQBz6gjDPq8H7KkAkbDpd0DYfs6wTGdiC4ihpJWJFQPzJwiruwRIMf8rxXyzZmSdmPi+VyfFJawfEoYDrzan7WJi2W+xbSymWQG4Y0wnURvNYg0yiZ-ksHn20LxXsIUSMbpQ8AoVVGRXtoG05zTrmdOaDS9oshL5hkUL9D3KKVD8s2ru5Y1tj2g17OvQNtYcwZ2smy+Qp8WggrOnbCHYCLmNX6CW9eiHgb22YoG2cDqrJWR+Dwf6DQaaTG6GluJNy13nR44LUWzZROXWhq7eRAbiXnNabc7pr7zdDV7cQ+1CbthnyBkXUAA */ - createMachine( - { - context: initialContext, - predictableActionArguments: true, - id: 'LogExplorerProfile', - initial: 'uninitialized', - states: { - uninitialized: { - always: 'initializingFromUrl', - }, - initializingFromUrl: { - invoke: { - src: 'initializeFromUrl', - onDone: { - target: 'initializingDataView', - actions: ['storeDatasetSelection'], - }, - onError: { - target: 'initializingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - initializingDataView: { - invoke: { - src: 'createDataView', - onDone: { - target: 'initializingControlPanels', - }, - onError: { - target: 'initialized', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - initializingControlPanels: { - invoke: { - src: 'initializeControlPanels', - onDone: { - target: 'initializingStateContainer', - actions: ['storeControlPanels'], - }, - onError: { - target: 'initializingStateContainer', - }, - }, - }, - initializingStateContainer: { - invoke: { - src: 'updateStateContainer', - onDone: { - target: 'initialized', - }, - onError: { - target: 'initialized', - }, - }, - }, - initialized: { - type: 'parallel', - states: { - datasetSelection: { - initial: 'validatingSelection', - states: { - validatingSelection: { - invoke: { - src: 'validateSelection', - }, - on: { - LISTEN_TO_CHANGES: { - target: 'idle', - }, - UPDATE_DATASET_SELECTION: { - target: 'updatingDataView', - actions: ['storeDatasetSelection'], - }, - DATASET_SELECTION_RESTORE_FAILURE: { - target: 'updatingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - idle: { - invoke: { - src: 'listenUrlChange', - }, - on: { - UPDATE_DATASET_SELECTION: { - target: 'updatingDataView', - actions: ['storeDatasetSelection'], - }, - DATASET_SELECTION_RESTORE_FAILURE: { - target: 'updatingDataView', - actions: ['notifyDatasetSelectionRestoreFailed'], - }, - }, - }, - updatingDataView: { - invoke: { - src: 'createDataView', - onDone: { - target: 'updatingStateContainer', - }, - onError: { - target: 'updatingStateContainer', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - updatingStateContainer: { - invoke: { - src: 'updateStateContainer', - onDone: { - target: 'idle', - actions: ['notifyDataViewUpdate'], - }, - onError: { - target: 'idle', - actions: ['notifyCreateDataViewFailed'], - }, - }, - }, - }, - }, - controlGroups: { - initial: 'uninitialized', - states: { - uninitialized: { - on: { - INITIALIZE_CONTROL_GROUP_API: { - target: 'idle', - cond: 'controlGroupAPIExists', - actions: ['storeControlGroupAPI'], - }, - }, - }, - idle: { - invoke: { - src: 'subscribeControlGroup', - }, - on: { - DATA_VIEW_UPDATED: { - target: 'updatingControlPanels', - }, - UPDATE_CONTROL_PANELS: { - target: 'updatingControlPanels', - }, - }, - }, - updatingControlPanels: { - invoke: { - src: 'updateControlPanels', - onDone: { - target: 'idle', - actions: ['storeControlPanels'], - }, - onError: { - target: 'idle', - }, - }, - }, - }, - }, - }, - }, - }, - }, - { - actions: { - storeDatasetSelection: actions.assign((_context, event) => - 'data' in event && isDatasetSelection(event.data) - ? { - datasetSelection: event.data, - } - : {} - ), - storeControlGroupAPI: actions.assign((_context, event) => - 'controlGroupAPI' in event - ? { - controlGroupAPI: event.controlGroupAPI, - } - : {} - ), - storeControlPanels: actions.assign((_context, event) => - 'data' in event && ControlPanelRT.is(event.data) - ? { - controlPanels: event.data, - } - : {} - ), - notifyDataViewUpdate: raise('DATA_VIEW_UPDATED'), - }, - guards: { - controlGroupAPIExists: (_context, event) => { - return 'controlGroupAPI' in event && event.controlGroupAPI != null; - }, - }, - } - ); - -export interface LogExplorerProfileStateMachineDependencies { - initialContext?: LogExplorerProfileContext; - datasetsClient: IDatasetsClient; - stateContainer: DiscoverStateContainer; - toasts: IToasts; -} - -export const createLogExplorerProfileStateMachine = ({ - initialContext = DEFAULT_CONTEXT, - datasetsClient, - stateContainer, - toasts, -}: LogExplorerProfileStateMachineDependencies) => - createPureLogExplorerProfileStateMachine(initialContext).withConfig({ - actions: { - notifyCreateDataViewFailed: createCreateDataViewFailedNotifier(toasts), - notifyDatasetSelectionRestoreFailed: createDatasetSelectionRestoreFailedNotifier(toasts), - }, - services: { - createDataView: createAndSetDataView({ stateContainer }), - initializeFromUrl: initializeFromUrl({ stateContainer }), - initializeControlPanels: initializeControlPanels({ stateContainer }), - listenUrlChange: listenUrlChange({ stateContainer }), - subscribeControlGroup: subscribeControlGroup({ stateContainer }), - updateControlPanels: updateControlPanels({ stateContainer }), - updateStateContainer: updateStateContainer({ stateContainer }), - validateSelection: validateSelection({ datasetsClient }), - }, - }); - -export const initializeLogExplorerProfileStateService = ( - deps: LogExplorerProfileStateMachineDependencies -) => { - const machine = createLogExplorerProfileStateMachine(deps); - - return interpret(machine).start(); -}; - -export type LogExplorerProfileStateService = InterpreterFrom< - typeof createLogExplorerProfileStateMachine ->; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts deleted file mode 100644 index fe4323fac0cd4..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/types.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { ControlGroupAPI } from '@kbn/controls-plugin/public'; -import { DoneInvokeEvent } from 'xstate'; -import type { DatasetEncodingError, DatasetSelection } from '../../../../common/dataset_selection'; - -export interface WithDatasetSelection { - datasetSelection: DatasetSelection; -} - -export interface WithControlPanelGroupAPI { - controlGroupAPI: ControlGroupAPI; -} - -export interface WithControlPanels { - controlPanels: ControlPanels; -} - -export type DefaultLogExplorerProfileState = WithDatasetSelection; - -export type LogExplorerProfileTypeState = - | { - value: 'uninitialized'; - context: WithDatasetSelection; - } - | { - value: 'initializingFromUrl'; - context: WithDatasetSelection; - } - | { - value: 'initializingDataView'; - context: WithDatasetSelection; - } - | { - value: 'initializingControlPanels'; - context: WithDatasetSelection; - } - | { - value: 'initializingStateContainer'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.validatingSelection'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.idle'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.updatingDataView'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.datasetSelection.updatingStateContainer'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.controlGroups.uninitialized'; - context: WithDatasetSelection & WithControlPanels; - } - | { - value: 'initialized.controlGroups.idle'; - context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels; - } - | { - value: 'initialized.controlGroups.updatingControlPanels'; - context: WithDatasetSelection & WithControlPanelGroupAPI & WithControlPanels; - }; - -export type LogExplorerProfileContext = LogExplorerProfileTypeState['context']; - -export type LogExplorerProfileStateValue = LogExplorerProfileTypeState['value']; - -export type LogExplorerProfileEvent = - | { - type: 'LISTEN_TO_CHANGES'; - } - | { - type: 'UPDATE_DATASET_SELECTION'; - data: DatasetSelection; - } - | { - type: 'DATASET_SELECTION_RESTORE_FAILURE'; - } - | { - type: 'INITIALIZE_CONTROL_GROUP_API'; - controlGroupAPI: ControlGroupAPI | undefined; - } - | { - type: 'UPDATE_CONTROL_PANELS'; - controlPanels: ControlPanels | null; - } - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent - | DoneInvokeEvent; - -const PanelRT = rt.type({ - order: rt.number, - width: rt.union([rt.literal('medium'), rt.literal('small'), rt.literal('large')]), - grow: rt.boolean, - type: rt.string, - explicitInput: rt.intersection([ - rt.type({ id: rt.string }), - rt.partial({ - dataViewId: rt.string, - fieldName: rt.string, - title: rt.union([rt.string, rt.undefined]), - selectedOptions: rt.array(rt.string), - }), - ]), -}); - -export const ControlPanelRT = rt.record(rt.string, PanelRT); - -export type ControlPanels = rt.TypeOf; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts deleted file mode 100644 index b84cce4dbf2cf..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/url_state_storage_service.ts +++ /dev/null @@ -1,284 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { InvokeCreator } from 'xstate'; -import { pick, mapValues } from 'lodash'; -import deepEqual from 'fast-deep-equal'; -import { DiscoverAppState, DiscoverStateContainer } from '@kbn/discover-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table'; -import { - AllDatasetSelection, - decodeDatasetSelectionId, - hydrateDatasetSelection, - isDatasetSelection, -} from '../../../../common/dataset_selection'; -import { - DATA_GRID_COLUMNS_PREFERENCES, - DATA_GRID_DEFAULT_COLUMNS, - LOG_LEVEL_FIELD, -} from '../../../../common/constants'; -import { - ControlPanelRT, - ControlPanels, - LogExplorerProfileContext, - LogExplorerProfileEvent, -} from './types'; -import { - availableControlPanelFields, - controlPanelConfigs, - CONTROL_PANELS_URL_KEY, -} from './defaults'; - -interface LogExplorerProfileUrlStateDependencies { - stateContainer: DiscoverStateContainer; -} - -export const listenUrlChange = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - (context) => - (send) => { - const unsubscribe = stateContainer.appState.subscribe((nextState) => { - const { index } = nextState; - const prevIndex = stateContainer.appState.getPrevious().index; - - // Preventing update if the index didn't change - if (prevIndex === index) return; - - try { - const datasetSelection = extractDatasetSelectionFromIndex({ index, context }); - - if (isDatasetSelection(datasetSelection)) { - send({ type: 'UPDATE_DATASET_SELECTION', data: datasetSelection }); - } - } catch (error) { - send({ type: 'DATASET_SELECTION_RESTORE_FAILURE' }); - } - }); - - return () => unsubscribe(); - }; - -export const initializeFromUrl = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context) => { - const { index } = stateContainer.appState.getState(); - - return extractDatasetSelectionFromIndex({ index, context }); - }; - -export const initializeControlPanels = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context) => { - const urlPanels = stateContainer.stateStorage.get(CONTROL_PANELS_URL_KEY); - const controlPanelsWithId = constructControlPanelsWithDataViewId(stateContainer, urlPanels!); - - return controlPanelsWithId; - }; - -const extractDatasetSelectionFromIndex = ({ - index, - context, -}: { - index?: string; - context: LogExplorerProfileContext; -}) => { - // If the index parameter doesn't exists, use initialContext value or fallback to AllDatasetSelection - if (!index) { - return context.datasetSelection ?? AllDatasetSelection.create(); - } - - const rawDatasetSelection = decodeDatasetSelectionId(index); - const datasetSelection = hydrateDatasetSelection(rawDatasetSelection); - - return datasetSelection; -}; - -export const subscribeControlGroup = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - (context) => - (send) => { - if (!('controlGroupAPI' in context)) return; - - const filtersSubscription = context.controlGroupAPI.onFiltersPublished$.subscribe( - (newFilters) => { - stateContainer.internalState.transitions.setCustomFilters(newFilters); - stateContainer.actions.fetchData(); - } - ); - - // Keeps our state in sync with the url changes and makes sure it adheres to correct schema - const urlSubscription = stateContainer.stateStorage - .change$(CONTROL_PANELS_URL_KEY) - .subscribe((controlPanels) => { - if (!deepEqual(controlPanels, context.controlPanels)) { - send({ type: 'UPDATE_CONTROL_PANELS', controlPanels }); - } - }); - - // Keeps the url in sync with the controls state after change - const inputSubscription = context.controlGroupAPI.getInput$().subscribe(({ panels }) => { - if (!deepEqual(panels, context.controlPanels)) { - send({ type: 'UPDATE_CONTROL_PANELS', controlPanels: panels }); - } - }); - - return () => { - filtersSubscription.unsubscribe(); - urlSubscription.unsubscribe(); - inputSubscription.unsubscribe(); - }; - }; - -export const updateControlPanels = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async (context, event) => { - if (!('controlGroupAPI' in context)) return; - - const newControlPanels = - ('controlPanels' in event && event.controlPanels) || context.controlPanels; - const controlPanelsWithId = constructControlPanelsWithDataViewId( - stateContainer, - newControlPanels! - ); - - context.controlGroupAPI.updateInput({ panels: controlPanelsWithId }); - - return controlPanelsWithId; - }; - -export const updateStateContainer = - ({ - stateContainer, - }: LogExplorerProfileUrlStateDependencies): InvokeCreator< - LogExplorerProfileContext, - LogExplorerProfileEvent - > => - async () => { - const { breakdownField, columns, grid, rowHeight } = stateContainer.appState.getState(); - const stateUpdates: DiscoverAppState = {}; - - // Update data grid columns list - const shouldSetDefaultColumns = - stateContainer.appState.isEmptyURL() || !columns || columns.length === 0; - if (shouldSetDefaultColumns) { - stateUpdates.columns = DATA_GRID_DEFAULT_COLUMNS; - } - - // Configure DataGrid columns preferences - const initialColumnsPreferences = grid?.columns ?? {}; - stateUpdates.grid = { - columns: { ...DATA_GRID_COLUMNS_PREFERENCES, ...initialColumnsPreferences }, - }; - - // Configure rowHeight preference - stateUpdates.rowHeight = rowHeight ?? ROWS_HEIGHT_OPTIONS.single; - - // Configure breakdown field preference - stateUpdates.breakdownField = breakdownField ?? LOG_LEVEL_FIELD; - - // Finally batch update state app state - stateContainer.appState.update(stateUpdates, true); - }; - -/** - * Utils - */ - -const constructControlPanelsWithDataViewId = ( - stateContainer: DiscoverStateContainer, - newControlPanels: ControlPanels -) => { - const dataView = stateContainer.internalState.getState().dataView!; - - const validatedControlPanels = isValidState(newControlPanels) - ? newControlPanels - : getVisibleControlPanelsConfig(dataView); - - const controlsPanelsWithId = mergeDefaultPanelsWithUrlConfig(dataView, validatedControlPanels!); - - if (!deepEqual(controlsPanelsWithId, stateContainer.stateStorage.get(CONTROL_PANELS_URL_KEY))) { - stateContainer.stateStorage.set( - CONTROL_PANELS_URL_KEY, - cleanControlPanels(controlsPanelsWithId), - { replace: true } - ); - } - - return controlsPanelsWithId; -}; - -const isValidState = (state: ControlPanels | undefined | null): boolean => { - return Object.keys(state ?? {}).length > 0 && ControlPanelRT.is(state); -}; - -const getVisibleControlPanels = (dataView: DataView | undefined) => - availableControlPanelFields.filter( - (panelKey) => dataView?.fields.getByName(panelKey) !== undefined - ); - -export const getVisibleControlPanelsConfig = (dataView?: DataView) => { - return getVisibleControlPanels(dataView).reduce((panelsMap, panelKey) => { - const config = controlPanelConfigs[panelKey]; - - return { ...panelsMap, [panelKey]: config }; - }, {} as ControlPanels); -}; - -const addDataViewIdToControlPanels = (controlPanels: ControlPanels, dataViewId: string = '') => { - return mapValues(controlPanels, (controlPanelConfig) => ({ - ...controlPanelConfig, - explicitInput: { ...controlPanelConfig.explicitInput, dataViewId }, - })); -}; - -const cleanControlPanels = (controlPanels: ControlPanels) => { - return mapValues(controlPanels, (controlPanelConfig) => { - const { explicitInput } = controlPanelConfig; - const { dataViewId, ...rest } = explicitInput; - return { ...controlPanelConfig, explicitInput: rest }; - }); -}; - -const mergeDefaultPanelsWithUrlConfig = (dataView: DataView, urlPanels: ControlPanels) => { - // Get default panel configs from existing fields in data view - const visiblePanels = getVisibleControlPanelsConfig(dataView); - - // Get list of panel which can be overridden to avoid merging additional config from url - const existingKeys = Object.keys(visiblePanels); - const controlPanelsToOverride = pick(urlPanels, existingKeys); - - // Merge default and existing configs and add dataView.id to each of them - return addDataViewIdToControlPanels( - { ...visiblePanels, ...controlPanelsToOverride }, - dataView.id - ); -}; diff --git a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts b/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts deleted file mode 100644 index c7907cb358e0c..0000000000000 --- a/x-pack/plugins/log_explorer/public/state_machines/log_explorer_profile/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LogExplorerProfileStateService } from './state_machine'; -import { LogExplorerProfileStateValue } from './types'; - -export const waitForState = ( - service: LogExplorerProfileStateService, - targetState: LogExplorerProfileStateValue -) => { - return new Promise((resolve) => { - const { unsubscribe } = service.subscribe((state) => { - if (state.matches(targetState)) { - resolve(state); - unsubscribe(); - } - }); - }); -}; diff --git a/x-pack/plugins/log_explorer/public/types.ts b/x-pack/plugins/log_explorer/public/types.ts index e07b8fdb14b3f..32fcf5315e27c 100644 --- a/x-pack/plugins/log_explorer/public/types.ts +++ b/x-pack/plugins/log_explorer/public/types.ts @@ -6,18 +6,22 @@ */ import type { ComponentType } from 'react'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; +import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public'; -import { SharePluginSetup } from '@kbn/share-plugin/public'; -import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; -import { LogExplorerLocators } from '../common/locators'; +import type { SharePluginSetup } from '@kbn/share-plugin/public'; +import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; +import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; +import type { LogExplorerLocators } from '../common/locators'; import type { LogExplorerProps } from './components/log_explorer'; +import type { CreateLogExplorerController } from './controller'; export interface LogExplorerPluginSetup { locators: LogExplorerLocators; } export interface LogExplorerPluginStart { LogExplorer: ComponentType; + createLogExplorerController: CreateLogExplorerController; } export interface LogExplorerSetupDeps { @@ -30,4 +34,6 @@ export interface LogExplorerStartDeps { dataViews: DataViewsPublicPluginStart; discover: DiscoverStart; fieldFormats: FieldFormatsStart; + navigation: NavigationPublicPluginStart; + unifiedSearch: UnifiedSearchPublicPluginStart; } diff --git a/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts b/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts new file mode 100644 index 0000000000000..dea02a0bec002 --- /dev/null +++ b/x-pack/plugins/log_explorer/public/utils/convert_discover_app_state.ts @@ -0,0 +1,73 @@ +/* + * 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 { QueryState } from '@kbn/data-plugin/public'; +import { DiscoverAppState } from '@kbn/discover-plugin/public'; +import { cloneDeep } from 'lodash'; +import { + ChartDisplayOptions, + DisplayOptions, + GridColumnDisplayOptions, + GridRowsDisplayOptions, +} from '../../common'; + +export const getGridColumnDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): GridColumnDisplayOptions[] | undefined => + discoverAppState.columns?.map((field) => ({ + field, + width: discoverAppState.grid?.columns?.[field]?.width, + })); + +export const getGridRowsDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): Partial => ({ + ...(discoverAppState.rowHeight != null ? { rowHeight: discoverAppState.rowHeight } : {}), + ...(discoverAppState.rowsPerPage != null ? { rowsPerPage: discoverAppState.rowsPerPage } : {}), +}); + +export const getChartDisplayOptionsFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): Partial => ({ + breakdownField: discoverAppState.breakdownField ?? null, +}); + +export const getQueryStateFromDiscoverAppState = ( + discoverAppState: DiscoverAppState +): QueryState => ({ + query: discoverAppState.query, + filters: discoverAppState.filters, +}); + +export const getDiscoverAppStateFromContext = ( + displayOptions: DisplayOptions & QueryState +): Partial => ({ + breakdownField: displayOptions.chart.breakdownField ?? undefined, + columns: getDiscoverColumnsFromDisplayOptions(displayOptions), + grid: getDiscoverGridFromDisplayOptions(displayOptions), + rowHeight: displayOptions.grid.rows.rowHeight, + rowsPerPage: displayOptions.grid.rows.rowsPerPage, + query: cloneDeep(displayOptions.query), + filters: cloneDeep(displayOptions.filters), +}); + +export const getDiscoverColumnsFromDisplayOptions = ( + displayOptions: DisplayOptions +): DiscoverAppState['columns'] => displayOptions.grid.columns.map(({ field }) => field); + +export const getDiscoverGridFromDisplayOptions = ( + displayOptions: DisplayOptions +): DiscoverAppState['grid'] => ({ + columns: displayOptions.grid.columns.reduce< + NonNullable['columns']> + >((gridColumns, { field, width }) => { + if (width != null) { + gridColumns[field] = { width }; + } + return gridColumns; + }, {}), +}); diff --git a/x-pack/plugins/log_explorer/tsconfig.json b/x-pack/plugins/log_explorer/tsconfig.json index a36d764cbee56..b236e7f03ced3 100644 --- a/x-pack/plugins/log_explorer/tsconfig.json +++ b/x-pack/plugins/log_explorer/tsconfig.json @@ -3,31 +3,43 @@ "compilerOptions": { "outDir": "target/types" }, - "include": ["../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*", ".storybook/**/*.tsx"], + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + ".storybook/**/*.tsx" + ], "kbn_references": [ + "@kbn/controls-plugin", "@kbn/core", + "@kbn/core-application-browser", + "@kbn/core-http-browser", + "@kbn/core-ui-settings-browser", + "@kbn/custom-icons", + "@kbn/data-plugin", + "@kbn/data-views-plugin", + "@kbn/deeplinks-observability", "@kbn/discover-plugin", + "@kbn/discover-utils", + "@kbn/elastic-agent-utils", + "@kbn/embeddable-plugin", + "@kbn/es-query", + "@kbn/field-formats-plugin", + "@kbn/fleet-plugin", "@kbn/i18n", "@kbn/i18n-react", - "@kbn/fleet-plugin", "@kbn/io-ts-utils", - "@kbn/data-views-plugin", - "@kbn/rison", - "@kbn/controls-plugin", - "@kbn/embeddable-plugin", - "@kbn/es-query", "@kbn/kibana-react-plugin", - "@kbn/data-plugin", - "@kbn/unified-field-list", - "@kbn/core-application-browser", + "@kbn/kibana-utils-plugin", + "@kbn/navigation-plugin", "@kbn/share-plugin", "@kbn/unified-data-table", - "@kbn/core-ui-settings-browser", - "@kbn/discover-utils", - "@kbn/deeplinks-observability", - "@kbn/field-formats-plugin", - "@kbn/custom-icons", - "@kbn/elastic-agent-utils" + "@kbn/unified-field-list", + "@kbn/unified-search-plugin", + "@kbn/xstate-utils" ], - "exclude": ["target/**/*"] + "exclude": [ + "target/**/*" + ] } diff --git a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx index 8d4d45a08d709..cd3459975d011 100644 --- a/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx +++ b/x-pack/plugins/observability/public/pages/slo_edit/slo_edit.test.tsx @@ -128,6 +128,8 @@ describe('SLO Edit Page', () => { const mockCreate = jest.fn(); const mockUpdate = jest.fn(); + const history = createBrowserHistory(); + beforeEach(() => { jest.clearAllMocks(); mockKibana(); @@ -136,9 +138,8 @@ describe('SLO Edit Page', () => { jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(console, 'error').mockImplementation(() => {}); - const history = createBrowserHistory(); history.replace(''); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); + jest.spyOn(Router, 'useHistory').mockReturnValue(history); useFetchDataViewsMock.mockReturnValue({ isLoading: false, @@ -256,11 +257,9 @@ describe('SLO Edit Page', () => { it('prefills the form with values from URL', () => { jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined }); - const history = createBrowserHistory(); history.replace( '/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))' ); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); jest .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); @@ -336,11 +335,9 @@ describe('SLO Edit Page', () => { const slo = buildSlo({ id: '123' }); jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' }); - const history = createBrowserHistory(); history.push( '/slos/123/edit?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))' ); - jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history); jest .spyOn(Router, 'useLocation') .mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' }); diff --git a/x-pack/plugins/observability_log_explorer/common/index.ts b/x-pack/plugins/observability_log_explorer/common/index.ts index f7639f742e67e..f94c83117e7e8 100644 --- a/x-pack/plugins/observability_log_explorer/common/index.ts +++ b/x-pack/plugins/observability_log_explorer/common/index.ts @@ -10,3 +10,5 @@ export { SingleDatasetLocatorDefinition, AllDatasetsLocatorDefinition, } from './locators'; +export { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from './url_schema'; +export { deepCompactObject } from './utils/deep_compact_object'; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts b/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts index 17c8b2ae02047..41cf67546c78d 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/all_datasets/all_datasets_locator.ts @@ -23,11 +23,10 @@ export class AllDatasetsLocatorDefinition implements LocatorDefinition { const { useHash } = this.deps; - const index = AllDatasetSelection.create().toDataviewSpec().id; return constructLocatorPath({ + datasetSelection: AllDatasetSelection.create().toPlainSelection(), locatorParams: params, - index, useHash, }); }; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts b/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts index d26ff4e133ea6..ff12691e8c2fd 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/locators.test.ts @@ -5,13 +5,11 @@ * 2.0. */ -import { FilterStateStore } from '@kbn/es-query'; -import { getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { AllDatasetsLocatorParams, SingleDatasetLocatorParams, } from '@kbn/deeplinks-observability/locators'; -import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability'; import { AllDatasetsLocatorDefinition } from './all_datasets/all_datasets_locator'; import { SingleDatasetLocatorDefinition } from './single_dataset'; import { DatasetLocatorDependencies } from './types'; @@ -38,8 +36,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation({}); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),v:1)', state: {}, }); }); @@ -53,8 +51,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),time:(from:now-30m,to:now),v:1)', state: {}, }); }); @@ -70,8 +68,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,query:(language:kuery,query:foo))', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),query:(language:kuery,query:foo),v:1)', state: {}, }); }); @@ -88,29 +86,28 @@ describe('Observability Logs Explorer Locators', () => { const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: '/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)', + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: '/?pageState=(datasetSelection:(selectionType:all),refreshInterval:(pause:!f,value:666),v:1)', state: {}, }); }); - it('should allow specifiying columns and sort', async () => { + it('should allow specifiying columns', async () => { const params: AllDatasetsLocatorParams = { columns: ['_source'], - sort: [['timestamp, asc']] as string[][], }; const { allDatasetsLocator } = await setup(); const location = await allDatasetsLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,sort:!(!('timestamp,%20asc')))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`, state: {}, }); }); - it('should allow specifiying filters', async () => { + it('should allow specifying filters', async () => { const params: AllDatasetsLocatorParams = { filters: [ { @@ -119,9 +116,6 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.APP_STATE, - }, }, { meta: { @@ -129,47 +123,16 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, }, ], }; const { allDatasetsLocator } = await setup(); - const { path } = await allDatasetsLocator.getLocation(params); - - const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false }); + const location = await allDatasetsLocator.getLocation(params); - expect(_a).toEqual({ - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - alias: 'foo', - disabled: false, - negate: false, - }, - }, - ], - index: 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA', - }); - expect(_g).toEqual({ - filters: [ - { - $state: { - store: 'globalState', - }, - meta: { - alias: 'bar', - disabled: false, - negate: false, - }, - }, - ], - }); + expect(location.path).toMatchInlineSnapshot( + `"/?pageState=(datasetSelection:(selectionType:all),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"` + ); }); }); @@ -184,8 +147,8 @@ describe('Observability Logs Explorer Locators', () => { }); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, state: {}, }); }); @@ -201,8 +164,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),time:(from:now-30m,to:now),v:1)`, state: {}, }); }); @@ -221,8 +184,8 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,query:(language:kuery,query:foo))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),query:(language:kuery,query:foo),v:1)`, state: {}, }); }); @@ -241,26 +204,25 @@ describe('Observability Logs Explorer Locators', () => { const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),refreshInterval:(pause:!f,value:666),v:1)`, state: {}, }); }); - it('should allow specifiying columns and sort', async () => { + it('should allow specifiying columns', async () => { const params: SingleDatasetLocatorParams = { integration, dataset, columns: ['_source'], - sort: [['timestamp, asc']] as string[][], }; const { singleDatasetLocator } = await setup(); const location = await singleDatasetLocator.getLocation(params); expect(location).toMatchObject({ - app: OBSERVABILITY_LOG_EXPLORER, - path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,sort:!(!('timestamp,%20asc')))`, + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`, state: {}, }); }); @@ -276,9 +238,6 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.APP_STATE, - }, }, { meta: { @@ -286,48 +245,16 @@ describe('Observability Logs Explorer Locators', () => { disabled: false, negate: false, }, - $state: { - store: FilterStateStore.GLOBAL_STATE, - }, }, ], }; const { singleDatasetLocator } = await setup(); - const { path } = await singleDatasetLocator.getLocation(params); - - const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false }); + const location = await singleDatasetLocator.getLocation(params); - expect(_a).toEqual({ - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - alias: 'foo', - disabled: false, - negate: false, - }, - }, - ], - index: - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA', - }); - expect(_g).toEqual({ - filters: [ - { - $state: { - store: 'globalState', - }, - meta: { - alias: 'bar', - disabled: false, - negate: false, - }, - }, - ], - }); + expect(location.path).toMatchInlineSnapshot( + `"/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"` + ); }); }); }); diff --git a/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts b/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts index 632d0d8d93de6..7f1ed847a9094 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/single_dataset/single_dataset_locator.ts @@ -35,11 +35,9 @@ export class SingleDatasetLocatorDefinition }, }); - const index = unresolvedDatasetSelection.toDataviewSpec().id; - return constructLocatorPath({ + datasetSelection: unresolvedDatasetSelection.toPlainSelection(), locatorParams: params, - index, useHash, }); }; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/types.ts b/x-pack/plugins/observability_log_explorer/common/locators/types.ts index a181fdce10a02..25228c1f56c04 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/types.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/types.ts @@ -5,16 +5,6 @@ * 2.0. */ -import { AggregateQuery, Filter, Query } from '@kbn/es-query'; - -export interface AppState { - index?: string; - query?: Query | AggregateQuery; - filters?: Filter[]; - columns?: string[]; - sort?: string[][]; -} - export interface DatasetLocatorDependencies { useHash: boolean; } diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts new file mode 100644 index 0000000000000..57c5cb018a77d --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/locators/utils/construct_locator_path.ts @@ -0,0 +1,89 @@ +/* + * 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 { + DatasetLocatorParams, + FilterControls, + ListFilterControl, +} from '@kbn/deeplinks-observability/locators'; +import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; +import { + AvailableControlPanels, + availableControlsPanels, + DatasetSelectionPlain, +} from '@kbn/log-explorer-plugin/common'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; +import { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from '../../url_schema'; +import { deepCompactObject } from '../../utils/deep_compact_object'; + +type ControlsPageState = NonNullable; + +interface LocatorPathConstructionParams { + datasetSelection: DatasetSelectionPlain; + locatorParams: DatasetLocatorParams; + useHash: boolean; +} + +export const constructLocatorPath = async (params: LocatorPathConstructionParams) => { + const { + datasetSelection, + locatorParams: { filterControls, filters, query, refreshInterval, timeRange, columns, origin }, + useHash, + } = params; + + const pageState = urlSchemaV1.urlSchemaRT.encode( + deepCompactObject({ + v: 1, + datasetSelection, + filters, + query, + refreshInterval, + time: timeRange, + columns: columns?.map((field) => ({ field })), + controls: getControlsPageStateFromFilterControlsParams(filterControls ?? {}), + }) + ); + + const path = setStateToKbnUrl( + OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, + pageState, + { useHash, storeInHashQuery: false }, + '/' + ); + + return { + app: OBSERVABILITY_LOG_EXPLORER_APP_ID, + path, + state: { + ...(origin ? { origin } : {}), + }, + }; +}; + +const getControlsPageStateFromFilterControlsParams = ( + filterControls: FilterControls +): ControlsPageState => ({ + ...(filterControls.namespace != null + ? getFilterControlPageStateFromListFilterControlsParams( + availableControlsPanels.NAMESPACE, + filterControls.namespace + ) + : {}), +}); + +const getFilterControlPageStateFromListFilterControlsParams = ( + controlId: AvailableControlPanels[keyof AvailableControlPanels], + listFilterControl: ListFilterControl +): ControlsPageState => ({ + [controlId]: { + mode: listFilterControl.mode, + selection: { + type: 'options', + selectedOptions: listFilterControl.values, + }, + }, +}); diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts deleted file mode 100644 index 5a93709e76522..0000000000000 --- a/x-pack/plugins/observability_log_explorer/common/locators/utils/helpers.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public'; -import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common'; -import { DatasetLocatorParams } from '@kbn/deeplinks-observability/locators'; -import { AppState } from '../types'; - -interface LocatorPathCosntructionParams { - locatorParams: DatasetLocatorParams; - index: string; - useHash: boolean; -} - -export const constructLocatorPath = async (params: LocatorPathCosntructionParams) => { - const { isFilterPinned } = await import('@kbn/es-query'); - - const { - locatorParams: { filters, query, refreshInterval, timeRange, columns, sort, origin }, - index, - useHash, - } = params; - const appState: AppState = {}; - const queryState: GlobalQueryStateFromUrl = {}; - - // App state - if (index) appState.index = index; - if (query) appState.query = query; - if (filters && filters.length) appState.filters = filters?.filter((f) => !isFilterPinned(f)); - if (columns) appState.columns = columns; - if (sort) appState.sort = sort; - - // Global State - if (timeRange) queryState.time = timeRange; - if (filters && filters.length) queryState.filters = filters?.filter((f) => isFilterPinned(f)); - if (refreshInterval) queryState.refreshInterval = refreshInterval; - - let path = '/'; - - if (Object.keys(queryState).length) { - path = setStateToKbnUrl( - '_g', - queryState, - { useHash, storeInHashQuery: false }, - path - ); - } - - path = setStateToKbnUrl('_a', appState, { useHash, storeInHashQuery: false }, path); - - return { - app: 'observability-log-explorer', - path, - state: { - ...(origin ? { origin } : {}), - }, - }; -}; diff --git a/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts b/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts index 6c315f929b9bb..6e5aad44fbe9a 100644 --- a/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts +++ b/x-pack/plugins/observability_log_explorer/common/locators/utils/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './helpers'; +export * from './construct_locator_path'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/common.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/common.ts new file mode 100644 index 0000000000000..fa3b3f72383d1 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/common.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 const OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY = 'pageState'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts new file mode 100644 index 0000000000000..d8f53e47b6058 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/index.ts @@ -0,0 +1,9 @@ +/* + * 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 { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY } from './common'; +export * as urlSchemaV1 from './url_schema_v1'; diff --git a/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts b/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts new file mode 100644 index 0000000000000..c4f611466a4f6 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/url_schema/url_schema_v1.ts @@ -0,0 +1,122 @@ +/* + * 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 { availableControlsPanels, datasetSelectionPlainRT } from '@kbn/log-explorer-plugin/common'; +import * as rt from 'io-ts'; + +export const columnRT = rt.intersection([ + rt.strict({ + field: rt.string, + }), + rt.exact( + rt.partial({ + width: rt.number, + }) + ), +]); + +export const columnsRT = rt.array(columnRT); + +export const optionsListControlRT = rt.strict({ + mode: rt.keyof({ + exclude: null, + include: null, + }), + selection: rt.union([ + rt.strict({ + type: rt.literal('exists'), + }), + rt.strict({ + type: rt.literal('options'), + selectedOptions: rt.array(rt.string), + }), + ]), +}); + +export const controlsRT = rt.exact( + rt.partial({ + [availableControlsPanels.NAMESPACE]: optionsListControlRT, + }) +); + +export const filterMetaRT = rt.partial({ + alias: rt.union([rt.string, rt.null]), + disabled: rt.boolean, + negate: rt.boolean, + controlledBy: rt.string, + group: rt.string, + index: rt.string, + isMultiIndex: rt.boolean, + type: rt.string, + key: rt.string, + params: rt.any, + value: rt.any, +}); + +export const filterRT = rt.intersection([ + rt.strict({ + meta: filterMetaRT, + }), + rt.exact( + rt.partial({ + query: rt.UnknownRecord, + }) + ), +]); + +export const filtersRT = rt.array(filterRT); + +const queryRT = rt.union([ + rt.strict({ + language: rt.string, + query: rt.union([rt.string, rt.record(rt.string, rt.unknown)]), + }), + rt.strict({ + sql: rt.string, + }), + rt.strict({ + esql: rt.string, + }), +]); + +const timeRangeRT = rt.intersection([ + rt.strict({ + from: rt.string, + to: rt.string, + }), + rt.exact( + rt.partial({ + mode: rt.keyof({ + absolute: null, + relative: null, + }), + }) + ), +]); + +const refreshIntervalRT = rt.strict({ + pause: rt.boolean, + value: rt.number, +}); + +export const urlSchemaRT = rt.exact( + rt.partial({ + v: rt.literal(1), + breakdownField: rt.union([rt.string, rt.null]), + columns: columnsRT, + datasetSelection: datasetSelectionPlainRT, + filters: filtersRT, + query: queryRT, + refreshInterval: refreshIntervalRT, + rowHeight: rt.number, + rowsPerPage: rt.number, + time: timeRangeRT, + controls: controlsRT, + }) +); + +export type UrlSchema = rt.TypeOf; diff --git a/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts b/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts new file mode 100644 index 0000000000000..eed8b1b83e1f4 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/common/utils/deep_compact_object.ts @@ -0,0 +1,15 @@ +/* + * 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 { isEmpty, isPlainObject, isUndefined } from 'lodash'; + +export const deepCompactObject = >(obj: Value): Value => + Object.fromEntries( + Object.entries(obj) + .map(([key, value]) => [key, isPlainObject(value) ? deepCompactObject(value) : value]) + .filter(([, value]) => !isUndefined(value) && !(isPlainObject(value) && isEmpty(value))) + ); diff --git a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx index 8a49db7536350..a823ad1a840cd 100644 --- a/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx +++ b/x-pack/plugins/observability_log_explorer/public/applications/observability_log_explorer.tsx @@ -10,12 +10,13 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import React from 'react'; import ReactDOM from 'react-dom'; -import { DatasetQualityRoute, ObservablityLogExplorerMainRoute } from '../routes/main'; +import { DatasetQualityRoute, ObservabilityLogExplorerMainRoute } from '../routes/main'; import { ObservabilityLogExplorerAppMountParameters, ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps, } from '../types'; +import { KbnUrlStateStorageFromRouterProvider } from '../utils/kbn_url_state_context'; import { useKibanaContextForPluginProvider } from '../utils/use_kibana'; export const renderObservabilityLogExplorer = ( @@ -59,26 +60,25 @@ export const ObservabilityLogExplorerApp = ({ const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider( core, plugins, - pluginStart + pluginStart, + appParams ); return ( - - - } - /> - } - /> - - + + + + } /> + } + /> + + + ); diff --git a/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx new file mode 100644 index 0000000000000..2d12de11731c0 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/discover_link.tsx @@ -0,0 +1,111 @@ +/* + * 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 { EuiHeaderLink } from '@elastic/eui'; +import { DiscoverAppLocatorParams } from '@kbn/discover-plugin/common'; +import { DiscoverStart } from '@kbn/discover-plugin/public'; +import { hydrateDatasetSelection } from '@kbn/log-explorer-plugin/common'; +import { getDiscoverColumnsFromDisplayOptions } from '@kbn/log-explorer-plugin/public'; +import { MatchedStateFromActor } from '@kbn/xstate-utils'; +import { useActor } from '@xstate/react'; +import React, { useMemo } from 'react'; +import { discoverLinkTitle } from '../../common/translations'; +import { + ObservabilityLogExplorerService, + useObservabilityLogExplorerPageStateContext, +} from '../state_machines/observability_log_explorer/src'; +import { getRouterLinkProps } from '../utils/get_router_link_props'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +export const ConnectedDiscoverLink = React.memo(() => { + const { + services: { discover }, + } = useKibanaContextForPlugin(); + + const [pageState] = useActor(useObservabilityLogExplorerPageStateContext()); + + if (pageState.matches({ initialized: 'validLogExplorerState' })) { + return ; + } else { + return ; + } +}); + +type InitializedPageState = MatchedStateFromActor< + ObservabilityLogExplorerService, + { initialized: 'validLogExplorerState' } +>; + +export const DiscoverLinkForValidState = React.memo( + ({ + discover, + pageState: { + context: { logExplorerState }, + }, + }: { + discover: DiscoverStart; + pageState: InitializedPageState; + }) => { + const discoverLinkParams = useMemo( + () => ({ + breakdownField: logExplorerState.chart.breakdownField ?? undefined, + columns: getDiscoverColumnsFromDisplayOptions(logExplorerState), + filters: logExplorerState.filters, + query: logExplorerState.query, + refreshInterval: logExplorerState.refreshInterval, + timeRange: logExplorerState.time, + dataViewSpec: hydrateDatasetSelection(logExplorerState.datasetSelection).toDataviewSpec(), + }), + [logExplorerState] + ); + + return ; + } +); + +export const DiscoverLinkForUnknownState = React.memo(() => ( + + {discoverLinkTitle} + +)); + +export const DiscoverLink = React.memo( + ({ + discover, + discoverLinkParams, + }: { + discover: DiscoverStart; + discoverLinkParams: DiscoverAppLocatorParams; + }) => { + const discoverUrl = discover.locator?.getRedirectUrl(discoverLinkParams); + + const navigateToDiscover = () => { + discover.locator?.navigate(discoverLinkParams); + }; + + const discoverLinkProps = getRouterLinkProps({ + href: discoverUrl, + onClick: navigateToDiscover, + }); + + return ( + + {discoverLinkTitle} + + ); + } +); diff --git a/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx new file mode 100644 index 0000000000000..efd37f8ccd3d4 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/feedback_link.tsx @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common'; +import React from 'react'; +import { feedbackLinkTitle } from '../../common/translations'; + +export const FeedbackLink = React.memo(() => { + return ( + + {feedbackLinkTitle} + + ); +}); diff --git a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx index c627f0760bb2d..6a76caae25406 100644 --- a/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx +++ b/x-pack/plugins/observability_log_explorer/public/components/log_explorer_top_nav_menu.tsx @@ -5,74 +5,39 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; -import useObservable from 'react-use/lib/useObservable'; -import { type BehaviorSubject, distinctUntilChanged, filter, take } from 'rxjs'; -import styled from '@emotion/styled'; -import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; import { EuiBetaBadge, - EuiButton, EuiHeader, - EuiHeaderLink, EuiHeaderLinks, EuiHeaderSection, EuiHeaderSectionItem, } from '@elastic/eui'; -import { LogExplorerStateContainer } from '@kbn/log-explorer-plugin/public'; -import { - OBSERVABILITY_ONBOARDING_LOCATOR, - ObservabilityOnboardingLocatorParams, -} from '@kbn/deeplinks-observability/locators'; -import { KibanaReactContextValue } from '@kbn/kibana-react-plugin/public'; -import { toMountPoint } from '@kbn/react-kibana-mount'; import { css } from '@emotion/react'; -import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common'; +import styled from '@emotion/styled'; +import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public'; +import { toMountPoint } from '@kbn/react-kibana-mount'; import { euiThemeVars } from '@kbn/ui-theme'; import { LogExplorerTabs } from '@kbn/discover-plugin/public'; -import { PluginKibanaContextValue } from '../utils/use_kibana'; -import { - betaBadgeDescription, - betaBadgeTitle, - discoverLinkTitle, - feedbackLinkTitle, - onboardingLinkTitle, -} from '../../common/translations'; -import { getRouterLinkProps } from '../utils/get_router_link_props'; -import { ObservabilityLogExplorerAppMountParameters } from '../types'; - -interface LogExplorerTopNavMenuProps { - setHeaderActionMenu: ObservabilityLogExplorerAppMountParameters['setHeaderActionMenu']; - services: KibanaReactContextValue['services']; - state$: BehaviorSubject; - theme$: ObservabilityLogExplorerAppMountParameters['theme$']; -} - -export const LogExplorerTopNavMenu = ({ - setHeaderActionMenu, - services, - state$, - theme$, -}: LogExplorerTopNavMenuProps) => { - const { serverless } = services; - - return Boolean(serverless) ? ( - - ) : ( - - ); +import React, { useEffect, useState } from 'react'; +import useObservable from 'react-use/lib/useObservable'; +import { filter, take } from 'rxjs'; +import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; +import { ConnectedDiscoverLink } from './discover_link'; +import { FeedbackLink } from './feedback_link'; +import { ConnectedOnboardingLink } from './onboarding_link'; + +export const LogExplorerTopNavMenu = () => { + const { + services: { serverless }, + } = useKibanaContextForPlugin(); + + return Boolean(serverless) ? : ; }; -const ServerlessTopNav = ({ - services, - state$, -}: Pick) => { +const ServerlessTopNav = () => { + const { services } = useKibanaContextForPlugin(); + return ( @@ -97,38 +62,40 @@ const ServerlessTopNav = ({ - + - + ); }; -const StatefulTopNav = ({ - setHeaderActionMenu, - services, - state$, - theme$, -}: LogExplorerTopNavMenuProps) => { +const StatefulTopNav = () => { + const { + services: { + appParams: { setHeaderActionMenu }, + chrome, + i18n, + theme, + }, + } = useKibanaContextForPlugin(); + /** * Since the breadcrumbsAppendExtension might be set only during a plugin start (e.g. search session) * we retrieve the latest valid extension in order to restore it once we unmount the beta badge. */ const [previousAppendExtension$] = useState(() => - services.chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1)) + chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1)) ); const previousAppendExtension = useObservable(previousAppendExtension$); useEffect(() => { - const { chrome, i18n, theme } = services; - if (chrome) { chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( @@ -161,15 +128,15 @@ const StatefulTopNav = ({ chrome.setBreadcrumbsAppendExtension(previousAppendExtension); } }; - }, [services, previousAppendExtension]); + }, [chrome, i18n, previousAppendExtension, theme]); return ( - + - - + + @@ -177,113 +144,8 @@ const StatefulTopNav = ({ ); }; -const DiscoverLink = React.memo( - ({ services, state$ }: Pick) => { - const discoverLinkParams = useDiscoverLinkParams(state$); - const discoverUrl = services.discover.locator?.getRedirectUrl(discoverLinkParams); - - const navigateToDiscover = () => { - services.discover.locator?.navigate(discoverLinkParams); - }; - - const discoverLinkProps = getRouterLinkProps({ - href: discoverUrl, - onClick: navigateToDiscover, - }); - - return ( - - {discoverLinkTitle} - - ); - } -); - -const OnboardingLink = React.memo(({ services }: Pick) => { - const locator = services.share.url.locators.get( - OBSERVABILITY_ONBOARDING_LOCATOR - ); - - const onboardingUrl = locator?.useUrl({}); - - const navigateToOnboarding = () => { - locator?.navigate({}); - }; - - const onboardingLinkProps = getRouterLinkProps({ - href: onboardingUrl, - onClick: navigateToOnboarding, - }); - - return ( - - {onboardingLinkTitle} - - ); -}); - -const FeedbackLink = React.memo(() => { - return ( - - {feedbackLinkTitle} - - ); -}); - const VerticalRule = styled.span` width: 1px; height: 20px; background-color: ${euiThemeVars.euiColorLightShade}; `; - -const useDiscoverLinkParams = (state$: BehaviorSubject) => { - const { appState, logExplorerState } = useObservable( - state$.pipe( - distinctUntilChanged((prev, curr) => { - if (!prev.appState || !curr.appState) return false; - return deepEqual( - [ - prev.appState.columns, - prev.appState.sort, - prev.appState.filters, - prev.appState.index, - prev.appState.query, - ], - [ - curr.appState.columns, - curr.appState.sort, - curr.appState.filters, - curr.appState.index, - curr.appState.query, - ] - ); - }) - ), - { appState: {}, logExplorerState: {} } - ); - - return { - columns: appState?.columns, - sort: appState?.sort, - filters: appState?.filters, - query: appState?.query, - dataViewSpec: logExplorerState?.datasetSelection?.selection.dataset.toDataviewSpec(), - }; -}; diff --git a/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx b/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx new file mode 100644 index 0000000000000..abdc585c22768 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/components/onboarding_link.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiButton } from '@elastic/eui'; +import { + ObservabilityOnboardingLocatorParams, + OBSERVABILITY_ONBOARDING_LOCATOR, +} from '@kbn/deeplinks-observability/locators'; +import { BrowserUrlService } from '@kbn/share-plugin/public'; +import React from 'react'; +import { onboardingLinkTitle } from '../../common/translations'; +import { getRouterLinkProps } from '../utils/get_router_link_props'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; + +export const ConnectedOnboardingLink = React.memo(() => { + const { + services: { + share: { url }, + }, + } = useKibanaContextForPlugin(); + + return ; +}); + +export const OnboardingLink = React.memo(({ urlService }: { urlService: BrowserUrlService }) => { + const locator = urlService.locators.get( + OBSERVABILITY_ONBOARDING_LOCATOR + ); + + const onboardingUrl = locator?.useUrl({}); + + const navigateToOnboarding = () => { + locator?.navigate({}); + }; + + const onboardingLinkProps = getRouterLinkProps({ + href: onboardingUrl, + onClick: navigateToOnboarding, + }); + + return ( + + {onboardingLinkTitle} + + ); +}); diff --git a/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx index d128c6e8a7779..5b57333dd8c86 100644 --- a/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx +++ b/x-pack/plugins/observability_log_explorer/public/components/page_template.tsx @@ -7,23 +7,27 @@ import { EuiPageSectionProps } from '@elastic/eui'; import { css } from '@emotion/react'; -import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public'; import React from 'react'; +import { useKibanaContextForPlugin } from '../utils/use_kibana'; export const ObservabilityLogExplorerPageTemplate = ({ children, - observabilityShared, pageProps, }: React.PropsWithChildren<{ - observabilityShared: ObservabilitySharedPluginStart; pageProps?: EuiPageSectionProps; -}>) => ( - - {children} - -); +}>) => { + const { + services: { observabilityShared }, + } = useKibanaContextForPlugin(); + + return ( + + {children} + + ); +}; const fullHeightContentStyles = css` display: flex; diff --git a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx index 53d34a71a7237..7983c6ed39433 100644 --- a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx +++ b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/flyout_content.tsx @@ -13,6 +13,9 @@ import type { LogAIAssistantDocument } from '@kbn/logs-shared-plugin/public'; import React, { useMemo } from 'react'; import { useKibanaContextForPlugin } from '../utils/use_kibana'; +type RenderFlyoutContentCustomization = + Required['flyout']['renderContent']; + const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => { const { services } = useKibanaContextForPlugin(); const { LogAIAssistant } = services.logsShared; @@ -22,19 +25,17 @@ const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => return ; }; -export const renderFlyoutContent: Required['flyout']['renderContent'] = ( - renderPreviousContent, - props -) => { - return ( - <> - {renderPreviousContent()} - - - - - ); -}; +export const renderFlyoutContent: RenderFlyoutContentCustomization = + (renderPreviousContent) => (props) => { + return ( + <> + {renderPreviousContent(props)} + + + + + ); + }; /** * Utils diff --git a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts index a86b47e92cf01..02909efed0d81 100644 --- a/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts +++ b/x-pack/plugins/observability_log_explorer/public/log_explorer_customizations/index.ts @@ -5,11 +5,18 @@ * 2.0. */ -import { LogExplorerCustomizations } from '@kbn/log-explorer-plugin/public'; +import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; import { renderFlyoutContent } from './flyout_content'; -export const createLogExplorerCustomizations = (): LogExplorerCustomizations => ({ - flyout: { - renderContent: renderFlyoutContent, - }, -}); +export const createLogExplorerControllerWithCustomizations = + (createLogExplorerController: CreateLogExplorerController): CreateLogExplorerController => + (args) => + createLogExplorerController({ + ...args, + customizations: { + ...args.customizations, + flyout: { + renderContent: renderFlyoutContent, + }, + }, + }); diff --git a/x-pack/plugins/observability_log_explorer/public/plugin.ts b/x-pack/plugins/observability_log_explorer/public/plugin.ts index 5b5a640e2a6ae..6af6484e883b5 100644 --- a/x-pack/plugins/observability_log_explorer/public/plugin.ts +++ b/x-pack/plugins/observability_log_explorer/public/plugin.ts @@ -13,15 +13,14 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/public'; -import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability'; +import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; import { + AllDatasetsLocatorDefinition, ObservabilityLogExplorerLocators, SingleDatasetLocatorDefinition, - AllDatasetsLocatorDefinition, } from '../common/locators'; import { type ObservabilityLogExplorerConfig } from '../common/plugin_config'; import { logExplorerAppTitle } from '../common/translations'; -import { renderObservabilityLogExplorer } from './applications/observability_log_explorer'; import type { ObservabilityLogExplorerAppMountParameters, ObservabilityLogExplorerPluginSetup, @@ -48,7 +47,7 @@ export class ObservabilityLogExplorerPlugin const useHash = core.uiSettings.get('state:storeInSessionStorage'); core.application.register({ - id: OBSERVABILITY_LOG_EXPLORER, + id: OBSERVABILITY_LOG_EXPLORER_APP_ID, title: logExplorerAppTitle, category: DEFAULT_APP_CATEGORIES.observability, euiIconType: 'logoLogging', @@ -59,6 +58,9 @@ export class ObservabilityLogExplorerPlugin keywords: ['logs', 'log', 'explorer', 'logs explorer'], mount: async (appMountParams: ObservabilityLogExplorerAppMountParameters) => { const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices(); + const { renderObservabilityLogExplorer } = await import( + './applications/observability_log_explorer' + ); return renderObservabilityLogExplorer( coreStart, diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx index b76a462eba25d..a58df53c7b1a7 100644 --- a/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/dataset_quality_route.tsx @@ -19,7 +19,7 @@ export interface DatasetQualityRouteProps { export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => { const { services } = useKibanaContextForPlugin(); - const { observabilityShared, serverless, datasetQuality: DatasetQuality } = services; + const { serverless, datasetQuality: DatasetQuality } = services; const breadcrumb: EuiBreadcrumb[] = [ { text: datasetQualityAppTitle, @@ -29,13 +29,8 @@ export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => { useBreadcrumbs(breadcrumb, core.chrome, serverless); return ( - <> - - - - + + + ); }; diff --git a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx index e17f92a46c23b..333d79bc6dd3c 100644 --- a/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx +++ b/x-pack/plugins/observability_log_explorer/public/routes/main/main_route.tsx @@ -4,52 +4,106 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { CoreStart } from '@kbn/core/public'; -import React, { useMemo, useState } from 'react'; -import { BehaviorSubject } from 'rxjs'; +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { + LogExplorerController, + LogExplorerPluginStart, +} from '@kbn/log-explorer-plugin/public'; +import { useActor } from '@xstate/react'; +import React, { useMemo } from 'react'; import { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu'; import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template'; +import { createLogExplorerControllerWithCustomizations } from '../../log_explorer_customizations'; +import { + ObservabilityLogExplorerPageStateProvider, + useObservabilityLogExplorerPageStateContext, +} from '../../state_machines/observability_log_explorer/src'; +import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component'; +import { ObservabilityLogExplorerHistory } from '../../types'; import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs'; +import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; -import { ObservabilityLogExplorerAppMountParameters } from '../../types'; -import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component'; -import { createLogExplorerCustomizations } from '../../log_explorer_customizations'; -export interface ObservablityLogExplorerMainRouteProps { - appParams: ObservabilityLogExplorerAppMountParameters; - core: CoreStart; -} - -export const ObservablityLogExplorerMainRoute = ({ - appParams, - core, -}: ObservablityLogExplorerMainRouteProps) => { + +export const ObservabilityLogExplorerMainRoute = () => { const { services } = useKibanaContextForPlugin(); - const { logExplorer, observabilityShared, serverless } = services; - useBreadcrumbs(noBreadcrumbs, core.chrome, serverless); + const { logExplorer, serverless, chrome, notifications, appParams } = services; + const { history } = appParams; - const { history, setHeaderActionMenu, theme$ } = appParams; + useBreadcrumbs(noBreadcrumbs, chrome, serverless); - const [state$] = useState(() => new BehaviorSubject({})); + const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); - const customizations = useMemo(() => createLogExplorerCustomizations(), []); + const createLogExplorerController = useMemo( + () => createLogExplorerControllerWithCustomizations(logExplorer.createLogExplorerController), + [logExplorer.createLogExplorerController] + ); return ( - <> - + + + + + ); +}; + +const ConnectedContent = React.memo(() => { + const { + services: { + appParams: { history }, + logExplorer, + }, + } = useKibanaContextForPlugin(); + + const [state] = useActor(useObservabilityLogExplorerPageStateContext()); + + if (state.matches('initialized')) { + return ( + - - - ; + } +}); + +const InitializingContent = React.memo(() => ( + + } + title={ + + } + /> + +)); + +const InitializedContent = React.memo( + ({ + history, + logExplorer, + logExplorerController, + }: { + history: ObservabilityLogExplorerHistory; + logExplorer: LogExplorerPluginStart; + logExplorerController: LogExplorerController; + }) => { + return ( + + - - ); -}; + ); + } +); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts new file mode 100644 index 0000000000000..05e2b1ba03922 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/controller_service.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; +import type { InvokeCreator } from 'xstate'; +import type { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; + +export const createController = + ({ + createLogExplorerController, + }: { + createLogExplorerController: CreateLogExplorerController; + }): InvokeCreator => + (context, event) => + (send) => { + createLogExplorerController({ + initialState: context.initialLogExplorerState, + }).then((controller) => { + send({ + type: 'CONTROLLER_CREATED', + controller, + }); + }); + }; + +export const subscribeToLogExplorerState: InvokeCreator< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent +> = (context, event) => (send) => { + if (!('controller' in context)) { + throw new Error('Failed to subscribe to controller: no controller in context'); + } + + const { controller } = context; + + const subscription = controller.state$.subscribe({ + next: (state) => { + send({ type: 'LOG_EXPLORER_STATE_CHANGED', state }); + }, + }); + + controller.service.start(); + + return () => { + subscription.unsubscribe(); + controller.service.stop(); + }; +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.ts new file mode 100644 index 0000000000000..5e8c1eb5d3e2e --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/defaults.ts @@ -0,0 +1,12 @@ +/* + * 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 { CommonObservabilityLogExplorerContext } from './types'; + +export const DEFAULT_CONTEXT: CommonObservabilityLogExplorerContext = { + initialLogExplorerState: {}, +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/index.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/index.ts new file mode 100644 index 0000000000000..88b4fdbbd5d86 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './provider'; +export * from './state_machine'; +export * from './types'; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx new file mode 100644 index 0000000000000..050420de8875c --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/notifications.tsx @@ -0,0 +1,21 @@ +/* + * 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 { IToasts } from '@kbn/core-notifications-browser'; +import { mountReactNode } from '@kbn/core-mount-utils-browser-internal'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +export const createRequestFeedbackNotifier = (toasts: IToasts) => () => { + toasts.addInfo({ + title: i18n.translate('xpack.observabilityLogExplorer.feedbackToast.title', { + defaultMessage: 'Tell us what you think!', + }), + text: mountReactNode(<>), + iconType: 'editorComment', + }); +}; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts new file mode 100644 index 0000000000000..bfceafb1872df --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/provider.ts @@ -0,0 +1,30 @@ +/* + * 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 { getDevToolsOptions } from '@kbn/xstate-utils'; +import { useInterpret } from '@xstate/react'; +import createContainer from 'constate'; +import { + createObservabilityLogExplorerStateMachine, + ObservabilityLogExplorerStateMachineDependencies, +} from './state_machine'; + +export const useObservabilityLogExplorerPageState = ( + deps: ObservabilityLogExplorerStateMachineDependencies +) => { + const observabilityLogExplorerPageStateService = useInterpret( + () => createObservabilityLogExplorerStateMachine(deps), + { devTools: getDevToolsOptions() } + ); + + return observabilityLogExplorerPageStateService; +}; + +export const [ + ObservabilityLogExplorerPageStateProvider, + useObservabilityLogExplorerPageStateContext, +] = createContainer(useObservabilityLogExplorerPageState); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts new file mode 100644 index 0000000000000..ecf77d069e08f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/state_machine.ts @@ -0,0 +1,173 @@ +/* + * 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 { IToasts } from '@kbn/core-notifications-browser'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public'; +import { actions, createMachine, InterpreterFrom } from 'xstate'; +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent, + ObservabilityLogExplorerTypeState, +} from './types'; +import { initializeFromUrl, updateUrlFromLogExplorerState } from './url_state_storage_service'; +import { createController, subscribeToLogExplorerState } from './controller_service'; +import { initializeFromTimeFilterService } from './time_filter_service'; + +export const createPureObservabilityLogExplorerStateMachine = ( + initialContext: ObservabilityLogExplorerContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QHkBGswCcBuBDVAlgDYEAuAngDID2UAogB4AOR1mWAdAK4B2BfpArhIAvSAGIA2gAYAuolBNqsMgWo8FIBogDMOjgFYAbAEZpAdgCcRgwBYAHAcsGANCHK7bHWztsGTRjomevbmRpYAvhFuaBg4+MRkVLSMLGyc-KrCBCL8UABimNQAtgAqBMVg+cSkWADKWNgEAMZg4gCSAHLtpe0AgpTtAFp0ACIA+vkASsgAsuO9s3ST7ZSldFPjdRsAau0AwnQy8kggSiqC6praCAC0ZvrhRua24Tp2jiZuHggATEb2Dj-aQ2PQmcEGSFRGLoRoJEgUGj0ZisdiYDiZQTZXI8ApFYoAVUwRA63V6A2GY0mM3mBKmlGOmnOqiupxutx0Rg49ksv2kBl+9hMQp0oV+30QBnsXgsAX8lhevx0lki0RAsThhARyWRaTRHGa7Fwglx+3UpCKRCIWHE+2QnVKM0olA2432UzofXWo0Zp2Zlw0bMQt0FHCMAN85jC9k5tl+cYlCFCJg4QRMxnM0lsZkcRmh6th8S1SSRqVRGQEQlEkG4PAA1jxqAB3HillHpTB1UjGtqUZAAcXGdAAGgAFPsezZ1Upe5b7AASfU6-bGvsUyhZgdANyVBg4JmzRl+UtFFks9nsiYh0n3tn59lM-2FNnzGqLiURKXb+sxVZyNbwEgIDbPV6m7WpxD7QcR3HZBJy2Gd1jdRdl1XOQmQ3ANrklUxDFCSxDwVX4TAIq9BXMIFnnTc9gXseMojVRsIDgTQ3zwYtP11ctMAwi41C3LRg3TFMnheN4Pn8RN7h0CjpDk6QDyFcxiOkX5VRhOJ2I-HUyw7Wtf2xSBeM3bCEAot4LyFHl42cEEpNsSxDHkkwlTMKwH2cV9Cy07UQO4jFK2xPJChKcpKmqIhak7RoWjAYysKDO57Bvayswc0EDxeMiuVscwdEFAUBRci9fi8zT4RLL9QPRAzRGC-EiSIeL+NMjkuXeNT3PDF5fFcdxEDjFNlNBSxwQvXKdDKzVtL8vTDTAY08jNHgLWoK0sGa1lt2DTkOGsJUHNGuSSJ8RMXhTEw8t8S61LkpwpvfXyqv82r-wgTaBPZOjvGSz49F5RxzEvfqEDMdMwzvBwAnMAwoxIh6fMqri9NesQIFrBtm1bZ6Oy7HsPta3wfukP7lQKoGr2fDhpEsTllNCAwdAUya1TYirON0n9AurdHAIIYCcbRPHagJxKjBp-cDCzA9LDuqxLEph9qdp55yMZhSDAYiIgA */ + createMachine< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent, + ObservabilityLogExplorerTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'ObservabilityLogExplorer', + initial: 'uninitialized', + states: { + uninitialized: { + always: 'initializingFromTimeFilterService', + }, + initializingFromTimeFilterService: { + invoke: { + src: 'initializeFromTimeFilterService', + }, + on: { + INITIALIZED_FROM_TIME_FILTER_SERVICE: { + target: 'initializingFromUrl', + actions: ['storeInitialTimeFilter'], + }, + }, + }, + initializingFromUrl: { + invoke: { + src: 'initializeFromUrl', + }, + on: { + INITIALIZED_FROM_URL: { + target: '#creatingController', + actions: ['storeInitialUrlState'], + }, + }, + }, + creatingController: { + id: 'creatingController', + invoke: { + src: 'createController', + }, + on: { + CONTROLLER_CREATED: { + target: 'initialized', + actions: ['storeController'], + }, + }, + }, + initialized: { + invoke: { + src: 'subscribeToLogExplorerState', + }, + + states: { + unknownLogExplorerState: { + on: { + LOG_EXPLORER_STATE_CHANGED: { + target: 'validLogExplorerState', + actions: ['storeLogExplorerState', 'updateUrlFromLogExplorerState'], + }, + }, + }, + + validLogExplorerState: { + on: { + LOG_EXPLORER_STATE_CHANGED: { + actions: ['storeLogExplorerState', 'updateUrlFromLogExplorerState'], + target: 'validLogExplorerState', + internal: true, + }, + }, + }, + }, + + initial: 'unknownLogExplorerState', + }, + }, + }, + { + actions: { + storeController: actions.assign((context, event) => { + return 'controller' in event && event.type === 'CONTROLLER_CREATED' + ? { controller: event.controller } + : {}; + }), + storeInitialTimeFilter: actions.assign((context, event) => { + return 'time' in event && + 'refreshInterval' in event && + event.type === 'INITIALIZED_FROM_TIME_FILTER_SERVICE' + ? { + initialLogExplorerState: { + ...('initialLogExplorerState' in context ? context.initialLogExplorerState : {}), + ...{ time: event.time, refreshInterval: event.refreshInterval }, + }, + } + : {}; + }), + storeInitialUrlState: actions.assign((context, event) => { + return 'stateFromUrl' in event && event.type === 'INITIALIZED_FROM_URL' + ? { + initialLogExplorerState: { + ...('initialLogExplorerState' in context ? context.initialLogExplorerState : {}), + ...event.stateFromUrl, + }, + } + : {}; + }), + storeLogExplorerState: actions.assign((context, event) => { + return 'state' in event && event.type === 'LOG_EXPLORER_STATE_CHANGED' + ? { logExplorerState: event.state } + : {}; + }), + }, + guards: {}, + } + ); + +export interface ObservabilityLogExplorerStateMachineDependencies { + createLogExplorerController: CreateLogExplorerController; + initialContext?: ObservabilityLogExplorerContext; + timeFilterService: TimefilterContract; + toasts: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const createObservabilityLogExplorerStateMachine = ({ + initialContext = DEFAULT_CONTEXT, + toasts, + urlStateStorageContainer, + createLogExplorerController, + timeFilterService, +}: ObservabilityLogExplorerStateMachineDependencies) => + createPureObservabilityLogExplorerStateMachine(initialContext).withConfig({ + actions: { + updateUrlFromLogExplorerState: updateUrlFromLogExplorerState({ urlStateStorageContainer }), + }, + services: { + createController: createController({ createLogExplorerController }), + initializeFromTimeFilterService: initializeFromTimeFilterService({ timeFilterService }), + initializeFromUrl: initializeFromUrl({ urlStateStorageContainer, toastsService: toasts }), + subscribeToLogExplorerState, + }, + }); + +export type ObservabilityLogExplorerService = InterpreterFrom< + typeof createObservabilityLogExplorerStateMachine +>; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts new file mode 100644 index 0000000000000..97b23a6b5198f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/time_filter_service.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimefilterContract } from '@kbn/data-plugin/public'; +import { InvokeCreator } from 'xstate'; +import { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; + +export const initializeFromTimeFilterService = + ({ + timeFilterService, + }: { + timeFilterService: TimefilterContract; + }): InvokeCreator => + (_context, _event) => + (send) => { + const time = timeFilterService.getTime(); + const refreshInterval = timeFilterService.getRefreshInterval(); + + send({ + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE', + time, + refreshInterval, + }); + }; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts new file mode 100644 index 0000000000000..be039af61708f --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/types.ts @@ -0,0 +1,64 @@ +/* + * 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 { QueryState } from '@kbn/data-plugin/common'; +import { + LogExplorerController, + LogExplorerPublicState, + LogExplorerPublicStateUpdate, +} from '@kbn/log-explorer-plugin/public'; + +export type ObservabilityLogExplorerContext = ObservabilityLogExplorerTypeState['context']; + +export interface CommonObservabilityLogExplorerContext { + initialLogExplorerState: LogExplorerPublicStateUpdate; +} + +export interface WithLogExplorerState { + logExplorerState: LogExplorerPublicState; +} + +export interface WithController { + controller: LogExplorerController; +} + +export type ObservabilityLogExplorerEvent = + | { + type: 'INITIALIZED_FROM_URL'; + stateFromUrl?: LogExplorerPublicStateUpdate; + } + | { + type: 'INITIALIZED_FROM_TIME_FILTER_SERVICE'; + time: QueryState['time']; + refreshInterval: QueryState['refreshInterval']; + } + | { + type: 'CONTROLLER_CREATED'; + controller: LogExplorerController; + } + | { + type: 'LOG_EXPLORER_STATE_CHANGED'; + state: LogExplorerPublicState; + }; + +export type ObservabilityLogExplorerTypeState = + | { + value: + | 'uninitialized' + | 'initializingFromUrl' + | 'initializingFromTimeFilterService' + | 'creatingController'; + context: CommonObservabilityLogExplorerContext; + } + | { + value: 'initialized' | { initialized: 'unknownLogExplorerState' }; + context: CommonObservabilityLogExplorerContext & WithController; + } + | { + value: { initialized: 'validLogExplorerState' }; + context: CommonObservabilityLogExplorerContext & WithLogExplorerState & WithController; + }; diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts new file mode 100644 index 0000000000000..353d55ea94324 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts @@ -0,0 +1,60 @@ +/* + * 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 { LogExplorerPublicStateUpdate } from '@kbn/log-explorer-plugin/public'; +import * as rt from 'io-ts'; +import { deepCompactObject, urlSchemaV1 } from '../../../../common'; + +export const getStateFromUrlValue = ( + urlValue: urlSchemaV1.UrlSchema +): LogExplorerPublicStateUpdate => + deepCompactObject({ + chart: { + breakdownField: urlValue.breakdownField, + }, + controls: urlValue.controls, + datasetSelection: urlValue.datasetSelection, + filters: urlValue.filters, + grid: { + columns: urlValue.columns, + rows: { + rowHeight: urlValue.rowHeight, + rowsPerPage: urlValue.rowsPerPage, + }, + }, + query: urlValue.query, + refreshInterval: urlValue.refreshInterval, + time: urlValue.time, + }); + +export const getUrlValueFromState = (state: LogExplorerPublicStateUpdate): urlSchemaV1.UrlSchema => + deepCompactObject({ + breakdownField: state.chart?.breakdownField, + columns: state.grid?.columns, + controls: state.controls, + datasetSelection: state.datasetSelection, + filters: state.filters, + query: state.query, + refreshInterval: state.refreshInterval, + rowHeight: state.grid?.rows?.rowHeight, + rowsPerPage: state.grid?.rows?.rowsPerPage, + time: state.time, + v: 1, + }); + +const stateFromUrlSchemaRT = new rt.Type< + LogExplorerPublicStateUpdate, + urlSchemaV1.UrlSchema, + urlSchemaV1.UrlSchema +>( + 'stateFromUrlSchemaRT', + rt.never.is, + (urlSchema, context) => rt.success(getStateFromUrlValue(urlSchema)), + getUrlValueFromState +); + +export const stateFromUntrustedUrlRT = urlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); diff --git a/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts new file mode 100644 index 0000000000000..2eef22e0e5269 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/state_machines/observability_log_explorer/src/url_state_storage_service.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IToasts } from '@kbn/core-notifications-browser'; +import { createPlainError, formatErrors } from '@kbn/io-ts-utils'; +import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import * as Either from 'fp-ts/lib/Either'; +import * as rt from 'io-ts'; +import { InvokeCreator } from 'xstate'; +import { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY } from '../../../../common'; +import type { ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent } from './types'; +import * as urlSchemaV1 from './url_schema_v1'; + +interface ObservabilityLogExplorerUrlStateDependencies { + toastsService: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const updateUrlFromLogExplorerState = + ({ urlStateStorageContainer }: { urlStateStorageContainer: IKbnUrlStateStorage }) => + (context: ObservabilityLogExplorerContext, event: ObservabilityLogExplorerEvent) => { + if (!('logExplorerState' in context)) { + return; + } + + // we want to write in the newest schema + const encodedUrlStateValues = urlSchemaV1.stateFromUntrustedUrlRT.encode( + context.logExplorerState + ); + + urlStateStorageContainer.set(OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, encodedUrlStateValues, { + replace: true, + }); + }; + +export const initializeFromUrl = + ({ + toastsService, + urlStateStorageContainer, + }: ObservabilityLogExplorerUrlStateDependencies): InvokeCreator< + ObservabilityLogExplorerContext, + ObservabilityLogExplorerEvent + > => + (_context, _event) => + (send) => { + const urlStateValues = + urlStateStorageContainer.get(OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY) ?? undefined; + + // in the future we'll have to more schema versions to the union + const stateValuesE = rt + .union([rt.undefined, urlSchemaV1.stateFromUntrustedUrlRT]) + .decode(urlStateValues); + + if (Either.isLeft(stateValuesE)) { + withNotifyOnErrors(toastsService).onGetError( + createPlainError(formatErrors(stateValuesE.left)) + ); + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: undefined, + }); + } else { + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: stateValuesE.right, + }); + } + }; diff --git a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx index 55b4b9359fd0b..9de71eb8069d7 100644 --- a/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx +++ b/x-pack/plugins/observability_log_explorer/public/utils/breadcrumbs.tsx @@ -7,14 +7,14 @@ import { EuiBreadcrumb } from '@elastic/eui'; import type { ChromeStart } from '@kbn/core-chrome-browser'; -import type { ServerlessPluginStart } from '@kbn/serverless/public'; -import { useEffect } from 'react'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { LOGS_APP_ID, - OBSERVABILITY_LOG_EXPLORER, + OBSERVABILITY_LOG_EXPLORER_APP_ID, OBSERVABILITY_OVERVIEW_APP_ID, } from '@kbn/deeplinks-observability'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; +import { useEffect } from 'react'; import { logExplorerAppTitle, logsAppTitle, @@ -28,7 +28,7 @@ export const useBreadcrumbs = ( ) => { const observabilityLinkProps = useLinkProps({ app: OBSERVABILITY_OVERVIEW_APP_ID }); const logsLinkProps = useLinkProps({ app: LOGS_APP_ID }); - const logExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOG_EXPLORER }); + const logExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOG_EXPLORER_APP_ID }); useEffect(() => { setBreadcrumbs( diff --git a/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.ts b/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.ts new file mode 100644 index 0000000000000..167af63d0d9e6 --- /dev/null +++ b/x-pack/plugins/observability_log_explorer/public/utils/kbn_url_state_context.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import createContainer from 'constate'; +import { useState } from 'react'; +import { useKibanaContextForPlugin } from './use_kibana'; + +const useKbnUrlStateStorageFromRouter = () => { + const { + services: { + appParams: { history }, + notifications: { toasts }, + uiSettings, + }, + } = useKibanaContextForPlugin(); + + const [urlStateStorage] = useState(() => + createKbnUrlStateStorage({ + history, + useHash: uiSettings.get('state:storeInSessionStorage'), + useHashQuery: false, + ...withNotifyOnErrors(toasts), + }) + ); + + return urlStateStorage; +}; + +export const [KbnUrlStateStorageFromRouterProvider, useKbnUrlStateStorageFromRouterContext] = + createContainer(useKbnUrlStateStorageFromRouter); diff --git a/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx index d8b2235586e55..bf18f5b6d1b2e 100644 --- a/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx +++ b/x-pack/plugins/observability_log_explorer/public/utils/use_kibana.tsx @@ -12,21 +12,29 @@ import { useKibana, } from '@kbn/kibana-react-plugin/public'; import { useMemo } from 'react'; -import { ObservabilityLogExplorerPluginStart, ObservabilityLogExplorerStartDeps } from '../types'; +import { + ObservabilityLogExplorerAppMountParameters, + ObservabilityLogExplorerPluginStart, + ObservabilityLogExplorerStartDeps, +} from '../types'; export type PluginKibanaContextValue = CoreStart & ObservabilityLogExplorerStartDeps & - ObservabilityLogExplorerPluginStart; + ObservabilityLogExplorerPluginStart & { + appParams: ObservabilityLogExplorerAppMountParameters; + }; export const createKibanaContextForPlugin = ( core: CoreStart, plugins: ObservabilityLogExplorerStartDeps, - pluginStart: ObservabilityLogExplorerPluginStart + pluginStart: ObservabilityLogExplorerPluginStart, + appParams: ObservabilityLogExplorerAppMountParameters ) => createKibanaReactContext({ ...core, ...plugins, ...pluginStart, + appParams, }); export const useKibanaContextForPlugin = @@ -35,11 +43,12 @@ export const useKibanaContextForPlugin = export const useKibanaContextForPluginProvider = ( core: CoreStart, plugins: ObservabilityLogExplorerStartDeps, - pluginStart: ObservabilityLogExplorerPluginStart + pluginStart: ObservabilityLogExplorerPluginStart, + appParams: ObservabilityLogExplorerAppMountParameters ) => { const { Provider } = useMemo( - () => createKibanaContextForPlugin(core, plugins, pluginStart), - [core, pluginStart, plugins] + () => createKibanaContextForPlugin(core, plugins, pluginStart, appParams), + [appParams, core, pluginStart, plugins] ); return Provider; diff --git a/x-pack/plugins/observability_log_explorer/tsconfig.json b/x-pack/plugins/observability_log_explorer/tsconfig.json index 24327c31c26a3..be61fb9926a8c 100644 --- a/x-pack/plugins/observability_log_explorer/tsconfig.json +++ b/x-pack/plugins/observability_log_explorer/tsconfig.json @@ -11,31 +11,31 @@ ".storybook/**/*.tsx" ], "kbn_references": [ + "@kbn/config-schema", "@kbn/core", - "@kbn/log-explorer-plugin", - "@kbn/i18n", - "@kbn/react-kibana-context-render", - "@kbn/shared-ux-router", - "@kbn/observability-shared-plugin", + "@kbn/core-chrome-browser", + "@kbn/core-mount-utils-browser-internal", + "@kbn/core-notifications-browser", "@kbn/data-plugin", + "@kbn/dataset-quality-plugin", + "@kbn/deeplinks-observability", + "@kbn/discover-plugin", + "@kbn/i18n", + "@kbn/i18n-react", + "@kbn/io-ts-utils", "@kbn/kibana-react-plugin", - "@kbn/serverless", - "@kbn/core-chrome-browser", - "@kbn/config-schema", "@kbn/kibana-utils-plugin", - "@kbn/discover-plugin", - "@kbn/es-query", + "@kbn/log-explorer-plugin", + "@kbn/logs-shared-plugin", + "@kbn/observability-shared-plugin", + "@kbn/react-kibana-context-render", "@kbn/react-kibana-mount", + "@kbn/serverless", "@kbn/share-plugin", - "@kbn/io-ts-utils", - "@kbn/deeplinks-observability", - "@kbn/core-notifications-browser", - "@kbn/core-mount-utils-browser-internal", - "@kbn/xstate-utils", + "@kbn/shared-ux-router", "@kbn/shared-ux-utility", "@kbn/ui-theme", - "@kbn/logs-shared-plugin", - "@kbn/dataset-quality-plugin" + "@kbn/xstate-utils" ], "exclude": [ "target/**/*" diff --git a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts index b23fd6813c0f6..5da73c3e5ecb5 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/columns_selection.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; import { FtrProviderContext } from './config'; const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; @@ -39,10 +38,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ - columns: ['service.name', 'host.name', 'message', 'data_stream.namespace'], - }), + pageState: { + columns: [ + { field: 'service.name' }, + { field: 'host.name' }, + { field: 'message' }, + { field: 'data_stream.namespace' }, + ], }, }); diff --git a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts index 9fc2cca312551..583313ec8cb9a 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/dataset_selection_state.ts @@ -5,16 +5,30 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; +import { decodeOrThrow, indexPatternRt } from '@kbn/io-ts-utils'; +import { DatasetSelectionPlain } from '@kbn/log-explorer-plugin/common'; import { FtrProviderContext } from './config'; +const azureActivityDatasetSelection: DatasetSelectionPlain = { + selection: { + dataset: { + name: decodeOrThrow(indexPatternRt)('logs-azure.activitylogs-*'), + title: 'activitylogs', + }, + name: 'azure', + title: 'Azure Logs', + version: '1.5.23', + }, + selectionType: 'single', +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'observabilityLogExplorer']); describe('DatasetSelection initialization and update', () => { - describe('when the "index" query param does not exist', () => { + describe('when no dataset selection is given', () => { it('should initialize the "All logs" selection', async () => { await PageObjects.observabilityLogExplorer.navigateTo(); const datasetSelectionTitle = @@ -24,13 +38,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('when the "index" query param exists', () => { - it('should decode and restore the selection from a valid encoded index', async () => { - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; + describe('when a dataset selection is given', () => { + it('should restore the selection from a valid encoded index', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); @@ -41,10 +53,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should fallback to the "All logs" selection and notify the user of an invalid encoded index', async () => { - const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: invalidEncodedIndex }), + await PageObjects.observabilityLogExplorer.navigateToWithUncheckedState({ + pageState: { + v: 1, + datasetSelection: { + selectionType: 'invalid', + }, }, }); @@ -63,12 +77,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All logs'); - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), - controlPanels: rison.encode({}), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); const azureDatasetSelectionTitle = diff --git a/x-pack/test/functional/apps/observability_log_explorer/flyout.ts b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts index b2b71c817255a..11c161b5f4f8d 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/flyout.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/flyout.ts @@ -49,8 +49,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts b/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts index 3b71935cd6fbc..11dc71409b463 100644 --- a/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts +++ b/x-pack/test/functional/apps/observability_log_explorer/flyout_highlights.ts @@ -68,8 +68,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -125,8 +130,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -187,8 +197,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -252,8 +267,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test/functional/page_objects/observability_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts index 0b3266465c32e..a992aeefd0541 100644 --- a/x-pack/test/functional/page_objects/observability_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -5,6 +5,10 @@ * 2.0. */ import expect from '@kbn/expect'; +import { + OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, + urlSchemaV1, +} from '@kbn/observability-log-explorer-plugin/common'; import rison from '@kbn/rison'; import querystring from 'querystring'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; @@ -101,8 +105,14 @@ const packages: IntegrationPackage[] = [ const initialPackages = packages.slice(0, 3); const additionalPackages = packages.slice(3); -const FROM = '2023-08-03T10:24:14.035Z'; -const TO = '2023-08-03T10:24:14.091Z'; +const defaultPageState: urlSchemaV1.UrlSchema = { + v: 1, + time: { + from: '2023-08-03T10:24:14.035Z', + to: '2023-08-03T10:24:14.091Z', + mode: 'absolute', + }, +}; export function ObservabilityLogExplorerPageObject({ getPageObjects, @@ -117,15 +127,6 @@ export function ObservabilityLogExplorerPageObject({ const testSubjects = getService('testSubjects'); const toasts = getService('toasts'); - type NavigateToAppOptions = Omit< - Parameters[1], - 'search' - > & { - search?: Record; - from?: string; - to?: string; - }; - return { uninstallPackage: ({ name, version }: IntegrationPackage) => { return supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx'); @@ -208,19 +209,45 @@ export function ObservabilityLogExplorerPageObject({ }; }, - async navigateTo(options: NavigateToAppOptions = {}) { - const { search = {}, from = FROM, to = TO, ...extraOptions } = options; - const composedSearch = querystring.stringify({ - ...search, - _g: rison.encode({ - time: { from, to }, - }), + async navigateTo({ + pageState, + }: { + pageState?: urlSchemaV1.UrlSchema; + } = {}) { + const queryStringParams = querystring.stringify({ + [OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY]: rison.encode( + urlSchemaV1.urlSchemaRT.encode({ + ...defaultPageState, + ...pageState, + }) + ), }); - return await PageObjects.common.navigateToApp('observabilityLogExplorer', { - search: composedSearch, - ...extraOptions, + return await PageObjects.common.navigateToUrlWithBrowserHistory( + 'observabilityLogExplorer', + '/', + queryStringParams + ); + }, + + async navigateToWithUncheckedState({ + pageState: uncheckedPageState, + }: { + pageState?: {}; + } = {}) { + const queryStringParams = querystring.stringify({ + [OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY]: rison.encode({ + ...uncheckedPageState, + }), }); + + log.info('queryStringParams'); + + return await PageObjects.common.navigateToUrlWithBrowserHistory( + 'observabilityLogExplorer', + '/', + queryStringParams + ); }, getDatasetSelector() { @@ -357,9 +384,7 @@ export function ObservabilityLogExplorerPageObject({ async assertRestoreFailureToastExist() { const successToast = await toasts.getToastElement(1); - expect(await successToast.getVisibleText()).to.contain( - "We couldn't restore your datasets selection" - ); + expect(await successToast.getVisibleText()).to.contain('Error restoring state from URL'); }, assertLoadingSkeletonExists() { diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index cd9bde5d36a73..e5c89162f923d 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -2,7 +2,10 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "types": ["node", "@kbn/ambient-ftr-types"], + "types": [ + "node", + "@kbn/ambient-ftr-types" + ], // there is still a decent amount of JS in this plugin and we are taking // advantage of the fact that TS doesn't know the types of that code and // gives us `any`. Once that code is converted to .ts we can remove this @@ -18,9 +21,18 @@ "../../typings/**/*", "../../packages/kbn-test/types/ftr_globals/**/*" ], - "exclude": ["security_solution_cypress/cypress/**/*", "target/**/*", "*/plugins/**/*", "*/packages/**/*", "*/*/packages/**/*","security_solution_api_integration/**/*" ], + "exclude": [ + "security_solution_cypress/cypress/**/*", + "target/**/*", + "*/plugins/**/*", + "*/packages/**/*", + "*/*/packages/**/*", + "security_solution_api_integration/**/*" + ], "kbn_references": [ - { "path": "../../test/tsconfig.json" }, + { + "path": "../../test/tsconfig.json" + }, "@kbn/core", "@kbn/data-plugin", "@kbn/kibana-usage-collection-plugin", @@ -149,6 +161,9 @@ "@kbn/reporting-export-types-pdf-common", "@kbn/reporting-export-types-png-common", "@kbn/reporting-common", + "@kbn/observability-log-explorer-plugin", + "@kbn/io-ts-utils", + "@kbn/log-explorer-plugin", "@kbn/security-plugin-types-common", ] } diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts index a011a00a24f9c..d19cb269891d7 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/columns_selection.ts @@ -5,7 +5,6 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; import { FtrProviderContext } from '../../../ftr_provider_context'; const defaultLogColumns = ['@timestamp', 'service.name', 'host.name', 'message']; @@ -41,10 +40,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('should restore the table columns from the URL state if exists', async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ - columns: ['service.name', 'host.name', 'message', 'data_stream.namespace'], - }), + pageState: { + columns: [ + { field: 'service.name' }, + { field: 'host.name' }, + { field: 'message' }, + { field: 'data_stream.namespace' }, + ], }, }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts index 9f6e978e1f270..36a2451f4115d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/dataset_selection_state.ts @@ -5,9 +5,23 @@ * 2.0. */ import expect from '@kbn/expect'; -import rison from '@kbn/rison'; +import { decodeOrThrow, indexPatternRt } from '@kbn/io-ts-utils'; +import { DatasetSelectionPlain } from '@kbn/log-explorer-plugin/common'; import { FtrProviderContext } from '../../../ftr_provider_context'; +const azureActivityDatasetSelection: DatasetSelectionPlain = { + selection: { + dataset: { + name: decodeOrThrow(indexPatternRt)('logs-azure.activitylogs-*'), + title: 'activitylogs', + }, + name: 'azure', + title: 'Azure Logs', + version: '1.5.23', + }, + selectionType: 'single', +}; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const browser = getService('browser'); const retry = getService('retry'); @@ -27,7 +41,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.svlCommonPage.forceLogout(); }); - describe('when the "index" query param does not exist', () => { + describe('when no dataset selection is given', () => { it('should initialize the "All logs" selection', async () => { await PageObjects.observabilityLogExplorer.navigateTo(); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -38,13 +52,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); }); - describe('when the "index" query param exists', () => { + describe('when a dataset selection is given', () => { it('should decode and restore the selection from a valid encoded index', async () => { - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -56,10 +68,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should fallback to the "All logs" selection and notify the user of an invalid encoded index', async () => { - const invalidEncodedIndex = 'invalid-encoded-index'; - await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: invalidEncodedIndex }), + await PageObjects.observabilityLogExplorer.navigateToWithUncheckedState({ + pageState: { + v: 1, + datasetSelection: { + selectionType: 'invalid', + }, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); @@ -80,12 +94,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.observabilityLogExplorer.getDatasetSelectorButtonText(); expect(allDatasetSelectionTitle).to.be('All logs'); - const azureActivitylogsIndex = - 'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtFgF4CuATmAHRZzwBu8sAJ5VadAFTkANAlhRU3BPyEiQASklFS8lu2kC55AII6wAAgAyNEFN5hWIJGnIBGDgFYOAJgDM5deCgeFAAVQQAHMgdkaihVIA==='; await PageObjects.observabilityLogExplorer.navigateTo({ - search: { - _a: rison.encode({ index: azureActivitylogsIndex }), - controlPanels: rison.encode({}), + pageState: { + datasetSelection: azureActivityDatasetSelection, }, }); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts index 79f241c8948b6..d33d9e97d18c6 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout.ts @@ -50,8 +50,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts index 98a3914fed4e8..dd19471c5620d 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/flyout_highlights.ts @@ -70,8 +70,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -129,8 +134,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -193,8 +203,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); @@ -260,8 +275,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { beforeEach(async () => { await PageObjects.observabilityLogExplorer.navigateTo({ - from: new Date(NOW - 60_000).toISOString(), - to: new Date(NOW + 60_000).toISOString(), + pageState: { + time: { + from: new Date(NOW - 60_000).toISOString(), + to: new Date(NOW + 60_000).toISOString(), + mode: 'absolute', + }, + }, }); }); diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index af6f6e4b25e99..72039a6ad1368 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -2,8 +2,18 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "target/types", - "rootDirs": [".", "../test"], - "types": ["node", "@kbn/ambient-ftr-types", "cypress", "cypress-file-upload", "cypress-real-events", "cypress-recurse"], + "rootDirs": [ + ".", + "../test" + ], + "types": [ + "node", + "@kbn/ambient-ftr-types", + "cypress", + "cypress-file-upload", + "cypress-real-events", + "cypress-recurse" + ], }, "include": [ "**/*", @@ -17,7 +27,9 @@ "*/*/packages/**/*", ], "kbn_references": [ - { "path": "../test/tsconfig.json" }, + { + "path": "../test/tsconfig.json" + }, "@kbn/expect", "@kbn/test", "@kbn/repo-info", @@ -68,6 +80,8 @@ "@kbn/apm-synthtrace-client", "@kbn/reporting-export-types-csv-common", "@kbn/mock-idp-plugin", + "@kbn/io-ts-utils", + "@kbn/log-explorer-plugin", "@kbn/index-management-plugin", ] }