mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[Logs+] Refactor state and URL persistence of Log Explorer (#170200)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Felix Stürmer <felix.stuermer@elastic.co>
This commit is contained in:
parent
2d3d21500f
commit
dbabd6d16e
122 changed files with 3434 additions and 1856 deletions
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
export const LOGS_APP_ID = 'logs';
|
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';
|
export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview';
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LOGS_APP_ID,
|
|
||||||
OBSERVABILITY_LOG_EXPLORER,
|
|
||||||
OBSERVABILITY_OVERVIEW_APP_ID,
|
|
||||||
METRICS_APP_ID,
|
|
||||||
APM_APP_ID,
|
APM_APP_ID,
|
||||||
|
LOGS_APP_ID,
|
||||||
|
METRICS_APP_ID,
|
||||||
|
OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
OBSERVABILITY_ONBOARDING_APP_ID,
|
OBSERVABILITY_ONBOARDING_APP_ID,
|
||||||
|
OBSERVABILITY_OVERVIEW_APP_ID,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
type LogsApp = typeof LOGS_APP_ID;
|
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 ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID;
|
||||||
type MetricsApp = typeof METRICS_APP_ID;
|
type MetricsApp = typeof METRICS_APP_ID;
|
||||||
type ApmApp = typeof APM_APP_ID;
|
type ApmApp = typeof APM_APP_ID;
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export {
|
export {
|
||||||
OBSERVABILITY_ONBOARDING_APP_ID,
|
|
||||||
LOGS_APP_ID,
|
LOGS_APP_ID,
|
||||||
OBSERVABILITY_LOG_EXPLORER,
|
OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
|
OBSERVABILITY_ONBOARDING_APP_ID,
|
||||||
OBSERVABILITY_OVERVIEW_APP_ID,
|
OBSERVABILITY_OVERVIEW_APP_ID,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
|
||||||
export type { AppId, DeepLinkId } from './deep_links';
|
export type { AppId, DeepLinkId } from './deep_links';
|
||||||
|
|
||||||
export * from './locators';
|
export * from './locators';
|
||||||
|
|
|
@ -14,6 +14,17 @@ export type RefreshInterval = {
|
||||||
value: number;
|
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 const LOG_EXPLORER_LOCATOR_ID = 'LOG_EXPLORER_LOCATOR';
|
||||||
|
|
||||||
export interface LogExplorerNavigationParams extends SerializableRecord {
|
export interface LogExplorerNavigationParams extends SerializableRecord {
|
||||||
|
@ -34,13 +45,13 @@ export interface LogExplorerNavigationParams extends SerializableRecord {
|
||||||
*/
|
*/
|
||||||
columns?: string[];
|
columns?: string[];
|
||||||
/**
|
/**
|
||||||
* Array of the used sorting [[field,direction],...]
|
* Optionally apply free-form filters.
|
||||||
*/
|
|
||||||
sort?: string[][];
|
|
||||||
/**
|
|
||||||
* Optionally apply filters.
|
|
||||||
*/
|
*/
|
||||||
filters?: Filter[];
|
filters?: Filter[];
|
||||||
|
/**
|
||||||
|
* Optionally apply curated filter controls
|
||||||
|
*/
|
||||||
|
filterControls?: FilterControls;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogExplorerLocatorParams extends LogExplorerNavigationParams {
|
export interface LogExplorerLocatorParams extends LogExplorerNavigationParams {
|
||||||
|
|
|
@ -6,4 +6,52 @@
|
||||||
* Side Public License, v 1.
|
* 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 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<string, unknown>) => ({
|
||||||
|
value: state.value,
|
||||||
|
context: redactComplexValues(state.context),
|
||||||
|
});
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './actions';
|
export * from './actions';
|
||||||
|
export * from './dev_tools';
|
||||||
export * from './notification_channel';
|
export * from './notification_channel';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './dev_tools';
|
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
import React, { useEffect, useState, memo, useCallback, useMemo } from 'react';
|
import React, { useEffect, useState, memo, useCallback, useMemo } from 'react';
|
||||||
import { useParams, useHistory } from 'react-router-dom';
|
import { useParams, useHistory } from 'react-router-dom';
|
||||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
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 { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||||
import {
|
import {
|
||||||
AnalyticsNoDataPageKibanaProvider,
|
AnalyticsNoDataPageKibanaProvider,
|
||||||
|
@ -46,6 +50,7 @@ interface DiscoverLandingParams {
|
||||||
|
|
||||||
export interface MainRouteProps {
|
export interface MainRouteProps {
|
||||||
customizationCallbacks: CustomizationCallback[];
|
customizationCallbacks: CustomizationCallback[];
|
||||||
|
stateStorageContainer?: IKbnUrlStateStorage;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
customizationContext: DiscoverCustomizationContext;
|
customizationContext: DiscoverCustomizationContext;
|
||||||
}
|
}
|
||||||
|
@ -53,6 +58,7 @@ export interface MainRouteProps {
|
||||||
export function DiscoverMainRoute({
|
export function DiscoverMainRoute({
|
||||||
customizationCallbacks,
|
customizationCallbacks,
|
||||||
customizationContext,
|
customizationContext,
|
||||||
|
stateStorageContainer,
|
||||||
}: MainRouteProps) {
|
}: MainRouteProps) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const services = useDiscoverServices();
|
const services = useDiscoverServices();
|
||||||
|
@ -70,6 +76,7 @@ export function DiscoverMainRoute({
|
||||||
history,
|
history,
|
||||||
services,
|
services,
|
||||||
customizationContext,
|
customizationContext,
|
||||||
|
stateStorageContainer,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
|
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
DiscoverStateContainer,
|
DiscoverStateContainer,
|
||||||
createSearchSessionRestorationDataProvider,
|
createSearchSessionRestorationDataProvider,
|
||||||
} from './discover_state';
|
} from './discover_state';
|
||||||
import { createBrowserHistory, History } from 'history';
|
import { createBrowserHistory, createMemoryHistory, History } from 'history';
|
||||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||||
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
|
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
|
||||||
import {
|
import {
|
||||||
|
@ -27,6 +27,7 @@ import { waitFor } from '@testing-library/react';
|
||||||
import { DiscoverCustomizationContext, FetchStatus } from '../../types';
|
import { DiscoverCustomizationContext, FetchStatus } from '../../types';
|
||||||
import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex';
|
import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex';
|
||||||
import { copySavedSearch } from './discover_saved_search_container';
|
import { copySavedSearch } from './discover_saved_search_container';
|
||||||
|
import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||||
|
|
||||||
const startSync = (appState: DiscoverAppStateContainer) => {
|
const startSync = (appState: DiscoverAppStateContainer) => {
|
||||||
const { start, stop } = appState.syncState();
|
const { start, stop } = appState.syncState();
|
||||||
|
@ -151,6 +152,68 @@ describe('Test discover state', () => {
|
||||||
expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!t,value:5000))');
|
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', () => {
|
describe('Test discover initial state sort handling', () => {
|
||||||
test('Non-empty sort in URL should not be overwritten by saved search sort', async () => {
|
test('Non-empty sort in URL should not be overwritten by saved search sort', async () => {
|
||||||
const savedSearch = {
|
const savedSearch = {
|
||||||
|
|
|
@ -71,6 +71,10 @@ interface DiscoverStateContainerParams {
|
||||||
* Context object for customization related properties
|
* Context object for customization related properties
|
||||||
*/
|
*/
|
||||||
customizationContext: DiscoverCustomizationContext;
|
customizationContext: DiscoverCustomizationContext;
|
||||||
|
/**
|
||||||
|
* a custom url state storage
|
||||||
|
*/
|
||||||
|
stateStorageContainer?: IKbnUrlStateStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LoadParams {
|
export interface LoadParams {
|
||||||
|
@ -204,6 +208,7 @@ export function getDiscoverStateContainer({
|
||||||
history,
|
history,
|
||||||
services,
|
services,
|
||||||
customizationContext,
|
customizationContext,
|
||||||
|
stateStorageContainer,
|
||||||
}: DiscoverStateContainerParams): DiscoverStateContainer {
|
}: DiscoverStateContainerParams): DiscoverStateContainer {
|
||||||
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
|
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
|
||||||
const toasts = services.core.notifications.toasts;
|
const toasts = services.core.notifications.toasts;
|
||||||
|
@ -211,12 +216,14 @@ export function getDiscoverStateContainer({
|
||||||
/**
|
/**
|
||||||
* state storage for state in the URL
|
* state storage for state in the URL
|
||||||
*/
|
*/
|
||||||
const stateStorage = createKbnUrlStateStorage({
|
const stateStorage =
|
||||||
useHash: storeInSessionStorage,
|
stateStorageContainer ??
|
||||||
history,
|
createKbnUrlStateStorage({
|
||||||
useHashQuery: customizationContext.displayMode !== 'embedded',
|
useHash: storeInSessionStorage,
|
||||||
...(toasts && withNotifyOnErrors(toasts)),
|
history,
|
||||||
});
|
useHashQuery: customizationContext.displayMode !== 'embedded',
|
||||||
|
...(toasts && withNotifyOnErrors(toasts)),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search session logic
|
* Search session logic
|
||||||
|
|
|
@ -11,6 +11,7 @@ import type { ScopedHistory } from '@kbn/core/public';
|
||||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
|
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||||
import { DiscoverMainRoute } from '../../application/main';
|
import { DiscoverMainRoute } from '../../application/main';
|
||||||
import type { DiscoverServices } from '../../build_services';
|
import type { DiscoverServices } from '../../build_services';
|
||||||
import type { CustomizationCallback } from '../../customizations';
|
import type { CustomizationCallback } from '../../customizations';
|
||||||
|
@ -29,6 +30,7 @@ export interface DiscoverContainerInternalProps {
|
||||||
getDiscoverServices: () => Promise<DiscoverServices>;
|
getDiscoverServices: () => Promise<DiscoverServices>;
|
||||||
scopedHistory: ScopedHistory;
|
scopedHistory: ScopedHistory;
|
||||||
customizationCallbacks: CustomizationCallback[];
|
customizationCallbacks: CustomizationCallback[];
|
||||||
|
stateStorageContainer?: IKbnUrlStateStorage;
|
||||||
isDev: boolean;
|
isDev: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +57,7 @@ export const DiscoverContainerInternal = ({
|
||||||
customizationCallbacks,
|
customizationCallbacks,
|
||||||
isDev,
|
isDev,
|
||||||
getDiscoverServices,
|
getDiscoverServices,
|
||||||
|
stateStorageContainer,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: DiscoverContainerInternalProps) => {
|
}: DiscoverContainerInternalProps) => {
|
||||||
const [discoverServices, setDiscoverServices] = useState<DiscoverServices | undefined>();
|
const [discoverServices, setDiscoverServices] = useState<DiscoverServices | undefined>();
|
||||||
|
@ -97,6 +100,7 @@ export const DiscoverContainerInternal = ({
|
||||||
<DiscoverMainRoute
|
<DiscoverMainRoute
|
||||||
customizationCallbacks={customizationCallbacks}
|
customizationCallbacks={customizationCallbacks}
|
||||||
customizationContext={customizationContext}
|
customizationContext={customizationContext}
|
||||||
|
stateStorageContainer={stateStorageContainer}
|
||||||
isDev={isDev}
|
isDev={isDev}
|
||||||
/>
|
/>
|
||||||
</KibanaContextProvider>
|
</KibanaContextProvider>
|
||||||
|
|
|
@ -202,7 +202,7 @@ describe('kbn_url_storage', () => {
|
||||||
await Promise.all([pr1, pr2, pr3]);
|
await Promise.all([pr1, pr2, pr3]);
|
||||||
expect(getCurrentUrl()).toBe('/3');
|
expect(getCurrentUrl()).toBe('/3');
|
||||||
|
|
||||||
expect(urlControls.getPendingUrl()).toBeUndefined();
|
expect(urlControls.getPendingUrl()).toEqual(getCurrentUrl());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -193,17 +193,17 @@ export const createKbnUrlControls = (
|
||||||
|
|
||||||
// runs scheduled url updates
|
// runs scheduled url updates
|
||||||
function flush(replace = shouldReplace) {
|
function flush(replace = shouldReplace) {
|
||||||
|
if (updateQueue.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const nextUrl = getPendingUrl();
|
const nextUrl = getPendingUrl();
|
||||||
|
|
||||||
if (!nextUrl) return;
|
|
||||||
|
|
||||||
cleanUp();
|
cleanUp();
|
||||||
const newUrl = updateUrl(nextUrl, replace);
|
const newUrl = updateUrl(nextUrl, replace);
|
||||||
return newUrl;
|
return newUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPendingUrl() {
|
function getPendingUrl() {
|
||||||
if (updateQueue.length === 0) return undefined;
|
|
||||||
const resultUrl = updateQueue.reduce(
|
const resultUrl = updateQueue.reduce(
|
||||||
(url, nextUpdate) => nextUpdate(url) ?? url,
|
(url, nextUpdate) => nextUpdate(url) ?? url,
|
||||||
getCurrentUrl(history)
|
getCurrentUrl(history)
|
||||||
|
|
|
@ -116,7 +116,11 @@ export const createKbnUrlStateStorage = (
|
||||||
unlisten();
|
unlisten();
|
||||||
};
|
};
|
||||||
}).pipe(
|
}).pipe(
|
||||||
map(() => getStateFromKbnUrl<State>(key, undefined, { getFromHashQuery: useHashQuery })),
|
map(() =>
|
||||||
|
getStateFromKbnUrl<State>(key, history?.createHref(history.location), {
|
||||||
|
getFromHashQuery: useHashQuery,
|
||||||
|
})
|
||||||
|
),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
if (onGetErrorThrottled) onGetErrorThrottled(error);
|
if (onGetErrorThrottled) onGetErrorThrottled(error);
|
||||||
return of(null);
|
return of(null);
|
||||||
|
|
|
@ -32,8 +32,17 @@ export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
|
||||||
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
|
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
|
||||||
|
|
||||||
// UI preferences
|
// UI preferences
|
||||||
export const DATA_GRID_DEFAULT_COLUMNS = [SERVICE_NAME_FIELD, HOST_NAME_FIELD, MESSAGE_FIELD];
|
export const DEFAULT_COLUMNS = [
|
||||||
export const DATA_GRID_COLUMNS_PREFERENCES = {
|
{
|
||||||
[HOST_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_MEDIUM },
|
field: SERVICE_NAME_FIELD,
|
||||||
[SERVICE_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_SMALL },
|
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;
|
||||||
|
|
|
@ -5,18 +5,13 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AllDatasetSelection } from '../../../../common/dataset_selection';
|
import { ControlPanels } from './types';
|
||||||
import { ControlPanels, DefaultLogExplorerProfileState } from './types';
|
|
||||||
|
|
||||||
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
|
||||||
datasetSelection: AllDatasetSelection.create(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CONTROL_PANELS_URL_KEY = 'controlPanels';
|
|
||||||
|
|
||||||
export const availableControlsPanels = {
|
export const availableControlsPanels = {
|
||||||
NAMESPACE: 'data_stream.namespace',
|
NAMESPACE: 'data_stream.namespace',
|
||||||
};
|
} as const;
|
||||||
|
|
||||||
|
export type AvailableControlPanels = typeof availableControlsPanels;
|
||||||
|
|
||||||
export const controlPanelConfigs: ControlPanels = {
|
export const controlPanelConfigs: ControlPanels = {
|
||||||
[availableControlsPanels.NAMESPACE]: {
|
[availableControlsPanels.NAMESPACE]: {
|
|
@ -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';
|
29
x-pack/plugins/log_explorer/common/control_panels/types.ts
Normal file
29
x-pack/plugins/log_explorer/common/control_panels/types.ts
Normal file
|
@ -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<typeof ControlPanelRT>;
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dataset } from '../datasets';
|
import { Dataset } from '../datasets';
|
||||||
import { encodeDatasetSelection } from './encoding';
|
|
||||||
import { DatasetSelectionStrategy } from './types';
|
import { DatasetSelectionStrategy } from './types';
|
||||||
|
|
||||||
export class AllDatasetSelection implements DatasetSelectionStrategy {
|
export class AllDatasetSelection implements DatasetSelectionStrategy {
|
||||||
|
@ -23,18 +22,13 @@ export class AllDatasetSelection implements DatasetSelectionStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toDataviewSpec() {
|
toDataviewSpec() {
|
||||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
return this.selection.dataset.toDataviewSpec();
|
||||||
return {
|
|
||||||
id: this.toURLSelectionId(),
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toURLSelectionId() {
|
toPlainSelection() {
|
||||||
return encodeDatasetSelection({
|
return {
|
||||||
selectionType: this.selectionType,
|
selectionType: this.selectionType,
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static create() {
|
public static create() {
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
};
|
|
|
@ -13,11 +13,9 @@ import { UnresolvedDatasetSelection } from './unresolved_dataset_selection';
|
||||||
export const hydrateDatasetSelection = (datasetSelection: DatasetSelectionPlain) => {
|
export const hydrateDatasetSelection = (datasetSelection: DatasetSelectionPlain) => {
|
||||||
if (datasetSelection.selectionType === 'all') {
|
if (datasetSelection.selectionType === 'all') {
|
||||||
return AllDatasetSelection.create();
|
return AllDatasetSelection.create();
|
||||||
}
|
} else if (datasetSelection.selectionType === 'single') {
|
||||||
if (datasetSelection.selectionType === 'single') {
|
|
||||||
return SingleDatasetSelection.fromSelection(datasetSelection.selection);
|
return SingleDatasetSelection.fromSelection(datasetSelection.selection);
|
||||||
}
|
} else {
|
||||||
if (datasetSelection.selectionType === 'unresolved') {
|
|
||||||
return UnresolvedDatasetSelection.fromSelection(datasetSelection.selection);
|
return UnresolvedDatasetSelection.fromSelection(datasetSelection.selection);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,7 +28,6 @@ export const isDatasetSelection = (input: any): input is DatasetSelection => {
|
||||||
export * from './all_dataset_selection';
|
export * from './all_dataset_selection';
|
||||||
export * from './single_dataset_selection';
|
export * from './single_dataset_selection';
|
||||||
export * from './unresolved_dataset_selection';
|
export * from './unresolved_dataset_selection';
|
||||||
export * from './encoding';
|
|
||||||
export * from './errors';
|
export * from './errors';
|
||||||
export * from './hydrate_dataset_selection.ts';
|
export * from './hydrate_dataset_selection.ts';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dataset } from '../datasets';
|
import { Dataset } from '../datasets';
|
||||||
import { encodeDatasetSelection } from './encoding';
|
|
||||||
import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types';
|
import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types';
|
||||||
|
|
||||||
export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
||||||
|
@ -29,16 +28,11 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toDataviewSpec() {
|
toDataviewSpec() {
|
||||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
return this.selection.dataset.toDataviewSpec();
|
||||||
return {
|
|
||||||
id: this.toURLSelectionId(),
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toURLSelectionId() {
|
toPlainSelection() {
|
||||||
return encodeDatasetSelection({
|
return {
|
||||||
selectionType: this.selectionType,
|
selectionType: this.selectionType,
|
||||||
selection: {
|
selection: {
|
||||||
name: this.selection.name,
|
name: this.selection.name,
|
||||||
|
@ -46,7 +40,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
||||||
version: this.selection.version,
|
version: this.selection.version,
|
||||||
dataset: this.selection.dataset.toPlain(),
|
dataset: this.selection.dataset.toPlain(),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromSelection(selection: SingleDatasetSelectionPayload) {
|
public static fromSelection(selection: SingleDatasetSelectionPayload) {
|
||||||
|
|
|
@ -62,7 +62,11 @@ export type UnresolvedDatasetSelectionPayload = rt.TypeOf<
|
||||||
>;
|
>;
|
||||||
export type DatasetSelectionPlain = rt.TypeOf<typeof datasetSelectionPlainRT>;
|
export type DatasetSelectionPlain = rt.TypeOf<typeof datasetSelectionPlainRT>;
|
||||||
|
|
||||||
|
export type DataViewSpecWithId = DataViewSpec & {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface DatasetSelectionStrategy {
|
export interface DatasetSelectionStrategy {
|
||||||
toDataviewSpec(): DataViewSpec;
|
toDataviewSpec(): DataViewSpecWithId;
|
||||||
toURLSelectionId(): string;
|
toPlainSelection(): DatasetSelectionPlain;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Dataset } from '../datasets';
|
import { Dataset } from '../datasets';
|
||||||
import { encodeDatasetSelection } from './encoding';
|
|
||||||
import { DatasetSelectionStrategy, UnresolvedDatasetSelectionPayload } from './types';
|
import { DatasetSelectionStrategy, UnresolvedDatasetSelectionPayload } from './types';
|
||||||
|
|
||||||
export class UnresolvedDatasetSelection implements DatasetSelectionStrategy {
|
export class UnresolvedDatasetSelection implements DatasetSelectionStrategy {
|
||||||
|
@ -25,22 +24,17 @@ export class UnresolvedDatasetSelection implements DatasetSelectionStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
toDataviewSpec() {
|
toDataviewSpec() {
|
||||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
return this.selection.dataset.toDataviewSpec();
|
||||||
return {
|
|
||||||
id: this.toURLSelectionId(),
|
|
||||||
name,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toURLSelectionId() {
|
toPlainSelection() {
|
||||||
return encodeDatasetSelection({
|
return {
|
||||||
selectionType: this.selectionType,
|
selectionType: this.selectionType,
|
||||||
selection: {
|
selection: {
|
||||||
name: this.selection.name,
|
name: this.selection.name,
|
||||||
dataset: this.selection.dataset.toPlain(),
|
dataset: this.selection.dataset.toPlain(),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromSelection(selection: UnresolvedDatasetSelectionPayload) {
|
public static fromSelection(selection: UnresolvedDatasetSelectionPayload) {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IconType } from '@elastic/eui';
|
import { IconType } from '@elastic/eui';
|
||||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
|
||||||
import { IndexPattern } from '@kbn/io-ts-utils';
|
import { IndexPattern } from '@kbn/io-ts-utils';
|
||||||
import { TIMESTAMP_FIELD } from '../../constants';
|
import { TIMESTAMP_FIELD } from '../../constants';
|
||||||
|
import { DataViewSpecWithId } from '../../dataset_selection';
|
||||||
import { DatasetId, DatasetType, IntegrationType } from '../types';
|
import { DatasetId, DatasetType, IntegrationType } from '../types';
|
||||||
|
|
||||||
type IntegrationBase = Partial<Pick<IntegrationType, 'name' | 'title' | 'icons' | 'version'>>;
|
type IntegrationBase = Partial<Pick<IntegrationType, 'name' | 'title' | 'icons' | 'version'>>;
|
||||||
|
@ -49,7 +49,7 @@ export class Dataset {
|
||||||
return `${type}-${dataset}-*` as IndexPattern;
|
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`
|
// Invert the property because the API returns the index pattern as `name` and a readable name as `title`
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
|
@ -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';
|
43
x-pack/plugins/log_explorer/common/display_options/types.ts
Normal file
43
x-pack/plugins/log_explorer/common/display_options/types.ts
Normal file
|
@ -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<ChartDisplayOptions>;
|
||||||
|
|
||||||
|
export interface GridColumnDisplayOptions {
|
||||||
|
field: string;
|
||||||
|
width?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GridRowsDisplayOptions {
|
||||||
|
rowHeight: number;
|
||||||
|
rowsPerPage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartialGridRowsDisplayOptions = Partial<GridRowsDisplayOptions>;
|
||||||
|
|
||||||
|
export interface GridDisplayOptions {
|
||||||
|
columns: GridColumnDisplayOptions[];
|
||||||
|
rows: GridRowsDisplayOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartialGridDisplayOptions = Partial<
|
||||||
|
Omit<GridDisplayOptions, 'rows'> & { rows?: PartialGridRowsDisplayOptions }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface DisplayOptions {
|
||||||
|
grid: GridDisplayOptions;
|
||||||
|
chart: ChartDisplayOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartialDisplayOptions {
|
||||||
|
grid?: PartialGridDisplayOptions;
|
||||||
|
chart?: PartialChartDisplayOptions;
|
||||||
|
}
|
|
@ -5,4 +5,28 @@
|
||||||
* 2.0.
|
* 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';
|
||||||
|
|
|
@ -12,16 +12,18 @@
|
||||||
"logExplorer"
|
"logExplorer"
|
||||||
],
|
],
|
||||||
"requiredPlugins": [
|
"requiredPlugins": [
|
||||||
|
"controls",
|
||||||
"data",
|
"data",
|
||||||
"dataViews",
|
"dataViews",
|
||||||
"discover",
|
"discover",
|
||||||
|
"embeddable",
|
||||||
"fieldFormats",
|
"fieldFormats",
|
||||||
"fleet",
|
"fleet",
|
||||||
"kibanaReact",
|
"kibanaReact",
|
||||||
"kibanaUtils",
|
"kibanaUtils",
|
||||||
"controls",
|
"navigation",
|
||||||
"embeddable",
|
|
||||||
"share",
|
"share",
|
||||||
|
"unifiedSearch"
|
||||||
],
|
],
|
||||||
"optionalPlugins": [],
|
"optionalPlugins": [],
|
||||||
"requiredBundles": [],
|
"requiredBundles": [],
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FlyoutProps, LogDocument } from './types';
|
import { LogExplorerFlyoutContentProps, LogDocument } from './types';
|
||||||
import { useDocDetail } from './use_doc_detail';
|
import { useDocDetail } from './use_doc_detail';
|
||||||
import { FlyoutHeader } from './flyout_header';
|
import { FlyoutHeader } from './flyout_header';
|
||||||
import { FlyoutHighlights } from './flyout_highlights';
|
import { FlyoutHighlights } from './flyout_highlights';
|
||||||
|
@ -16,7 +16,7 @@ export function FlyoutDetail({
|
||||||
dataView,
|
dataView,
|
||||||
doc,
|
doc,
|
||||||
actions,
|
actions,
|
||||||
}: Pick<FlyoutProps, 'dataView' | 'doc' | 'actions'>) {
|
}: Pick<LogExplorerFlyoutContentProps, 'dataView' | 'doc' | 'actions'>) {
|
||||||
const parsedDoc = useDocDetail(doc as LogDocument, { dataView });
|
const parsedDoc = useDocDetail(doc as LogDocument, { dataView });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -5,55 +5,4 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
export type { FlyoutDoc, LogDocument, LogExplorerFlyoutContentProps } from '../../controller';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,11 +7,11 @@
|
||||||
import { formatFieldValue } from '@kbn/discover-utils';
|
import { formatFieldValue } from '@kbn/discover-utils';
|
||||||
import * as constants from '../../../common/constants';
|
import * as constants from '../../../common/constants';
|
||||||
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
||||||
import { FlyoutDoc, FlyoutProps, LogDocument } from './types';
|
import { FlyoutDoc, LogExplorerFlyoutContentProps, LogDocument } from './types';
|
||||||
|
|
||||||
export function useDocDetail(
|
export function useDocDetail(
|
||||||
doc: LogDocument,
|
doc: LogDocument,
|
||||||
{ dataView }: Pick<FlyoutProps, 'dataView'>
|
{ dataView }: Pick<LogExplorerFlyoutContentProps, 'dataView'>
|
||||||
): FlyoutDoc {
|
): FlyoutDoc {
|
||||||
const { services } = useKibanaContextForPlugin();
|
const { services } = useKibanaContextForPlugin();
|
||||||
|
|
||||||
|
|
|
@ -5,100 +5,43 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { ScopedHistory } from '@kbn/core-application-browser';
|
||||||
|
import type { CoreStart } from '@kbn/core/public';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { ScopedHistory } from '@kbn/core-application-browser';
|
import type { LogExplorerController } from '../../controller';
|
||||||
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 { createLogExplorerProfileCustomizations } from '../../customizations/log_explorer_profile';
|
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 { LogExplorerStartDeps } from '../../types';
|
||||||
import { LogExplorerCustomizations } from './types';
|
|
||||||
|
|
||||||
export interface CreateLogExplorerArgs {
|
export interface CreateLogExplorerArgs {
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
plugins: LogExplorerStartDeps;
|
plugins: LogExplorerStartDeps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogExplorerStateContainer {
|
|
||||||
appState?: DiscoverAppState;
|
|
||||||
logExplorerState?: Partial<LogExplorerProfileContext>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LogExplorerProps {
|
export interface LogExplorerProps {
|
||||||
customizations?: LogExplorerCustomizations;
|
|
||||||
scopedHistory: ScopedHistory;
|
scopedHistory: ScopedHistory;
|
||||||
state$?: BehaviorSubject<LogExplorerStateContainer>;
|
controller: LogExplorerController;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => {
|
export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => {
|
||||||
const {
|
const {
|
||||||
data,
|
|
||||||
discover: { DiscoverContainer },
|
discover: { DiscoverContainer },
|
||||||
} = plugins;
|
} = plugins;
|
||||||
|
|
||||||
const overrideServices = {
|
return ({ scopedHistory, controller }: LogExplorerProps) => {
|
||||||
data: createDataServiceProxy(data),
|
|
||||||
uiSettings: createUiSettingsServiceProxy(core.uiSettings),
|
|
||||||
};
|
|
||||||
|
|
||||||
return ({ customizations = {}, scopedHistory, state$ }: LogExplorerProps) => {
|
|
||||||
const logExplorerCustomizations = useMemo(
|
const logExplorerCustomizations = useMemo(
|
||||||
() => [createLogExplorerProfileCustomizations({ core, customizations, plugins, state$ })],
|
() => [createLogExplorerProfileCustomizations({ controller, core, plugins })],
|
||||||
[customizations, state$]
|
[controller]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { urlStateStorage, ...overrideServices } = controller.discoverServices;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DiscoverContainer
|
<DiscoverContainer
|
||||||
customizationCallbacks={logExplorerCustomizations}
|
customizationCallbacks={logExplorerCustomizations}
|
||||||
overrideServices={overrideServices}
|
overrideServices={overrideServices}
|
||||||
scopedHistory={scopedHistory}
|
scopedHistory={scopedHistory}
|
||||||
|
stateStorageContainer={urlStateStorage}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<string, any> = {
|
|
||||||
[HIDE_ANNOUNCEMENTS]: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
return createPropertyGetProxy(uiSettings, {
|
|
||||||
get:
|
|
||||||
() =>
|
|
||||||
(key, ...args) => {
|
|
||||||
if (key in overrides) {
|
|
||||||
return overrides[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return uiSettings.get(key, ...args);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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<LogExplorerFlyoutContentProps>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Props> = (
|
||||||
|
renderPreviousContent: RenderPreviousContent<Props>
|
||||||
|
) => (props: Props) => React.ReactNode;
|
||||||
|
|
||||||
|
export type RenderPreviousContent<Props> = (props: Props) => React.ReactNode;
|
|
@ -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<LogExplorerController> => {
|
||||||
|
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<typeof createLogExplorerControllerFactory>;
|
|
@ -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,
|
||||||
|
});
|
||||||
|
};
|
|
@ -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<string, any> = {
|
||||||
|
[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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -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<LogExplorerDiscoverServices['history']>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
});
|
11
x-pack/plugins/log_explorer/public/controller/index.ts
Normal file
11
x-pack/plugins/log_explorer/public/controller/index.ts
Normal file
|
@ -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';
|
|
@ -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);
|
||||||
|
};
|
15
x-pack/plugins/log_explorer/public/controller/provider.ts
Normal file
15
x-pack/plugins/log_explorer/public/controller/provider.ts
Normal file
|
@ -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);
|
134
x-pack/plugins/log_explorer/public/controller/public_state.ts
Normal file
134
x-pack/plugins/log_explorer/public/controller/public_state.ts
Normal file
|
@ -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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
73
x-pack/plugins/log_explorer/public/controller/types.ts
Normal file
73
x-pack/plugins/log_explorer/public/controller/types.ts
Normal file
|
@ -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<LogExplorerPublicEvent>;
|
||||||
|
service: LogExplorerControllerStateService;
|
||||||
|
state$: Observable<LogExplorerPublicState>;
|
||||||
|
stateMachine: LogExplorerControllerStateMachine;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LogExplorerDiscoverServices = Pick<
|
||||||
|
Required<DiscoverContainerProps['overrideServices']>,
|
||||||
|
'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;
|
|
@ -10,21 +10,21 @@ import { Query } from '@kbn/es-query';
|
||||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||||
import { useControlPanels } from '../hooks/use_control_panels';
|
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';
|
const DATASET_FILTERS_CUSTOMIZATION_ID = 'datasetFiltersCustomization';
|
||||||
|
|
||||||
interface CustomDatasetFiltersProps {
|
interface CustomDatasetFiltersProps {
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
logExplorerControllerStateService: LogExplorerControllerStateService;
|
||||||
data: DataPublicPluginStart;
|
data: DataPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CustomDatasetFilters = ({
|
const CustomDatasetFilters = ({
|
||||||
logExplorerProfileStateService,
|
logExplorerControllerStateService,
|
||||||
data,
|
data,
|
||||||
}: CustomDatasetFiltersProps) => {
|
}: CustomDatasetFiltersProps) => {
|
||||||
const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
||||||
logExplorerProfileStateService,
|
logExplorerControllerStateService,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,15 @@ import { DataViewsProvider, useDataViewsContext } from '../hooks/use_data_views'
|
||||||
import { useEsql } from '../hooks/use_esql';
|
import { useEsql } from '../hooks/use_esql';
|
||||||
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
||||||
import { IDatasetsClient } from '../services/datasets';
|
import { IDatasetsClient } from '../services/datasets';
|
||||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||||
|
|
||||||
interface CustomDatasetSelectorProps {
|
interface CustomDatasetSelectorProps {
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
logExplorerControllerStateService: LogExplorerControllerStateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
export const CustomDatasetSelector = withProviders(({ logExplorerControllerStateService }) => {
|
||||||
const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection(
|
const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection(
|
||||||
logExplorerProfileStateService
|
logExplorerControllerStateService
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -111,13 +111,13 @@ function withProviders(Component: React.FunctionComponent<CustomDatasetSelectorP
|
||||||
datasetsClient,
|
datasetsClient,
|
||||||
dataViews,
|
dataViews,
|
||||||
discover,
|
discover,
|
||||||
logExplorerProfileStateService,
|
logExplorerControllerStateService,
|
||||||
}: CustomDatasetSelectorBuilderProps) {
|
}: CustomDatasetSelectorBuilderProps) {
|
||||||
return (
|
return (
|
||||||
<IntegrationsProvider datasetsClient={datasetsClient}>
|
<IntegrationsProvider datasetsClient={datasetsClient}>
|
||||||
<DatasetsProvider datasetsClient={datasetsClient}>
|
<DatasetsProvider datasetsClient={datasetsClient}>
|
||||||
<DataViewsProvider dataViewsService={dataViews} discoverService={discover}>
|
<DataViewsProvider dataViewsService={dataViews} discoverService={discover}>
|
||||||
<Component logExplorerProfileStateService={logExplorerProfileStateService} />
|
<Component logExplorerControllerStateService={logExplorerControllerStateService} />
|
||||||
</DataViewsProvider>
|
</DataViewsProvider>
|
||||||
</DatasetsProvider>
|
</DatasetsProvider>
|
||||||
</IntegrationsProvider>
|
</IntegrationsProvider>
|
||||||
|
|
|
@ -5,46 +5,41 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||||
import { FlyoutDetail } from '../components/flyout_detail/flyout_detail';
|
import { FlyoutDetail } from '../components/flyout_detail/flyout_detail';
|
||||||
import { FlyoutProps } from '../components/flyout_detail';
|
import { LogExplorerFlyoutContentProps } from '../components/flyout_detail';
|
||||||
import { useLogExplorerCustomizationsContext } from '../hooks/use_log_explorer_customizations';
|
import { useLogExplorerControllerContext } from '../controller';
|
||||||
|
|
||||||
export const CustomFlyoutContent = ({
|
export const CustomFlyoutContent = (props: LogExplorerFlyoutContentProps) => {
|
||||||
actions,
|
const {
|
||||||
dataView,
|
customizations: { flyout },
|
||||||
doc,
|
} = useLogExplorerControllerContext();
|
||||||
renderDefaultContent,
|
|
||||||
}: FlyoutProps) => {
|
|
||||||
const { flyout } = useLogExplorerCustomizationsContext();
|
|
||||||
|
|
||||||
const renderPreviousContent = useCallback(
|
const renderCustomizedContent = useMemo(
|
||||||
() => (
|
() => flyout?.renderContent?.(renderContent) ?? renderContent,
|
||||||
<>
|
[flyout]
|
||||||
{/* Apply custom Log Explorer detail */}
|
|
||||||
<EuiFlexItem>
|
|
||||||
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
|
|
||||||
</EuiFlexItem>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
[actions, dataView, doc]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = flyout?.renderContent
|
|
||||||
? flyout?.renderContent(renderPreviousContent, { doc })
|
|
||||||
: renderPreviousContent();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EuiFlexGroup direction="column" gutterSize="m">
|
<EuiFlexGroup direction="column" gutterSize="m">
|
||||||
{/* Apply custom Log Explorer detail */}
|
{/* Apply custom Log Explorer detail */}
|
||||||
{content}
|
{renderCustomizedContent(props)}
|
||||||
{/* Restore default content */}
|
{/* Restore default content */}
|
||||||
<EuiHorizontalRule margin="xs" />
|
<EuiHorizontalRule margin="xs" />
|
||||||
<EuiFlexItem>{renderDefaultContent()}</EuiFlexItem>
|
<EuiFlexItem>{props.renderDefaultContent()}</EuiFlexItem>
|
||||||
</EuiFlexGroup>
|
</EuiFlexGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderContent = ({ actions, dataView, doc }: LogExplorerFlyoutContentProps) => (
|
||||||
|
<>
|
||||||
|
{/* Apply custom Log Explorer detail */}
|
||||||
|
<EuiFlexItem>
|
||||||
|
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
|
||||||
|
</EuiFlexItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-default-export
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default CustomFlyoutContent;
|
export default CustomFlyoutContent;
|
||||||
|
|
|
@ -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);
|
||||||
|
};
|
|
@ -4,19 +4,19 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
|
||||||
import type { CoreStart } from '@kbn/core/public';
|
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 { i18n } from '@kbn/i18n';
|
||||||
|
import React from 'react';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
import { combineLatest, from, map, Subscription, type BehaviorSubject } from 'rxjs';
|
import { waitFor } from 'xstate/lib/waitFor';
|
||||||
import { LogExplorerStateContainer } from '../components/log_explorer';
|
import type { LogExplorerController } from '../controller';
|
||||||
import { LogExplorerCustomizations } from '../components/log_explorer/types';
|
import { LogExplorerControllerProvider } from '../controller/provider';
|
||||||
import { LogExplorerCustomizationsProvider } from '../hooks/use_log_explorer_customizations';
|
import type { LogExplorerStartDeps } from '../types';
|
||||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
|
||||||
import { LogExplorerStartDeps } from '../types';
|
|
||||||
import { dynamic } from '../utils/dynamic';
|
import { dynamic } from '../utils/dynamic';
|
||||||
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
||||||
|
import { createCustomSearchBar } from './custom_search_bar';
|
||||||
|
|
||||||
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
|
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
|
||||||
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
||||||
|
@ -24,56 +24,31 @@ const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content'))
|
||||||
|
|
||||||
export interface CreateLogExplorerProfileCustomizationsDeps {
|
export interface CreateLogExplorerProfileCustomizationsDeps {
|
||||||
core: CoreStart;
|
core: CoreStart;
|
||||||
customizations: LogExplorerCustomizations;
|
|
||||||
plugins: LogExplorerStartDeps;
|
plugins: LogExplorerStartDeps;
|
||||||
state$?: BehaviorSubject<LogExplorerStateContainer>;
|
controller: LogExplorerController;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createLogExplorerProfileCustomizations =
|
export const createLogExplorerProfileCustomizations =
|
||||||
({
|
({
|
||||||
core,
|
core,
|
||||||
customizations: logExplorerCustomizations,
|
|
||||||
plugins,
|
plugins,
|
||||||
state$,
|
controller,
|
||||||
}: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
}: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||||
async ({ customizations, stateContainer }) => {
|
async ({ customizations, stateContainer }) => {
|
||||||
const { data, dataViews, discover } = plugins;
|
const { discoverServices, service } = controller;
|
||||||
// Lazy load dependencies
|
const pluginsWithOverrides = {
|
||||||
const datasetServiceModuleLoadable = import('../services/datasets');
|
...plugins,
|
||||||
const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile');
|
...discoverServices,
|
||||||
|
};
|
||||||
|
const { data, dataViews, discover, navigation, unifiedSearch } = pluginsWithOverrides;
|
||||||
|
|
||||||
const [{ DatasetsService }, { initializeLogExplorerProfileStateService, waitForState }] =
|
service.send('RECEIVED_STATE_CONTAINER', { discoverStateContainer: stateContainer });
|
||||||
await Promise.all([datasetServiceModuleLoadable, logExplorerMachineModuleLoadable]);
|
|
||||||
|
|
||||||
const datasetsClient = new DatasetsService().start({
|
|
||||||
http: core.http,
|
|
||||||
}).client;
|
|
||||||
|
|
||||||
const logExplorerProfileStateService = initializeLogExplorerProfileStateService({
|
|
||||||
datasetsClient,
|
|
||||||
stateContainer,
|
|
||||||
toasts: core.notifications.toasts,
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for the machine to be fully initialized to set the restored selection
|
* Wait for the machine to be fully initialized to set the restored selection
|
||||||
* create the DataView and set it in the stateContainer from Discover
|
* create the DataView and set it in the stateContainer from Discover
|
||||||
*/
|
*/
|
||||||
await waitForState(logExplorerProfileStateService, 'initialized');
|
await waitFor(service, (state) => state.matches('initialized'), { timeout: 30000 });
|
||||||
|
|
||||||
/**
|
|
||||||
* 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$);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
||||||
|
@ -87,20 +62,22 @@ export const createLogExplorerProfileCustomizations =
|
||||||
return (
|
return (
|
||||||
<KibanaContextProviderForPlugin>
|
<KibanaContextProviderForPlugin>
|
||||||
<LazyCustomDatasetSelector
|
<LazyCustomDatasetSelector
|
||||||
datasetsClient={datasetsClient}
|
datasetsClient={controller.datasetsClient}
|
||||||
dataViews={dataViews}
|
dataViews={dataViews}
|
||||||
discover={discover}
|
discover={discover}
|
||||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
logExplorerControllerStateService={service}
|
||||||
/>
|
/>
|
||||||
</KibanaContextProviderForPlugin>
|
</KibanaContextProviderForPlugin>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
PrependFilterBar: () => (
|
PrependFilterBar: () => (
|
||||||
<LazyCustomDatasetFilters
|
<LazyCustomDatasetFilters logExplorerControllerStateService={service} data={data} />
|
||||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
|
CustomSearchBar: createCustomSearchBar({
|
||||||
|
data,
|
||||||
|
navigation,
|
||||||
|
unifiedSearch,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -143,32 +120,13 @@ export const createLogExplorerProfileCustomizations =
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KibanaContextProviderForPlugin>
|
<KibanaContextProviderForPlugin>
|
||||||
<LogExplorerCustomizationsProvider value={logExplorerCustomizations}>
|
<LogExplorerControllerProvider controller={controller}>
|
||||||
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
|
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
|
||||||
</LogExplorerCustomizationsProvider>
|
</LogExplorerControllerProvider>
|
||||||
</KibanaContextProviderForPlugin>
|
</KibanaContextProviderForPlugin>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {};
|
||||||
if (stateSubscription) {
|
|
||||||
stateSubscription.unsubscribe();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createStateUpdater = ({
|
|
||||||
logExplorerProfileStateService,
|
|
||||||
stateContainer,
|
|
||||||
}: {
|
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
|
||||||
stateContainer: DiscoverStateContainer;
|
|
||||||
}) => {
|
|
||||||
return combineLatest([from(logExplorerProfileStateService), stateContainer.appState.state$]).pipe(
|
|
||||||
map(([logExplorerState, appState]) => ({
|
|
||||||
logExplorerState: logExplorerState.context,
|
|
||||||
appState,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -13,16 +13,16 @@ import { Query, TimeRange } from '@kbn/es-query';
|
||||||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||||
import { useSelector } from '@xstate/react';
|
import { useSelector } from '@xstate/react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||||
|
|
||||||
export const useControlPanels = (
|
export const useControlPanels = (
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService,
|
logExplorerControllerStateService: LogExplorerControllerStateService,
|
||||||
data: DataPublicPluginStart
|
data: DataPublicPluginStart
|
||||||
) => {
|
) => {
|
||||||
const { query, filters, fromDate, toDate } = useQuerySubscriber({ data });
|
const { query, filters, fromDate, toDate } = useQuerySubscriber({ data });
|
||||||
const timeRange: TimeRange = { from: fromDate!, to: toDate! };
|
const timeRange: TimeRange = { from: fromDate!, to: toDate! };
|
||||||
|
|
||||||
const controlPanels = useSelector(logExplorerProfileStateService, (state) => {
|
const controlPanels = useSelector(logExplorerControllerStateService, (state) => {
|
||||||
if (!('controlPanels' in state.context)) return;
|
if (!('controlPanels' in state.context)) return;
|
||||||
return state.context.controlPanels;
|
return state.context.controlPanels;
|
||||||
});
|
});
|
||||||
|
@ -45,12 +45,12 @@ export const useControlPanels = (
|
||||||
|
|
||||||
const setControlGroupAPI = useCallback(
|
const setControlGroupAPI = useCallback(
|
||||||
(controlGroupAPI: ControlGroupAPI) => {
|
(controlGroupAPI: ControlGroupAPI) => {
|
||||||
logExplorerProfileStateService.send({
|
logExplorerControllerStateService.send({
|
||||||
type: 'INITIALIZE_CONTROL_GROUP_API',
|
type: 'INITIALIZE_CONTROL_GROUP_API',
|
||||||
controlGroupAPI,
|
controlGroupAPI,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[logExplorerProfileStateService]
|
[logExplorerControllerStateService]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { getInitialInput, setControlGroupAPI, query, filters, timeRange };
|
return { getInitialInput, setControlGroupAPI, query, filters, timeRange };
|
||||||
|
|
|
@ -8,20 +8,20 @@
|
||||||
import { useSelector } from '@xstate/react';
|
import { useSelector } from '@xstate/react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { DatasetSelectionChange } from '../../common/dataset_selection';
|
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 = (
|
export const useDatasetSelection = (
|
||||||
logExplorerProfileStateService: LogExplorerProfileStateService
|
logExplorerControllerStateService: LogExplorerControllerStateService
|
||||||
) => {
|
) => {
|
||||||
const datasetSelection = useSelector(logExplorerProfileStateService, (state) => {
|
const datasetSelection = useSelector(logExplorerControllerStateService, (state) => {
|
||||||
return state.context.datasetSelection;
|
return state.context.datasetSelection;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
||||||
(data) => {
|
(data) => {
|
||||||
logExplorerProfileStateService.send({ type: 'UPDATE_DATASET_SELECTION', data });
|
logExplorerControllerStateService.send({ type: 'UPDATE_DATASET_SELECTION', data });
|
||||||
},
|
},
|
||||||
[logExplorerProfileStateService]
|
[logExplorerControllerStateService]
|
||||||
);
|
);
|
||||||
|
|
||||||
return { datasetSelection, handleDatasetSelectionChange };
|
return { datasetSelection, handleDatasetSelectionChange };
|
||||||
|
|
|
@ -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);
|
|
|
@ -8,12 +8,20 @@
|
||||||
import type { PluginInitializerContext } from '@kbn/core/public';
|
import type { PluginInitializerContext } from '@kbn/core/public';
|
||||||
import type { LogExplorerConfig } from '../common/plugin_config';
|
import type { LogExplorerConfig } from '../common/plugin_config';
|
||||||
import { LogExplorerPlugin } from './plugin';
|
import { LogExplorerPlugin } from './plugin';
|
||||||
export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
|
|
||||||
export type { LogExplorerStateContainer } from './components/log_explorer';
|
|
||||||
export type {
|
export type {
|
||||||
|
CreateLogExplorerController,
|
||||||
|
LogExplorerController,
|
||||||
LogExplorerCustomizations,
|
LogExplorerCustomizations,
|
||||||
LogExplorerFlyoutContentProps,
|
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<LogExplorerConfig>) {
|
export function plugin(context: PluginInitializerContext<LogExplorerConfig>) {
|
||||||
return new LogExplorerPlugin(context);
|
return new LogExplorerPlugin(context);
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
* 2.0.
|
* 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 { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||||
import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators';
|
import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators';
|
||||||
import { createLogExplorer } from './components/log_explorer';
|
import { createLogExplorer } from './components/log_explorer';
|
||||||
import {
|
import { createLogExplorerControllerLazyFactory } from './controller/lazy_create_controller';
|
||||||
|
import type {
|
||||||
LogExplorerPluginSetup,
|
LogExplorerPluginSetup,
|
||||||
LogExplorerPluginStart,
|
LogExplorerPluginStart,
|
||||||
LogExplorerSetupDeps,
|
LogExplorerSetupDeps,
|
||||||
|
@ -48,8 +49,14 @@ export class LogExplorerPlugin implements Plugin<LogExplorerPluginSetup, LogExpl
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createLogExplorerController = createLogExplorerControllerLazyFactory({
|
||||||
|
core,
|
||||||
|
plugins,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
LogExplorer,
|
LogExplorer,
|
||||||
|
createLogExplorerController,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
|
||||||
|
import {
|
||||||
|
DEFAULT_COLUMNS,
|
||||||
|
DEFAULT_ROWS_PER_PAGE,
|
||||||
|
LOG_LEVEL_FIELD,
|
||||||
|
} from '../../../../common/constants';
|
||||||
|
import { AllDatasetSelection } from '../../../../common/dataset_selection';
|
||||||
|
import { DefaultLogExplorerControllerState } from './types';
|
||||||
|
|
||||||
|
export const DEFAULT_CONTEXT: DefaultLogExplorerControllerState = {
|
||||||
|
datasetSelection: AllDatasetSelection.create(),
|
||||||
|
grid: {
|
||||||
|
columns: DEFAULT_COLUMNS,
|
||||||
|
rows: {
|
||||||
|
rowHeight: ROWS_HEIGHT_OPTIONS.single,
|
||||||
|
rowsPerPage: DEFAULT_ROWS_PER_PAGE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
chart: {
|
||||||
|
breakdownField: LOG_LEVEL_FIELD,
|
||||||
|
},
|
||||||
|
filters: [],
|
||||||
|
query: {
|
||||||
|
language: 'kuery',
|
||||||
|
query: '',
|
||||||
|
},
|
||||||
|
refreshInterval: {
|
||||||
|
pause: true,
|
||||||
|
value: 60000,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
mode: 'relative',
|
||||||
|
from: 'now-15m/m',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
};
|
|
@ -5,6 +5,6 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from './defaults';
|
||||||
export * from './state_machine';
|
export * from './state_machine';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './utils';
|
|
|
@ -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 type { DataView } from '@kbn/data-views-plugin/public';
|
||||||
|
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||||
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import { mapValues, pick } from 'lodash';
|
||||||
|
import { InvokeCreator } from 'xstate';
|
||||||
|
import {
|
||||||
|
availableControlPanelFields,
|
||||||
|
controlPanelConfigs,
|
||||||
|
ControlPanelRT,
|
||||||
|
ControlPanels,
|
||||||
|
} from '../../../../../common';
|
||||||
|
import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types';
|
||||||
|
|
||||||
|
export const initializeControlPanels =
|
||||||
|
(): InvokeCreator<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||||
|
async (context) => {
|
||||||
|
if (!('discoverStateContainer' in context)) return;
|
||||||
|
return context.controlPanels
|
||||||
|
? constructControlPanelsWithDataViewId(context.discoverStateContainer, context.controlPanels)
|
||||||
|
: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subscribeControlGroup =
|
||||||
|
(): InvokeCreator<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||||
|
(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<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||||
|
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
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,23 +5,15 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
|
||||||
import { InvokeCreator } from 'xstate';
|
import { InvokeCreator } from 'xstate';
|
||||||
import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types';
|
||||||
|
|
||||||
interface LogExplorerProfileDataViewStateDependencies {
|
|
||||||
stateContainer: DiscoverStateContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createAndSetDataView =
|
export const createAndSetDataView =
|
||||||
({
|
(): InvokeCreator<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||||
stateContainer,
|
|
||||||
}: LogExplorerProfileDataViewStateDependencies): InvokeCreator<
|
|
||||||
LogExplorerProfileContext,
|
|
||||||
LogExplorerProfileEvent
|
|
||||||
> =>
|
|
||||||
async (context) => {
|
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()
|
context.datasetSelection.toDataviewSpec()
|
||||||
);
|
);
|
||||||
/**
|
/**
|
||||||
|
@ -32,5 +24,5 @@ export const createAndSetDataView =
|
||||||
* to the existing one or the default logs-*.
|
* 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.
|
* 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);
|
||||||
};
|
};
|
|
@ -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<LogExplorerControllerEvent, LogExplorerControllerEvent> =>
|
||||||
|
(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));
|
||||||
|
};
|
|
@ -6,21 +6,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { InvokeCreator } from 'xstate';
|
import { InvokeCreator } from 'xstate';
|
||||||
import { Dataset } from '../../../../common/datasets';
|
import { Dataset } from '../../../../../common/datasets';
|
||||||
import { SingleDatasetSelection } from '../../../../common/dataset_selection';
|
import { SingleDatasetSelection } from '../../../../../common/dataset_selection';
|
||||||
import { IDatasetsClient } from '../../../services/datasets';
|
import { IDatasetsClient } from '../../../../services/datasets';
|
||||||
import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types';
|
||||||
|
|
||||||
interface LogExplorerProfileUrlStateDependencies {
|
interface LogExplorerControllerUrlStateDependencies {
|
||||||
datasetsClient: IDatasetsClient;
|
datasetsClient: IDatasetsClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateSelection =
|
export const validateSelection =
|
||||||
({
|
({
|
||||||
datasetsClient,
|
datasetsClient,
|
||||||
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
}: LogExplorerControllerUrlStateDependencies): InvokeCreator<
|
||||||
LogExplorerProfileContext,
|
LogExplorerControllerContext,
|
||||||
LogExplorerProfileEvent
|
LogExplorerControllerEvent
|
||||||
> =>
|
> =>
|
||||||
(context) =>
|
(context) =>
|
||||||
async (send) => {
|
async (send) => {
|
|
@ -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<LogExplorerControllerEvent> => {
|
||||||
|
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<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||||
|
(context, _event) => {
|
||||||
|
if (context.time != null) {
|
||||||
|
query.timefilter.timefilter.setTime(context.time);
|
||||||
|
}
|
||||||
|
if (context.refreshInterval != null) {
|
||||||
|
query.timefilter.timefilter.setRefreshInterval(context.refreshInterval);
|
||||||
|
}
|
||||||
|
};
|
|
@ -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
|
||||||
|
>;
|
|
@ -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<DatasetSelection>
|
||||||
|
| DoneInvokeEvent<ControlPanels>
|
||||||
|
| DoneInvokeEvent<ControlGroupAPI>
|
||||||
|
| DoneInvokeEvent<DatasetEncodingError>
|
||||||
|
| DoneInvokeEvent<Error>;
|
|
@ -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<LogExplorerProfileContext, LogExplorerProfileEvent, LogExplorerProfileTypeState>(
|
|
||||||
{
|
|
||||||
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
|
|
||||||
>;
|
|
|
@ -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<DatasetSelection>
|
|
||||||
| DoneInvokeEvent<ControlPanels>
|
|
||||||
| DoneInvokeEvent<ControlGroupAPI>
|
|
||||||
| DoneInvokeEvent<DatasetEncodingError>
|
|
||||||
| DoneInvokeEvent<Error>;
|
|
||||||
|
|
||||||
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<typeof ControlPanelRT>;
|
|
|
@ -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<ControlPanels>(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$<ControlPanels>(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
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -6,18 +6,22 @@
|
||||||
*/
|
*/
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
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 type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
|
||||||
import { SharePluginSetup } from '@kbn/share-plugin/public';
|
import type { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||||
import { LogExplorerLocators } from '../common/locators';
|
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 { LogExplorerProps } from './components/log_explorer';
|
||||||
|
import type { CreateLogExplorerController } from './controller';
|
||||||
|
|
||||||
export interface LogExplorerPluginSetup {
|
export interface LogExplorerPluginSetup {
|
||||||
locators: LogExplorerLocators;
|
locators: LogExplorerLocators;
|
||||||
}
|
}
|
||||||
export interface LogExplorerPluginStart {
|
export interface LogExplorerPluginStart {
|
||||||
LogExplorer: ComponentType<LogExplorerProps>;
|
LogExplorer: ComponentType<LogExplorerProps>;
|
||||||
|
createLogExplorerController: CreateLogExplorerController;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogExplorerSetupDeps {
|
export interface LogExplorerSetupDeps {
|
||||||
|
@ -30,4 +34,6 @@ export interface LogExplorerStartDeps {
|
||||||
dataViews: DataViewsPublicPluginStart;
|
dataViews: DataViewsPublicPluginStart;
|
||||||
discover: DiscoverStart;
|
discover: DiscoverStart;
|
||||||
fieldFormats: FieldFormatsStart;
|
fieldFormats: FieldFormatsStart;
|
||||||
|
navigation: NavigationPublicPluginStart;
|
||||||
|
unifiedSearch: UnifiedSearchPublicPluginStart;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<GridRowsDisplayOptions> => ({
|
||||||
|
...(discoverAppState.rowHeight != null ? { rowHeight: discoverAppState.rowHeight } : {}),
|
||||||
|
...(discoverAppState.rowsPerPage != null ? { rowsPerPage: discoverAppState.rowsPerPage } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getChartDisplayOptionsFromDiscoverAppState = (
|
||||||
|
discoverAppState: DiscoverAppState
|
||||||
|
): Partial<ChartDisplayOptions> => ({
|
||||||
|
breakdownField: discoverAppState.breakdownField ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getQueryStateFromDiscoverAppState = (
|
||||||
|
discoverAppState: DiscoverAppState
|
||||||
|
): QueryState => ({
|
||||||
|
query: discoverAppState.query,
|
||||||
|
filters: discoverAppState.filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getDiscoverAppStateFromContext = (
|
||||||
|
displayOptions: DisplayOptions & QueryState
|
||||||
|
): Partial<DiscoverAppState> => ({
|
||||||
|
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<NonNullable<DiscoverAppState['grid']>['columns']>
|
||||||
|
>((gridColumns, { field, width }) => {
|
||||||
|
if (width != null) {
|
||||||
|
gridColumns[field] = { width };
|
||||||
|
}
|
||||||
|
return gridColumns;
|
||||||
|
}, {}),
|
||||||
|
});
|
|
@ -3,31 +3,43 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "target/types"
|
"outDir": "target/types"
|
||||||
},
|
},
|
||||||
"include": ["../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*", ".storybook/**/*.tsx"],
|
"include": [
|
||||||
|
"../../../typings/**/*",
|
||||||
|
"common/**/*",
|
||||||
|
"public/**/*",
|
||||||
|
"server/**/*",
|
||||||
|
".storybook/**/*.tsx"
|
||||||
|
],
|
||||||
"kbn_references": [
|
"kbn_references": [
|
||||||
"@kbn/core",
|
|
||||||
"@kbn/discover-plugin",
|
|
||||||
"@kbn/i18n",
|
|
||||||
"@kbn/i18n-react",
|
|
||||||
"@kbn/fleet-plugin",
|
|
||||||
"@kbn/io-ts-utils",
|
|
||||||
"@kbn/data-views-plugin",
|
|
||||||
"@kbn/rison",
|
|
||||||
"@kbn/controls-plugin",
|
"@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/embeddable-plugin",
|
||||||
"@kbn/es-query",
|
"@kbn/es-query",
|
||||||
|
"@kbn/field-formats-plugin",
|
||||||
|
"@kbn/fleet-plugin",
|
||||||
|
"@kbn/i18n",
|
||||||
|
"@kbn/i18n-react",
|
||||||
|
"@kbn/io-ts-utils",
|
||||||
"@kbn/kibana-react-plugin",
|
"@kbn/kibana-react-plugin",
|
||||||
"@kbn/data-plugin",
|
"@kbn/kibana-utils-plugin",
|
||||||
"@kbn/unified-field-list",
|
"@kbn/navigation-plugin",
|
||||||
"@kbn/core-application-browser",
|
|
||||||
"@kbn/share-plugin",
|
"@kbn/share-plugin",
|
||||||
"@kbn/unified-data-table",
|
"@kbn/unified-data-table",
|
||||||
"@kbn/core-ui-settings-browser",
|
"@kbn/unified-field-list",
|
||||||
"@kbn/discover-utils",
|
"@kbn/unified-search-plugin",
|
||||||
"@kbn/deeplinks-observability",
|
"@kbn/xstate-utils"
|
||||||
"@kbn/field-formats-plugin",
|
|
||||||
"@kbn/custom-icons",
|
|
||||||
"@kbn/elastic-agent-utils"
|
|
||||||
],
|
],
|
||||||
"exclude": ["target/**/*"]
|
"exclude": [
|
||||||
|
"target/**/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,8 @@ describe('SLO Edit Page', () => {
|
||||||
const mockCreate = jest.fn();
|
const mockCreate = jest.fn();
|
||||||
const mockUpdate = jest.fn();
|
const mockUpdate = jest.fn();
|
||||||
|
|
||||||
|
const history = createBrowserHistory();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
mockKibana();
|
mockKibana();
|
||||||
|
@ -136,9 +138,8 @@ describe('SLO Edit Page', () => {
|
||||||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
|
||||||
history.replace('');
|
history.replace('');
|
||||||
jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history);
|
jest.spyOn(Router, 'useHistory').mockReturnValue(history);
|
||||||
|
|
||||||
useFetchDataViewsMock.mockReturnValue({
|
useFetchDataViewsMock.mockReturnValue({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
@ -256,11 +257,9 @@ describe('SLO Edit Page', () => {
|
||||||
it('prefills the form with values from URL', () => {
|
it('prefills the form with values from URL', () => {
|
||||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
|
||||||
history.replace(
|
history.replace(
|
||||||
'/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))'
|
'/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))'
|
||||||
);
|
);
|
||||||
jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history);
|
|
||||||
jest
|
jest
|
||||||
.spyOn(Router, 'useLocation')
|
.spyOn(Router, 'useLocation')
|
||||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||||
|
@ -336,11 +335,9 @@ describe('SLO Edit Page', () => {
|
||||||
const slo = buildSlo({ id: '123' });
|
const slo = buildSlo({ id: '123' });
|
||||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||||
|
|
||||||
const history = createBrowserHistory();
|
|
||||||
history.push(
|
history.push(
|
||||||
'/slos/123/edit?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))'
|
'/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
|
jest
|
||||||
.spyOn(Router, 'useLocation')
|
.spyOn(Router, 'useLocation')
|
||||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||||
|
|
|
@ -10,3 +10,5 @@ export {
|
||||||
SingleDatasetLocatorDefinition,
|
SingleDatasetLocatorDefinition,
|
||||||
AllDatasetsLocatorDefinition,
|
AllDatasetsLocatorDefinition,
|
||||||
} from './locators';
|
} from './locators';
|
||||||
|
export { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from './url_schema';
|
||||||
|
export { deepCompactObject } from './utils/deep_compact_object';
|
||||||
|
|
|
@ -23,11 +23,10 @@ export class AllDatasetsLocatorDefinition implements LocatorDefinition<AllDatase
|
||||||
|
|
||||||
public readonly getLocation = (params: AllDatasetsLocatorParams) => {
|
public readonly getLocation = (params: AllDatasetsLocatorParams) => {
|
||||||
const { useHash } = this.deps;
|
const { useHash } = this.deps;
|
||||||
const index = AllDatasetSelection.create().toDataviewSpec().id;
|
|
||||||
|
|
||||||
return constructLocatorPath({
|
return constructLocatorPath({
|
||||||
|
datasetSelection: AllDatasetSelection.create().toPlainSelection(),
|
||||||
locatorParams: params,
|
locatorParams: params,
|
||||||
index,
|
|
||||||
useHash,
|
useHash,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,13 +5,11 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FilterStateStore } from '@kbn/es-query';
|
import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
|
||||||
import { getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public';
|
|
||||||
import {
|
import {
|
||||||
AllDatasetsLocatorParams,
|
AllDatasetsLocatorParams,
|
||||||
SingleDatasetLocatorParams,
|
SingleDatasetLocatorParams,
|
||||||
} from '@kbn/deeplinks-observability/locators';
|
} from '@kbn/deeplinks-observability/locators';
|
||||||
import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability';
|
|
||||||
import { AllDatasetsLocatorDefinition } from './all_datasets/all_datasets_locator';
|
import { AllDatasetsLocatorDefinition } from './all_datasets/all_datasets_locator';
|
||||||
import { SingleDatasetLocatorDefinition } from './single_dataset';
|
import { SingleDatasetLocatorDefinition } from './single_dataset';
|
||||||
import { DatasetLocatorDependencies } from './types';
|
import { DatasetLocatorDependencies } from './types';
|
||||||
|
@ -38,8 +36,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await allDatasetsLocator.getLocation({});
|
const location = await allDatasetsLocator.getLocation({});
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
path: '/?pageState=(datasetSelection:(selectionType:all),v:1)',
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -53,8 +51,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await allDatasetsLocator.getLocation(params);
|
const location = await allDatasetsLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: '/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
path: '/?pageState=(datasetSelection:(selectionType:all),time:(from:now-30m,to:now),v:1)',
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -70,8 +68,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await allDatasetsLocator.getLocation(params);
|
const location = await allDatasetsLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,query:(language:kuery,query:foo))',
|
path: '/?pageState=(datasetSelection:(selectionType:all),query:(language:kuery,query:foo),v:1)',
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -88,29 +86,28 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await allDatasetsLocator.getLocation(params);
|
const location = await allDatasetsLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: '/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
path: '/?pageState=(datasetSelection:(selectionType:all),refreshInterval:(pause:!f,value:666),v:1)',
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow specifiying columns and sort', async () => {
|
it('should allow specifiying columns', async () => {
|
||||||
const params: AllDatasetsLocatorParams = {
|
const params: AllDatasetsLocatorParams = {
|
||||||
columns: ['_source'],
|
columns: ['_source'],
|
||||||
sort: [['timestamp, asc']] as string[][],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { allDatasetsLocator } = await setup();
|
const { allDatasetsLocator } = await setup();
|
||||||
const location = await allDatasetsLocator.getLocation(params);
|
const location = await allDatasetsLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,sort:!(!('timestamp,%20asc')))`,
|
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow specifiying filters', async () => {
|
it('should allow specifying filters', async () => {
|
||||||
const params: AllDatasetsLocatorParams = {
|
const params: AllDatasetsLocatorParams = {
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
|
@ -119,9 +116,6 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
negate: false,
|
negate: false,
|
||||||
},
|
},
|
||||||
$state: {
|
|
||||||
store: FilterStateStore.APP_STATE,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -129,47 +123,16 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
negate: false,
|
negate: false,
|
||||||
},
|
},
|
||||||
$state: {
|
|
||||||
store: FilterStateStore.GLOBAL_STATE,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const { allDatasetsLocator } = await setup();
|
const { allDatasetsLocator } = await setup();
|
||||||
const { path } = await allDatasetsLocator.getLocation(params);
|
const location = await allDatasetsLocator.getLocation(params);
|
||||||
|
|
||||||
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: 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)"`
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,8 +147,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -201,8 +164,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await singleDatasetLocator.getLocation(params);
|
const location = await singleDatasetLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),time:(from:now-30m,to:now),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -221,8 +184,8 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await singleDatasetLocator.getLocation(params);
|
const location = await singleDatasetLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,query:(language:kuery,query:foo))`,
|
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),query:(language:kuery,query:foo),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -241,26 +204,25 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
const location = await singleDatasetLocator.getLocation(params);
|
const location = await singleDatasetLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),refreshInterval:(pause:!f,value:666),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow specifiying columns and sort', async () => {
|
it('should allow specifiying columns', async () => {
|
||||||
const params: SingleDatasetLocatorParams = {
|
const params: SingleDatasetLocatorParams = {
|
||||||
integration,
|
integration,
|
||||||
dataset,
|
dataset,
|
||||||
columns: ['_source'],
|
columns: ['_source'],
|
||||||
sort: [['timestamp, asc']] as string[][],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { singleDatasetLocator } = await setup();
|
const { singleDatasetLocator } = await setup();
|
||||||
const location = await singleDatasetLocator.getLocation(params);
|
const location = await singleDatasetLocator.getLocation(params);
|
||||||
|
|
||||||
expect(location).toMatchObject({
|
expect(location).toMatchObject({
|
||||||
app: OBSERVABILITY_LOG_EXPLORER,
|
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,sort:!(!('timestamp,%20asc')))`,
|
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
|
||||||
state: {},
|
state: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -276,9 +238,6 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
negate: false,
|
negate: false,
|
||||||
},
|
},
|
||||||
$state: {
|
|
||||||
store: FilterStateStore.APP_STATE,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -286,48 +245,16 @@ describe('Observability Logs Explorer Locators', () => {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
negate: false,
|
negate: false,
|
||||||
},
|
},
|
||||||
$state: {
|
|
||||||
store: FilterStateStore.GLOBAL_STATE,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const { singleDatasetLocator } = await setup();
|
const { singleDatasetLocator } = await setup();
|
||||||
const { path } = await singleDatasetLocator.getLocation(params);
|
const location = await singleDatasetLocator.getLocation(params);
|
||||||
|
|
||||||
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: 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)"`
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,11 +35,9 @@ export class SingleDatasetLocatorDefinition
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const index = unresolvedDatasetSelection.toDataviewSpec().id;
|
|
||||||
|
|
||||||
return constructLocatorPath({
|
return constructLocatorPath({
|
||||||
|
datasetSelection: unresolvedDatasetSelection.toPlainSelection(),
|
||||||
locatorParams: params,
|
locatorParams: params,
|
||||||
index,
|
|
||||||
useHash,
|
useHash,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,16 +5,6 @@
|
||||||
* 2.0.
|
* 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 {
|
export interface DatasetLocatorDependencies {
|
||||||
useHash: boolean;
|
useHash: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<urlSchemaV1.UrlSchema['controls']>;
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -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<GlobalQueryStateFromUrl>(
|
|
||||||
'_g',
|
|
||||||
queryState,
|
|
||||||
{ useHash, storeInHashQuery: false },
|
|
||||||
path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
path = setStateToKbnUrl('_a', appState, { useHash, storeInHashQuery: false }, path);
|
|
||||||
|
|
||||||
return {
|
|
||||||
app: 'observability-log-explorer',
|
|
||||||
path,
|
|
||||||
state: {
|
|
||||||
...(origin ? { origin } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -5,4 +5,4 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from './helpers';
|
export * from './construct_locator_path';
|
||||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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<typeof urlSchemaRT>;
|
|
@ -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 = <Value extends Record<string, any>>(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)))
|
||||||
|
);
|
|
@ -10,12 +10,13 @@ import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
|
||||||
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
import { Route, Router, Routes } from '@kbn/shared-ux-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { DatasetQualityRoute, ObservablityLogExplorerMainRoute } from '../routes/main';
|
import { DatasetQualityRoute, ObservabilityLogExplorerMainRoute } from '../routes/main';
|
||||||
import {
|
import {
|
||||||
ObservabilityLogExplorerAppMountParameters,
|
ObservabilityLogExplorerAppMountParameters,
|
||||||
ObservabilityLogExplorerPluginStart,
|
ObservabilityLogExplorerPluginStart,
|
||||||
ObservabilityLogExplorerStartDeps,
|
ObservabilityLogExplorerStartDeps,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { KbnUrlStateStorageFromRouterProvider } from '../utils/kbn_url_state_context';
|
||||||
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
||||||
|
|
||||||
export const renderObservabilityLogExplorer = (
|
export const renderObservabilityLogExplorer = (
|
||||||
|
@ -59,26 +60,25 @@ export const ObservabilityLogExplorerApp = ({
|
||||||
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
|
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
|
||||||
core,
|
core,
|
||||||
plugins,
|
plugins,
|
||||||
pluginStart
|
pluginStart,
|
||||||
|
appParams
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
|
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
|
||||||
<KibanaContextProviderForPlugin>
|
<KibanaContextProviderForPlugin>
|
||||||
<Router history={appParams.history}>
|
<KbnUrlStateStorageFromRouterProvider>
|
||||||
<Routes>
|
<Router history={appParams.history}>
|
||||||
<Route
|
<Routes>
|
||||||
path="/"
|
<Route path="/" exact={true} render={() => <ObservabilityLogExplorerMainRoute />} />
|
||||||
exact={true}
|
<Route
|
||||||
render={() => <ObservablityLogExplorerMainRoute appParams={appParams} core={core} />}
|
path="/dataset-quality"
|
||||||
/>
|
exact={true}
|
||||||
<Route
|
render={() => <DatasetQualityRoute core={core} />}
|
||||||
path="/dataset-quality"
|
/>
|
||||||
exact={true}
|
</Routes>
|
||||||
render={() => <DatasetQualityRoute core={core} />}
|
</Router>
|
||||||
/>
|
</KbnUrlStateStorageFromRouterProvider>
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
</KibanaContextProviderForPlugin>
|
</KibanaContextProviderForPlugin>
|
||||||
</KibanaRenderContextProvider>
|
</KibanaRenderContextProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 <DiscoverLinkForValidState discover={discover} pageState={pageState} />;
|
||||||
|
} else {
|
||||||
|
return <DiscoverLinkForUnknownState />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
type InitializedPageState = MatchedStateFromActor<
|
||||||
|
ObservabilityLogExplorerService,
|
||||||
|
{ initialized: 'validLogExplorerState' }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const DiscoverLinkForValidState = React.memo(
|
||||||
|
({
|
||||||
|
discover,
|
||||||
|
pageState: {
|
||||||
|
context: { logExplorerState },
|
||||||
|
},
|
||||||
|
}: {
|
||||||
|
discover: DiscoverStart;
|
||||||
|
pageState: InitializedPageState;
|
||||||
|
}) => {
|
||||||
|
const discoverLinkParams = useMemo<DiscoverAppLocatorParams>(
|
||||||
|
() => ({
|
||||||
|
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 <DiscoverLink discover={discover} discoverLinkParams={discoverLinkParams} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DiscoverLinkForUnknownState = React.memo(() => (
|
||||||
|
<EuiHeaderLink
|
||||||
|
color="primary"
|
||||||
|
iconType="discoverApp"
|
||||||
|
data-test-subj="logExplorerDiscoverFallbackLink"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{discoverLinkTitle}
|
||||||
|
</EuiHeaderLink>
|
||||||
|
));
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<EuiHeaderLink
|
||||||
|
{...discoverLinkProps}
|
||||||
|
color="primary"
|
||||||
|
iconType="discoverApp"
|
||||||
|
data-test-subj="logExplorerDiscoverFallbackLink"
|
||||||
|
>
|
||||||
|
{discoverLinkTitle}
|
||||||
|
</EuiHeaderLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -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 (
|
||||||
|
<EuiHeaderLink
|
||||||
|
color="primary"
|
||||||
|
href={LOG_EXPLORER_FEEDBACK_LINK}
|
||||||
|
iconType="popout"
|
||||||
|
iconSide="right"
|
||||||
|
iconSize="s"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{feedbackLinkTitle}
|
||||||
|
</EuiHeaderLink>
|
||||||
|
);
|
||||||
|
});
|
|
@ -5,74 +5,39 @@
|
||||||
* 2.0.
|
* 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 {
|
import {
|
||||||
EuiBetaBadge,
|
EuiBetaBadge,
|
||||||
EuiButton,
|
|
||||||
EuiHeader,
|
EuiHeader,
|
||||||
EuiHeaderLink,
|
|
||||||
EuiHeaderLinks,
|
EuiHeaderLinks,
|
||||||
EuiHeaderSection,
|
EuiHeaderSection,
|
||||||
EuiHeaderSectionItem,
|
EuiHeaderSectionItem,
|
||||||
} from '@elastic/eui';
|
} 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 { 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 { euiThemeVars } from '@kbn/ui-theme';
|
||||||
import { LogExplorerTabs } from '@kbn/discover-plugin/public';
|
import { LogExplorerTabs } from '@kbn/discover-plugin/public';
|
||||||
import { PluginKibanaContextValue } from '../utils/use_kibana';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
betaBadgeDescription,
|
import { filter, take } from 'rxjs';
|
||||||
betaBadgeTitle,
|
import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations';
|
||||||
discoverLinkTitle,
|
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||||
feedbackLinkTitle,
|
import { ConnectedDiscoverLink } from './discover_link';
|
||||||
onboardingLinkTitle,
|
import { FeedbackLink } from './feedback_link';
|
||||||
} from '../../common/translations';
|
import { ConnectedOnboardingLink } from './onboarding_link';
|
||||||
import { getRouterLinkProps } from '../utils/get_router_link_props';
|
|
||||||
import { ObservabilityLogExplorerAppMountParameters } from '../types';
|
|
||||||
|
|
||||||
interface LogExplorerTopNavMenuProps {
|
export const LogExplorerTopNavMenu = () => {
|
||||||
setHeaderActionMenu: ObservabilityLogExplorerAppMountParameters['setHeaderActionMenu'];
|
const {
|
||||||
services: KibanaReactContextValue<PluginKibanaContextValue>['services'];
|
services: { serverless },
|
||||||
state$: BehaviorSubject<LogExplorerStateContainer>;
|
} = useKibanaContextForPlugin();
|
||||||
theme$: ObservabilityLogExplorerAppMountParameters['theme$'];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LogExplorerTopNavMenu = ({
|
return Boolean(serverless) ? <ServerlessTopNav /> : <StatefulTopNav />;
|
||||||
setHeaderActionMenu,
|
|
||||||
services,
|
|
||||||
state$,
|
|
||||||
theme$,
|
|
||||||
}: LogExplorerTopNavMenuProps) => {
|
|
||||||
const { serverless } = services;
|
|
||||||
|
|
||||||
return Boolean(serverless) ? (
|
|
||||||
<ServerlessTopNav services={services} state$={state$} />
|
|
||||||
) : (
|
|
||||||
<StatefulTopNav
|
|
||||||
services={services}
|
|
||||||
setHeaderActionMenu={setHeaderActionMenu}
|
|
||||||
state$={state$}
|
|
||||||
theme$={theme$}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ServerlessTopNav = ({
|
const ServerlessTopNav = () => {
|
||||||
services,
|
const { services } = useKibanaContextForPlugin();
|
||||||
state$,
|
|
||||||
}: Pick<LogExplorerTopNavMenuProps, 'services' | 'state$'>) => {
|
|
||||||
return (
|
return (
|
||||||
<EuiHeader data-test-subj="logExplorerHeaderMenu" css={{ boxShadow: 'none' }}>
|
<EuiHeader data-test-subj="logExplorerHeaderMenu" css={{ boxShadow: 'none' }}>
|
||||||
<EuiHeaderSection>
|
<EuiHeaderSection>
|
||||||
|
@ -97,38 +62,40 @@ const ServerlessTopNav = ({
|
||||||
</EuiHeaderSectionItem>
|
</EuiHeaderSectionItem>
|
||||||
<EuiHeaderSectionItem>
|
<EuiHeaderSectionItem>
|
||||||
<EuiHeaderLinks gutterSize="xs">
|
<EuiHeaderLinks gutterSize="xs">
|
||||||
<DiscoverLink services={services} state$={state$} />
|
<ConnectedDiscoverLink />
|
||||||
<FeedbackLink />
|
<FeedbackLink />
|
||||||
</EuiHeaderLinks>
|
</EuiHeaderLinks>
|
||||||
<VerticalRule />
|
<VerticalRule />
|
||||||
</EuiHeaderSectionItem>
|
</EuiHeaderSectionItem>
|
||||||
<EuiHeaderSectionItem>
|
<EuiHeaderSectionItem>
|
||||||
<OnboardingLink services={services} />
|
<ConnectedOnboardingLink />
|
||||||
</EuiHeaderSectionItem>
|
</EuiHeaderSectionItem>
|
||||||
</EuiHeaderSection>
|
</EuiHeaderSection>
|
||||||
</EuiHeader>
|
</EuiHeader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatefulTopNav = ({
|
const StatefulTopNav = () => {
|
||||||
setHeaderActionMenu,
|
const {
|
||||||
services,
|
services: {
|
||||||
state$,
|
appParams: { setHeaderActionMenu },
|
||||||
theme$,
|
chrome,
|
||||||
}: LogExplorerTopNavMenuProps) => {
|
i18n,
|
||||||
|
theme,
|
||||||
|
},
|
||||||
|
} = useKibanaContextForPlugin();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Since the breadcrumbsAppendExtension might be set only during a plugin start (e.g. search session)
|
* 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.
|
* we retrieve the latest valid extension in order to restore it once we unmount the beta badge.
|
||||||
*/
|
*/
|
||||||
const [previousAppendExtension$] = useState(() =>
|
const [previousAppendExtension$] = useState(() =>
|
||||||
services.chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1))
|
chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1))
|
||||||
);
|
);
|
||||||
|
|
||||||
const previousAppendExtension = useObservable(previousAppendExtension$);
|
const previousAppendExtension = useObservable(previousAppendExtension$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { chrome, i18n, theme } = services;
|
|
||||||
|
|
||||||
if (chrome) {
|
if (chrome) {
|
||||||
chrome.setBreadcrumbsAppendExtension({
|
chrome.setBreadcrumbsAppendExtension({
|
||||||
content: toMountPoint(
|
content: toMountPoint(
|
||||||
|
@ -161,15 +128,15 @@ const StatefulTopNav = ({
|
||||||
chrome.setBreadcrumbsAppendExtension(previousAppendExtension);
|
chrome.setBreadcrumbsAppendExtension(previousAppendExtension);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [services, previousAppendExtension]);
|
}, [chrome, i18n, previousAppendExtension, theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
|
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme.theme$}>
|
||||||
<EuiHeaderSection data-test-subj="logExplorerHeaderMenu">
|
<EuiHeaderSection data-test-subj="logExplorerHeaderMenu">
|
||||||
<EuiHeaderSectionItem>
|
<EuiHeaderSectionItem>
|
||||||
<EuiHeaderLinks gutterSize="xs">
|
<EuiHeaderLinks gutterSize="xs">
|
||||||
<DiscoverLink services={services} state$={state$} />
|
<ConnectedDiscoverLink />
|
||||||
<OnboardingLink services={services} />
|
<ConnectedOnboardingLink />
|
||||||
</EuiHeaderLinks>
|
</EuiHeaderLinks>
|
||||||
</EuiHeaderSectionItem>
|
</EuiHeaderSectionItem>
|
||||||
</EuiHeaderSection>
|
</EuiHeaderSection>
|
||||||
|
@ -177,113 +144,8 @@ const StatefulTopNav = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DiscoverLink = React.memo(
|
|
||||||
({ services, state$ }: Pick<LogExplorerTopNavMenuProps, 'services' | 'state$'>) => {
|
|
||||||
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 (
|
|
||||||
<EuiHeaderLink
|
|
||||||
{...discoverLinkProps}
|
|
||||||
color="primary"
|
|
||||||
data-test-subj="logExplorerDiscoverFallbackLink"
|
|
||||||
>
|
|
||||||
{discoverLinkTitle}
|
|
||||||
</EuiHeaderLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const OnboardingLink = React.memo(({ services }: Pick<LogExplorerTopNavMenuProps, 'services'>) => {
|
|
||||||
const locator = services.share.url.locators.get<ObservabilityOnboardingLocatorParams>(
|
|
||||||
OBSERVABILITY_ONBOARDING_LOCATOR
|
|
||||||
);
|
|
||||||
|
|
||||||
const onboardingUrl = locator?.useUrl({});
|
|
||||||
|
|
||||||
const navigateToOnboarding = () => {
|
|
||||||
locator?.navigate({});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onboardingLinkProps = getRouterLinkProps({
|
|
||||||
href: onboardingUrl,
|
|
||||||
onClick: navigateToOnboarding,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EuiButton
|
|
||||||
{...onboardingLinkProps}
|
|
||||||
fill
|
|
||||||
size="s"
|
|
||||||
iconType="indexOpen"
|
|
||||||
data-test-subj="logExplorerOnboardingLink"
|
|
||||||
>
|
|
||||||
{onboardingLinkTitle}
|
|
||||||
</EuiButton>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const FeedbackLink = React.memo(() => {
|
|
||||||
return (
|
|
||||||
<EuiHeaderLink
|
|
||||||
color="primary"
|
|
||||||
href={LOG_EXPLORER_FEEDBACK_LINK}
|
|
||||||
iconType="popout"
|
|
||||||
iconSide="right"
|
|
||||||
iconSize="s"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{feedbackLinkTitle}
|
|
||||||
</EuiHeaderLink>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const VerticalRule = styled.span`
|
const VerticalRule = styled.span`
|
||||||
width: 1px;
|
width: 1px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background-color: ${euiThemeVars.euiColorLightShade};
|
background-color: ${euiThemeVars.euiColorLightShade};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const useDiscoverLinkParams = (state$: BehaviorSubject<LogExplorerStateContainer>) => {
|
|
||||||
const { appState, logExplorerState } = useObservable<LogExplorerStateContainer>(
|
|
||||||
state$.pipe(
|
|
||||||
distinctUntilChanged<LogExplorerStateContainer>((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(),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -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 <OnboardingLink urlService={url} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const OnboardingLink = React.memo(({ urlService }: { urlService: BrowserUrlService }) => {
|
||||||
|
const locator = urlService.locators.get<ObservabilityOnboardingLocatorParams>(
|
||||||
|
OBSERVABILITY_ONBOARDING_LOCATOR
|
||||||
|
);
|
||||||
|
|
||||||
|
const onboardingUrl = locator?.useUrl({});
|
||||||
|
|
||||||
|
const navigateToOnboarding = () => {
|
||||||
|
locator?.navigate({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onboardingLinkProps = getRouterLinkProps({
|
||||||
|
href: onboardingUrl,
|
||||||
|
onClick: navigateToOnboarding,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EuiButton
|
||||||
|
{...onboardingLinkProps}
|
||||||
|
fill
|
||||||
|
size="s"
|
||||||
|
iconType="indexOpen"
|
||||||
|
data-test-subj="logExplorerOnboardingLink"
|
||||||
|
>
|
||||||
|
{onboardingLinkTitle}
|
||||||
|
</EuiButton>
|
||||||
|
);
|
||||||
|
});
|
|
@ -7,23 +7,27 @@
|
||||||
|
|
||||||
import { EuiPageSectionProps } from '@elastic/eui';
|
import { EuiPageSectionProps } from '@elastic/eui';
|
||||||
import { css } from '@emotion/react';
|
import { css } from '@emotion/react';
|
||||||
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||||
|
|
||||||
export const ObservabilityLogExplorerPageTemplate = ({
|
export const ObservabilityLogExplorerPageTemplate = ({
|
||||||
children,
|
children,
|
||||||
observabilityShared,
|
|
||||||
pageProps,
|
pageProps,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
observabilityShared: ObservabilitySharedPluginStart;
|
|
||||||
pageProps?: EuiPageSectionProps;
|
pageProps?: EuiPageSectionProps;
|
||||||
}>) => (
|
}>) => {
|
||||||
<observabilityShared.navigation.PageTemplate
|
const {
|
||||||
pageSectionProps={{ ...pageSectionProps, ...pageProps }}
|
services: { observabilityShared },
|
||||||
>
|
} = useKibanaContextForPlugin();
|
||||||
{children}
|
|
||||||
</observabilityShared.navigation.PageTemplate>
|
return (
|
||||||
);
|
<observabilityShared.navigation.PageTemplate
|
||||||
|
pageSectionProps={{ ...pageSectionProps, ...pageProps }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</observabilityShared.navigation.PageTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const fullHeightContentStyles = css`
|
const fullHeightContentStyles = css`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -13,6 +13,9 @@ import type { LogAIAssistantDocument } from '@kbn/logs-shared-plugin/public';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||||
|
|
||||||
|
type RenderFlyoutContentCustomization =
|
||||||
|
Required<LogExplorerCustomizations>['flyout']['renderContent'];
|
||||||
|
|
||||||
const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => {
|
const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => {
|
||||||
const { services } = useKibanaContextForPlugin();
|
const { services } = useKibanaContextForPlugin();
|
||||||
const { LogAIAssistant } = services.logsShared;
|
const { LogAIAssistant } = services.logsShared;
|
||||||
|
@ -22,19 +25,17 @@ const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) =>
|
||||||
return <LogAIAssistant key={doc.id} doc={mappedDoc} />;
|
return <LogAIAssistant key={doc.id} doc={mappedDoc} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderFlyoutContent: Required<LogExplorerCustomizations>['flyout']['renderContent'] = (
|
export const renderFlyoutContent: RenderFlyoutContentCustomization =
|
||||||
renderPreviousContent,
|
(renderPreviousContent) => (props) => {
|
||||||
props
|
return (
|
||||||
) => {
|
<>
|
||||||
return (
|
{renderPreviousContent(props)}
|
||||||
<>
|
<EuiFlexItem>
|
||||||
{renderPreviousContent()}
|
<ObservabilityLogAIAssistant {...props} />
|
||||||
<EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<ObservabilityLogAIAssistant {...props} />
|
</>
|
||||||
</EuiFlexItem>
|
);
|
||||||
</>
|
};
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utils
|
* Utils
|
||||||
|
|
|
@ -5,11 +5,18 @@
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LogExplorerCustomizations } from '@kbn/log-explorer-plugin/public';
|
import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public';
|
||||||
import { renderFlyoutContent } from './flyout_content';
|
import { renderFlyoutContent } from './flyout_content';
|
||||||
|
|
||||||
export const createLogExplorerCustomizations = (): LogExplorerCustomizations => ({
|
export const createLogExplorerControllerWithCustomizations =
|
||||||
flyout: {
|
(createLogExplorerController: CreateLogExplorerController): CreateLogExplorerController =>
|
||||||
renderContent: renderFlyoutContent,
|
(args) =>
|
||||||
},
|
createLogExplorerController({
|
||||||
});
|
...args,
|
||||||
|
customizations: {
|
||||||
|
...args.customizations,
|
||||||
|
flyout: {
|
||||||
|
renderContent: renderFlyoutContent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -13,15 +13,14 @@ import {
|
||||||
Plugin,
|
Plugin,
|
||||||
PluginInitializerContext,
|
PluginInitializerContext,
|
||||||
} from '@kbn/core/public';
|
} from '@kbn/core/public';
|
||||||
import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability';
|
import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
|
||||||
import {
|
import {
|
||||||
|
AllDatasetsLocatorDefinition,
|
||||||
ObservabilityLogExplorerLocators,
|
ObservabilityLogExplorerLocators,
|
||||||
SingleDatasetLocatorDefinition,
|
SingleDatasetLocatorDefinition,
|
||||||
AllDatasetsLocatorDefinition,
|
|
||||||
} from '../common/locators';
|
} from '../common/locators';
|
||||||
import { type ObservabilityLogExplorerConfig } from '../common/plugin_config';
|
import { type ObservabilityLogExplorerConfig } from '../common/plugin_config';
|
||||||
import { logExplorerAppTitle } from '../common/translations';
|
import { logExplorerAppTitle } from '../common/translations';
|
||||||
import { renderObservabilityLogExplorer } from './applications/observability_log_explorer';
|
|
||||||
import type {
|
import type {
|
||||||
ObservabilityLogExplorerAppMountParameters,
|
ObservabilityLogExplorerAppMountParameters,
|
||||||
ObservabilityLogExplorerPluginSetup,
|
ObservabilityLogExplorerPluginSetup,
|
||||||
|
@ -48,7 +47,7 @@ export class ObservabilityLogExplorerPlugin
|
||||||
const useHash = core.uiSettings.get('state:storeInSessionStorage');
|
const useHash = core.uiSettings.get('state:storeInSessionStorage');
|
||||||
|
|
||||||
core.application.register({
|
core.application.register({
|
||||||
id: OBSERVABILITY_LOG_EXPLORER,
|
id: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||||
title: logExplorerAppTitle,
|
title: logExplorerAppTitle,
|
||||||
category: DEFAULT_APP_CATEGORIES.observability,
|
category: DEFAULT_APP_CATEGORIES.observability,
|
||||||
euiIconType: 'logoLogging',
|
euiIconType: 'logoLogging',
|
||||||
|
@ -59,6 +58,9 @@ export class ObservabilityLogExplorerPlugin
|
||||||
keywords: ['logs', 'log', 'explorer', 'logs explorer'],
|
keywords: ['logs', 'log', 'explorer', 'logs explorer'],
|
||||||
mount: async (appMountParams: ObservabilityLogExplorerAppMountParameters) => {
|
mount: async (appMountParams: ObservabilityLogExplorerAppMountParameters) => {
|
||||||
const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices();
|
const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices();
|
||||||
|
const { renderObservabilityLogExplorer } = await import(
|
||||||
|
'./applications/observability_log_explorer'
|
||||||
|
);
|
||||||
|
|
||||||
return renderObservabilityLogExplorer(
|
return renderObservabilityLogExplorer(
|
||||||
coreStart,
|
coreStart,
|
||||||
|
|
|
@ -19,7 +19,7 @@ export interface DatasetQualityRouteProps {
|
||||||
|
|
||||||
export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => {
|
export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => {
|
||||||
const { services } = useKibanaContextForPlugin();
|
const { services } = useKibanaContextForPlugin();
|
||||||
const { observabilityShared, serverless, datasetQuality: DatasetQuality } = services;
|
const { serverless, datasetQuality: DatasetQuality } = services;
|
||||||
const breadcrumb: EuiBreadcrumb[] = [
|
const breadcrumb: EuiBreadcrumb[] = [
|
||||||
{
|
{
|
||||||
text: datasetQualityAppTitle,
|
text: datasetQualityAppTitle,
|
||||||
|
@ -29,13 +29,8 @@ export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => {
|
||||||
useBreadcrumbs(breadcrumb, core.chrome, serverless);
|
useBreadcrumbs(breadcrumb, core.chrome, serverless);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ObservabilityLogExplorerPageTemplate pageProps={{ paddingSize: 'l' }}>
|
||||||
<ObservabilityLogExplorerPageTemplate
|
<DatasetQuality.DatasetQuality />
|
||||||
observabilityShared={observabilityShared}
|
</ObservabilityLogExplorerPageTemplate>
|
||||||
pageProps={{ paddingSize: 'l' }}
|
|
||||||
>
|
|
||||||
<DatasetQuality.DatasetQuality />
|
|
||||||
</ObservabilityLogExplorerPageTemplate>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,52 +4,106 @@
|
||||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||||
* 2.0.
|
* 2.0.
|
||||||
*/
|
*/
|
||||||
|
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
|
||||||
import { CoreStart } from '@kbn/core/public';
|
import { FormattedMessage } from '@kbn/i18n-react';
|
||||||
import React, { useMemo, useState } from 'react';
|
import type {
|
||||||
import { BehaviorSubject } from 'rxjs';
|
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 { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu';
|
||||||
import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template';
|
import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template';
|
||||||
import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs';
|
import { createLogExplorerControllerWithCustomizations } from '../../log_explorer_customizations';
|
||||||
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
import {
|
||||||
import { ObservabilityLogExplorerAppMountParameters } from '../../types';
|
ObservabilityLogExplorerPageStateProvider,
|
||||||
|
useObservabilityLogExplorerPageStateContext,
|
||||||
|
} from '../../state_machines/observability_log_explorer/src';
|
||||||
import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component';
|
import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component';
|
||||||
import { createLogExplorerCustomizations } from '../../log_explorer_customizations';
|
import { ObservabilityLogExplorerHistory } from '../../types';
|
||||||
export interface ObservablityLogExplorerMainRouteProps {
|
import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs';
|
||||||
appParams: ObservabilityLogExplorerAppMountParameters;
|
import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context';
|
||||||
core: CoreStart;
|
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
||||||
}
|
|
||||||
|
|
||||||
export const ObservablityLogExplorerMainRoute = ({
|
export const ObservabilityLogExplorerMainRoute = () => {
|
||||||
appParams,
|
|
||||||
core,
|
|
||||||
}: ObservablityLogExplorerMainRouteProps) => {
|
|
||||||
const { services } = useKibanaContextForPlugin();
|
const { services } = useKibanaContextForPlugin();
|
||||||
const { logExplorer, observabilityShared, serverless } = services;
|
const { logExplorer, serverless, chrome, notifications, appParams } = services;
|
||||||
useBreadcrumbs(noBreadcrumbs, core.chrome, serverless);
|
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 (
|
return (
|
||||||
<>
|
<ObservabilityLogExplorerPageStateProvider
|
||||||
<LogExplorerTopNavMenu
|
createLogExplorerController={createLogExplorerController}
|
||||||
setHeaderActionMenu={setHeaderActionMenu}
|
toasts={notifications.toasts}
|
||||||
services={services}
|
urlStateStorageContainer={urlStateStorageContainer}
|
||||||
state$={state$}
|
timeFilterService={services.data.query.timefilter.timefilter}
|
||||||
theme$={theme$}
|
>
|
||||||
/>
|
<LogExplorerTopNavMenu />
|
||||||
<LazyOriginInterpreter history={history} toasts={core.notifications.toasts} />
|
<LazyOriginInterpreter history={history} toasts={notifications.toasts} />
|
||||||
<ObservabilityLogExplorerPageTemplate observabilityShared={observabilityShared}>
|
<ConnectedContent />
|
||||||
<logExplorer.LogExplorer
|
</ObservabilityLogExplorerPageStateProvider>
|
||||||
customizations={customizations}
|
|
||||||
scopedHistory={history}
|
|
||||||
state$={state$}
|
|
||||||
/>
|
|
||||||
</ObservabilityLogExplorerPageTemplate>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ConnectedContent = React.memo(() => {
|
||||||
|
const {
|
||||||
|
services: {
|
||||||
|
appParams: { history },
|
||||||
|
logExplorer,
|
||||||
|
},
|
||||||
|
} = useKibanaContextForPlugin();
|
||||||
|
|
||||||
|
const [state] = useActor(useObservabilityLogExplorerPageStateContext());
|
||||||
|
|
||||||
|
if (state.matches('initialized')) {
|
||||||
|
return (
|
||||||
|
<InitializedContent
|
||||||
|
logExplorerController={state.context.controller}
|
||||||
|
history={history}
|
||||||
|
logExplorer={logExplorer}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <InitializingContent />;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const InitializingContent = React.memo(() => (
|
||||||
|
<ObservabilityLogExplorerPageTemplate>
|
||||||
|
<EuiEmptyPrompt
|
||||||
|
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id="xpack.observabilityLogExplorer.InitializingTitle"
|
||||||
|
defaultMessage="Initializing the Log Explorer"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ObservabilityLogExplorerPageTemplate>
|
||||||
|
));
|
||||||
|
|
||||||
|
const InitializedContent = React.memo(
|
||||||
|
({
|
||||||
|
history,
|
||||||
|
logExplorer,
|
||||||
|
logExplorerController,
|
||||||
|
}: {
|
||||||
|
history: ObservabilityLogExplorerHistory;
|
||||||
|
logExplorer: LogExplorerPluginStart;
|
||||||
|
logExplorerController: LogExplorerController;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ObservabilityLogExplorerPageTemplate>
|
||||||
|
<logExplorer.LogExplorer controller={logExplorerController} scopedHistory={history} />
|
||||||
|
</ObservabilityLogExplorerPageTemplate>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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<ObservabilityLogExplorerContext, ObservabilityLogExplorerEvent> =>
|
||||||
|
(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();
|
||||||
|
};
|
||||||
|
};
|
|
@ -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: {},
|
||||||
|
};
|
|
@ -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';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue