[ML] Move nested property utilities and url state to packages (#147912)

Effort to deduplicate code. Move nested property utilities and url state
to packages.

Boilerplate for the packages was created likes this:

```
node scripts/generate package @kbn/ml-url-state --web --dir ./x-pack/packages/ml/url_state
node scripts/generate package @kbn/ml-nested-property --web --dir ./x-pack/packages/ml/nested_property
```

I consolidated the different `url_state.ts` files. One thing to note:
Each one had its own definition for `pageKey: AppStateKey`. I changed
that and made it just `pageKey: string`, I suspect it's good enough.
Otherwise we'd have a reverse dependency on all consuming code.
Alternative: We could refactor to require overriding a generic to pass
in allowed values.
This commit is contained in:
Walter Rafelsberger 2022-12-27 15:59:14 +01:00 committed by GitHub
parent 4ec4acaaaf
commit 8a44ba3158
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 467 additions and 614 deletions

2
.github/CODEOWNERS vendored
View file

@ -1072,4 +1072,6 @@ x-pack/packages/ml/agg_utils @elastic/ml-ui
x-pack/packages/ml/aiops_components @elastic/ml-ui
x-pack/packages/ml/aiops_utils @elastic/ml-ui
x-pack/packages/ml/is_populated_object @elastic/ml-ui
x-pack/packages/ml/nested_property @elastic/ml-ui
x-pack/packages/ml/string_hash @elastic/ml-ui
x-pack/packages/ml/url_state @elastic/ml-ui

View file

@ -350,7 +350,9 @@
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
"@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils",
"@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object",
"@kbn/ml-nested-property": "link:x-pack/packages/ml/nested_property",
"@kbn/ml-string-hash": "link:x-pack/packages/ml/string_hash",
"@kbn/ml-url-state": "link:x-pack/packages/ml/url_state",
"@kbn/monaco": "link:packages/kbn-monaco",
"@kbn/osquery-io-ts-types": "link:packages/kbn-osquery-io-ts-types",
"@kbn/plugin-discovery": "link:packages/kbn-plugin-discovery",

View file

@ -816,10 +816,14 @@
"@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"],
"@kbn/ml-is-populated-object": ["x-pack/packages/ml/is_populated_object"],
"@kbn/ml-is-populated-object/*": ["x-pack/packages/ml/is_populated_object/*"],
"@kbn/ml-nested-property": ["x-pack/packages/ml/nested_property"],
"@kbn/ml-nested-property/*": ["x-pack/packages/ml/nested_property/*"],
"@kbn/ml-plugin": ["x-pack/plugins/ml"],
"@kbn/ml-plugin/*": ["x-pack/plugins/ml/*"],
"@kbn/ml-string-hash": ["x-pack/packages/ml/string_hash"],
"@kbn/ml-string-hash/*": ["x-pack/packages/ml/string_hash/*"],
"@kbn/ml-url-state": ["x-pack/packages/ml/url_state"],
"@kbn/ml-url-state/*": ["x-pack/packages/ml/url_state/*"],
"@kbn/monaco": ["packages/kbn-monaco"],
"@kbn/monaco/*": ["packages/kbn-monaco/*"],
"@kbn/monitoring-collection-plugin": ["x-pack/plugins/monitoring_collection"],

View file

@ -0,0 +1,3 @@
# @kbn/ml-nested-property
Provides functionality similar to lodash's get() except that it's TypeScript aware and able to infer return types.

View file

