Skip to content

Commit

Permalink
[Logs+] Refactor state and URL persistence of Log Explorer (#170200)
Browse files Browse the repository at this point in the history
Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Felix Stürmer <[email protected]>
  • Loading branch information
3 people authored Dec 11, 2023
1 parent 2d3d215 commit dbabd6d
Show file tree
Hide file tree
Showing 122 changed files with 3,427 additions and 1,849 deletions.
2 changes: 1 addition & 1 deletion packages/deeplinks/observability/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
8 changes: 4 additions & 4 deletions packages/deeplinks/observability/deep_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
*/

import {
APM_APP_ID,
LOGS_APP_ID,
OBSERVABILITY_LOG_EXPLORER,
OBSERVABILITY_OVERVIEW_APP_ID,
METRICS_APP_ID,
APM_APP_ID,
OBSERVABILITY_LOG_EXPLORER_APP_ID,
OBSERVABILITY_ONBOARDING_APP_ID,
OBSERVABILITY_OVERVIEW_APP_ID,
} from './constants';

type LogsApp = typeof LOGS_APP_ID;
type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER;
type ObservabilityLogExplorerApp = typeof OBSERVABILITY_LOG_EXPLORER_APP_ID;
type ObservabilityOverviewApp = typeof OBSERVABILITY_OVERVIEW_APP_ID;
type MetricsApp = typeof METRICS_APP_ID;
type ApmApp = typeof APM_APP_ID;
Expand Down
6 changes: 2 additions & 4 deletions packages/deeplinks/observability/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
19 changes: 15 additions & 4 deletions packages/deeplinks/observability/locators/log_explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,13 +45,13 @@ export interface LogExplorerNavigationParams extends SerializableRecord {
*/
columns?: string[];
/**
* Array of the used sorting [[field,direction],...]
* Optionally apply free-form filters.
*/
sort?: string[][];
filters?: Filter[];
/**
* Optionally apply filters.
* Optionally apply curated filter controls
*/
filters?: Filter[];
filterControls?: FilterControls;
}

export interface LogExplorerLocatorParams extends LogExplorerNavigationParams {
Expand Down
48 changes: 48 additions & 0 deletions packages/kbn-xstate-utils/src/dev_tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
});
2 changes: 1 addition & 1 deletion packages/kbn-xstate-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
*/

export * from './actions';
export * from './dev_tools';
export * from './notification_channel';
export * from './types';
export * from './dev_tools';
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -46,13 +50,15 @@ interface DiscoverLandingParams {

export interface MainRouteProps {
customizationCallbacks: CustomizationCallback[];
stateStorageContainer?: IKbnUrlStateStorage;
isDev: boolean;
customizationContext: DiscoverCustomizationContext;
}

export function DiscoverMainRoute({
customizationCallbacks,
customizationContext,
stateStorageContainer,
}: MainRouteProps) {
const history = useHistory();
const services = useDiscoverServices();
Expand All @@ -70,6 +76,7 @@ export function DiscoverMainRoute({
history,
services,
customizationContext,
stateStorageContainer,
})
);
const { customizationService, isInitialized: isCustomizationServiceInitialized } =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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();
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ interface DiscoverStateContainerParams {
* Context object for customization related properties
*/
customizationContext: DiscoverCustomizationContext;
/**
* a custom url state storage
*/
stateStorageContainer?: IKbnUrlStateStorage;
}

export interface LoadParams {
Expand Down Expand Up @@ -204,19 +208,22 @@ export function getDiscoverStateContainer({
history,
services,
customizationContext,
stateStorageContainer,
}: DiscoverStateContainerParams): DiscoverStateContainer {
const storeInSessionStorage = services.uiSettings.get('state:storeInSessionStorage');
const toasts = services.core.notifications.toasts;

/**
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +30,7 @@ export interface DiscoverContainerInternalProps {
getDiscoverServices: () => Promise<DiscoverServices>;
scopedHistory: ScopedHistory;
customizationCallbacks: CustomizationCallback[];
stateStorageContainer?: IKbnUrlStateStorage;
isDev: boolean;
isLoading?: boolean;
}
Expand All @@ -55,6 +57,7 @@ export const DiscoverContainerInternal = ({
customizationCallbacks,
isDev,
getDiscoverServices,
stateStorageContainer,
isLoading = false,
}: DiscoverContainerInternalProps) => {
const [discoverServices, setDiscoverServices] = useState<DiscoverServices | undefined>();
Expand Down Expand Up @@ -97,6 +100,7 @@ export const DiscoverContainerInternal = ({
<DiscoverMainRoute
customizationCallbacks={customizationCallbacks}
customizationContext={customizationContext}
stateStorageContainer={stateStorageContainer}
isDev={isDev}
/>
</KibanaContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,17 +193,17 @@ export const createKbnUrlControls = (

// runs scheduled url updates
function flush(replace = shouldReplace) {
const nextUrl = getPendingUrl();

if (!nextUrl) return;
if (updateQueue.length === 0) {
return;
}

const nextUrl = getPendingUrl();
cleanUp();
const newUrl = updateUrl(nextUrl, replace);
return newUrl;
}

function getPendingUrl() {
if (updateQueue.length === 0) return undefined;
const resultUrl = updateQueue.reduce(
(url, nextUpdate) => nextUpdate(url) ?? url,
getCurrentUrl(history)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit dbabd6d

Please sign in to comment.