mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -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 OBSERVABILITY_LOG_EXPLORER = 'observability-log-explorer';
|
||||
export const OBSERVABILITY_LOG_EXPLORER_APP_ID = 'observability-log-explorer';
|
||||
|
||||
export const OBSERVABILITY_OVERVIEW_APP_ID = 'observability-overview';
|
||||
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
LOGS_APP_ID,
|
||||
OBSERVABILITY_LOG_EXPLORER,
|
||||
OBSERVABILITY_OVERVIEW_APP_ID,
|
||||
METRICS_APP_ID,
|
||||
APM_APP_ID,
|
||||
LOGS_APP_ID,
|
||||
METRICS_APP_ID,
|
||||
OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
OBSERVABILITY_ONBOARDING_APP_ID,
|
||||
OBSERVABILITY_OVERVIEW_APP_ID,
|
||||
} from './constants';
|
||||
|
||||
type LogsApp = typeof LOGS_APP_ID;
|
||||
type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER;
|
||||
type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER_APP_ID;
|
||||
type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID;
|
||||
type MetricsApp = typeof METRICS_APP_ID;
|
||||
type ApmApp = typeof APM_APP_ID;
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
*/
|
||||
|
||||
export {
|
||||
OBSERVABILITY_ONBOARDING_APP_ID,
|
||||
LOGS_APP_ID,
|
||||
OBSERVABILITY_LOG_EXPLORER,
|
||||
OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
OBSERVABILITY_ONBOARDING_APP_ID,
|
||||
OBSERVABILITY_OVERVIEW_APP_ID,
|
||||
} from './constants';
|
||||
|
||||
export type { AppId, DeepLinkId } from './deep_links';
|
||||
|
||||
export * from './locators';
|
||||
|
|
|
@ -14,6 +14,17 @@ export type RefreshInterval = {
|
|||
value: number;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type FilterControls = {
|
||||
namespace?: ListFilterControl;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type ListFilterControl = {
|
||||
mode: 'include';
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export const LOG_EXPLORER_LOCATOR_ID = 'LOG_EXPLORER_LOCATOR';
|
||||
|
||||
export interface LogExplorerNavigationParams extends SerializableRecord {
|
||||
|
@ -34,13 +45,13 @@ export interface LogExplorerNavigationParams extends SerializableRecord {
|
|||
*/
|
||||
columns?: string[];
|
||||
/**
|
||||
* Array of the used sorting [[field,direction],...]
|
||||
*/
|
||||
sort?: string[][];
|
||||
/**
|
||||
* Optionally apply filters.
|
||||
* Optionally apply free-form filters.
|
||||
*/
|
||||
filters?: Filter[];
|
||||
/**
|
||||
* Optionally apply curated filter controls
|
||||
*/
|
||||
filterControls?: FilterControls;
|
||||
}
|
||||
|
||||
export interface LogExplorerLocatorParams extends LogExplorerNavigationParams {
|
||||
|
|
|
@ -6,4 +6,52 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import {
|
||||
isArray,
|
||||
isBoolean,
|
||||
isDate,
|
||||
isNil,
|
||||
isNumber,
|
||||
isPlainObject,
|
||||
isString,
|
||||
mapValues,
|
||||
} from 'lodash';
|
||||
|
||||
export const isDevMode = () => process.env.NODE_ENV !== 'production';
|
||||
|
||||
export const getDevToolsOptions = (): boolean | object =>
|
||||
isDevMode()
|
||||
? {
|
||||
actionSanitizer: sanitizeAction,
|
||||
stateSanitizer: sanitizeState,
|
||||
}
|
||||
: false;
|
||||
|
||||
const redactComplexValues = (value: unknown): unknown => {
|
||||
if (isString(value) || isNumber(value) || isBoolean(value) || isDate(value) || isNil(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (isArray(value)) {
|
||||
if (value.length > 100) {
|
||||
return '[redacted large array]';
|
||||
}
|
||||
return value.map(redactComplexValues);
|
||||
}
|
||||
|
||||
if ((isPlainObject as (v: unknown) => v is object)(value)) {
|
||||
if (Object.keys(value).length > 100) {
|
||||
return '[redacted large object]';
|
||||
}
|
||||
return mapValues(value, (innerValue: unknown) => redactComplexValues(innerValue));
|
||||
}
|
||||
|
||||
return `[redacted complex value of type ${typeof value}]`;
|
||||
};
|
||||
|
||||
const sanitizeAction = redactComplexValues;
|
||||
|
||||
const sanitizeState = (state: Record<string, unknown>) => ({
|
||||
value: state.value,
|
||||
context: redactComplexValues(state.context),
|
||||
});
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
*/
|
||||
|
||||
export * from './actions';
|
||||
export * from './dev_tools';
|
||||
export * from './notification_channel';
|
||||
export * from './types';
|
||||
export * from './dev_tools';
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
import React, { useEffect, useState, memo, useCallback, useMemo } from 'react';
|
||||
import { useParams, useHistory } from 'react-router-dom';
|
||||
import type { DataView } from '@kbn/data-views-plugin/public';
|
||||
import { redirectWhenMissing, SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import {
|
||||
type IKbnUrlStateStorage,
|
||||
redirectWhenMissing,
|
||||
SavedObjectNotFound,
|
||||
} from '@kbn/kibana-utils-plugin/public';
|
||||
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
|
||||
import {
|
||||
AnalyticsNoDataPageKibanaProvider,
|
||||
|
@ -46,6 +50,7 @@ interface DiscoverLandingParams {
|
|||
|
||||
export interface MainRouteProps {
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
stateStorageContainer?: IKbnUrlStateStorage;
|
||||
isDev: boolean;
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
}
|
||||
|
@ -53,6 +58,7 @@ export interface MainRouteProps {
|
|||
export function DiscoverMainRoute({
|
||||
customizationCallbacks,
|
||||
customizationContext,
|
||||
stateStorageContainer,
|
||||
}: MainRouteProps) {
|
||||
const history = useHistory();
|
||||
const services = useDiscoverServices();
|
||||
|
@ -70,6 +76,7 @@ export function DiscoverMainRoute({
|
|||
history,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer,
|
||||
})
|
||||
);
|
||||
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
DiscoverStateContainer,
|
||||
createSearchSessionRestorationDataProvider,
|
||||
} from './discover_state';
|
||||
import { createBrowserHistory, History } from 'history';
|
||||
import { createBrowserHistory, createMemoryHistory, History } from 'history';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import type { SavedSearch, SortOrder } from '@kbn/saved-search-plugin/public';
|
||||
import {
|
||||
|
@ -27,6 +27,7 @@ import { waitFor } from '@testing-library/react';
|
|||
import { DiscoverCustomizationContext, FetchStatus } from '../../types';
|
||||
import { dataViewAdHoc, dataViewComplexMock } from '../../../__mocks__/data_view_complex';
|
||||
import { copySavedSearch } from './discover_saved_search_container';
|
||||
import { createKbnUrlStateStorage, IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
const startSync = (appState: DiscoverAppStateContainer) => {
|
||||
const { start, stop } = appState.syncState();
|
||||
|
@ -151,6 +152,68 @@ describe('Test discover state', () => {
|
|||
expect(getCurrentUrl()).toBe('/#?_g=(refreshInterval:(pause:!t,value:5000))');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test discover state with overridden state storage', () => {
|
||||
let stopSync = () => {};
|
||||
let history: History;
|
||||
let stateStorage: IKbnUrlStateStorage;
|
||||
let state: DiscoverStateContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
history = createMemoryHistory({
|
||||
initialEntries: [
|
||||
{
|
||||
pathname: '/',
|
||||
hash: `?_a=()`,
|
||||
},
|
||||
],
|
||||
});
|
||||
stateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
useHashQuery: true,
|
||||
});
|
||||
state = getDiscoverStateContainer({
|
||||
services: discoverServiceMock,
|
||||
history,
|
||||
customizationContext,
|
||||
stateStorageContainer: stateStorage,
|
||||
});
|
||||
state.savedSearchState.set(savedSearchMock);
|
||||
state.appState.update({}, true);
|
||||
stopSync = startSync(state.appState);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stopSync();
|
||||
stopSync = () => {};
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('setting app state and syncing to URL', async () => {
|
||||
state.appState.update({ index: 'modified' });
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(history.createHref(history.location)).toMatchInlineSnapshot(
|
||||
`"/#?_a=(columns:!(default_column),index:modified,interval:auto,sort:!())"`
|
||||
);
|
||||
});
|
||||
|
||||
test('changing URL to be propagated to appState', async () => {
|
||||
history.push('/#?_a=(index:modified)');
|
||||
|
||||
await jest.runAllTimersAsync();
|
||||
|
||||
expect(state.appState.getState()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"index": "modified",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test discover initial state sort handling', () => {
|
||||
test('Non-empty sort in URL should not be overwritten by saved search sort', async () => {
|
||||
const savedSearch = {
|
||||
|
|
|
@ -71,6 +71,10 @@ interface DiscoverStateContainerParams {
|
|||
* Context object for customization related properties
|
||||
*/
|
||||
customizationContext: DiscoverCustomizationContext;
|
||||
/**
|
||||
* a custom url state storage
|
||||
*/
|
||||
stateStorageContainer?: IKbnUrlStateStorage;
|
||||
}
|
||||
|
||||
export interface LoadParams {
|
||||
|
@ -204,6 +208,7 @@ export function getDiscoverStateContainer({
|
|||
history,
|
||||
services,
|
||||
customizationContext,
|
||||
stateStorageContainer,
|
||||
}: DiscoverStateContainerParams): DiscoverStateContainer {
|
||||
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
|
||||
const toasts = services.core.notifications.toasts;
|
||||
|
@ -211,12 +216,14 @@ export function getDiscoverStateContainer({
|
|||
/**
|
||||
* state storage for state in the URL
|
||||
*/
|
||||
const stateStorage = createKbnUrlStateStorage({
|
||||
useHash: storeInSessionStorage,
|
||||
history,
|
||||
useHashQuery: customizationContext.displayMode !== 'embedded',
|
||||
...(toasts && withNotifyOnErrors(toasts)),
|
||||
});
|
||||
const stateStorage =
|
||||
stateStorageContainer ??
|
||||
createKbnUrlStateStorage({
|
||||
useHash: storeInSessionStorage,
|
||||
history,
|
||||
useHashQuery: customizationContext.displayMode !== 'embedded',
|
||||
...(toasts && withNotifyOnErrors(toasts)),
|
||||
});
|
||||
|
||||
/**
|
||||
* Search session logic
|
||||
|
|
|
@ -11,6 +11,7 @@ import type { ScopedHistory } from '@kbn/core/public';
|
|||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { css } from '@emotion/react';
|
||||
import type { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { DiscoverMainRoute } from '../../application/main';
|
||||
import type { DiscoverServices } from '../../build_services';
|
||||
import type { CustomizationCallback } from '../../customizations';
|
||||
|
@ -29,6 +30,7 @@ export interface DiscoverContainerInternalProps {
|
|||
getDiscoverServices: () => Promise<DiscoverServices>;
|
||||
scopedHistory: ScopedHistory;
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
stateStorageContainer?: IKbnUrlStateStorage;
|
||||
isDev: boolean;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
@ -55,6 +57,7 @@ export const DiscoverContainerInternal = ({
|
|||
customizationCallbacks,
|
||||
isDev,
|
||||
getDiscoverServices,
|
||||
stateStorageContainer,
|
||||
isLoading = false,
|
||||
}: DiscoverContainerInternalProps) => {
|
||||
const [discoverServices, setDiscoverServices] = useState<DiscoverServices | undefined>();
|
||||
|
@ -97,6 +100,7 @@ export const DiscoverContainerInternal = ({
|
|||
<DiscoverMainRoute
|
||||
customizationCallbacks={customizationCallbacks}
|
||||
customizationContext={customizationContext}
|
||||
stateStorageContainer={stateStorageContainer}
|
||||
isDev={isDev}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
|
|
|
@ -202,7 +202,7 @@ describe('kbn_url_storage', () => {
|
|||
await Promise.all([pr1, pr2, pr3]);
|
||||
expect(getCurrentUrl()).toBe('/3');
|
||||
|
||||
expect(urlControls.getPendingUrl()).toBeUndefined();
|
||||
expect(urlControls.getPendingUrl()).toEqual(getCurrentUrl());
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -193,17 +193,17 @@ export const createKbnUrlControls = (
|
|||
|
||||
// runs scheduled url updates
|
||||
function flush(replace = shouldReplace) {
|
||||
if (updateQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextUrl = getPendingUrl();
|
||||
|
||||
if (!nextUrl) return;
|
||||
|
||||
cleanUp();
|
||||
const newUrl = updateUrl(nextUrl, replace);
|
||||
return newUrl;
|
||||
}
|
||||
|
||||
function getPendingUrl() {
|
||||
if (updateQueue.length === 0) return undefined;
|
||||
const resultUrl = updateQueue.reduce(
|
||||
(url, nextUpdate) => nextUpdate(url) ?? url,
|
||||
getCurrentUrl(history)
|
||||
|
|
|
@ -116,7 +116,11 @@ export const createKbnUrlStateStorage = (
|
|||
unlisten();
|
||||
};
|
||||
}).pipe(
|
||||
map(() => getStateFromKbnUrl<State>(key, undefined, { getFromHashQuery: useHashQuery })),
|
||||
map(() =>
|
||||
getStateFromKbnUrl<State>(key, history?.createHref(history.location), {
|
||||
getFromHashQuery: useHashQuery,
|
||||
})
|
||||
),
|
||||
catchError((error) => {
|
||||
if (onGetErrorThrottled) onGetErrorThrottled(error);
|
||||
return of(null);
|
||||
|
|
|
@ -32,8 +32,17 @@ export const DATA_GRID_COLUMN_WIDTH_SMALL = 240;
|
|||
export const DATA_GRID_COLUMN_WIDTH_MEDIUM = 320;
|
||||
|
||||
// UI preferences
|
||||
export const DATA_GRID_DEFAULT_COLUMNS = [SERVICE_NAME_FIELD, HOST_NAME_FIELD, MESSAGE_FIELD];
|
||||
export const DATA_GRID_COLUMNS_PREFERENCES = {
|
||||
[HOST_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_MEDIUM },
|
||||
[SERVICE_NAME_FIELD]: { width: DATA_GRID_COLUMN_WIDTH_SMALL },
|
||||
};
|
||||
export const DEFAULT_COLUMNS = [
|
||||
{
|
||||
field: SERVICE_NAME_FIELD,
|
||||
width: DATA_GRID_COLUMN_WIDTH_SMALL,
|
||||
},
|
||||
{
|
||||
field: HOST_NAME_FIELD,
|
||||
width: DATA_GRID_COLUMN_WIDTH_MEDIUM,
|
||||
},
|
||||
{
|
||||
field: MESSAGE_FIELD,
|
||||
},
|
||||
];
|
||||
export const DEFAULT_ROWS_PER_PAGE = 100;
|
||||
|
|
|
@ -5,18 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AllDatasetSelection } from '../../../../common/dataset_selection';
|
||||
import { ControlPanels, DefaultLogExplorerProfileState } from './types';
|
||||
|
||||
export const DEFAULT_CONTEXT: DefaultLogExplorerProfileState = {
|
||||
datasetSelection: AllDatasetSelection.create(),
|
||||
};
|
||||
|
||||
export const CONTROL_PANELS_URL_KEY = 'controlPanels';
|
||||
import { ControlPanels } from './types';
|
||||
|
||||
export const availableControlsPanels = {
|
||||
NAMESPACE: 'data_stream.namespace',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export type AvailableControlPanels = typeof availableControlsPanels;
|
||||
|
||||
export const controlPanelConfigs: ControlPanels = {
|
||||
[availableControlsPanels.NAMESPACE]: {
|
|
@ -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 { encodeDatasetSelection } from './encoding';
|
||||
import { DatasetSelectionStrategy } from './types';
|
||||
|
||||
export class AllDatasetSelection implements DatasetSelectionStrategy {
|
||||
|
@ -23,18 +22,13 @@ export class AllDatasetSelection implements DatasetSelectionStrategy {
|
|||
}
|
||||
|
||||
toDataviewSpec() {
|
||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
||||
return {
|
||||
id: this.toURLSelectionId(),
|
||||
name,
|
||||
title,
|
||||
};
|
||||
return this.selection.dataset.toDataviewSpec();
|
||||
}
|
||||
|
||||
toURLSelectionId() {
|
||||
return encodeDatasetSelection({
|
||||
toPlainSelection() {
|
||||
return {
|
||||
selectionType: this.selectionType,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static create() {
|
||||
|
|
|
@ -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) => {
|
||||
if (datasetSelection.selectionType === 'all') {
|
||||
return AllDatasetSelection.create();
|
||||
}
|
||||
if (datasetSelection.selectionType === 'single') {
|
||||
} else if (datasetSelection.selectionType === 'single') {
|
||||
return SingleDatasetSelection.fromSelection(datasetSelection.selection);
|
||||
}
|
||||
if (datasetSelection.selectionType === 'unresolved') {
|
||||
} else {
|
||||
return UnresolvedDatasetSelection.fromSelection(datasetSelection.selection);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,7 +28,6 @@ export const isDatasetSelection = (input: any): input is DatasetSelection => {
|
|||
export * from './all_dataset_selection';
|
||||
export * from './single_dataset_selection';
|
||||
export * from './unresolved_dataset_selection';
|
||||
export * from './encoding';
|
||||
export * from './errors';
|
||||
export * from './hydrate_dataset_selection.ts';
|
||||
export * from './types';
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { Dataset } from '../datasets';
|
||||
import { encodeDatasetSelection } from './encoding';
|
||||
import { DatasetSelectionStrategy, SingleDatasetSelectionPayload } from './types';
|
||||
|
||||
export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
||||
|
@ -29,16 +28,11 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
}
|
||||
|
||||
toDataviewSpec() {
|
||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
||||
return {
|
||||
id: this.toURLSelectionId(),
|
||||
name,
|
||||
title,
|
||||
};
|
||||
return this.selection.dataset.toDataviewSpec();
|
||||
}
|
||||
|
||||
toURLSelectionId() {
|
||||
return encodeDatasetSelection({
|
||||
toPlainSelection() {
|
||||
return {
|
||||
selectionType: this.selectionType,
|
||||
selection: {
|
||||
name: this.selection.name,
|
||||
|
@ -46,7 +40,7 @@ export class SingleDatasetSelection implements DatasetSelectionStrategy {
|
|||
version: this.selection.version,
|
||||
dataset: this.selection.dataset.toPlain(),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static fromSelection(selection: SingleDatasetSelectionPayload) {
|
||||
|
|
|
@ -62,7 +62,11 @@ export type UnresolvedDatasetSelectionPayload = rt.TypeOf<
|
|||
>;
|
||||
export type DatasetSelectionPlain = rt.TypeOf<typeof datasetSelectionPlainRT>;
|
||||
|
||||
export type DataViewSpecWithId = DataViewSpec & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface DatasetSelectionStrategy {
|
||||
toDataviewSpec(): DataViewSpec;
|
||||
toURLSelectionId(): string;
|
||||
toDataviewSpec(): DataViewSpecWithId;
|
||||
toPlainSelection(): DatasetSelectionPlain;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
*/
|
||||
|
||||
import { Dataset } from '../datasets';
|
||||
import { encodeDatasetSelection } from './encoding';
|
||||
import { DatasetSelectionStrategy, UnresolvedDatasetSelectionPayload } from './types';
|
||||
|
||||
export class UnresolvedDatasetSelection implements DatasetSelectionStrategy {
|
||||
|
@ -25,22 +24,17 @@ export class UnresolvedDatasetSelection implements DatasetSelectionStrategy {
|
|||
}
|
||||
|
||||
toDataviewSpec() {
|
||||
const { name, title } = this.selection.dataset.toDataviewSpec();
|
||||
return {
|
||||
id: this.toURLSelectionId(),
|
||||
name,
|
||||
title,
|
||||
};
|
||||
return this.selection.dataset.toDataviewSpec();
|
||||
}
|
||||
|
||||
toURLSelectionId() {
|
||||
return encodeDatasetSelection({
|
||||
toPlainSelection() {
|
||||
return {
|
||||
selectionType: this.selectionType,
|
||||
selection: {
|
||||
name: this.selection.name,
|
||||
dataset: this.selection.dataset.toPlain(),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
public static fromSelection(selection: UnresolvedDatasetSelectionPayload) {
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import { IconType } from '@elastic/eui';
|
||||
import { DataViewSpec } from '@kbn/data-views-plugin/common';
|
||||
import { IndexPattern } from '@kbn/io-ts-utils';
|
||||
import { TIMESTAMP_FIELD } from '../../constants';
|
||||
import { DataViewSpecWithId } from '../../dataset_selection';
|
||||
import { DatasetId, DatasetType, IntegrationType } from '../types';
|
||||
|
||||
type IntegrationBase = Partial<Pick<IntegrationType, 'name' | 'title' | 'icons' | 'version'>>;
|
||||
|
@ -49,7 +49,7 @@ export class Dataset {
|
|||
return `${type}-${dataset}-*` as IndexPattern;
|
||||
}
|
||||
|
||||
toDataviewSpec(): DataViewSpec {
|
||||
toDataviewSpec(): DataViewSpecWithId {
|
||||
// Invert the property because the API returns the index pattern as `name` and a readable name as `title`
|
||||
return {
|
||||
id: this.id,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"controls",
|
||||
"data",
|
||||
"dataViews",
|
||||
"discover",
|
||||
"embeddable",
|
||||
"fieldFormats",
|
||||
"fleet",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"controls",
|
||||
"embeddable",
|
||||
"navigation",
|
||||
"share",
|
||||
"unifiedSearch"
|
||||
],
|
||||
"optionalPlugins": [],
|
||||
"requiredBundles": [],
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { FlyoutProps, LogDocument } from './types';
|
||||
import { LogExplorerFlyoutContentProps, LogDocument } from './types';
|
||||
import { useDocDetail } from './use_doc_detail';
|
||||
import { FlyoutHeader } from './flyout_header';
|
||||
import { FlyoutHighlights } from './flyout_highlights';
|
||||
|
@ -16,7 +16,7 @@ export function FlyoutDetail({
|
|||
dataView,
|
||||
doc,
|
||||
actions,
|
||||
}: Pick<FlyoutProps, 'dataView' | 'doc' | 'actions'>) {
|
||||
}: Pick<LogExplorerFlyoutContentProps, 'dataView' | 'doc' | 'actions'>) {
|
||||
const parsedDoc = useDocDetail(doc as LogDocument, { dataView });
|
||||
|
||||
return (
|
||||
|
|
|
@ -5,55 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { DataView } from '@kbn/data-views-plugin/common';
|
||||
import type { FlyoutContentProps } from '@kbn/discover-plugin/public';
|
||||
import type { DataTableRecord } from '@kbn/discover-utils/types';
|
||||
|
||||
export interface FlyoutProps extends FlyoutContentProps {
|
||||
dataView: DataView;
|
||||
doc: LogDocument;
|
||||
}
|
||||
|
||||
export interface LogDocument extends DataTableRecord {
|
||||
flattened: {
|
||||
'@timestamp': string;
|
||||
'log.level'?: [string];
|
||||
message?: [string];
|
||||
|
||||
'host.name'?: string;
|
||||
'service.name'?: string;
|
||||
'trace.id'?: string;
|
||||
'agent.name'?: string;
|
||||
'orchestrator.cluster.name'?: string;
|
||||
'orchestrator.resource.id'?: string;
|
||||
'cloud.provider'?: string;
|
||||
'cloud.region'?: string;
|
||||
'cloud.availability_zone'?: string;
|
||||
'cloud.project.id'?: string;
|
||||
'cloud.instance.id'?: string;
|
||||
'log.file.path'?: string;
|
||||
'data_stream.namespace': string;
|
||||
'data_stream.dataset': string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FlyoutDoc {
|
||||
'@timestamp': string;
|
||||
'log.level'?: string;
|
||||
message?: string;
|
||||
|
||||
'host.name'?: string;
|
||||
'service.name'?: string;
|
||||
'trace.id'?: string;
|
||||
'agent.name'?: string;
|
||||
'orchestrator.cluster.name'?: string;
|
||||
'orchestrator.resource.id'?: string;
|
||||
'cloud.provider'?: string;
|
||||
'cloud.region'?: string;
|
||||
'cloud.availability_zone'?: string;
|
||||
'cloud.project.id'?: string;
|
||||
'cloud.instance.id'?: string;
|
||||
'log.file.path'?: string;
|
||||
'data_stream.namespace': string;
|
||||
'data_stream.dataset': string;
|
||||
}
|
||||
export type { FlyoutDoc, LogDocument, LogExplorerFlyoutContentProps } from '../../controller';
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
import { formatFieldValue } from '@kbn/discover-utils';
|
||||
import * as constants from '../../../common/constants';
|
||||
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
||||
import { FlyoutDoc, FlyoutProps, LogDocument } from './types';
|
||||
import { FlyoutDoc, LogExplorerFlyoutContentProps, LogDocument } from './types';
|
||||
|
||||
export function useDocDetail(
|
||||
doc: LogDocument,
|
||||
{ dataView }: Pick<FlyoutProps, 'dataView'>
|
||||
{ dataView }: Pick<LogExplorerFlyoutContentProps, 'dataView'>
|
||||
): FlyoutDoc {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
|
||||
|
|
|
@ -5,100 +5,43 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ScopedHistory } from '@kbn/core-application-browser';
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import React, { useMemo } from 'react';
|
||||
import { ScopedHistory } from '@kbn/core-application-browser';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DiscoverAppState } from '@kbn/discover-plugin/public';
|
||||
import type { BehaviorSubject } from 'rxjs';
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import { HIDE_ANNOUNCEMENTS } from '@kbn/discover-utils';
|
||||
import type { LogExplorerController } from '../../controller';
|
||||
import { createLogExplorerProfileCustomizations } from '../../customizations/log_explorer_profile';
|
||||
import { createPropertyGetProxy } from '../../utils/proxies';
|
||||
import { LogExplorerProfileContext } from '../../state_machines/log_explorer_profile';
|
||||
import { LogExplorerStartDeps } from '../../types';
|
||||
import { LogExplorerCustomizations } from './types';
|
||||
|
||||
export interface CreateLogExplorerArgs {
|
||||
core: CoreStart;
|
||||
plugins: LogExplorerStartDeps;
|
||||
}
|
||||
|
||||
export interface LogExplorerStateContainer {
|
||||
appState?: DiscoverAppState;
|
||||
logExplorerState?: Partial<LogExplorerProfileContext>;
|
||||
}
|
||||
|
||||
export interface LogExplorerProps {
|
||||
customizations?: LogExplorerCustomizations;
|
||||
scopedHistory: ScopedHistory;
|
||||
state$?: BehaviorSubject<LogExplorerStateContainer>;
|
||||
controller: LogExplorerController;
|
||||
}
|
||||
|
||||
export const createLogExplorer = ({ core, plugins }: CreateLogExplorerArgs) => {
|
||||
const {
|
||||
data,
|
||||
discover: { DiscoverContainer },
|
||||
} = plugins;
|
||||
|
||||
const overrideServices = {
|
||||
data: createDataServiceProxy(data),
|
||||
uiSettings: createUiSettingsServiceProxy(core.uiSettings),
|
||||
};
|
||||
|
||||
return ({ customizations = {}, scopedHistory, state$ }: LogExplorerProps) => {
|
||||
return ({ scopedHistory, controller }: LogExplorerProps) => {
|
||||
const logExplorerCustomizations = useMemo(
|
||||
() => [createLogExplorerProfileCustomizations({ core, customizations, plugins, state$ })],
|
||||
[customizations, state$]
|
||||
() => [createLogExplorerProfileCustomizations({ controller, core, plugins })],
|
||||
[controller]
|
||||
);
|
||||
|
||||
const { urlStateStorage, ...overrideServices } = controller.discoverServices;
|
||||
|
||||
return (
|
||||
<DiscoverContainer
|
||||
customizationCallbacks={logExplorerCustomizations}
|
||||
overrideServices={overrideServices}
|
||||
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 { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { useControlPanels } from '../hooks/use_control_panels';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||
|
||||
const DATASET_FILTERS_CUSTOMIZATION_ID = 'datasetFiltersCustomization';
|
||||
|
||||
interface CustomDatasetFiltersProps {
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||
logExplorerControllerStateService: LogExplorerControllerStateService;
|
||||
data: DataPublicPluginStart;
|
||||
}
|
||||
|
||||
const CustomDatasetFilters = ({
|
||||
logExplorerProfileStateService,
|
||||
logExplorerControllerStateService,
|
||||
data,
|
||||
}: CustomDatasetFiltersProps) => {
|
||||
const { getInitialInput, setControlGroupAPI, query, filters, timeRange } = useControlPanels(
|
||||
logExplorerProfileStateService,
|
||||
logExplorerControllerStateService,
|
||||
data
|
||||
);
|
||||
|
||||
|
|
|
@ -15,15 +15,15 @@ import { DataViewsProvider, useDataViewsContext } from '../hooks/use_data_views'
|
|||
import { useEsql } from '../hooks/use_esql';
|
||||
import { IntegrationsProvider, useIntegrationsContext } from '../hooks/use_integrations';
|
||||
import { IDatasetsClient } from '../services/datasets';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||
|
||||
interface CustomDatasetSelectorProps {
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||
logExplorerControllerStateService: LogExplorerControllerStateService;
|
||||
}
|
||||
|
||||
export const CustomDatasetSelector = withProviders(({ logExplorerProfileStateService }) => {
|
||||
export const CustomDatasetSelector = withProviders(({ logExplorerControllerStateService }) => {
|
||||
const { datasetSelection, handleDatasetSelectionChange } = useDatasetSelection(
|
||||
logExplorerProfileStateService
|
||||
logExplorerControllerStateService
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -111,13 +111,13 @@ function withProviders(Component: React.FunctionComponent<CustomDatasetSelectorP
|
|||
datasetsClient,
|
||||
dataViews,
|
||||
discover,
|
||||
logExplorerProfileStateService,
|
||||
logExplorerControllerStateService,
|
||||
}: CustomDatasetSelectorBuilderProps) {
|
||||
return (
|
||||
<IntegrationsProvider datasetsClient={datasetsClient}>
|
||||
<DatasetsProvider datasetsClient={datasetsClient}>
|
||||
<DataViewsProvider dataViewsService={dataViews} discoverService={discover}>
|
||||
<Component logExplorerProfileStateService={logExplorerProfileStateService} />
|
||||
<Component logExplorerControllerStateService={logExplorerControllerStateService} />
|
||||
</DataViewsProvider>
|
||||
</DatasetsProvider>
|
||||
</IntegrationsProvider>
|
||||
|
|
|
@ -5,46 +5,41 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
|
||||
import { FlyoutDetail } from '../components/flyout_detail/flyout_detail';
|
||||
import { FlyoutProps } from '../components/flyout_detail';
|
||||
import { useLogExplorerCustomizationsContext } from '../hooks/use_log_explorer_customizations';
|
||||
import { LogExplorerFlyoutContentProps } from '../components/flyout_detail';
|
||||
import { useLogExplorerControllerContext } from '../controller';
|
||||
|
||||
export const CustomFlyoutContent = ({
|
||||
actions,
|
||||
dataView,
|
||||
doc,
|
||||
renderDefaultContent,
|
||||
}: FlyoutProps) => {
|
||||
const { flyout } = useLogExplorerCustomizationsContext();
|
||||
export const CustomFlyoutContent = (props: LogExplorerFlyoutContentProps) => {
|
||||
const {
|
||||
customizations: { flyout },
|
||||
} = useLogExplorerControllerContext();
|
||||
|
||||
const renderPreviousContent = useCallback(
|
||||
() => (
|
||||
<>
|
||||
{/* Apply custom Log Explorer detail */}
|
||||
<EuiFlexItem>
|
||||
<FlyoutDetail actions={actions} dataView={dataView} doc={doc} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
),
|
||||
[actions, dataView, doc]
|
||||
const renderCustomizedContent = useMemo(
|
||||
() => flyout?.renderContent?.(renderContent) ?? renderContent,
|
||||
[flyout]
|
||||
);
|
||||
|
||||
const content = flyout?.renderContent
|
||||
? flyout?.renderContent(renderPreviousContent, { doc })
|
||||
: renderPreviousContent();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{/* Apply custom Log Explorer detail */}
|
||||
{content}
|
||||
{renderCustomizedContent(props)}
|
||||
{/* Restore default content */}
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiFlexItem>{renderDefaultContent()}</EuiFlexItem>
|
||||
<EuiFlexItem>{props.renderDefaultContent()}</EuiFlexItem>
|
||||
</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
|
||||
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.
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
import type { CoreStart } from '@kbn/core/public';
|
||||
import { CustomizationCallback, DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import type { CustomizationCallback } from '@kbn/discover-plugin/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { combineLatest, from, map, Subscription, type BehaviorSubject } from 'rxjs';
|
||||
import { LogExplorerStateContainer } from '../components/log_explorer';
|
||||
import { LogExplorerCustomizations } from '../components/log_explorer/types';
|
||||
import { LogExplorerCustomizationsProvider } from '../hooks/use_log_explorer_customizations';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { LogExplorerStartDeps } from '../types';
|
||||
import { waitFor } from 'xstate/lib/waitFor';
|
||||
import type { LogExplorerController } from '../controller';
|
||||
import { LogExplorerControllerProvider } from '../controller/provider';
|
||||
import type { LogExplorerStartDeps } from '../types';
|
||||
import { dynamic } from '../utils/dynamic';
|
||||
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
||||
import { createCustomSearchBar } from './custom_search_bar';
|
||||
|
||||
const LazyCustomDatasetFilters = dynamic(() => import('./custom_dataset_filters'));
|
||||
const LazyCustomDatasetSelector = dynamic(() => import('./custom_dataset_selector'));
|
||||
|
@ -24,56 +24,31 @@ const LazyCustomFlyoutContent = dynamic(() => import('./custom_flyout_content'))
|
|||
|
||||
export interface CreateLogExplorerProfileCustomizationsDeps {
|
||||
core: CoreStart;
|
||||
customizations: LogExplorerCustomizations;
|
||||
plugins: LogExplorerStartDeps;
|
||||
state$?: BehaviorSubject<LogExplorerStateContainer>;
|
||||
controller: LogExplorerController;
|
||||
}
|
||||
|
||||
export const createLogExplorerProfileCustomizations =
|
||||
({
|
||||
core,
|
||||
customizations: logExplorerCustomizations,
|
||||
plugins,
|
||||
state$,
|
||||
controller,
|
||||
}: CreateLogExplorerProfileCustomizationsDeps): CustomizationCallback =>
|
||||
async ({ customizations, stateContainer }) => {
|
||||
const { data, dataViews, discover } = plugins;
|
||||
// Lazy load dependencies
|
||||
const datasetServiceModuleLoadable = import('../services/datasets');
|
||||
const logExplorerMachineModuleLoadable = import('../state_machines/log_explorer_profile');
|
||||
const { discoverServices, service } = controller;
|
||||
const pluginsWithOverrides = {
|
||||
...plugins,
|
||||
...discoverServices,
|
||||
};
|
||||
const { data, dataViews, discover, navigation, unifiedSearch } = pluginsWithOverrides;
|
||||
|
||||
const [{ DatasetsService }, { initializeLogExplorerProfileStateService, waitForState }] =
|
||||
await Promise.all([datasetServiceModuleLoadable, logExplorerMachineModuleLoadable]);
|
||||
|
||||
const datasetsClient = new DatasetsService().start({
|
||||
http: core.http,
|
||||
}).client;
|
||||
|
||||
const logExplorerProfileStateService = initializeLogExplorerProfileStateService({
|
||||
datasetsClient,
|
||||
stateContainer,
|
||||
toasts: core.notifications.toasts,
|
||||
});
|
||||
service.send('RECEIVED_STATE_CONTAINER', { discoverStateContainer: stateContainer });
|
||||
|
||||
/**
|
||||
* Wait for the machine to be fully initialized to set the restored selection
|
||||
* create the DataView and set it in the stateContainer from Discover
|
||||
*/
|
||||
await waitForState(logExplorerProfileStateService, 'initialized');
|
||||
|
||||
/**
|
||||
* Subscribe the state$ BehaviorSubject when the consumer app wants to react to state changes.
|
||||
* It emits a combined state of:
|
||||
* - log explorer state machine context
|
||||
* - appState from the discover stateContainer
|
||||
*/
|
||||
let stateSubscription: Subscription;
|
||||
if (state$) {
|
||||
stateSubscription = createStateUpdater({
|
||||
logExplorerProfileStateService,
|
||||
stateContainer,
|
||||
}).subscribe(state$);
|
||||
}
|
||||
await waitFor(service, (state) => state.matches('initialized'), { timeout: 30000 });
|
||||
|
||||
/**
|
||||
* Replace the DataViewPicker with a custom `DatasetSelector` to pick integrations streams
|
||||
|
@ -87,20 +62,22 @@ export const createLogExplorerProfileCustomizations =
|
|||
return (
|
||||
<KibanaContextProviderForPlugin>
|
||||
<LazyCustomDatasetSelector
|
||||
datasetsClient={datasetsClient}
|
||||
datasetsClient={controller.datasetsClient}
|
||||
dataViews={dataViews}
|
||||
discover={discover}
|
||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
||||
logExplorerControllerStateService={service}
|
||||
/>
|
||||
</KibanaContextProviderForPlugin>
|
||||
);
|
||||
},
|
||||
PrependFilterBar: () => (
|
||||
<LazyCustomDatasetFilters
|
||||
logExplorerProfileStateService={logExplorerProfileStateService}
|
||||
data={data}
|
||||
/>
|
||||
<LazyCustomDatasetFilters logExplorerControllerStateService={service} data={data} />
|
||||
),
|
||||
CustomSearchBar: createCustomSearchBar({
|
||||
data,
|
||||
navigation,
|
||||
unifiedSearch,
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -143,32 +120,13 @@ export const createLogExplorerProfileCustomizations =
|
|||
|
||||
return (
|
||||
<KibanaContextProviderForPlugin>
|
||||
<LogExplorerCustomizationsProvider value={logExplorerCustomizations}>
|
||||
<LogExplorerControllerProvider controller={controller}>
|
||||
<LazyCustomFlyoutContent {...props} dataView={internalState.dataView} />
|
||||
</LogExplorerCustomizationsProvider>
|
||||
</LogExplorerControllerProvider>
|
||||
</KibanaContextProviderForPlugin>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (stateSubscription) {
|
||||
stateSubscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
return () => {};
|
||||
};
|
||||
|
||||
const createStateUpdater = ({
|
||||
logExplorerProfileStateService,
|
||||
stateContainer,
|
||||
}: {
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService;
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}) => {
|
||||
return combineLatest([from(logExplorerProfileStateService), stateContainer.appState.state$]).pipe(
|
||||
map(([logExplorerState, appState]) => ({
|
||||
logExplorerState: logExplorerState.context,
|
||||
appState,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,16 +13,16 @@ import { Query, TimeRange } from '@kbn/es-query';
|
|||
import { useQuerySubscriber } from '@kbn/unified-field-list';
|
||||
import { useSelector } from '@xstate/react';
|
||||
import { useCallback } from 'react';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||
|
||||
export const useControlPanels = (
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService,
|
||||
logExplorerControllerStateService: LogExplorerControllerStateService,
|
||||
data: DataPublicPluginStart
|
||||
) => {
|
||||
const { query, filters, fromDate, toDate } = useQuerySubscriber({ data });
|
||||
const timeRange: TimeRange = { from: fromDate!, to: toDate! };
|
||||
|
||||
const controlPanels = useSelector(logExplorerProfileStateService, (state) => {
|
||||
const controlPanels = useSelector(logExplorerControllerStateService, (state) => {
|
||||
if (!('controlPanels' in state.context)) return;
|
||||
return state.context.controlPanels;
|
||||
});
|
||||
|
@ -45,12 +45,12 @@ export const useControlPanels = (
|
|||
|
||||
const setControlGroupAPI = useCallback(
|
||||
(controlGroupAPI: ControlGroupAPI) => {
|
||||
logExplorerProfileStateService.send({
|
||||
logExplorerControllerStateService.send({
|
||||
type: 'INITIALIZE_CONTROL_GROUP_API',
|
||||
controlGroupAPI,
|
||||
});
|
||||
},
|
||||
[logExplorerProfileStateService]
|
||||
[logExplorerControllerStateService]
|
||||
);
|
||||
|
||||
return { getInitialInput, setControlGroupAPI, query, filters, timeRange };
|
||||
|
|
|
@ -8,20 +8,20 @@
|
|||
import { useSelector } from '@xstate/react';
|
||||
import { useCallback } from 'react';
|
||||
import { DatasetSelectionChange } from '../../common/dataset_selection';
|
||||
import { LogExplorerProfileStateService } from '../state_machines/log_explorer_profile';
|
||||
import { LogExplorerControllerStateService } from '../state_machines/log_explorer_controller';
|
||||
|
||||
export const useDatasetSelection = (
|
||||
logExplorerProfileStateService: LogExplorerProfileStateService
|
||||
logExplorerControllerStateService: LogExplorerControllerStateService
|
||||
) => {
|
||||
const datasetSelection = useSelector(logExplorerProfileStateService, (state) => {
|
||||
const datasetSelection = useSelector(logExplorerControllerStateService, (state) => {
|
||||
return state.context.datasetSelection;
|
||||
});
|
||||
|
||||
const handleDatasetSelectionChange: DatasetSelectionChange = useCallback(
|
||||
(data) => {
|
||||
logExplorerProfileStateService.send({ type: 'UPDATE_DATASET_SELECTION', data });
|
||||
logExplorerControllerStateService.send({ type: 'UPDATE_DATASET_SELECTION', data });
|
||||
},
|
||||
[logExplorerProfileStateService]
|
||||
[logExplorerControllerStateService]
|
||||
);
|
||||
|
||||
return { datasetSelection, handleDatasetSelectionChange };
|
||||
|
|
|
@ -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 { LogExplorerConfig } from '../common/plugin_config';
|
||||
import { LogExplorerPlugin } from './plugin';
|
||||
export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
|
||||
export type { LogExplorerStateContainer } from './components/log_explorer';
|
||||
export type {
|
||||
CreateLogExplorerController,
|
||||
LogExplorerController,
|
||||
LogExplorerCustomizations,
|
||||
LogExplorerFlyoutContentProps,
|
||||
} from './components/log_explorer/types';
|
||||
LogExplorerPublicState,
|
||||
LogExplorerPublicStateUpdate,
|
||||
} from './controller';
|
||||
export type { LogExplorerControllerContext } from './state_machines/log_explorer_controller';
|
||||
export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
|
||||
export {
|
||||
getDiscoverColumnsFromDisplayOptions,
|
||||
getDiscoverGridFromDisplayOptions,
|
||||
} from './utils/convert_discover_app_state';
|
||||
|
||||
export function plugin(context: PluginInitializerContext<LogExplorerConfig>) {
|
||||
return new LogExplorerPlugin(context);
|
||||
|
|
|
@ -5,11 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import { DISCOVER_APP_LOCATOR, DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { LogExplorerLocatorDefinition, LogExplorerLocators } from '../common/locators';
|
||||
import { createLogExplorer } from './components/log_explorer';
|
||||
import {
|
||||
import { createLogExplorerControllerLazyFactory } from './controller/lazy_create_controller';
|
||||
import type {
|
||||
LogExplorerPluginSetup,
|
||||
LogExplorerPluginStart,
|
||||
LogExplorerSetupDeps,
|
||||
|
@ -48,8 +49,14 @@ export class LogExplorerPlugin implements Plugin<LogExplorerPluginSetup, LogExpl
|
|||
plugins,
|
||||
});
|
||||
|
||||
const createLogExplorerController = createLogExplorerControllerLazyFactory({
|
||||
core,
|
||||
plugins,
|
||||
});
|
||||
|
||||
return {
|
||||
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.
|
||||
*/
|
||||
|
||||
export * from './defaults';
|
||||
export * from './state_machine';
|
||||
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.
|
||||
*/
|
||||
|
||||
import { DiscoverStateContainer } from '@kbn/discover-plugin/public';
|
||||
import { InvokeCreator } from 'xstate';
|
||||
import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
||||
|
||||
interface LogExplorerProfileDataViewStateDependencies {
|
||||
stateContainer: DiscoverStateContainer;
|
||||
}
|
||||
import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types';
|
||||
|
||||
export const createAndSetDataView =
|
||||
({
|
||||
stateContainer,
|
||||
}: LogExplorerProfileDataViewStateDependencies): InvokeCreator<
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent
|
||||
> =>
|
||||
(): InvokeCreator<LogExplorerControllerContext, LogExplorerControllerEvent> =>
|
||||
async (context) => {
|
||||
const dataView = await stateContainer.actions.createAndAppendAdHocDataView(
|
||||
if (!('discoverStateContainer' in context)) return;
|
||||
const { discoverStateContainer } = context;
|
||||
const dataView = await discoverStateContainer.actions.createAndAppendAdHocDataView(
|
||||
context.datasetSelection.toDataviewSpec()
|
||||
);
|
||||
/**
|
||||
|
@ -32,5 +24,5 @@ export const createAndSetDataView =
|
|||
* to the existing one or the default logs-*.
|
||||
* We set explicitly the data view here to be used when restoring the data view on the initial load.
|
||||
*/
|
||||
stateContainer.actions.setDataView(dataView);
|
||||
discoverStateContainer.actions.setDataView(dataView);
|
||||
};
|
|
@ -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 { Dataset } from '../../../../common/datasets';
|
||||
import { SingleDatasetSelection } from '../../../../common/dataset_selection';
|
||||
import { IDatasetsClient } from '../../../services/datasets';
|
||||
import { LogExplorerProfileContext, LogExplorerProfileEvent } from './types';
|
||||
import { Dataset } from '../../../../../common/datasets';
|
||||
import { SingleDatasetSelection } from '../../../../../common/dataset_selection';
|
||||
import { IDatasetsClient } from '../../../../services/datasets';
|
||||
import { LogExplorerControllerContext, LogExplorerControllerEvent } from '../types';
|
||||
|
||||
interface LogExplorerProfileUrlStateDependencies {
|
||||
interface LogExplorerControllerUrlStateDependencies {
|
||||
datasetsClient: IDatasetsClient;
|
||||
}
|
||||
|
||||
export const validateSelection =
|
||||
({
|
||||
datasetsClient,
|
||||
}: LogExplorerProfileUrlStateDependencies): InvokeCreator<
|
||||
LogExplorerProfileContext,
|
||||
LogExplorerProfileEvent
|
||||
}: LogExplorerControllerUrlStateDependencies): InvokeCreator<
|
||||
LogExplorerControllerContext,
|
||||
LogExplorerControllerEvent
|
||||
> =>
|
||||
(context) =>
|
||||
async (send) => {
|
|
@ -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 { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import type { DiscoverSetup, DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import { LogExplorerLocators } from '../common/locators';
|
||||
import type { SharePluginSetup } from '@kbn/share-plugin/public';
|
||||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import type { LogExplorerLocators } from '../common/locators';
|
||||
import type { LogExplorerProps } from './components/log_explorer';
|
||||
import type { CreateLogExplorerController } from './controller';
|
||||
|
||||
export interface LogExplorerPluginSetup {
|
||||
locators: LogExplorerLocators;
|
||||
}
|
||||
export interface LogExplorerPluginStart {
|
||||
LogExplorer: ComponentType<LogExplorerProps>;
|
||||
createLogExplorerController: CreateLogExplorerController;
|
||||
}
|
||||
|
||||
export interface LogExplorerSetupDeps {
|
||||
|
@ -30,4 +34,6 @@ export interface LogExplorerStartDeps {
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
discover: DiscoverStart;
|
||||
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": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*", ".storybook/**/*.tsx"],
|
||||
"include": [
|
||||
"../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
".storybook/**/*.tsx"
|
||||
],
|
||||
"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/core",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/custom-icons",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/data-views-plugin",
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/elastic-agent-utils",
|
||||
"@kbn/embeddable-plugin",
|
||||
"@kbn/es-query",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/fleet-plugin",
|
||||
"@kbn/i18n",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/io-ts-utils",
|
||||
"@kbn/kibana-react-plugin",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/kibana-utils-plugin",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/share-plugin",
|
||||
"@kbn/unified-data-table",
|
||||
"@kbn/core-ui-settings-browser",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/deeplinks-observability",
|
||||
"@kbn/field-formats-plugin",
|
||||
"@kbn/custom-icons",
|
||||
"@kbn/elastic-agent-utils"
|
||||
"@kbn/unified-field-list",
|
||||
"@kbn/unified-search-plugin",
|
||||
"@kbn/xstate-utils"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -128,6 +128,8 @@ describe('SLO Edit Page', () => {
|
|||
const mockCreate = jest.fn();
|
||||
const mockUpdate = jest.fn();
|
||||
|
||||
const history = createBrowserHistory();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockKibana();
|
||||
|
@ -136,9 +138,8 @@ describe('SLO Edit Page', () => {
|
|||
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
const history = createBrowserHistory();
|
||||
history.replace('');
|
||||
jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history);
|
||||
jest.spyOn(Router, 'useHistory').mockReturnValue(history);
|
||||
|
||||
useFetchDataViewsMock.mockReturnValue({
|
||||
isLoading: false,
|
||||
|
@ -256,11 +257,9 @@ describe('SLO Edit Page', () => {
|
|||
it('prefills the form with values from URL', () => {
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: undefined });
|
||||
|
||||
const history = createBrowserHistory();
|
||||
history.replace(
|
||||
'/slos/create?_a=(indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration))'
|
||||
);
|
||||
jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history);
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||
|
@ -336,11 +335,9 @@ describe('SLO Edit Page', () => {
|
|||
const slo = buildSlo({ id: '123' });
|
||||
jest.spyOn(Router, 'useParams').mockReturnValue({ sloId: '123' });
|
||||
|
||||
const history = createBrowserHistory();
|
||||
history.push(
|
||||
'/slos/123/edit?_a=(name:%27updated-name%27,indicator:(params:(environment:prod,service:cartService),type:sli.apm.transactionDuration),objective:(target:0.92))'
|
||||
);
|
||||
jest.spyOn(Router, 'useHistory').mockReturnValueOnce(history);
|
||||
jest
|
||||
.spyOn(Router, 'useLocation')
|
||||
.mockReturnValue({ pathname: 'foo', search: '', state: '', hash: '' });
|
||||
|
|
|
@ -10,3 +10,5 @@ export {
|
|||
SingleDatasetLocatorDefinition,
|
||||
AllDatasetsLocatorDefinition,
|
||||
} from './locators';
|
||||
export { OBSERVABILITY_LOG_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from './url_schema';
|
||||
export { deepCompactObject } from './utils/deep_compact_object';
|
||||
|
|
|
@ -23,11 +23,10 @@ export class AllDatasetsLocatorDefinition implements LocatorDefinition<AllDatase
|
|||
|
||||
public readonly getLocation = (params: AllDatasetsLocatorParams) => {
|
||||
const { useHash } = this.deps;
|
||||
const index = AllDatasetSelection.create().toDataviewSpec().id;
|
||||
|
||||
return constructLocatorPath({
|
||||
datasetSelection: AllDatasetSelection.create().toPlainSelection(),
|
||||
locatorParams: params,
|
||||
index,
|
||||
useHash,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FilterStateStore } from '@kbn/es-query';
|
||||
import { getStatesFromKbnUrl } from '@kbn/kibana-utils-plugin/public';
|
||||
import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
|
||||
import {
|
||||
AllDatasetsLocatorParams,
|
||||
SingleDatasetLocatorParams,
|
||||
} from '@kbn/deeplinks-observability/locators';
|
||||
import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability';
|
||||
import { AllDatasetsLocatorDefinition } from './all_datasets/all_datasets_locator';
|
||||
import { SingleDatasetLocatorDefinition } from './single_dataset';
|
||||
import { DatasetLocatorDependencies } from './types';
|
||||
|
@ -38,8 +36,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await allDatasetsLocator.getLocation({});
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: '/?pageState=(datasetSelection:(selectionType:all),v:1)',
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -53,8 +51,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await allDatasetsLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: '/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: '/?pageState=(datasetSelection:(selectionType:all),time:(from:now-30m,to:now),v:1)',
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -70,8 +68,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await allDatasetsLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: '/?_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,query:(language:kuery,query:foo))',
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: '/?pageState=(datasetSelection:(selectionType:all),query:(language:kuery,query:foo),v:1)',
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -88,29 +86,28 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await allDatasetsLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: '/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA)',
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: '/?pageState=(datasetSelection:(selectionType:all),refreshInterval:(pause:!f,value:666),v:1)',
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow specifiying columns and sort', async () => {
|
||||
it('should allow specifiying columns', async () => {
|
||||
const params: AllDatasetsLocatorParams = {
|
||||
columns: ['_source'],
|
||||
sort: [['timestamp, asc']] as string[][],
|
||||
};
|
||||
|
||||
const { allDatasetsLocator } = await setup();
|
||||
const location = await allDatasetsLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA,sort:!(!('timestamp,%20asc')))`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selectionType:all),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow specifiying filters', async () => {
|
||||
it('should allow specifying filters', async () => {
|
||||
const params: AllDatasetsLocatorParams = {
|
||||
filters: [
|
||||
{
|
||||
|
@ -119,9 +116,6 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
|
@ -129,47 +123,16 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.GLOBAL_STATE,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { allDatasetsLocator } = await setup();
|
||||
const { path } = await allDatasetsLocator.getLocation(params);
|
||||
const location = await allDatasetsLocator.getLocation(params);
|
||||
|
||||
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false });
|
||||
|
||||
expect(_a).toEqual({
|
||||
filters: [
|
||||
{
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
meta: {
|
||||
alias: 'foo',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
index: 'BQZwpgNmDGAuCWB7AdgFQJ4AcwC4CGEEAlEA',
|
||||
});
|
||||
expect(_g).toEqual({
|
||||
filters: [
|
||||
{
|
||||
$state: {
|
||||
store: 'globalState',
|
||||
},
|
||||
meta: {
|
||||
alias: 'bar',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(location.path).toMatchInlineSnapshot(
|
||||
`"/?pageState=(datasetSelection:(selectionType:all),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -184,8 +147,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
});
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -201,8 +164,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await singleDatasetLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_g=(time:(from:now-30m,to:now))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),time:(from:now-30m,to:now),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -221,8 +184,8 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await singleDatasetLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,query:(language:kuery,query:foo))`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),query:(language:kuery,query:foo),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -241,26 +204,25 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
const location = await singleDatasetLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_g=(refreshInterval:(pause:!f,value:666))&_a=(index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA)`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),refreshInterval:(pause:!f,value:666),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow specifiying columns and sort', async () => {
|
||||
it('should allow specifiying columns', async () => {
|
||||
const params: SingleDatasetLocatorParams = {
|
||||
integration,
|
||||
dataset,
|
||||
columns: ['_source'],
|
||||
sort: [['timestamp, asc']] as string[][],
|
||||
};
|
||||
|
||||
const { singleDatasetLocator } = await setup();
|
||||
const location = await singleDatasetLocator.getLocation(params);
|
||||
|
||||
expect(location).toMatchObject({
|
||||
app: OBSERVABILITY_LOG_EXPLORER,
|
||||
path: `/?_a=(columns:!(_source),index:BQZwpgNmDGAuCWB7AdgLmAEwIay%2BW6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA,sort:!(!('timestamp,%20asc')))`,
|
||||
app: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
path: `/?pageState=(columns:!((field:_source)),datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),v:1)`,
|
||||
state: {},
|
||||
});
|
||||
});
|
||||
|
@ -276,9 +238,6 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.APP_STATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
|
@ -286,48 +245,16 @@ describe('Observability Logs Explorer Locators', () => {
|
|||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
$state: {
|
||||
store: FilterStateStore.GLOBAL_STATE,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { singleDatasetLocator } = await setup();
|
||||
const { path } = await singleDatasetLocator.getLocation(params);
|
||||
const location = await singleDatasetLocator.getLocation(params);
|
||||
|
||||
const { _a, _g } = getStatesFromKbnUrl(path, ['_a', '_g'], { getFromHashQuery: false });
|
||||
|
||||
expect(_a).toEqual({
|
||||
filters: [
|
||||
{
|
||||
$state: {
|
||||
store: 'appState',
|
||||
},
|
||||
meta: {
|
||||
alias: 'foo',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
index:
|
||||
'BQZwpgNmDGAuCWB7AdgLmAEwIay+W6yWAtmKgOQSIDmIAtLGCLHQFRvkA0CsUqjzAJScipVABUmsYeChwkycQE8ADmQCuyAE5NEEAG5gMgoA',
|
||||
});
|
||||
expect(_g).toEqual({
|
||||
filters: [
|
||||
{
|
||||
$state: {
|
||||
store: 'globalState',
|
||||
},
|
||||
meta: {
|
||||
alias: 'bar',
|
||||
disabled: false,
|
||||
negate: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(location.path).toMatchInlineSnapshot(
|
||||
`"/?pageState=(datasetSelection:(selection:(dataset:(name:'logs-test-*-*',title:test),name:Test),selectionType:unresolved),filters:!((meta:(alias:foo,disabled:!f,negate:!f)),(meta:(alias:bar,disabled:!f,negate:!f))),v:1)"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -35,11 +35,9 @@ export class SingleDatasetLocatorDefinition
|
|||
},
|
||||
});
|
||||
|
||||
const index = unresolvedDatasetSelection.toDataviewSpec().id;
|
||||
|
||||
return constructLocatorPath({
|
||||
datasetSelection: unresolvedDatasetSelection.toPlainSelection(),
|
||||
locatorParams: params,
|
||||
index,
|
||||
useHash,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,16 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AggregateQuery, Filter, Query } from '@kbn/es-query';
|
||||
|
||||
export interface AppState {
|
||||
index?: string;
|
||||
query?: Query | AggregateQuery;
|
||||
filters?: Filter[];
|
||||
columns?: string[];
|
||||
sort?: string[][];
|
||||
}
|
||||
|
||||
export interface DatasetLocatorDependencies {
|
||||
useHash: boolean;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
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 React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { DatasetQualityRoute, ObservablityLogExplorerMainRoute } from '../routes/main';
|
||||
import { DatasetQualityRoute, ObservabilityLogExplorerMainRoute } from '../routes/main';
|
||||
import {
|
||||
ObservabilityLogExplorerAppMountParameters,
|
||||
ObservabilityLogExplorerPluginStart,
|
||||
ObservabilityLogExplorerStartDeps,
|
||||
} from '../types';
|
||||
import { KbnUrlStateStorageFromRouterProvider } from '../utils/kbn_url_state_context';
|
||||
import { useKibanaContextForPluginProvider } from '../utils/use_kibana';
|
||||
|
||||
export const renderObservabilityLogExplorer = (
|
||||
|
@ -59,26 +60,25 @@ export const ObservabilityLogExplorerApp = ({
|
|||
const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(
|
||||
core,
|
||||
plugins,
|
||||
pluginStart
|
||||
pluginStart,
|
||||
appParams
|
||||
);
|
||||
|
||||
return (
|
||||
<KibanaRenderContextProvider i18n={core.i18n} theme={core.theme}>
|
||||
<KibanaContextProviderForPlugin>
|
||||
<Router history={appParams.history}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
exact={true}
|
||||
render={() => <ObservablityLogExplorerMainRoute appParams={appParams} core={core} />}
|
||||
/>
|
||||
<Route
|
||||
path="/dataset-quality"
|
||||
exact={true}
|
||||
render={() => <DatasetQualityRoute core={core} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
<KbnUrlStateStorageFromRouterProvider>
|
||||
<Router history={appParams.history}>
|
||||
<Routes>
|
||||
<Route path="/" exact={true} render={() => <ObservabilityLogExplorerMainRoute />} />
|
||||
<Route
|
||||
path="/dataset-quality"
|
||||
exact={true}
|
||||
render={() => <DatasetQualityRoute core={core} />}
|
||||
/>
|
||||
</Routes>
|
||||
</Router>
|
||||
</KbnUrlStateStorageFromRouterProvider>
|
||||
</KibanaContextProviderForPlugin>
|
||||
</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.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { type BehaviorSubject, distinctUntilChanged, filter, take } from 'rxjs';
|
||||
import styled from '@emotion/styled';
|
||||
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiButton,
|
||||
EuiHeader,
|
||||
EuiHeaderLink,
|
||||
EuiHeaderLinks,
|
||||
EuiHeaderSection,
|
||||
EuiHeaderSectionItem,
|
||||
} from '@elastic/eui';
|
||||
import { LogExplorerStateContainer } from '@kbn/log-explorer-plugin/public';
|
||||
import {
|
||||
OBSERVABILITY_ONBOARDING_LOCATOR,
|
||||
ObservabilityOnboardingLocatorParams,
|
||||
} from '@kbn/deeplinks-observability/locators';
|
||||
import { KibanaReactContextValue } from '@kbn/kibana-react-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { css } from '@emotion/react';
|
||||
import { LOG_EXPLORER_FEEDBACK_LINK } from '@kbn/observability-shared-plugin/common';
|
||||
import styled from '@emotion/styled';
|
||||
import { HeaderMenuPortal } from '@kbn/observability-shared-plugin/public';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import { LogExplorerTabs } from '@kbn/discover-plugin/public';
|
||||
import { PluginKibanaContextValue } from '../utils/use_kibana';
|
||||
import {
|
||||
betaBadgeDescription,
|
||||
betaBadgeTitle,
|
||||
discoverLinkTitle,
|
||||
feedbackLinkTitle,
|
||||
onboardingLinkTitle,
|
||||
} from '../../common/translations';
|
||||
import { getRouterLinkProps } from '../utils/get_router_link_props';
|
||||
import { ObservabilityLogExplorerAppMountParameters } from '../types';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import useObservable from 'react-use/lib/useObservable';
|
||||
import { filter, take } from 'rxjs';
|
||||
import { betaBadgeDescription, betaBadgeTitle } from '../../common/translations';
|
||||
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||
import { ConnectedDiscoverLink } from './discover_link';
|
||||
import { FeedbackLink } from './feedback_link';
|
||||
import { ConnectedOnboardingLink } from './onboarding_link';
|
||||
|
||||
interface LogExplorerTopNavMenuProps {
|
||||
setHeaderActionMenu: ObservabilityLogExplorerAppMountParameters['setHeaderActionMenu'];
|
||||
services: KibanaReactContextValue<PluginKibanaContextValue>['services'];
|
||||
state$: BehaviorSubject<LogExplorerStateContainer>;
|
||||
theme$: ObservabilityLogExplorerAppMountParameters['theme$'];
|
||||
}
|
||||
export const LogExplorerTopNavMenu = () => {
|
||||
const {
|
||||
services: { serverless },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
export const LogExplorerTopNavMenu = ({
|
||||
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$}
|
||||
/>
|
||||
);
|
||||
return Boolean(serverless) ? <ServerlessTopNav /> : <StatefulTopNav />;
|
||||
};
|
||||
|
||||
const ServerlessTopNav = ({
|
||||
services,
|
||||
state$,
|
||||
}: Pick<LogExplorerTopNavMenuProps, 'services' | 'state$'>) => {
|
||||
const ServerlessTopNav = () => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
|
||||
return (
|
||||
<EuiHeader data-test-subj="logExplorerHeaderMenu" css={{ boxShadow: 'none' }}>
|
||||
<EuiHeaderSection>
|
||||
|
@ -97,38 +62,40 @@ const ServerlessTopNav = ({
|
|||
</EuiHeaderSectionItem>
|
||||
<EuiHeaderSectionItem>
|
||||
<EuiHeaderLinks gutterSize="xs">
|
||||
<DiscoverLink services={services} state$={state$} />
|
||||
<ConnectedDiscoverLink />
|
||||
<FeedbackLink />
|
||||
</EuiHeaderLinks>
|
||||
<VerticalRule />
|
||||
</EuiHeaderSectionItem>
|
||||
<EuiHeaderSectionItem>
|
||||
<OnboardingLink services={services} />
|
||||
<ConnectedOnboardingLink />
|
||||
</EuiHeaderSectionItem>
|
||||
</EuiHeaderSection>
|
||||
</EuiHeader>
|
||||
);
|
||||
};
|
||||
|
||||
const StatefulTopNav = ({
|
||||
setHeaderActionMenu,
|
||||
services,
|
||||
state$,
|
||||
theme$,
|
||||
}: LogExplorerTopNavMenuProps) => {
|
||||
const StatefulTopNav = () => {
|
||||
const {
|
||||
services: {
|
||||
appParams: { setHeaderActionMenu },
|
||||
chrome,
|
||||
i18n,
|
||||
theme,
|
||||
},
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
/**
|
||||
* Since the breadcrumbsAppendExtension might be set only during a plugin start (e.g. search session)
|
||||
* we retrieve the latest valid extension in order to restore it once we unmount the beta badge.
|
||||
*/
|
||||
const [previousAppendExtension$] = useState(() =>
|
||||
services.chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1))
|
||||
chrome.getBreadcrumbsAppendExtension$().pipe(filter(Boolean), take(1))
|
||||
);
|
||||
|
||||
const previousAppendExtension = useObservable(previousAppendExtension$);
|
||||
|
||||
useEffect(() => {
|
||||
const { chrome, i18n, theme } = services;
|
||||
|
||||
if (chrome) {
|
||||
chrome.setBreadcrumbsAppendExtension({
|
||||
content: toMountPoint(
|
||||
|
@ -161,15 +128,15 @@ const StatefulTopNav = ({
|
|||
chrome.setBreadcrumbsAppendExtension(previousAppendExtension);
|
||||
}
|
||||
};
|
||||
}, [services, previousAppendExtension]);
|
||||
}, [chrome, i18n, previousAppendExtension, theme]);
|
||||
|
||||
return (
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme$}>
|
||||
<HeaderMenuPortal setHeaderActionMenu={setHeaderActionMenu} theme$={theme.theme$}>
|
||||
<EuiHeaderSection data-test-subj="logExplorerHeaderMenu">
|
||||
<EuiHeaderSectionItem>
|
||||
<EuiHeaderLinks gutterSize="xs">
|
||||
<DiscoverLink services={services} state$={state$} />
|
||||
<OnboardingLink services={services} />
|
||||
<ConnectedDiscoverLink />
|
||||
<ConnectedOnboardingLink />
|
||||
</EuiHeaderLinks>
|
||||
</EuiHeaderSectionItem>
|
||||
</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`
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
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 { css } from '@emotion/react';
|
||||
import type { ObservabilitySharedPluginStart } from '@kbn/observability-shared-plugin/public';
|
||||
import React from 'react';
|
||||
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||
|
||||
export const ObservabilityLogExplorerPageTemplate = ({
|
||||
children,
|
||||
observabilityShared,
|
||||
pageProps,
|
||||
}: React.PropsWithChildren<{
|
||||
observabilityShared: ObservabilitySharedPluginStart;
|
||||
pageProps?: EuiPageSectionProps;
|
||||
}>) => (
|
||||
<observabilityShared.navigation.PageTemplate
|
||||
pageSectionProps={{ ...pageSectionProps, ...pageProps }}
|
||||
>
|
||||
{children}
|
||||
</observabilityShared.navigation.PageTemplate>
|
||||
);
|
||||
}>) => {
|
||||
const {
|
||||
services: { observabilityShared },
|
||||
} = useKibanaContextForPlugin();
|
||||
|
||||
return (
|
||||
<observabilityShared.navigation.PageTemplate
|
||||
pageSectionProps={{ ...pageSectionProps, ...pageProps }}
|
||||
>
|
||||
{children}
|
||||
</observabilityShared.navigation.PageTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
const fullHeightContentStyles = css`
|
||||
display: flex;
|
||||
|
|
|
@ -13,6 +13,9 @@ import type { LogAIAssistantDocument } from '@kbn/logs-shared-plugin/public';
|
|||
import React, { useMemo } from 'react';
|
||||
import { useKibanaContextForPlugin } from '../utils/use_kibana';
|
||||
|
||||
type RenderFlyoutContentCustomization =
|
||||
Required<LogExplorerCustomizations>['flyout']['renderContent'];
|
||||
|
||||
const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { LogAIAssistant } = services.logsShared;
|
||||
|
@ -22,19 +25,17 @@ const ObservabilityLogAIAssistant = ({ doc }: LogExplorerFlyoutContentProps) =>
|
|||
return <LogAIAssistant key={doc.id} doc={mappedDoc} />;
|
||||
};
|
||||
|
||||
export const renderFlyoutContent: Required<LogExplorerCustomizations>['flyout']['renderContent'] = (
|
||||
renderPreviousContent,
|
||||
props
|
||||
) => {
|
||||
return (
|
||||
<>
|
||||
{renderPreviousContent()}
|
||||
<EuiFlexItem>
|
||||
<ObservabilityLogAIAssistant {...props} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
export const renderFlyoutContent: RenderFlyoutContentCustomization =
|
||||
(renderPreviousContent) => (props) => {
|
||||
return (
|
||||
<>
|
||||
{renderPreviousContent(props)}
|
||||
<EuiFlexItem>
|
||||
<ObservabilityLogAIAssistant {...props} />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utils
|
||||
|
|
|
@ -5,11 +5,18 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LogExplorerCustomizations } from '@kbn/log-explorer-plugin/public';
|
||||
import { CreateLogExplorerController } from '@kbn/log-explorer-plugin/public';
|
||||
import { renderFlyoutContent } from './flyout_content';
|
||||
|
||||
export const createLogExplorerCustomizations = (): LogExplorerCustomizations => ({
|
||||
flyout: {
|
||||
renderContent: renderFlyoutContent,
|
||||
},
|
||||
});
|
||||
export const createLogExplorerControllerWithCustomizations =
|
||||
(createLogExplorerController: CreateLogExplorerController): CreateLogExplorerController =>
|
||||
(args) =>
|
||||
createLogExplorerController({
|
||||
...args,
|
||||
customizations: {
|
||||
...args.customizations,
|
||||
flyout: {
|
||||
renderContent: renderFlyoutContent,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -13,15 +13,14 @@ import {
|
|||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core/public';
|
||||
import { OBSERVABILITY_LOG_EXPLORER } from '@kbn/deeplinks-observability';
|
||||
import { OBSERVABILITY_LOG_EXPLORER_APP_ID } from '@kbn/deeplinks-observability';
|
||||
import {
|
||||
AllDatasetsLocatorDefinition,
|
||||
ObservabilityLogExplorerLocators,
|
||||
SingleDatasetLocatorDefinition,
|
||||
AllDatasetsLocatorDefinition,
|
||||
} from '../common/locators';
|
||||
import { type ObservabilityLogExplorerConfig } from '../common/plugin_config';
|
||||
import { logExplorerAppTitle } from '../common/translations';
|
||||
import { renderObservabilityLogExplorer } from './applications/observability_log_explorer';
|
||||
import type {
|
||||
ObservabilityLogExplorerAppMountParameters,
|
||||
ObservabilityLogExplorerPluginSetup,
|
||||
|
@ -48,7 +47,7 @@ export class ObservabilityLogExplorerPlugin
|
|||
const useHash = core.uiSettings.get('state:storeInSessionStorage');
|
||||
|
||||
core.application.register({
|
||||
id: OBSERVABILITY_LOG_EXPLORER,
|
||||
id: OBSERVABILITY_LOG_EXPLORER_APP_ID,
|
||||
title: logExplorerAppTitle,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
euiIconType: 'logoLogging',
|
||||
|
@ -59,6 +58,9 @@ export class ObservabilityLogExplorerPlugin
|
|||
keywords: ['logs', 'log', 'explorer', 'logs explorer'],
|
||||
mount: async (appMountParams: ObservabilityLogExplorerAppMountParameters) => {
|
||||
const [coreStart, pluginsStart, ownPluginStart] = await core.getStartServices();
|
||||
const { renderObservabilityLogExplorer } = await import(
|
||||
'./applications/observability_log_explorer'
|
||||
);
|
||||
|
||||
return renderObservabilityLogExplorer(
|
||||
coreStart,
|
||||
|
|
|
@ -19,7 +19,7 @@ export interface DatasetQualityRouteProps {
|
|||
|
||||
export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { observabilityShared, serverless, datasetQuality: DatasetQuality } = services;
|
||||
const { serverless, datasetQuality: DatasetQuality } = services;
|
||||
const breadcrumb: EuiBreadcrumb[] = [
|
||||
{
|
||||
text: datasetQualityAppTitle,
|
||||
|
@ -29,13 +29,8 @@ export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => {
|
|||
useBreadcrumbs(breadcrumb, core.chrome, serverless);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ObservabilityLogExplorerPageTemplate
|
||||
observabilityShared={observabilityShared}
|
||||
pageProps={{ paddingSize: 'l' }}
|
||||
>
|
||||
<DatasetQuality.DatasetQuality />
|
||||
</ObservabilityLogExplorerPageTemplate>
|
||||
</>
|
||||
<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.
|
||||
*/
|
||||
|
||||
import { CoreStart } from '@kbn/core/public';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type {
|
||||
LogExplorerController,
|
||||
LogExplorerPluginStart,
|
||||
} from '@kbn/log-explorer-plugin/public';
|
||||
import { useActor } from '@xstate/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { LogExplorerTopNavMenu } from '../../components/log_explorer_top_nav_menu';
|
||||
import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template';
|
||||
import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs';
|
||||
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
||||
import { ObservabilityLogExplorerAppMountParameters } from '../../types';
|
||||
import { createLogExplorerControllerWithCustomizations } from '../../log_explorer_customizations';
|
||||
import {
|
||||
ObservabilityLogExplorerPageStateProvider,
|
||||
useObservabilityLogExplorerPageStateContext,
|
||||
} from '../../state_machines/observability_log_explorer/src';
|
||||
import { LazyOriginInterpreter } from '../../state_machines/origin_interpreter/src/lazy_component';
|
||||
import { createLogExplorerCustomizations } from '../../log_explorer_customizations';
|
||||
export interface ObservablityLogExplorerMainRouteProps {
|
||||
appParams: ObservabilityLogExplorerAppMountParameters;
|
||||
core: CoreStart;
|
||||
}
|
||||
import { ObservabilityLogExplorerHistory } from '../../types';
|
||||
import { noBreadcrumbs, useBreadcrumbs } from '../../utils/breadcrumbs';
|
||||
import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context';
|
||||
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
|
||||
|
||||
export const ObservablityLogExplorerMainRoute = ({
|
||||
appParams,
|
||||
core,
|
||||
}: ObservablityLogExplorerMainRouteProps) => {
|
||||
export const ObservabilityLogExplorerMainRoute = () => {
|
||||
const { services } = useKibanaContextForPlugin();
|
||||
const { logExplorer, observabilityShared, serverless } = services;
|
||||
useBreadcrumbs(noBreadcrumbs, core.chrome, serverless);
|
||||
const { logExplorer, serverless, chrome, notifications, appParams } = services;
|
||||
const { history } = appParams;
|
||||
|
||||
const { history, setHeaderActionMenu, theme$ } = appParams;
|
||||
useBreadcrumbs(noBreadcrumbs, chrome, serverless);
|
||||
|
||||
const [state$] = useState(() => new BehaviorSubject({}));
|
||||
const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext();
|
||||
|
||||
const customizations = useMemo(() => createLogExplorerCustomizations(), []);
|
||||
const createLogExplorerController = useMemo(
|
||||
() => createLogExplorerControllerWithCustomizations(logExplorer.createLogExplorerController),
|
||||
[logExplorer.createLogExplorerController]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LogExplorerTopNavMenu
|
||||
setHeaderActionMenu={setHeaderActionMenu}
|
||||
services={services}
|
||||
state$={state$}
|
||||
theme$={theme$}
|
||||
/>
|
||||
<LazyOriginInterpreter history={history} toasts={core.notifications.toasts} />
|
||||
<ObservabilityLogExplorerPageTemplate observabilityShared={observabilityShared}>
|
||||
<logExplorer.LogExplorer
|
||||
customizations={customizations}
|
||||
scopedHistory={history}
|
||||
state$={state$}
|
||||
/>
|
||||
</ObservabilityLogExplorerPageTemplate>
|
||||
</>
|
||||
<ObservabilityLogExplorerPageStateProvider
|
||||
createLogExplorerController={createLogExplorerController}
|
||||
toasts={notifications.toasts}
|
||||
urlStateStorageContainer={urlStateStorageContainer}
|
||||
timeFilterService={services.data.query.timefilter.timefilter}
|
||||
>
|
||||
<LogExplorerTopNavMenu />
|
||||
<LazyOriginInterpreter history={history} toasts={notifications.toasts} />
|
||||
<ConnectedContent />
|
||||
</ObservabilityLogExplorerPageStateProvider>
|
||||
);
|
||||
};
|
||||
|
||||
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