@ -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 { getNestedProperty } from './src/get_nested_property';
export { setNestedProperty } from './src/set_nested_property';

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/nested_property'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-nested-property",
"owner": "@elastic/ml-ui"
}

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/ml-nested-property",
"description": "TypeScript-aware utility functions to get/set attributes from objects.",
"author": "Machine Learning UI",
"homepage": "https://docs.elastic.dev/kibana-dev-docs/api/kbn-ml-nested-property",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getNestedProperty } from './object_utils';
import { getNestedProperty } from './get_nested_property';
describe('object_utils', () => {
test('getNestedProperty()', () => {

View file

@ -33,21 +33,3 @@ export function getNestedProperty(
return o;
}
export const setNestedProperty = (obj: Record<string, any>, accessor: string, value: any) => {
let ref = obj;
const accessors = accessor.split('.');
const len = accessors.length;
for (let i = 0; i < len - 1; i++) {
const attribute = accessors[i];
if (ref[attribute] === undefined) {
ref[attribute] = {};
}
ref = ref[attribute];
}
ref[accessors[len - 1]] = value;
return obj;
};

View file

@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setNestedProperty } from './set_nested_property';
describe('object_utils', () => {
test('setNestedProperty()', () => {
function getTestObj() {
return {
the: {
nested: {
value: 'the-nested-value',
},
},
};
}
function getFalseyObject() {
return {
the: {
nested: {
value: false,
},
other_nested: {
value: 0,
},
},
};
}
const test1 = setNestedProperty(getTestObj(), 'the', 'update');
expect(test1.the).toBe('update');
const test2 = setNestedProperty(getTestObj(), 'the$', 'update');
expect(test2.the$).toBe('update');
const test3 = setNestedProperty(getTestObj(), 'the$', 'the-default-value');
expect(test3.the$).toBe('the-default-value');
const test4 = setNestedProperty(getTestObj(), 'the.neSted', 'update');
expect(test4.the.neSted).toBe('update');
const test5 = setNestedProperty(getTestObj(), 'the.nested', 'update');
expect(test5.the.nested).toStrictEqual('update');
const test6 = setNestedProperty(getTestObj(), 'the.nested.vaLue', 'update');
expect(test6.the.nested.vaLue).toBe('update');
const test7 = setNestedProperty(getTestObj(), 'the.nested.value', 'update');
expect(test7.the.nested.value).toBe('update');
const test8 = setNestedProperty(getTestObj(), 'the.nested.value.didntExist', 'update');
expect(test8.the.nested.value.didntExist).toBe('update');
const test9 = setNestedProperty(
getTestObj(),
'the.nested.value.didntExist',
'the-default-value'
);
expect(test9.the.nested.value.didntExist).toBe('the-default-value');
const test10 = setNestedProperty(getFalseyObject(), 'the.nested.value', 'update');
expect(test10.the.nested.value).toBe('update');
const test11 = setNestedProperty(getFalseyObject(), 'the.other_nested.value', 'update');
expect(test11.the.other_nested.value).toBe('update');
});
});

View file

@ -0,0 +1,24 @@
/*
* 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 setNestedProperty = (obj: Record<string, any>, accessor: string, value: any) => {
let ref = obj;
const accessors = accessor.split('.');
const len = accessors.length;
for (let i = 0; i < len - 1; i++) {
const attribute = accessors[i];
if (typeof ref[attribute] !== 'object') {
ref[attribute] = {};
}
ref = ref[attribute];
}
ref[accessors[len - 1]] = value;
return obj;
};

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
]
}

View file

@ -0,0 +1,3 @@
# @kbn/ml-url-state
URL state management.

View file

@ -0,0 +1,19 @@
/*
* 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 {
isRisonSerializationRequired,
parseUrlState,
usePageUrlState,
useUrlState,
PageUrlStateService,
Provider,
UrlStateProvider,
type Accessor,
type Dictionary,
type SetUrlState,
} from './src/url_state';

View file

@ -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.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/url_state'],
};

View file

@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-url-state",
"owner": "@elastic/ml-ui"
}

View file

@ -0,0 +1,9 @@
{
"name": "@kbn/ml-url-state",
"description": "Url state management utilities.",
"author": "Machine Learning UI",
"homepage": "https://docs.elastic.dev/kibana-dev-docs/api/kbn-ml-url-state",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -11,23 +11,25 @@ import React, {
useCallback,
useContext,
useMemo,
FC,
useRef,
useEffect,
type FC,
} from 'react';
import { isEqual } from 'lodash';
import { decode, encode } from '@kbn/rison';
import { useHistory, useLocation } from 'react-router-dom';
import { isEqual } from 'lodash';
import { getNestedProperty } from '@kbn/ml-nested-property';
import { decode, encode } from '@kbn/rison';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { Dictionary } from '../../../common/types/common';
import { getNestedProperty } from './object_utils';
import { MlPages } from '../../../common/constants/locator';
export interface Dictionary<TValue> {
[id: string]: TValue;
}
type Accessor = '_a' | '_g';
export type Accessor = '_a' | '_g';
export type SetUrlState = (
accessor: Accessor,
attribute: string | Dictionary<any>,
@ -48,7 +50,7 @@ const risonSerializedParams = new Set(['_a', '_g']);
* Checks if the URL query parameter requires rison serialization.
* @param queryParam
*/
function isRisonSerializationRequired(queryParam: string): boolean {
export function isRisonSerializationRequired(queryParam: string): boolean {
return risonSerializedParams.has(queryParam);
}
@ -86,7 +88,7 @@ export const urlStateStore = createContext<UrlState>({
setUrlState: () => {},
});
const { Provider } = urlStateStore;
export const { Provider } = urlStateStore;
export const UrlStateProvider: FC = ({ children }) => {
const history = useHistory();
@ -183,15 +185,6 @@ export const useUrlState = (
return [urlState, setUrlState];
};
type LegacyUrlKeys = 'mlExplorerSwimlane';
export type AppStateKey =
| 'mlSelectSeverity'
| 'mlSelectInterval'
| 'mlAnomaliesTable'
| MlPages
| LegacyUrlKeys;
/**
* Service for managing URL state of particular page.
*/
@ -235,16 +228,21 @@ export class PageUrlStateService<T> {
}
}
interface PageUrlState {
pageKey: string;
pageUrlState: object;
}
/**
* Hook for managing the URL state of the page.
*/
export const usePageUrlState = <PageUrlState extends object>(
pageKey: AppStateKey,
defaultState?: PageUrlState
export const usePageUrlState = <T extends PageUrlState>(
pageKey: T['pageKey'],
defaultState?: T['pageUrlState']
): [
PageUrlState,
(update: Partial<PageUrlState>, replaceState?: boolean) => void,
PageUrlStateService<PageUrlState>
T['pageUrlState'],
(update: Partial<T['pageUrlState']>, replaceState?: boolean) => void,
PageUrlStateService<T['pageUrlState']>
] => {
const [appState, setAppState] = useUrlState('_a');
const pageState = appState?.[pageKey];
@ -255,9 +253,9 @@ export const usePageUrlState = <PageUrlState extends object>(
setCallback.current = setAppState;
}, [setAppState]);
const prevPageState = useRef<PageUrlState | undefined>();
const prevPageState = useRef<T['pageUrlState'] | undefined>();
const resultPageState: PageUrlState = useMemo(() => {
const resultPageState: T['pageUrlState'] = useMemo(() => {
const result = {
...(defaultState ?? {}),
...(pageState ?? {}),
@ -283,7 +281,7 @@ export const usePageUrlState = <PageUrlState extends object>(
}, [pageState]);
const onStateUpdate = useCallback(
(update: Partial<PageUrlState>, replaceState?: boolean) => {
(update: Partial<T['pageUrlState']>, replaceState?: boolean) => {
if (!setCallback?.current) {
throw new Error('Callback for URL state update has not been initialized.');
}
@ -300,7 +298,7 @@ export const usePageUrlState = <PageUrlState extends object>(
[pageKey, resultPageState]
);
const pageUrlStateService = useMemo(() => new PageUrlStateService<PageUrlState>(), []);
const pageUrlStateService = useMemo(() => new PageUrlStateService<T['pageUrlState']>(), []);
useEffect(
function updatePageUrlService() {

View file

@ -0,0 +1,23 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/ml-nested-property",
"@kbn/rison",
"@kbn/ml-is-populated-object",
]
}

View file

@ -18,6 +18,7 @@ import { type DataViewField } from '@kbn/data-views-plugin/public';
import { startWith } from 'rxjs';
import useMount from 'react-use/lib/useMount';
import type { Query, Filter } from '@kbn/es-query';
import { usePageUrlState } from '@kbn/ml-url-state';
import {
createMergedEsQuery,
getEsQueryFromSavedSearch,
@ -27,9 +28,13 @@ import { useTimefilter, useTimeRangeUpdates } from '../../hooks/use_time_filter'
import { useChangePointResults } from './use_change_point_agg_request';
import { type TimeBuckets, TimeBucketsInterval } from '../../../common/time_buckets';
import { useDataSource } from '../../hooks/use_data_source';
import { usePageUrlState } from '../../hooks/use_url_state';
import { useTimeBuckets } from '../../hooks/use_time_buckets';
export interface ChangePointDetectionPageUrlState {
pageKey: 'changePoint';
pageUrlState: ChangePointDetectionRequestParams;
}
export interface ChangePointDetectionRequestParams {
fn: string;
splitField: string;
@ -157,7 +162,7 @@ export const ChangePointDetectionContextProvider: FC = ({ children }) => {
}, [dataView]);
const [requestParamsFromUrl, updateRequestParams] =
usePageUrlState<ChangePointDetectionRequestParams>('changePoint');
usePageUrlState<ChangePointDetectionPageUrlState>('changePoint');
const resultQuery = useMemo<Query>(() => {
return (

View file

@ -8,10 +8,10 @@
import { DataView } from '@kbn/data-views-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import React, { FC } from 'react';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { PageHeader } from '../page_header';
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
import { DataSourceContext } from '../../hooks/use_data_source';
import { UrlStateProvider } from '../../hooks/use_url_state';
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { ChangePointDetectionPage } from './change_point_detection_page';

View file

@ -20,14 +20,15 @@ import {
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import type { TimeRange } from '@kbn/es-query';
import { TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { toMountPoint, wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { useUrlState } from '@kbn/ml-url-state';
import { useRefreshIntervalUpdates, useTimeRangeUpdates } from '../../hooks/use_time_filter';
import { useUrlState } from '../../hooks/use_url_state';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { aiopsRefresh$ } from '../../application/services/timefilter_refresh_service';

View file

@ -15,12 +15,12 @@ import { i18n } from '@kbn/i18n';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { UrlStateProvider } from '@kbn/ml-url-state';
import {
SEARCH_QUERY_LANGUAGE,
SearchQueryLanguage,
SavedSearchSavedObject,
} from '../../application/utils/search_utils';
import { UrlStateProvider } from '../../hooks/use_url_state';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
@ -41,6 +41,11 @@ const defaultSearchQuery = {
match_all: {},
};
export interface AiOpsPageUrlState {
pageKey: 'AIOPS_INDEX_VIEWER';
pageUrlState: AiOpsIndexBasedAppState;
}
export interface AiOpsIndexBasedAppState {
searchString?: Query['query'];
searchQuery?: Query['query'];

View file

@ -27,16 +27,16 @@ import { Filter, FilterStateStore, Query } from '@kbn/es-query';
import { FormattedMessage } from '@kbn/i18n-react';
import { SavedSearch } from '@kbn/discover-plugin/public';
import { useUrlState, usePageUrlState } from '@kbn/ml-url-state';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { SearchQueryLanguage, SavedSearchSavedObject } from '../../application/utils/search_utils';
import { useUrlState, usePageUrlState, AppStateKey } from '../../hooks/use_url_state';
import { useData } from '../../hooks/use_data';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { DocumentCountContent } from '../document_count_content/document_count_content';
import { DatePickerWrapper } from '../date_picker_wrapper';
import { SearchPanel } from '../search_panel';
import { restorableDefaults } from './explain_log_rate_spikes_app_state';
import { restorableDefaults, type AiOpsPageUrlState } from './explain_log_rate_spikes_app_state';
import { ExplainLogRateSpikesAnalysis } from './explain_log_rate_spikes_analysis';
import type { GroupTableItem } from '../spike_analysis_table/types';
import { useSpikeAnalysisTableRowContext } from '../spike_analysis_table/spike_analysis_table_row_provider';
@ -79,7 +79,10 @@ export const ExplainLogRateSpikesPage: FC<ExplainLogRateSpikesPageProps> = ({
setSelectedGroup,
} = useSpikeAnalysisTableRowContext();
const [aiopsListState, setAiopsListState] = usePageUrlState(AppStateKey, restorableDefaults);
const [aiopsListState, setAiopsListState] = usePageUrlState<AiOpsPageUrlState>(
'AIOPS_INDEX_VIEWER',
restorableDefaults
);
const [globalState, setGlobalState] = useUrlState('_g');
const [currentSavedSearch, setCurrentSavedSearch] = useState(savedSearch);

View file

@ -7,11 +7,11 @@
import React, { FC } from 'react';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { LogCategorizationPage } from './log_categorization_page';
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { UrlStateProvider } from '../../hooks/use_url_state';
export interface LogCategorizationAppStateProps {
dataView: DataView;

View file

@ -25,6 +25,7 @@ import {
EuiLoadingContent,
} from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { DatePickerWrapper } from '../date_picker_wrapper';
import { useData } from '../../hooks/use_data';
@ -33,7 +34,6 @@ import type {
SearchQueryLanguage,
SavedSearchSavedObject,
} from '../../application/utils/search_utils';
import { useUrlState } from '../../hooks/use_url_state';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import { restorableDefaults } from '../explain_log_rate_spikes/explain_log_rate_spikes_app_state';
import { useCategorizeRequest } from './use_categorize_request';

View file

@ -14,8 +14,8 @@ import {
EuiPageContentHeader_Deprecated as EuiPageContentHeader,
EuiPageContentHeaderSection_Deprecated as EuiPageContentHeaderSection,
} from '@elastic/eui';
import { useUrlState } from '@kbn/ml-url-state';
import { FullTimeRangeSelectorProps } from '../full_time_range_selector/full_time_range_selector';
import { useUrlState } from '../../hooks/use_url_state';
import { useDataSource } from '../../hooks/use_data_source';
import { useTimefilter } from '../../hooks/use_time_filter';
import { FullTimeRangeSelector } from '../full_time_range_selector';

View file

@ -13,6 +13,7 @@ import type { ChangePoint } from '@kbn/ml-agg-utils';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { Dictionary } from '@kbn/ml-url-state';
import { useTimeBuckets } from './use_time_buckets';
import { useAiopsAppContext } from './use_aiops_app_context';
@ -26,7 +27,6 @@ import {
import { useTimefilter } from './use_time_filter';
import { useDocumentCountStats } from './use_document_count_stats';
import type { Dictionary } from './use_url_state';
import type { GroupTableItem } from '../components/spike_analysis_table/types';
const DEFAULT_BAR_TARGET = 75;

View file

@ -1,223 +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 React, { FC } from 'react';
import { parse, stringify } from 'query-string';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { decode, encode } from '@kbn/rison';
import { useHistory, useLocation } from 'react-router-dom';
import { isEqual } from 'lodash';
export interface Dictionary<TValue> {
[id: string]: TValue;
}
// TODO duplicate of ml/object_utils
export const getNestedProperty = (
obj: Record<string, any>,
accessor: string,
defaultValue?: any
) => {
const value = accessor.split('.').reduce((o, i) => o?.[i], obj);
if (value === undefined) return defaultValue;
return value;
};
export type Accessor = '_a' | '_g';
export type SetUrlState = (
accessor: Accessor,
attribute: string | Dictionary<any>,
value?: any,
replaceState?: boolean
) => void;
export interface UrlState {
searchString: string;
setUrlState: SetUrlState;
}
/**
* Set of URL query parameters that require the rison serialization.
*/
const risonSerializedParams = new Set(['_a', '_g']);
/**
* Checks if the URL query parameter requires rison serialization.
* @param queryParam
*/
export function isRisonSerializationRequired(queryParam: string): boolean {
return risonSerializedParams.has(queryParam);
}
export function parseUrlState(search: string): Dictionary<any> {
const urlState: Dictionary<any> = {};
const parsedQueryString = parse(search, { sort: false });
try {
Object.keys(parsedQueryString).forEach((a) => {
if (isRisonSerializationRequired(a)) {
urlState[a] = decode(parsedQueryString[a] as string);
} else {
urlState[a] = parsedQueryString[a];
}
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not read url state', error);
}
return urlState;
}
// Compared to the original appState/globalState,
// this no longer makes use of fetch/save methods.
// - Reading from `location.search` is the successor of `fetch`.
// - `history.push()` is the successor of `save`.
// - The exposed state and set call make use of the above and make sure that
// different urlStates(e.g. `_a` / `_g`) don't overwrite each other.
// This uses a context to be able to maintain only one instance
// of the url state. It gets passed down with `UrlStateProvider`
// and can be used via `useUrlState`.
export const aiopsUrlStateStore = createContext<UrlState>({
searchString: '',
setUrlState: () => {},
});
export const { Provider } = aiopsUrlStateStore;
export const UrlStateProvider: FC = ({ children }) => {
const { Provider: StateProvider } = aiopsUrlStateStore;
const history = useHistory();
const { search: urlSearchString } = useLocation();
const setUrlState: SetUrlState = useCallback(
(
accessor: Accessor,
attribute: string | Dictionary<any>,
value?: any,
replaceState?: boolean
) => {
const prevSearchString = urlSearchString;
const urlState = parseUrlState(prevSearchString);
const parsedQueryString = parse(prevSearchString, { sort: false });
if (!Object.prototype.hasOwnProperty.call(urlState, accessor)) {
urlState[accessor] = {};
}
if (typeof attribute === 'string') {
if (isEqual(getNestedProperty(urlState, `${accessor}.${attribute}`), value)) {
return prevSearchString;
}
urlState[accessor][attribute] = value;
} else {
const attributes = attribute;
Object.keys(attributes).forEach((a) => {
urlState[accessor][a] = attributes[a];
});
}
try {
const oldLocationSearchString = stringify(parsedQueryString, {
sort: false,
encode: false,
});
Object.keys(urlState).forEach((a) => {
if (isRisonSerializationRequired(a)) {
parsedQueryString[a] = encode(urlState[a]);
} else {
parsedQueryString[a] = urlState[a];
}
});
const newLocationSearchString = stringify(parsedQueryString, {
sort: false,
encode: false,
});
if (oldLocationSearchString !== newLocationSearchString) {
const newSearchString = stringify(parsedQueryString, { sort: false });
if (replaceState) {
history.replace({ search: newSearchString });
} else {
history.push({ search: newSearchString });
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not save url state', error);
}
},
[history, urlSearchString]
);
return (
<StateProvider value={{ searchString: urlSearchString, setUrlState }}>{children}</StateProvider>
);
};
export const useUrlState = (accessor: Accessor) => {
const { searchString, setUrlState: setUrlStateContext } = useContext(aiopsUrlStateStore);
const urlState = useMemo(() => {
const fullUrlState = parseUrlState(searchString);
if (typeof fullUrlState === 'object') {
return fullUrlState[accessor];
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchString]);
const setUrlState = useCallback(
(attribute: string | Dictionary<any>, value?: any, replaceState?: boolean) => {
setUrlStateContext(accessor, attribute, value, replaceState);
},
[accessor, setUrlStateContext]
);
return [urlState, setUrlState];
};
export const AppStateKey = 'AIOPS_INDEX_VIEWER';
export const ChangePointStateKey = 'changePoint' as const;
/**
* Hook for managing the URL state of the page.
*/
export const usePageUrlState = <PageUrlState extends {}>(
pageKey: typeof AppStateKey | typeof ChangePointStateKey,
defaultState?: PageUrlState
): [PageUrlState, (update: Partial<PageUrlState>, replaceState?: boolean) => void] => {
const [appState, setAppState] = useUrlState('_a');
const pageState = appState?.[pageKey];
const resultPageState: PageUrlState = useMemo(() => {
return {
...(defaultState ?? {}),
...(pageState ?? {}),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageState]);
const onStateUpdate = useCallback(
(update: Partial<PageUrlState>, replaceState?: boolean) => {
setAppState(
pageKey,
{
...resultPageState,
...update,
},
replaceState
);
},
[pageKey, resultPageState, setAppState]
);
return useMemo(() => {
return [resultPageState, onStateUpdate];
}, [resultPageState, onStateUpdate]);
};

View file

@ -42,6 +42,7 @@
"@kbn/logging",
"@kbn/core-elasticsearch-server",
"@kbn/es-types",
"@kbn/ml-url-state",
],
"exclude": [
"target/**/*",

View file

@ -20,18 +20,20 @@ import {
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import type { TimeRange } from '@kbn/es-query';
import { TimeHistoryContract, UI_SETTINGS } from '@kbn/data-plugin/public';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import { wrapWithTheme } from '@kbn/kibana-react-plugin/public';
import { FormattedMessage } from '@kbn/i18n-react';
import {
useRefreshIntervalUpdates,
useTimeRangeUpdates,
} from '../../../index_data_visualizer/hooks/use_time_filter';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { dataVisualizerRefresh$ } from '../../../index_data_visualizer/services/timefilter_refresh_service';
import { useUrlState } from '../../util/url_state';
const DEFAULT_REFRESH_INTERVAL_MS = 5000;
const DATE_PICKER_MAX_WIDTH = 540;

View file

@ -1,147 +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 { parse } from 'query-string';
import { createContext, useCallback, useContext, useMemo } from 'react';
import { decode } from '@kbn/rison';
export interface Dictionary<TValue> {
[id: string]: TValue;
}
// duplicate of ml/object_utils
export const getNestedProperty = (
obj: Record<string, any>,
accessor: string,
defaultValue?: any
) => {
const value = accessor.split('.').reduce((o, i) => o?.[i], obj);
if (value === undefined) return defaultValue;
return value;
};
export type Accessor = '_a' | '_g';
export type SetUrlState = (
accessor: Accessor,
attribute: string | Dictionary<any>,
value?: any,
replaceState?: boolean
) => void;
export interface UrlState {
searchString: string;
setUrlState: SetUrlState;
}
/**
* Set of URL query parameters that require the rison serialization.
*/
const risonSerializedParams = new Set(['_a', '_g']);
/**
* Checks if the URL query parameter requires rison serialization.
* @param queryParam
*/
export function isRisonSerializationRequired(queryParam: string): boolean {
return risonSerializedParams.has(queryParam);
}
export function parseUrlState(search: string): Dictionary<any> {
const urlState: Dictionary<any> = {};
const parsedQueryString = parse(search, { sort: false });
try {
Object.keys(parsedQueryString).forEach((a) => {
if (isRisonSerializationRequired(a)) {
urlState[a] = decode(parsedQueryString[a] as string);
} else {
urlState[a] = parsedQueryString[a];
}
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('Could not read url state', error);
}
return urlState;
}
// Compared to the original appState/globalState,
// this no longer makes use of fetch/save methods.
// - Reading from `location.search` is the successor of `fetch`.
// - `history.push()` is the successor of `save`.
// - The exposed state and set call make use of the above and make sure that
// different urlStates(e.g. `_a` / `_g`) don't overwrite each other.
// This uses a context to be able to maintain only one instance
// of the url state. It gets passed down with `UrlStateProvider`
// and can be used via `useUrlState`.
export const dataVisualizerUrlStateStore = createContext<UrlState>({
searchString: '',
setUrlState: () => {},
});
export const { Provider } = dataVisualizerUrlStateStore;
export const useUrlState = (accessor: Accessor) => {
const { searchString, setUrlState: setUrlStateContext } = useContext(dataVisualizerUrlStateStore);
const urlState = useMemo(() => {
const fullUrlState = parseUrlState(searchString);
if (typeof fullUrlState === 'object') {
return fullUrlState[accessor];
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchString]);
const setUrlState = useCallback(
(attribute: string | Dictionary<any>, value?: any, replaceState?: boolean) => {
setUrlStateContext(accessor, attribute, value, replaceState);
},
[accessor, setUrlStateContext]
);
return [urlState, setUrlState];
};
export type AppStateKey = 'DATA_VISUALIZER_INDEX_VIEWER';
/**
* Hook for managing the URL state of the page.
*/
export const usePageUrlState = <PageUrlState extends {}>(
pageKey: AppStateKey,
defaultState?: PageUrlState
): [PageUrlState, (update: Partial<PageUrlState>, replaceState?: boolean) => void] => {
const [appState, setAppState] = useUrlState('_a');
const pageState = appState?.[pageKey];
const resultPageState: PageUrlState = useMemo(() => {
return {
...(defaultState ?? {}),
...(pageState ?? {}),
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageState]);
const onStateUpdate = useCallback(
(update: Partial<PageUrlState>, replaceState?: boolean) => {
setAppState(
pageKey,
{
...resultPageState,
...update,
},
replaceState
);
},
[pageKey, resultPageState, setAppState]
);
return useMemo(() => {
return [resultPageState, onStateUpdate];
}, [resultPageState, onStateUpdate]);
};

View file

@ -5,17 +5,18 @@
* 2.0.
*/
import { css } from '@emotion/react';
import { flatten } from 'lodash';
import React, { FC, useState, useEffect } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public';
import { css } from '@emotion/react';
import { flatten } from 'lodash';
import { useUrlState } from '@kbn/ml-url-state';
import { LinkCardProps } from '../../../common/components/link_card/link_card';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { useUrlState } from '../../../common/util/url_state';
import { LinkCard } from '../../../common/components/link_card';
import { GetAdditionalLinks } from '../../../common/components/results_links';
import { isDefined } from '../../../common/util/is_defined';

View file

@ -6,6 +6,8 @@
*/
import React, { FC, useEffect, useMemo, useState, useCallback, useRef } from 'react';
import type { Required } from 'utility-types';
import {
EuiFlexGroup,
EuiFlexItem,
@ -18,15 +20,16 @@ import {
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { Required } from 'utility-types';
import { i18n } from '@kbn/i18n';
import { Filter, FilterStateStore, Query } from '@kbn/es-query';
import { generateFilters } from '@kbn/data-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { usePageUrlState, useUrlState } from '@kbn/ml-url-state';
import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
import { DV_RANDOM_SAMPLER_PREFERENCE, useStorage } from '../../hooks/use_storage';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import { usePageUrlState, useUrlState } from '../../../common/util/url_state';
import {
DataVisualizerTable,
ItemIdToExpandedRowMap,
@ -36,7 +39,10 @@ import type { TotalFieldsStats } from '../../../common/components/stats_table/co
import { OverallStats } from '../../types/overall_stats';
import { IndexBasedDataVisualizerExpandedRow } from '../../../common/components/expanded_row/index_based_expanded_row';
import { DATA_VISUALIZER_INDEX_VIEWER } from '../../constants/index_data_visualizer_viewer';
import { DataVisualizerIndexBasedAppState } from '../../types/index_data_visualizer_state';
import {
DataVisualizerIndexBasedAppState,
DataVisualizerIndexBasedPageUrlState,
} from '../../types/index_data_visualizer_state';
import { SEARCH_QUERY_LANGUAGE, SearchQueryLanguage } from '../../types/combined_query';
import { SupportedFieldType, SavedSearchSavedObject } from '../../../../../common/types';
import { useDataVisualizerKibana } from '../../../kibana_context';
@ -140,10 +146,11 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
const { notifications, uiSettings, data } = services;
const { toasts } = notifications;
const [dataVisualizerListState, setDataVisualizerListState] = usePageUrlState(
DATA_VISUALIZER_INDEX_VIEWER,
restorableDefaults
);
const [dataVisualizerListState, setDataVisualizerListState] =
usePageUrlState<DataVisualizerIndexBasedPageUrlState>(
DATA_VISUALIZER_INDEX_VIEWER,
restorableDefaults
);
const [globalState, setGlobalState] = useUrlState('_g');
const [currentSavedSearch, setCurrentSavedSearch] = useState(

View file

@ -14,6 +14,7 @@ import { type DataViewField, UI_SETTINGS } from '@kbn/data-plugin/common';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import seedrandom from 'seedrandom';
import type { SamplingOption } from '@kbn/discover-plugin/public/application/main/components/field_stats_table/field_stats_table';
import type { Dictionary } from '@kbn/ml-url-state';
import type { RandomSamplerOption } from '../constants/random_sampler';
import type { DataVisualizerIndexBasedAppState } from '../types/index_data_visualizer_state';
import { useDataVisualizerKibana } from '../../kibana_context';
@ -36,7 +37,6 @@ import { getDefaultPageState } from '../components/index_data_visualizer_view/in
import { useFieldStatsSearchStrategy } from './use_field_stats';
import { useOverallStats } from './use_overall_stats';
import type { OverallStatsSearchStrategyParams } from '../../../../common/types/field_stats';
import type { Dictionary } from '../../common/util/url_state';
import type { AggregatableField, NonAggregatableField } from '../types/overall_stats';
const defaults = getDefaultPageState();

View file

@ -9,26 +9,26 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { parse, stringify } from 'query-string';
import { isEqual, throttle } from 'lodash';
import { EuiResizeObserver } from '@elastic/eui';
import { encode } from '@kbn/rison';
import { SimpleSavedObject } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { DataView } from '@kbn/data-views-plugin/public';
import { EuiResizeObserver } from '@elastic/eui';
import { getNestedProperty } from '@kbn/ml-nested-property';
import {
Provider as UrlStateContextProvider,
parseUrlState,
isRisonSerializationRequired,
type Accessor,
type Dictionary,
type SetUrlState,
} from '@kbn/ml-url-state';
import { getCoreStart, getPluginsStart } from '../../kibana_services';
import {
IndexDataVisualizerViewProps,
IndexDataVisualizerView,
} from './components/index_data_visualizer_view';
import {
Accessor,
Provider as UrlStateContextProvider,
Dictionary,
parseUrlState,
SetUrlState,
getNestedProperty,
isRisonSerializationRequired,
} from '../common/util/url_state';
import { useDataVisualizerKibana } from '../kibana_context';
import { GetAdditionalLinks } from '../common/components/results_links';
import { DATA_VISUALIZER_APP_LOCATOR, IndexDataVisualizerLocatorParams } from './locator';

View file

@ -11,7 +11,7 @@ import { Filter, TimeRange } from '@kbn/es-query';
import type { RefreshInterval } from '@kbn/data-plugin/common';
import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common';
import { GlobalQueryStateFromUrl } from '@kbn/data-plugin/public';
import { Dictionary, isRisonSerializationRequired } from '../../common/util/url_state';
import { type Dictionary, isRisonSerializationRequired } from '@kbn/ml-url-state';
import { SearchQueryLanguage } from '../types/combined_query';
export const DATA_VISUALIZER_APP_LOCATOR = 'DATA_VISUALIZER_APP_LOCATOR';

View file

@ -9,6 +9,14 @@ import type { Filter } from '@kbn/es-query';
import type { Query } from '@kbn/data-plugin/common/query';
import type { RandomSamplerOption } from '../constants/random_sampler';
import type { SearchQueryLanguage } from './combined_query';
import type { DATA_VISUALIZER_INDEX_VIEWER } from '../constants/index_data_visualizer_viewer';
export interface DataVisualizerIndexBasedPageUrlState {
pageKey: typeof DATA_VISUALIZER_INDEX_VIEWER;
pageUrlState: Required<DataVisualizerIndexBasedAppState>;
}
export interface ListingPageUrlState {
pageSize: number;
pageIndex: number;
@ -16,6 +24,7 @@ export interface ListingPageUrlState {
sortDirection: string;
queryText?: string;
}
export interface DataVisualizerIndexBasedAppState extends Omit<ListingPageUrlState, 'queryText'> {
searchString?: Query['query'];
searchQuery?: Query['query'];

View file

@ -50,6 +50,8 @@
"@kbn/ml-agg-utils",
"@kbn/test-jest-helpers",
"@kbn/field-types",
"@kbn/ml-nested-property",
"@kbn/ml-url-state",
],
"exclude": [
"target/**/*",

View file

@ -17,6 +17,7 @@ import React, { Component } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiInMemoryTable, EuiText } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { usePageUrlState } from '@kbn/ml-url-state';
import { getColumns } from './anomalies_table_columns';
@ -26,7 +27,6 @@ import { mlTableService } from '../../services/table_service';
import { RuleEditorFlyout } from '../rule_editor';
import { ml } from '../../services/ml_api_service';
import { INFLUENCERS_LIMIT, ANOMALIES_TABLE_TABS, MAX_CHARS } from './anomalies_table_constants';
import { usePageUrlState } from '../../util/url_state';
export class AnomaliesTableInternal extends Component {
constructor(props) {

View file

@ -10,9 +10,9 @@ import React, { FC, useMemo } from 'react';
import { EuiButtonGroup, EuiButtonGroupProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import type { ExplorerJob } from '../../explorer/explorer_utils';
import { useUrlState } from '../../util/url_state';
import { useMlLocator, useNavigateToPath } from '../../contexts/kibana';
import { ML_PAGES } from '../../../../common/constants/locator';

View file

@ -12,7 +12,7 @@ import { mount } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
import { UrlStateProvider } from '../../../util/url_state';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { SelectInterval } from './select_interval';

View file

@ -8,7 +8,12 @@
import React, { FC } from 'react';
import { EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { usePageUrlState } from '../../../util/url_state';
import { usePageUrlState } from '@kbn/ml-url-state';
interface TableIntervalPageUrlState {
pageKey: 'mlSelectInterval';
pageUrlState: TableInterval;
}
export interface TableInterval {
display: string;
@ -55,7 +60,7 @@ function optionValueToInterval(value: string) {
export const TABLE_INTERVAL_DEFAULT = optionValueToInterval('auto');
export const useTableInterval = (): [TableInterval, (v: TableInterval) => void] => {
const [interval, updateCallback] = usePageUrlState<TableInterval>(
const [interval, updateCallback] = usePageUrlState<TableIntervalPageUrlState>(
'mlSelectInterval',
TABLE_INTERVAL_DEFAULT
);

View file

@ -12,7 +12,7 @@ import { mount } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
import { UrlStateProvider } from '../../../util/url_state';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { SelectSeverity } from './select_severity';

View file

@ -9,25 +9,26 @@
* React component for rendering a select element with threshold levels.
*/
import React, { Fragment, FC, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText, EuiSuperSelectProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { usePageUrlState } from '@kbn/ml-url-state';
import { getSeverityColor } from '../../../../../common/util/anomaly_utils';
import { usePageUrlState } from '../../../util/url_state';
import { ANOMALY_THRESHOLD } from '../../../../../common';
const warningLabel = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', {
const warningLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.warningLabel', {
defaultMessage: 'warning',
});
const minorLabel = i18n.translate('xpack.ml.controls.selectSeverity.minorLabel', {
const minorLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.minorLabel', {
defaultMessage: 'minor',
});
const majorLabel = i18n.translate('xpack.ml.controls.selectSeverity.majorLabel', {
const majorLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.majorLabel', {
defaultMessage: 'major',
});
const criticalLabel = i18n.translate('xpack.ml.controls.selectSeverity.criticalLabel', {
const criticalLabel: string = i18n.translate('xpack.ml.controls.selectSeverity.criticalLabel', {
defaultMessage: 'critical',
});
@ -38,6 +39,11 @@ const optionsMap = {
[criticalLabel]: ANOMALY_THRESHOLD.CRITICAL,
};
export interface TableSeverityPageUrlState {
pageKey: 'mlSelectSeverity';
pageUrlState: TableSeverity;
}
export interface TableSeverity {
val: number;
display: string;
@ -82,7 +88,7 @@ export function optionValueToThreshold(value: number) {
const TABLE_SEVERITY_DEFAULT = SEVERITY_OPTIONS[0];
export const useTableSeverity = () => {
return usePageUrlState<TableSeverity>('mlSelectSeverity', TABLE_SEVERITY_DEFAULT);
return usePageUrlState<TableSeverityPageUrlState>('mlSelectSeverity', TABLE_SEVERITY_DEFAULT);
};
export const getSeverityOptions = () =>

View file

@ -15,9 +15,9 @@ import { i18n } from '@kbn/i18n';
import { CoreSetup } from '@kbn/core/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { getNestedProperty } from '@kbn/ml-nested-property';
import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics';
import { extractErrorMessage } from '../../../../common/util/errors';
@ -39,7 +39,6 @@ import {
TOP_CLASSES,
} from '../../data_frame_analytics/common/constants';
import { formatHumanReadableDateTimeSeconds } from '../../../../common/util/date_utils';
import { getNestedProperty } from '../../util/object_utils';
import { mlFieldFormatService } from '../../services/field_format_service';
import { DataGridItem, IndexPagination, RenderCellValue } from './types';

View file

@ -16,10 +16,10 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '@kbn/ml-url-state';
import './_index.scss';
import { Dictionary } from '../../../../common/types/common';
import { useUrlState } from '../../util/url_state';
import { IdBadges } from './id_badges';
import {
BADGE_LIMIT,

View file

@ -9,11 +9,10 @@ import { difference } from 'lodash';
import { useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
import { useUrlState } from '../../util/url_state';
import { useNotifications } from '../../contexts/kibana';
import { useJobSelectionFlyout } from '../../contexts/ml/use_job_selection_flyout';

View file

@ -9,9 +9,9 @@ import { i18n } from '@kbn/i18n';
import type { EuiSideNavItemType } from '@elastic/eui';
import React, { ReactNode, useCallback, useMemo } from 'react';
import { AIOPS_ENABLED, CHANGE_POINT_DETECTION_ENABLED } from '@kbn/aiops-plugin/common';
import { useUrlState } from '@kbn/ml-url-state';
import { NotificationsIndicator } from './notifications_indicator';
import type { MlLocatorParams } from '../../../../common/types/locator';
import { useUrlState } from '../../util/url_state';
import { useMlLocator, useNavigateToPath } from '../../contexts/kibana';
import { isFullLicense } from '../../license';
import type { MlRoute } from '../../routing';

View file

@ -11,7 +11,8 @@ import React from 'react';
import { EuiSuperDatePicker } from '@elastic/eui';
import { useUrlState } from '../../../util/url_state';
import { useUrlState } from '@kbn/ml-url-state';
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
import { useToastNotificationService } from '../../../services/toast_notification_service';
@ -34,7 +35,7 @@ jest.mock('@elastic/eui', () => {
};
});
jest.mock('../../../util/url_state', () => {
jest.mock('@kbn/ml-url-state', () => {
return {
useUrlState: jest.fn(() => {
return [{ refreshInterval: { value: 0, pause: true } }, jest.fn()];

View file

@ -24,8 +24,8 @@ import { TimeHistoryContract } from '@kbn/data-plugin/public';
import { UI_SETTINGS } from '@kbn/data-plugin/common';
import { wrapWithTheme, toMountPoint } from '@kbn/kibana-react-plugin/public';
import { useUrlState } from '@kbn/ml-url-state';
import { mlTimefilterRefresh$ } from '../../../services/timefilter_refresh_service';
import { useUrlState } from '../../../util/url_state';
import { useMlKibana } from '../../../contexts/kibana';
import {
useRefreshIntervalUpdates,

View file

@ -7,10 +7,10 @@
import { useCallback, useEffect, useState } from 'react';
import { LocatorGetUrlParams } from '@kbn/share-plugin/common/url_service';
import { useUrlState } from '@kbn/ml-url-state';
import { useMlKibana } from './kibana_context';
import { ML_APP_LOCATOR } from '../../../../common/constants/locator';
import { MlLocatorParams } from '../../../../common/types/locator';
import { useUrlState } from '../../util/url_state';
export const useMlLocator = () => {
const {

View file

@ -6,7 +6,7 @@
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { usePageUrlState } from '../../../../util/url_state';
import { usePageUrlState } from '@kbn/ml-url-state';
import { ML_PAGES } from '../../../../../../common/constants/locator';
import { ExplorationPageUrlState } from '../../../../../../common/types/locator';
import { SEARCH_QUERY_LANGUAGE } from '../../../../../../common/constants/search';
@ -28,8 +28,13 @@ export function getDefaultExplorationPageUrlState(
};
}
interface UsePageUrlState {
pageKey: typeof ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION;
pageUrlState: ExplorationPageUrlState;
}
export function useExplorationUrlState(overrides?: Partial<ExplorationPageUrlState>) {
return usePageUrlState<ExplorationPageUrlState>(
return usePageUrlState<UsePageUrlState>(
ML_PAGES.DATA_FRAME_ANALYTICS_EXPLORATION,
getDefaultExplorationPageUrlState(overrides)
);

View file

@ -9,6 +9,7 @@ import React, { FC, useState, useEffect } from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '@kbn/ml-url-state';
import { OutlierExploration } from './components/outlier_exploration';
import { RegressionExploration } from './components/regression_exploration';
import { ClassificationExploration } from './components/classification_exploration';
@ -24,7 +25,6 @@ import {
AnalyticsIdSelectorControls,
} from '../components/analytics_selector';
import { AnalyticsEmptyPrompt } from '../analytics_management/components/empty_prompt';
import { useUrlState } from '../../../util/url_state';
import { SavedObjectsWarning } from '../../../components/saved_objects_warning';
export const Page: FC<{

View file

@ -7,11 +7,11 @@
import React, { useCallback, useMemo } from 'react';
import { cloneDeep } from 'lodash';
import { useUrlState } from '@kbn/ml-url-state';
import { useMlLocator, useNavigateToPath } from '../../../../../contexts/kibana';
import { DataFrameAnalyticsListAction, DataFrameAnalyticsListRow } from '../analytics_list/common';
import { ML_PAGES } from '../../../../../../../common/constants/locator';
import { getViewLinkStatus } from '../action_view/get_view_link_status';
import { useUrlState } from '../../../../../util/url_state';
import { mapActionButtonText, MapButton } from './map_button';

View file

@ -7,8 +7,9 @@
import React, { useEffect } from 'react';
import { useUrlState } from '@kbn/ml-url-state';
import { useMlKibana } from '../../../../../contexts/kibana';
import { useUrlState } from '../../../../../util/url_state';
import {
DEFAULT_REFRESH_INTERVAL_MS,

View file

@ -6,7 +6,6 @@
*/
import React, { useState, FC } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiCallOut,
@ -16,14 +15,12 @@ import {
} from '@elastic/eui';
import type { SimpleSavedObject } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { getNestedProperty } from '@kbn/ml-nested-property';
import { SavedObjectFinderUi } from '@kbn/saved-objects-plugin/public';
import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana';
import { useToastNotificationService } from '../../../../../services/toast_notification_service';
import { getNestedProperty } from '../../../../../util/object_utils';
import { getDataViewAndSavedSearch, isCcsIndexPattern } from '../../../../../util/index_utils';
const fixedPageSize: number = 20;

View file

@ -9,14 +9,13 @@ import React, { FC, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '../../../util/url_state';
import { useUrlState, usePageUrlState } from '@kbn/ml-url-state';
import { DataFrameAnalyticsList } from './components/analytics_list';
import { useRefreshInterval } from './components/analytics_list/use_refresh_interval';
import { NodeAvailableWarning } from '../../../components/node_available_warning';
import { SavedObjectsWarning } from '../../../components/saved_objects_warning';
import { UpgradeWarning } from '../../../components/upgrade';
import { JobMap } from '../job_map';
import { usePageUrlState } from '../../../util/url_state';
import { ListingPageUrlState } from '../../../../../common/types/common';
import { DataFrameAnalyticsListColumn } from './components/analytics_list/common';
import { ML_PAGES } from '../../../../../common/constants/locator';
@ -25,6 +24,11 @@ import { useMlKibana } from '../../../contexts/kibana';
import { useRefreshAnalyticsList } from '../../common';
import { MlPageHeader } from '../../../components/page_header';
interface PageUrlState {
pageKey: typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE;
pageUrlState: ListingPageUrlState;
}
export const getDefaultDFAListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 10,
@ -36,7 +40,7 @@ export const Page: FC = () => {
const [blockRefresh, setBlockRefresh] = useState(false);
const [globalState] = useUrlState('_g');
const [dfaPageState, setDfaPageState] = usePageUrlState(
const [dfaPageState, setDfaPageState] = usePageUrlState<PageUrlState>(
ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE,
getDefaultDFAListState()
);

View file

@ -9,7 +9,7 @@ import React, { FC, useState, useEffect, useCallback } from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '../../../util/url_state';
import { useUrlState } from '@kbn/ml-url-state';
import { NodeAvailableWarning } from '../../../components/node_available_warning';
import { SavedObjectsWarning } from '../../../components/saved_objects_warning';
import { UpgradeWarning } from '../../../components/upgrade';

View file

@ -7,6 +7,7 @@
import { BehaviorSubject, combineLatest, Observable, of, Subscription } from 'rxjs';
import { distinctUntilChanged, map, skipWhile, switchMap } from 'rxjs/operators';
import type { PageUrlStateService } from '@kbn/ml-url-state';
import { StateService } from '../services/state_service';
import type { AnomalyExplorerCommonStateService } from './anomaly_explorer_common_state';
import type { AnomalyTimelineStateService } from './anomaly_timeline_state_service';
@ -16,7 +17,6 @@ import {
} from './explorer_charts/explorer_charts_container_service';
import { AnomalyExplorerChartsService } from '../services/anomaly_explorer_charts_service';
import { getSelectionInfluencers, getSelectionJobIds } from './explorer_utils';
import type { PageUrlStateService } from '../util/url_state';
import type { TableSeverity } from '../components/controls/select_severity/select_severity';
import { AnomalyExplorerUrlStateService } from './hooks/use_explorer_url_state';

View file

@ -5,21 +5,30 @@
* 2.0.
*/
import { PageUrlStateService, usePageUrlState } from '../../util/url_state';
import { PageUrlStateService, usePageUrlState } from '@kbn/ml-url-state';
import { ExplorerAppState } from '../../../../common/types/locator';
import { ML_PAGES } from '../../../../common/constants/locator';
export type AnomalyExplorerUrlStateService = PageUrlStateService<ExplorerAppState>;
interface LegacyExplorerPageUrlState {
pageKey: 'mlExplorerSwimlane';
pageUrlState: ExplorerAppState['mlExplorerSwimlane'];
}
interface ExplorerPageUrlState {
pageKey: typeof ML_PAGES.ANOMALY_EXPLORER;
pageUrlState: ExplorerAppState;
}
export function useExplorerUrlState() {
/**
* Originally `mlExplorerSwimlane` resided directly in the app URL state (`_a` URL state key).
* With current URL structure it has been moved under the `explorer` key of the app state (_a).
*/
const [legacyExplorerState] =
usePageUrlState<ExplorerAppState['mlExplorerSwimlane']>('mlExplorerSwimlane');
const [legacyExplorerState] = usePageUrlState<LegacyExplorerPageUrlState>('mlExplorerSwimlane');
return usePageUrlState<ExplorerAppState>(ML_PAGES.ANOMALY_EXPLORER, {
return usePageUrlState<ExplorerPageUrlState>(ML_PAGES.ANOMALY_EXPLORER, {
mlExplorerSwimlane: legacyExplorerState,
mlExplorerFilter: {},
});

View file

@ -7,9 +7,8 @@
import React, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
// @ts-ignore
import { usePageUrlState } from '@kbn/ml-url-state';
import { JobsListView } from './components/jobs_list_view';
import { usePageUrlState } from '../../util/url_state';
import { ML_PAGES } from '../../../../common/constants/locator';
import { ListingPageUrlState } from '../../../../common/types/common';
import { HelpMenu } from '../../components/help_menu';
@ -18,6 +17,11 @@ import { MlPageHeader } from '../../components/page_header';
import { HeaderMenuPortal } from '../../components/header_menu_portal';
import { JobsActionMenu } from '../components/jobs_action_menu';
interface PageUrlState {
pageKey: typeof ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE;
pageUrlState: ListingPageUrlState;
}
interface JobsPageProps {
isMlEnabledInSpace?: boolean;
lastRefresh?: number;
@ -31,7 +35,7 @@ export const getDefaultAnomalyDetectionJobsListState = (): ListingPageUrlState =
});
export const JobsPage: FC<JobsPageProps> = ({ isMlEnabledInSpace, lastRefresh }) => {
const [pageState, setPageState] = usePageUrlState(
const [pageState, setPageState] = usePageUrlState<PageUrlState>(
ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE,
getDefaultAnomalyDetectionJobsListState()
);

View file

@ -23,6 +23,7 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import useDebounce from 'react-use/lib/useDebounce';
import useMount from 'react-use/lib/useMount';
import { usePageUrlState } from '@kbn/ml-url-state';
import { EntityFilter } from './entity_filter';
import { useMlNotifications } from '../../contexts/ml/ml_notifications_context';
import { ML_NOTIFICATIONS_MESSAGE_LEVEL } from '../../../../common/constants/notifications';
@ -33,7 +34,6 @@ import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
import { useRefresh } from '../../routing/use_refresh';
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
import { ListingPageUrlState } from '../../../../common/types/common';
import { usePageUrlState } from '../../util/url_state';
import { ML_PAGES } from '../../../../common/constants/locator';
import type {
MlNotificationMessageLevel,
@ -47,6 +47,11 @@ const levelBadgeMap: Record<MlNotificationMessageLevel, IconColor> = {
[ML_NOTIFICATIONS_MESSAGE_LEVEL.INFO]: 'default',
};
interface PageUrlState {
pageKey: typeof ML_PAGES.NOTIFICATIONS;
pageUrlState: ListingPageUrlState;
}
export const getDefaultNotificationsListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 25,
@ -81,7 +86,7 @@ export const NotificationsList: FC = () => {
const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE);
const [pageState, updatePageState] = usePageUrlState(
const [pageState, updatePageState] = usePageUrlState<PageUrlState>(
ML_PAGES.NOTIFICATIONS,
getDefaultNotificationsListState()
);

View file

@ -18,9 +18,9 @@ import type {
import type { DataViewsContract } from '@kbn/data-views-plugin/public';
import { EuiLoadingContent } from '@elastic/eui';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { MlNotificationsContextProvider } from '../contexts/ml/ml_notifications_context';
import { MlContext, MlContextValue } from '../contexts/ml';
import { UrlStateProvider } from '../util/url_state';
import { MlPage } from '../components/ml_page';

View file

@ -9,6 +9,7 @@ import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import { NavigateToPath } from '../../../contexts/kibana';
import { MlRoute, PageLoader, PageProps } from '../../router';
@ -17,7 +18,6 @@ import { basicResolvers } from '../../resolvers';
import { Page } from '../../../data_frame_analytics/pages/analytics_exploration';
import { getBreadcrumbWithUrlForApp } from '../../breadcrumbs';
import { DataFrameAnalysisConfigType } from '../../../../../common/types/data_frame_analytics';
import { useUrlState } from '../../../util/url_state';
export const analyticsJobExplorationRouteFactory = (
navigateToPath: NavigateToPath,

View file

@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiThemeProvider as StyledComponentsThemeProvider } from '@kbn/kibana-react-plugin/common';
import { useUrlState } from '@kbn/ml-url-state';
import { NavigateToPath, useMlKibana, useTimefilter } from '../../contexts/kibana';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
@ -30,7 +31,6 @@ import { getDateFormatTz } from '../../explorer/explorer_utils';
import { useJobSelection } from '../../components/job_selector/use_job_selection';
import { useTableInterval } from '../../components/controls/select_interval';
import { useTableSeverity } from '../../components/controls/select_severity';
import { useUrlState } from '../../util/url_state';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { JOB_ID } from '../../../../common/constants/anomalies';
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';

View file

@ -43,7 +43,16 @@ const MockedTimeseriesexplorerNoJobsFound = TimeseriesexplorerNoJobsFound as jes
typeof TimeseriesexplorerNoJobsFound
>;
jest.mock('../../util/url_state');
jest.mock('@kbn/ml-url-state', () => {
return {
usePageUrlState: jest.fn(() => {
return [{}, jest.fn(), {}];
}),
useUrlState: jest.fn(() => {
return [{ refreshInterval: { value: 0, pause: true } }, jest.fn()];
}),
};
});
jest.mock('../../timeseriesexplorer/hooks/use_timeseriesexplorer_url_state');

View file

@ -12,6 +12,7 @@ import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { useUrlState } from '@kbn/ml-url-state';
import { getViewableDetectors } from '../../timeseriesexplorer/timeseriesexplorer_utils/get_viewable_detectors';
import { NavigateToPath, useNotifications } from '../../contexts/kibana';
import { useMlContext } from '../../contexts/ml';
@ -31,7 +32,6 @@ import {
} from '../../timeseriesexplorer/timeseriesexplorer_utils';
import { TimeSeriesExplorerPage } from '../../timeseriesexplorer/timeseriesexplorer_page';
import { TimeseriesexplorerNoJobsFound } from '../../timeseriesexplorer/components/timeseriesexplorer_no_jobs_found';
import { useUrlState } from '../../util/url_state';
import { useTableInterval } from '../../components/controls/select_interval';
import { useTableSeverity } from '../../components/controls/select_severity';

View file

@ -5,10 +5,15 @@
* 2.0.
*/
import { usePageUrlState } from '../../util/url_state';
import { usePageUrlState } from '@kbn/ml-url-state';
import { TimeSeriesExplorerAppState } from '../../../../common/types/locator';
import { ML_PAGES } from '../../../../common/constants/locator';
export function useTimeSeriesExplorerUrlState() {
return usePageUrlState<TimeSeriesExplorerAppState>(ML_PAGES.SINGLE_METRIC_VIEWER);
interface TimeSeriesExplorerPageUrlState {
pageKey: typeof ML_PAGES.SINGLE_METRIC_VIEWER;
pageUrlState: TimeSeriesExplorerAppState;
}
export function useTimeSeriesExplorerUrlState() {
return usePageUrlState<TimeSeriesExplorerPageUrlState>(ML_PAGES.SINGLE_METRIC_VIEWER);
}

View file

@ -25,6 +25,7 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas
import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { usePageUrlState } from '@kbn/ml-url-state';
import { useModelActions } from './model_actions';
import { ModelsTableToConfigMapping } from '.';
import { ModelsBarStats, StatsBar } from '../../components/stats_bar';
@ -39,7 +40,6 @@ import { BUILT_IN_MODEL_TAG } from '../../../../common/constants/data_frame_anal
import { DeleteModelsModal } from './delete_models_modal';
import { ML_PAGES } from '../../../../common/constants/locator';
import { ListingPageUrlState } from '../../../../common/types/common';
import { usePageUrlState } from '../../util/url_state';
import { ExpandedRow } from './expanded_row';
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
import { useToastNotificationService } from '../../services/toast_notification_service';
@ -59,6 +59,11 @@ export type ModelItem = TrainedModelConfigResponse & {
export type ModelItemFull = Required<ModelItem>;
interface PageUrlState {
pageKey: typeof ML_PAGES.TRAINED_MODELS_MANAGE;
pageUrlState: ListingPageUrlState;
}
export const getDefaultModelsListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 10,
@ -88,7 +93,7 @@ export const ModelsList: FC<Props> = ({
// allow for an internally controlled page state which stores the state in the URL
// or an external page state, which is passed in as a prop.
// external page state is used on the management page.
const [pageStateInternal, updatePageStateInternal] = usePageUrlState(
const [pageStateInternal, updatePageStateInternal] = usePageUrlState<PageUrlState>(
ML_PAGES.TRAINED_MODELS_MANAGE,
getDefaultModelsListState()
);

View file

@ -17,9 +17,9 @@ import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/bas
import { i18n } from '@kbn/i18n';
import { cloneDeep } from 'lodash';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { usePageUrlState } from '@kbn/ml-url-state';
import { ModelsBarStats, StatsBar } from '../../components/stats_bar';
import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models';
import { usePageUrlState } from '../../util/url_state';
import { ML_PAGES } from '../../../../common/constants/locator';
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
@ -32,6 +32,11 @@ import { useRefresh } from '../../routing/use_refresh';
export type NodeItem = NodeDeploymentStatsResponse;
interface PageUrlState {
pageKey: typeof ML_PAGES.TRAINED_MODELS_NODES;
pageUrlState: ListingPageUrlState;
}
export const getDefaultNodesListState = (): ListingPageUrlState => ({
pageIndex: 0,
pageSize: 10,
@ -55,7 +60,7 @@ export const NodesList: FC<NodesListProps> = ({ compactView = false }) => {
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
{}
);
const [pageState, updatePageState] = usePageUrlState(
const [pageState, updatePageState] = usePageUrlState<PageUrlState>(
ML_PAGES.TRAINED_MODELS_NODES,
getDefaultNodesListState()
);

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { AppStateKey } from '../url_state';
import { TABLE_INTERVAL_DEFAULT } from '../../components/controls/select_interval/select_interval';
export const useUrlState = jest.fn((accessor: '_a' | '_g') => {
@ -14,7 +13,7 @@ export const useUrlState = jest.fn((accessor: '_a' | '_g') => {
}
});
export const usePageUrlState = jest.fn((pageKey: AppStateKey) => {
export const usePageUrlState = jest.fn((pageKey: string) => {
let state: unknown;
switch (pageKey) {
case 'timeseriesexplorer':

View file

@ -1,71 +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 { getNestedProperty } from './object_utils';
describe('object_utils', () => {
test('getNestedProperty()', () => {
const testObj = {
the: {
nested: {
value: 'the-nested-value',
},
},
};
const falseyObj = {
the: {
nested: {
value: false,
},
other_nested: {
value: 0,
},
},
};
const test1 = getNestedProperty(testObj, 'the');
expect(typeof test1).toBe('object');
expect(Object.keys(test1)).toStrictEqual(['nested']);
const test2 = getNestedProperty(testObj, 'the$');
expect(typeof test2).toBe('undefined');
const test3 = getNestedProperty(testObj, 'the$', 'the-default-value');
expect(typeof test3).toBe('string');
expect(test3).toBe('the-default-value');
const test4 = getNestedProperty(testObj, 'the.neSted');
expect(typeof test4).toBe('undefined');
const test5 = getNestedProperty(testObj, 'the.nested');
expect(typeof test5).toBe('object');
expect(Object.keys(test5)).toStrictEqual(['value']);
const test6 = getNestedProperty(testObj, 'the.nested.vaLue');
expect(typeof test6).toBe('undefined');
const test7 = getNestedProperty(testObj, 'the.nested.value');
expect(typeof test7).toBe('string');
expect(test7).toBe('the-nested-value');
const test8 = getNestedProperty(testObj, 'the.nested.value.doesntExist');
expect(typeof test8).toBe('undefined');
const test9 = getNestedProperty(testObj, 'the.nested.value.doesntExist', 'the-default-value');
expect(typeof test9).toBe('string');
expect(test9).toBe('the-default-value');
const test10 = getNestedProperty(falseyObj, 'the.nested.value');
expect(typeof test10).toBe('boolean');
expect(test10).toBe(false);
const test11 = getNestedProperty(falseyObj, 'the.other_nested.value');
expect(typeof test11).toBe('number');
expect(test11).toBe(0);
});
});

View file

@ -1,20 +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.
*/
// This is similar to lodash's get() except that it's TypeScript aware and is able to infer return types.
// It splits the attribute key string and uses reduce with an idx check to access nested attributes.
export const getNestedProperty = (
obj: Record<string, any>,
accessor: string,
defaultValue?: any
) => {
const value = accessor.split('.').reduce((o, i) => o?.[i], obj);
if (value === undefined) return defaultValue;
return value;
};

View file

@ -66,6 +66,8 @@
"@kbn/task-manager-plugin",
"@kbn/config-schema",
"@kbn/repo-info",
"@kbn/ml-url-state",
"@kbn/ml-nested-property",
],
"exclude": [
"target/**/*",

View file

@ -8,8 +8,8 @@
import { EuiComboBoxOptionOption } from '@elastic/eui';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { DataView } from '@kbn/data-views-plugin/public';
import { getNestedProperty } from '@kbn/ml-nested-property';
import { getNestedProperty } from '../../../../../../../common/utils/object_utils';
import { removeKeywordPostfix } from '../../../../../../../common/utils/field_utils';
import { isRuntimeMappings } from '../../../../../../../common/shared_imports';

View file

@ -5,13 +5,12 @@
* 2.0.
*/
import { isEqual } from 'lodash';
import { merge } from 'lodash';
import { numberValidator } from '@kbn/ml-agg-utils';
import { isEqual, merge } from 'lodash';
import { useReducer } from 'react';
import { i18n } from '@kbn/i18n';
import { numberValidator } from '@kbn/ml-agg-utils';
import { getNestedProperty, setNestedProperty } from '@kbn/ml-nested-property';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms';
@ -20,7 +19,6 @@ import {
DEFAULT_TRANSFORM_SETTINGS_MAX_PAGE_SEARCH_SIZE,
} from '../../../../../../common/constants';
import { TransformConfigUnion } from '../../../../../../common/types/transform';
import { getNestedProperty, setNestedProperty } from '../../../../../../common/utils/object_utils';
import {
isValidFrequency,

View file

@ -44,6 +44,7 @@
"@kbn/ml-string-hash",
"@kbn/ui-theme",
"@kbn/field-types",
"@kbn/ml-nested-property",
],
"exclude": [
"target/**/*",

View file

@ -3741,7 +3741,7 @@
version "0.0.0"
uid ""
"@kbn/ml-is-populated-object@link:x-pack/packages/ml/is_populated_object":
"@kbn/ml-nested-property@link:x-pack/packages/ml/nested_property":
version "0.0.0"
uid ""
@ -3749,6 +3749,14 @@
version "0.0.0"
uid ""
"@kbn/ml-url-state@link:x-pack/packages/ml/url_state":
version "0.0.0"
uid ""
"@kbn/ml-is-populated-object@link:x-pack/packages/ml/is_populated_object":
version "0.0.0"
uid ""
"@kbn/monaco@link:packages/kbn-monaco":
version "0.0.0"
uid ""