[ML] Move local storage utilities to package. (#148049)

Moves multiple copies of `useStorage()` and related code to a package as
a single source. The different copies with hard coded types have been
adapted so `useStorage()` is now based on generics. Also moves
duplicates of `isDefined()` to its own package.
This commit is contained in:
Walter Rafelsberger 2023-01-05 11:05:07 +01:00 committed by GitHub
parent 9a0e692eb8
commit dc1ae9e06c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 596 additions and 376 deletions

2
.github/CODEOWNERS vendored
View file

@ -1072,7 +1072,9 @@ packages/shared-ux/storybook/mock @elastic/kibana-global-experience
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_defined @elastic/ml-ui
x-pack/packages/ml/is_populated_object @elastic/ml-ui
x-pack/packages/ml/local_storage @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/logging-mocks": "link:packages/kbn-logging-mocks",
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
"@kbn/ml-agg-utils": "link:x-pack/packages/ml/agg_utils",
"@kbn/ml-is-defined": "link:x-pack/packages/ml/is_defined",
"@kbn/ml-is-populated-object": "link:x-pack/packages/ml/is_populated_object",
"@kbn/ml-local-storage": "link:x-pack/packages/ml/local_storage",
"@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",

View file

@ -816,8 +816,12 @@
"@kbn/maps-plugin/*": ["x-pack/plugins/maps/*"],
"@kbn/ml-agg-utils": ["x-pack/packages/ml/agg_utils"],
"@kbn/ml-agg-utils/*": ["x-pack/packages/ml/agg_utils/*"],
"@kbn/ml-is-defined": ["x-pack/packages/ml/is_defined"],
"@kbn/ml-is-defined/*": ["x-pack/packages/ml/is_defined/*"],
"@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-local-storage": ["x-pack/packages/ml/local_storage"],
"@kbn/ml-local-storage/*": ["x-pack/packages/ml/local_storage/*"],
"@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"],

View file

@ -0,0 +1,3 @@
# @kbn/ml-is-defined
Utility function to determine if a value is not `undefined` and not `null`.

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { MlStorageContextProvider, useStorage } from './storage_context';
export { isDefined } from './src/is_defined';

View file

@ -5,6 +5,8 @@
* 2.0.
*/
export function isDefined<T>(argument: T | undefined | null): argument is T {
return argument !== undefined && argument !== null;
}
module.exports = {
preset: '@kbn/test',
rootDir: '../../../..',
roots: ['<rootDir>/x-pack/packages/ml/is_defined'],
};

View file

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

View file

@ -0,0 +1,6 @@
{
"name": "@kbn/ml-is-defined",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}

View file

@ -5,6 +5,12 @@
* 2.0.
*/
/**
* Checks whether the supplied argument is not `undefined` and not `null`.
*
* @param argument
* @returns boolean
*/
export function isDefined<T>(argument: T | undefined | null): argument is T {
return argument !== undefined && argument !== null;
}

View file

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

View file

@ -0,0 +1,3 @@
# @kbn/ml-local-storage
Utilities to combine url state management with local storage.

View file

@ -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 { StorageContextProvider, useStorage } from './src/storage_context';

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/local_storage'],
};

View file

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

View file

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

View file

@ -0,0 +1,193 @@
/*
* 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, {
type PropsWithChildren,
useEffect,
useMemo,
useCallback,
useState,
useContext,
} from 'react';
import { omit } from 'lodash';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
/**
* StorageDefinition is a dictionary with `string` based keys.
*/
interface StorageDefinition {
[key: string]: unknown;
}
/**
* TStorage, a partial `StorageDefinition` or `null`.
*/
type TStorage = Partial<StorageDefinition> | null;
/**
* TStorageKey, keys of StorageDefintion.
*/
type TStorageKey = keyof Exclude<TStorage, null>;
/**
* TStorageMapped, mapping of TStorage with TStorageKey.
*/
type TStorageMapped<T extends TStorageKey> = T extends string ? unknown : null;
/**
* StorageAPI definition of store TStorage with accessors.
*/
interface StorageAPI {
value: TStorage;
setValue: <K extends TStorageKey, T extends TStorageMapped<K>>(key: K, value: T) => void;
removeValue: <K extends TStorageKey>(key: K) => void;
}
/**
* Type guard to check if a supplied `key` is in `storageKey`.
*
* @param key
* @param storageKeys
* @returns boolean
*/
export function isStorageKey<T>(key: unknown, storageKeys: readonly T[]): key is T {
return storageKeys.includes(key as T);
}
/**
* React context to hold storage API.
*/
export const MlStorageContext = React.createContext<StorageAPI>({
value: null,
setValue() {
throw new Error('MlStorageContext set method is not implemented');
},
removeValue() {
throw new Error('MlStorageContext remove method is not implemented');
},
});
/**
* Props for StorageContextProvider
*/
interface StorageContextProviderProps<K extends TStorageKey> {
storage: Storage;
storageKeys: readonly K[];
}
/**
* Provider to manage context for the `useStorage` hook.
*/
export function StorageContextProvider<K extends TStorageKey, T extends TStorage>({
children,
storage,
storageKeys,
}: PropsWithChildren<StorageContextProviderProps<K>>) {
const initialValue = useMemo(() => {
return storageKeys.reduce((acc, curr) => {
acc[curr as K] = storage.get(curr as string);
return acc;
}, {} as Exclude<T, null>);
}, [storage, storageKeys]);
const [state, setState] = useState<T>(initialValue);
const setStorageValue = useCallback(
<TM extends TStorageMapped<K>>(key: K, value: TM) => {
storage.set(key as string, value);
setState((prevState) => ({
...prevState,
[key]: value,
}));
},
[storage]
);
const removeStorageValue = useCallback(
(key: K) => {
storage.remove(key as string);
setState((prevState) => omit(prevState, key) as T);
},
[storage]
);
useEffect(
function updateStorageOnExternalChange() {
const eventListener = (event: StorageEvent) => {
if (!isStorageKey(event.key, storageKeys)) return;
if (isDefined(event.newValue)) {
setState((prev) => {
return {
...prev,
[event.key as K]:
typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue,
};
});
} else {
setState((prev) => omit(prev, event.key as K) as T);
}
};
/**
* This event listener is only invoked when
* the change happens in another browser's tab.
*/
window.addEventListener('storage', eventListener);
return () => {
window.removeEventListener('storage', eventListener);
};
},
[storageKeys]
);
const value = useMemo(() => {
return {
value: state,
setValue: setStorageValue,
removeValue: removeStorageValue,
} as StorageAPI;
}, [state, setStorageValue, removeStorageValue]);
return <MlStorageContext.Provider value={value}>{children}</MlStorageContext.Provider>;
}
/**
* Hook for consuming a storage value
* @param key
* @param initValue
*/
export function useStorage<K extends TStorageKey, T extends TStorageMapped<K>>(
key: K,
initValue?: T
): [
typeof initValue extends undefined ? T | undefined : Exclude<T, undefined>,
(value: T) => void
] {
const { value, setValue, removeValue } = useContext(MlStorageContext);
const resultValue = useMemo(() => {
return (value?.[key] ?? initValue) as typeof initValue extends undefined
? T | undefined
: Exclude<T, undefined>;
}, [value, key, initValue]);
const setVal = useCallback(
(v: T) => {
if (isDefined(v)) {
setValue(key, v);
} else {
removeValue(key);
}
},
[setValue, removeValue, key]
);
return [resultValue, setVal];
}

View file

@ -0,0 +1,22 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*",
],
"kbn_references": [
"@kbn/kibana-utils-plugin",
"@kbn/ml-is-defined",
]
}

View file

@ -16,6 +16,6 @@
"licensing"
],
"optionalPlugins": [],
"requiredBundles": ["fieldFormats", "kibanaReact"],
"requiredBundles": ["fieldFormats", "kibanaReact", "kibanaUtils"],
"extraPublicDirs": ["common"]
}

View file

@ -5,16 +5,25 @@
* 2.0.
*/
import React, { FC } from 'react';
import { DataView } from '@kbn/data-views-plugin/common';
import { SavedSearch } from '@kbn/saved-search-plugin/public';
import React, { FC } from 'react';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { PageHeader } from '../page_header';
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { DataSourceContext } from '../../hooks/use_data_source';
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import { AiopsAppContext, AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { PageHeader } from '../page_header';
import { ChangePointDetectionPage } from './change_point_detection_page';
import { ChangePointDetectionContextProvider } from './change_point_detection_context';
const localStorage = new Storage(window.localStorage);
export interface ChangePointDetectionAppStateProps {
dataView: DataView;
@ -31,10 +40,12 @@ export const ChangePointDetectionAppState: FC<ChangePointDetectionAppStateProps>
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<DataSourceContext.Provider value={{ dataView, savedSearch }}>
<PageHeader />
<ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionContextProvider>
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<PageHeader />
<ChangePointDetectionContextProvider>
<ChangePointDetectionPage />
</ChangePointDetectionContextProvider>
</StorageContextProvider>
</DataSourceContext.Provider>
</UrlStateProvider>
</AiopsAppContext.Provider>

View file

@ -11,11 +11,12 @@ import { EuiCallOut } from '@elastic/eui';
import type { Filter, Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import {
SEARCH_QUERY_LANGUAGE,
SearchQueryLanguage,
@ -23,11 +24,14 @@ import {
} from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { SpikeAnalysisTableRowStateProvider } from '../spike_analysis_table/spike_analysis_table_row_provider';
import { ExplainLogRateSpikesPage } from './explain_log_rate_spikes_page';
const localStorage = new Storage(window.localStorage);
export interface ExplainLogRateSpikesAppStateProps {
/** The data view to analyze. */
dataView: DataView;
@ -95,7 +99,9 @@ export const ExplainLogRateSpikesAppState: FC<ExplainLogRateSpikesAppStateProps>
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<SpikeAnalysisTableRowStateProvider>
<ExplainLogRateSpikesPage dataView={dataView} savedSearch={savedSearch} />
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<ExplainLogRateSpikesPage dataView={dataView} savedSearch={savedSearch} />
</StorageContextProvider>
</SpikeAnalysisTableRowStateProvider>
</UrlStateProvider>
</AiopsAppContext.Provider>

View file

@ -25,12 +25,19 @@ import {
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useStorage } from '@kbn/ml-local-storage';
import { useAiopsAppContext } from '../../hooks/use_aiops_app_context';
import {
type GetTimeFieldRangeResponse,
setFullTimeRange,
} from './full_time_range_selector_service';
import { AIOPS_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage';
import {
AIOPS_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type AiOpsKey,
type AiOpsStorageMapped,
type FrozenTierPreference,
} from '../../types/storage';
export interface FullTimeRangeSelectorProps {
timefilter: TimefilterContract;
@ -40,13 +47,6 @@ export interface FullTimeRangeSelectorProps {
callback?: (a: GetTimeFieldRangeResponse) => void;
}
const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
timefilter,
dataView,
@ -90,7 +90,10 @@ export const FullTimeRangeSelector: FC<FullTimeRangeSelectorProps> = ({
const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<FrozenTierPreference>(
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
AiOpsKey,
AiOpsStorageMapped<typeof AIOPS_FROZEN_TIER_PREFERENCE>
>(
AIOPS_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE

View file

@ -7,12 +7,19 @@
import React, { FC } from 'react';
import type { SavedSearch } from '@kbn/discover-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { UrlStateProvider } from '@kbn/ml-url-state';
import { LogCategorizationPage } from './log_categorization_page';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { SavedSearchSavedObject } from '../../application/utils/search_utils';
import type { AiopsAppDependencies } from '../../hooks/use_aiops_app_context';
import { AIOPS_STORAGE_KEYS } from '../../types/storage';
import { AiopsAppContext } from '../../hooks/use_aiops_app_context';
import { LogCategorizationPage } from './log_categorization_page';
const localStorage = new Storage(window.localStorage);
export interface LogCategorizationAppStateProps {
dataView: DataView;
savedSearch: SavedSearch | SavedSearchSavedObject | null;
@ -27,7 +34,9 @@ export const LogCategorizationAppState: FC<LogCategorizationAppStateProps> = ({
return (
<AiopsAppContext.Provider value={appDependencies}>
<UrlStateProvider>
<LogCategorizationPage dataView={dataView} savedSearch={savedSearch} />
<StorageContextProvider storage={localStorage} storageKeys={AIOPS_STORAGE_KEYS}>
<LogCategorizationPage dataView={dataView} savedSearch={savedSearch} />
</StorageContextProvider>
</UrlStateProvider>
</AiopsAppContext.Provider>
);

View file

@ -1,42 +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 { useCallback, useState } from 'react';
import { useAiopsAppContext } from './use_aiops_app_context';
export const AIOPS_FROZEN_TIER_PREFERENCE = 'aiops.frozenDataTierPreference';
export type AiOps = Partial<{
[AIOPS_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen';
}> | null;
export type AiOpsKey = keyof Exclude<AiOps, null>;
/**
* Hook for accessing and changing a value in the storage.
* @param key - Storage key
* @param initValue
*/
export function useStorage<T>(key: AiOpsKey, initValue?: T): [T, (value: T) => void] {
const { storage } = useAiopsAppContext();
const [val, setVal] = useState<T>(storage.get(key) ?? initValue);
const setStorage = useCallback(
(value: T): void => {
try {
storage.set(key, value);
setVal(value);
} catch (e) {
throw new Error('Unable to update storage with provided value');
}
},
[key, storage]
);
return [val, setStorage];
}

View file

@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export const AIOPS_FROZEN_TIER_PREFERENCE = 'aiops.frozenDataTierPreference';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type AiOps = Partial<{
[AIOPS_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
}> | null;
export type AiOpsKey = keyof Exclude<AiOps, null>;
export type AiOpsStorageMapped<T extends AiOpsKey> = T extends typeof AIOPS_FROZEN_TIER_PREFERENCE
? FrozenTierPreference | undefined
: null;
export const AIOPS_STORAGE_KEYS = [AIOPS_FROZEN_TIER_PREFERENCE] as const;

View file

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

View file

@ -26,6 +26,7 @@
],
"requiredBundles": [
"kibanaReact",
"kibanaUtils",
"maps",
"esUiShared",
"fieldFormats",

View file

@ -22,7 +22,7 @@ import {
import { i18n } from '@kbn/i18n';
import { debounce, sortedIndex } from 'lodash';
import { FormattedMessage } from '@kbn/i18n-react';
import { isDefined } from '../../util/is_defined';
import { isDefined } from '@kbn/ml-is-defined';
import type { DocumentCountChartPoint } from './document_count_chart';
import {
RANDOM_SAMPLER_STEP,

View file

@ -14,9 +14,9 @@ import { RefreshInterval } from '@kbn/data-plugin/public';
import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public';
import { flatten } from 'lodash';
import { isDefined } from '@kbn/ml-is-defined';
import { LinkCardProps } from '../link_card/link_card';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { isDefined } from '../../util/is_defined';
type LinkType = 'file' | 'index';

View file

@ -6,7 +6,7 @@
*/
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from './is_defined';
import { isDefined } from '@kbn/ml-is-defined';
import { GeoPointExample, LatLongExample } from '../../../../common/types/field_request_config';
export function isGeoPointExample(arg: unknown): arg is GeoPointExample {

View file

@ -14,12 +14,12 @@ import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public';
import { useUrlState } from '@kbn/ml-url-state';
import { isDefined } from '@kbn/ml-is-defined';
import { LinkCardProps } from '../../../common/components/link_card/link_card';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { LinkCard } from '../../../common/components/link_card';
import { GetAdditionalLinks } from '../../../common/components/results_links';
import { isDefined } from '../../../common/util/is_defined';
interface Props {
dataView: DataView;

View file

@ -23,9 +23,16 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { useStorage } from '@kbn/ml-local-storage';
import { setFullTimeRange } from './full_time_range_selector_service';
import { useDataVisualizerKibana } from '../../../kibana_context';
import { DV_FROZEN_TIER_PREFERENCE, useStorage } from '../../hooks/use_storage';
import {
DV_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type DVKey,
type DVStorageMapped,
type FrozenTierPreference,
} from '../../types/storage';
export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
@ -37,13 +44,6 @@ interface Props {
callback?: (a: any) => void;
}
const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
// Component for rendering a button which automatically sets the range of the time filter
// to the time range of data in the index(es) mapped to the supplied Kibana data view or query.
export const FullTimeRangeSelector: FC<Props> = ({
@ -83,7 +83,10 @@ export const FullTimeRangeSelector: FC<Props> = ({
const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<FrozenTierPreference>(
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
DVKey,
DVStorageMapped<typeof DV_FROZEN_TIER_PREFERENCE>
>(
DV_FROZEN_TIER_PREFERENCE,
// By default we will exclude frozen data tier
FROZEN_TIER_PREFERENCE.EXCLUDE

View file

@ -27,8 +27,13 @@ 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 { useStorage } from '@kbn/ml-local-storage';
import { useCurrentEuiTheme } from '../../../common/hooks/use_current_eui_theme';
import { DV_RANDOM_SAMPLER_PREFERENCE, useStorage } from '../../hooks/use_storage';
import {
DV_RANDOM_SAMPLER_PREFERENCE,
type DVKey,
type DVStorageMapped,
} from '../../types/storage';
import { FullTimeRangeSelector } from '../full_time_range_selector';
import {
DataVisualizerTable,
@ -58,7 +63,7 @@ import { DataVisualizerDataViewManagement } from '../data_view_management';
import { GetAdditionalLinks } from '../../../common/components/results_links';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable';
import { RANDOM_SAMPLER_OPTION, RandomSamplerOption } from '../../constants/random_sampler';
import { RANDOM_SAMPLER_OPTION } from '../../constants/random_sampler';
interface DataVisualizerPageState {
overallStats: OverallStats;
@ -126,18 +131,17 @@ export interface IndexDataVisualizerViewProps {
export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVisualizerProps) => {
const euiTheme = useCurrentEuiTheme();
const [savedRandomSamplerPreference, saveRandomSamplerPreference] =
useStorage<RandomSamplerOption>(
DV_RANDOM_SAMPLER_PREFERENCE,
RANDOM_SAMPLER_OPTION.ON_AUTOMATIC
);
const [savedRandomSamplerPreference, saveRandomSamplerPreference] = useStorage<
DVKey,
DVStorageMapped<typeof DV_RANDOM_SAMPLER_PREFERENCE>
>(DV_RANDOM_SAMPLER_PREFERENCE, RANDOM_SAMPLER_OPTION.ON_AUTOMATIC);
const restorableDefaults = useMemo(
() =>
getDefaultDataVisualizerListState({
rndSamplerPref: savedRandomSamplerPreference,
}),
// We just need to load the saved preference when the page is first loaded
// We just need to load the saved preference when the page is first loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

View file

@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { Query, Filter } from '@kbn/es-query';
import type { TimeRange } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { isDefined } from '../../../common/util/is_defined';
import { isDefined } from '@kbn/ml-is-defined';
import { DataVisualizerFieldNamesFilter } from './field_name_filter';
import { DataVisualizerFieldTypeFilter } from './field_type_filter';
import { SupportedFieldType } from '../../../../../common/types';

View file

@ -1,48 +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 { useCallback, useState } from 'react';
import { useDataVisualizerKibana } from '../../kibana_context';
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';
export const DV_RANDOM_SAMPLER_PREFERENCE = 'dataVisualizer.randomSamplerPreference';
export const DV_RANDOM_SAMPLER_P_VALUE = 'dataVisualizer.randomSamplerPValue';
export type DV = Partial<{
[DV_FROZEN_TIER_PREFERENCE]: 'exclude_frozen' | 'include_frozen';
[DV_RANDOM_SAMPLER_PREFERENCE]: 'true' | 'false';
[DV_RANDOM_SAMPLER_P_VALUE]: number;
}> | null;
export type DVKey = keyof Exclude<DV, null>;
/**
* Hook for accessing and changing a value in the storage.
* @param key - Storage key
* @param initValue
*/
export function useStorage<T>(key: DVKey, initValue?: T): [T, (value: T) => void] {
const {
services: { storage },
} = useDataVisualizerKibana();
const [val, setVal] = useState<T>(storage.get(key) ?? initValue);
const setStorage = useCallback(
(value: T): void => {
try {
storage.set(key, value);
setVal(value);
} catch (e) {
throw new Error('Unable to update storage with provided value');
}
},
[key, storage]
);
return [val, setStorage];
}

View file

@ -13,7 +13,9 @@ import { EuiResizeObserver } from '@elastic/eui';
import { encode } from '@kbn/rison';
import { SimpleSavedObject } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { DataView } from '@kbn/data-views-plugin/public';
import { getNestedProperty } from '@kbn/ml-nested-property';
import {
@ -34,6 +36,9 @@ import { GetAdditionalLinks } from '../common/components/results_links';
import { DATA_VISUALIZER_APP_LOCATOR, IndexDataVisualizerLocatorParams } from './locator';
import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_viewer';
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
import { DV_STORAGE_KEYS } from './types/storage';
const localStorage = new Storage(window.localStorage);
export interface DataVisualizerStateContextProviderProps {
IndexDataVisualizerComponent: FC<IndexDataVisualizerViewProps>;
@ -316,10 +321,12 @@ export const IndexDataVisualizer: FC<{
return (
<KibanaThemeProvider theme$={coreStart.theme.theme$}>
<KibanaContextProvider services={{ ...services }}>
<DataVisualizerStateContextProvider
IndexDataVisualizerComponent={IndexDataVisualizerView}
getAdditionalLinks={getAdditionalLinks}
/>
<StorageContextProvider storage={localStorage} storageKeys={DV_STORAGE_KEYS}>
<DataVisualizerStateContextProvider
IndexDataVisualizerComponent={IndexDataVisualizerView}
getAdditionalLinks={getAdditionalLinks}
/>
</StorageContextProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
);

View file

@ -10,7 +10,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { DataPublicPluginStart, ISearchOptions } from '@kbn/data-plugin/public';
import seedrandom from 'seedrandom';
import { isDefined } from '../../../common/util/is_defined';
import { isDefined } from '@kbn/ml-is-defined';
import { RANDOM_SAMPLER_PROBABILITIES } from '../../constants/random_sampler';
import { buildBaseFilterCriteria } from '../../../../../common/utils/query_utils';
import type {

View file

@ -17,8 +17,8 @@ import type {
} from '@kbn/data-plugin/common';
import type { ISearchStart } from '@kbn/data-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import { processTopValues } from './utils';
import { isDefined } from '../../../common/util/is_defined';
import { buildAggregationWithSamplingOption } from './build_random_sampler_agg';
import { MAX_PERCENT, PERCENTILE_SPACING, SAMPLER_TOP_TERMS_THRESHOLD } from './constants';
import type {

View file

@ -0,0 +1,42 @@
/*
* 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 { RandomSamplerOption } from '../constants/random_sampler';
export const DV_FROZEN_TIER_PREFERENCE = 'dataVisualizer.frozenDataTierPreference';
export const DV_RANDOM_SAMPLER_PREFERENCE = 'dataVisualizer.randomSamplerPreference';
export const DV_RANDOM_SAMPLER_P_VALUE = 'dataVisualizer.randomSamplerPValue';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type DV = Partial<{
[DV_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
[DV_RANDOM_SAMPLER_PREFERENCE]: RandomSamplerOption;
[DV_RANDOM_SAMPLER_P_VALUE]: number;
}> | null;
export type DVKey = keyof Exclude<DV, null>;
export type DVStorageMapped<T extends DVKey> = T extends typeof DV_FROZEN_TIER_PREFERENCE
? FrozenTierPreference | undefined
: T extends typeof DV_RANDOM_SAMPLER_PREFERENCE
? RandomSamplerOption | undefined
: T extends typeof DV_RANDOM_SAMPLER_P_VALUE
? number | undefined
: null;
export const DV_STORAGE_KEYS = [
DV_FROZEN_TIER_PREFERENCE,
DV_RANDOM_SAMPLER_PREFERENCE,
DV_RANDOM_SAMPLER_P_VALUE,
] as const;

View file

@ -52,6 +52,8 @@
"@kbn/field-types",
"@kbn/ml-nested-property",
"@kbn/ml-url-state",
"@kbn/ml-local-storage",
"@kbn/ml-is-defined",
],
"exclude": [
"target/**/*",

View file

@ -5,33 +5,38 @@
* 2.0.
*/
import React, { FC } from 'react';
import { renderHook, act } from '@testing-library/react-hooks';
import { MlStorageContextProvider, useStorage } from './storage_context';
import { MlStorageKey } from '../../../../common/types/storage';
import type { Storage } from '@kbn/kibana-utils-plugin/public';
import { StorageContextProvider, useStorage } from '@kbn/ml-local-storage';
import { ML_STORAGE_KEYS } from './storage';
const mockSet = jest.fn();
const mockRemove = jest.fn();
const mockStorage: Storage = {
set: mockSet,
get: jest.fn((key: string) => {
switch (key) {
case 'ml.gettingStarted.isDismissed':
return true;
default:
return;
}
}),
remove: mockRemove,
store: jest.fn() as any,
clear: jest.fn(),
};
jest.mock('../kibana', () => ({
useMlKibana: () => {
return {
services: {
storage: {
set: mockSet,
get: jest.fn((key: MlStorageKey) => {
switch (key) {
case 'ml.gettingStarted.isDismissed':
return true;
default:
return;
}
}),
remove: mockRemove,
},
},
};
},
}));
const Provider: FC = ({ children }) => {
return (
<StorageContextProvider storage={mockStorage} storageKeys={ML_STORAGE_KEYS}>
{children}
</StorageContextProvider>
);
};
describe('useStorage', () => {
afterEach(() => {
@ -40,7 +45,7 @@ describe('useStorage', () => {
test('returns the default value', () => {
const { result } = renderHook(() => useStorage('ml.jobSelectorFlyout.applyTimeRange', true), {
wrapper: MlStorageContextProvider,
wrapper: Provider,
});
expect(result.current[0]).toBe(true);
@ -48,7 +53,7 @@ describe('useStorage', () => {
test('returns the value from storage', () => {
const { result } = renderHook(() => useStorage('ml.gettingStarted.isDismissed', false), {
wrapper: MlStorageContextProvider,
wrapper: Provider,
});
expect(result.current[0]).toBe(true);
@ -58,7 +63,7 @@ describe('useStorage', () => {
const { result, waitForNextUpdate } = renderHook(
() => useStorage('ml.gettingStarted.isDismissed'),
{
wrapper: MlStorageContextProvider,
wrapper: Provider,
}
);
@ -79,7 +84,7 @@ describe('useStorage', () => {
const { result, waitForNextUpdate } = renderHook(
() => useStorage('ml.gettingStarted.isDismissed'),
{
wrapper: MlStorageContextProvider,
wrapper: Provider,
}
);
@ -100,7 +105,7 @@ describe('useStorage', () => {
const { result, waitForNextUpdate } = renderHook(
() => useStorage('ml.gettingStarted.isDismissed'),
{
wrapper: MlStorageContextProvider,
wrapper: Provider,
}
);

View file

@ -14,6 +14,14 @@ export const ML_FROZEN_TIER_PREFERENCE = 'ml.frozenDataTierPreference';
export const ML_ANOMALY_EXPLORER_PANELS = 'ml.anomalyExplorerPanels';
export const ML_NOTIFICATIONS_LAST_CHECKED_AT = 'ml.notificationsLastCheckedAt';
export const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
export type FrozenTierPreference =
typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
export type PartitionFieldConfig =
| {
/**
@ -51,14 +59,17 @@ export interface AnomalyExplorerPanelsState {
mainPage: { size: number };
}
export type MlStorage = Partial<{
export interface MlStorageRecord {
[key: string]: unknown;
[ML_ENTITY_FIELDS_CONFIG]: PartitionFieldsConfig;
[ML_APPLY_TIME_RANGE_CONFIG]: ApplyTimeRangeConfig;
[ML_GETTING_STARTED_CALLOUT_DISMISSED]: boolean | undefined;
[ML_FROZEN_TIER_PREFERENCE]: 'exclude-frozen' | 'include-frozen';
[ML_FROZEN_TIER_PREFERENCE]: FrozenTierPreference;
[ML_ANOMALY_EXPLORER_PANELS]: AnomalyExplorerPanelsState | undefined;
[ML_NOTIFICATIONS_LAST_CHECKED_AT]: number | undefined;
}> | null;
}
export type MlStorage = Partial<MlStorageRecord> | null;
export type MlStorageKey = keyof Exclude<MlStorage, null>;
@ -69,7 +80,7 @@ export type TMlStorageMapped<T extends MlStorageKey> = T extends typeof ML_ENTIT
: T extends typeof ML_GETTING_STARTED_CALLOUT_DISMISSED
? boolean | undefined
: T extends typeof ML_FROZEN_TIER_PREFERENCE
? 'exclude-frozen' | 'include-frozen' | undefined
? FrozenTierPreference | undefined
: T extends typeof ML_ANOMALY_EXPLORER_PANELS
? AnomalyExplorerPanelsState | undefined
: T extends typeof ML_NOTIFICATIONS_LAST_CHECKED_AT
@ -83,8 +94,4 @@ export const ML_STORAGE_KEYS = [
ML_FROZEN_TIER_PREFERENCE,
ML_ANOMALY_EXPLORER_PANELS,
ML_NOTIFICATIONS_LAST_CHECKED_AT,
];
export function isMlStorageKey(key: unknown): key is MlStorageKey {
return typeof key === 'string' && ML_STORAGE_KEYS.includes(key);
}
] as const;

View file

@ -6,9 +6,9 @@
*/
import { pick } from 'lodash';
import { isDefined } from '@kbn/ml-is-defined';
import { CombinedJobWithStats, Datafeed, Job } from '../types/anomaly_detection_jobs';
import { resolveMaxTimeInterval } from './job_utils';
import { isDefined } from '../types/guards';
import { parseInterval } from './parse_interval';
import { JobsHealthRuleTestsConfig, JobsHealthTests } from '../types/alerts';

View file

@ -16,6 +16,7 @@ import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import type { SerializableRecord } from '@kbn/utility-types';
import { FilterStateStore } from '@kbn/es-query';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isDefined } from '@kbn/ml-is-defined';
import { ALLOWED_DATA_UNITS, JOB_ID_MAX_LENGTH } from '../constants/validation';
import { parseInterval } from './parse_interval';
import { maxLengthValidator } from './validators';
@ -35,7 +36,6 @@ import { MLCATEGORY } from '../constants/field_types';
import { getAggregations, getDatafeedAggregations } from './datafeed_utils';
import { findAggField } from './validation_utils';
import { getFirstKeyInObject } from './object_utils';
import { isDefined } from '../types/guards';
export interface ValidationResults {
valid: boolean;

View file

@ -12,6 +12,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import useDebounce from 'react-use/lib/useDebounce';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import { MlAnomalyDetectionJobsHealthRuleParams } from '../../../common/types/alerts';
import { JobSelectorControl } from '../job_selector';
import { jobsApiProvider } from '../../application/services/ml_api_service/jobs';
@ -20,7 +21,6 @@ import { useMlKibana } from '../../application/contexts/kibana';
import { TestsSelectionControl } from './tests_selection_control';
import { ALL_JOBS_SELECTION } from '../../../common/constants/alerts';
import { BetaBadge } from '../beta_badge';
import { isDefined } from '../../../common/types/guards';
export type MlAnomalyAlertTriggerProps =
RuleTypeParamsExpressionProps<MlAnomalyDetectionJobsHealthRuleParams>;

View file

@ -10,6 +10,7 @@ import { EuiSpacer, EuiForm } from '@elastic/eui';
import useMount from 'react-use/lib/useMount';
import { i18n } from '@kbn/i18n';
import { RuleTypeParamsExpressionProps } from '@kbn/triggers-actions-ui-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
import { JobSelectorControl } from './job_selector';
import { useMlKibana } from '../application/contexts/kibana';
import { jobsApiProvider } from '../application/services/ml_api_service/jobs';
@ -29,7 +30,6 @@ import { ConfigValidator } from './config_validator';
import { CombinedJobWithStats } from '../../common/types/anomaly_detection_jobs';
import { AdvancedSettings } from './advanced_settings';
import { getLookbackInterval, getTopNBuckets } from '../../common/util/alerts';
import { isDefined } from '../../common/types/guards';
import { parseInterval } from '../../common/util/parse_interval';
import { BetaBadge } from './beta_badge';

View file

@ -15,7 +15,8 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { KibanaContextProvider, KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { MlStorageContextProvider } from './contexts/storage';
import { StorageContextProvider } from '@kbn/ml-local-storage';
import { ML_STORAGE_KEYS } from '../../common/types/storage';
import { setDependencyCache, clearCache } from './util/dependency_cache';
import { setLicenseCache } from './license';
import type { MlSetupDependencies, MlStartDependencies } from '../plugin';
@ -111,9 +112,9 @@ const App: FC<AppProps> = ({ coreStart, deps, appMountParams }) => {
mlServices: getMlGlobalServices(coreStart.http, deps.usageCollection),
}}
>
<MlStorageContextProvider>
<StorageContextProvider storage={localStorage} storageKeys={ML_STORAGE_KEYS}>
<MlRouter pageDeps={pageDeps} />
</MlStorageContextProvider>
</StorageContextProvider>
</KibanaContextProvider>
</KibanaThemeProvider>
</I18nContext>

View file

@ -20,7 +20,7 @@ jest.mock('./full_time_range_selector_service', () => ({
mockSetFullTimeRange(indexPattern, query),
}));
jest.mock('../../contexts/storage', () => {
jest.mock('@kbn/ml-local-storage', () => {
return {
useStorage: jest.fn(() => 'exclude-frozen'),
};

View file

@ -22,9 +22,15 @@ import {
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { DataView } from '@kbn/data-views-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import { setFullTimeRange } from './full_time_range_selector_service';
import { useStorage } from '../../contexts/storage';
import { ML_FROZEN_TIER_PREFERENCE } from '../../../../common/types/storage';
import {
ML_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE,
type MlStorageKey,
type TMlStorageMapped,
type FrozenTierPreference,
} from '../../../../common/types/storage';
import { GetTimeFieldRangeResponse } from '../../services/ml_api_service';
interface Props {
@ -34,13 +40,6 @@ interface Props {
callback?: (a: GetTimeFieldRangeResponse) => void;
}
const FROZEN_TIER_PREFERENCE = {
EXCLUDE: 'exclude-frozen',
INCLUDE: 'include-frozen',
} as const;
type FrozenTierPreference = typeof FROZEN_TIER_PREFERENCE[keyof typeof FROZEN_TIER_PREFERENCE];
// Component for rendering a button which automatically sets the range of the time filter
// to the time range of data in the index(es) mapped to the supplied Kibana index pattern or query.
export const FullTimeRangeSelector: FC<Props> = ({ dataView, query, disabled, callback }) => {
@ -53,10 +52,10 @@ export const FullTimeRangeSelector: FC<Props> = ({ dataView, query, disabled, ca
}
const [isPopoverOpen, setPopover] = useState(false);
const [frozenDataPreference, setFrozenDataPreference] = useStorage(
ML_FROZEN_TIER_PREFERENCE,
FROZEN_TIER_PREFERENCE.EXCLUDE
);
const [frozenDataPreference, setFrozenDataPreference] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_FROZEN_TIER_PREFERENCE>
>(ML_FROZEN_TIER_PREFERENCE, FROZEN_TIER_PREFERENCE.EXCLUDE);
const onButtonClick = () => {
setPopover(!isPopoverOpen);

View file

@ -19,6 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { useUrlState } from '@kbn/ml-url-state';
import './_index.scss';
import { useStorage } from '@kbn/ml-local-storage';
import { Dictionary } from '../../../../common/types/common';
import { IdBadges } from './id_badges';
import {
@ -27,7 +28,6 @@ import {
JobSelectorFlyoutProps,
} from './job_selector_flyout';
import { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs';
import { useStorage } from '../../contexts/storage';
import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage';
interface GroupObj {

View file

@ -8,7 +8,7 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { of, throwError } from 'rxjs';
import { useMlNotifications, MlNotificationsContextProvider } from './ml_notifications_context';
import { useStorage } from '../storage';
import { useStorage } from '@kbn/ml-local-storage';
import { useMlKibana } from '../kibana';
const mockCountMessages = jest.fn(() => {
@ -43,7 +43,7 @@ jest.mock('../kibana', () => ({
}));
const mockSetStorageValue = jest.fn();
jest.mock('../storage', () => ({
jest.mock('@kbn/ml-local-storage', () => ({
useStorage: jest.fn(() => {
return [undefined, mockSetStorageValue];
}),

View file

@ -10,9 +10,13 @@ import { combineLatest, timer } from 'rxjs';
import { switchMap, map, tap, retry } from 'rxjs/operators';
import moment from 'moment';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { useStorage } from '@kbn/ml-local-storage';
import { useMlKibana } from '../kibana';
import { useStorage } from '../storage';
import { ML_NOTIFICATIONS_LAST_CHECKED_AT } from '../../../../common/types/storage';
import {
ML_NOTIFICATIONS_LAST_CHECKED_AT,
type MlStorageKey,
type TMlStorageMapped,
} from '../../../../common/types/storage';
import { useAsObservable } from '../../hooks';
import type { NotificationsCountResponse } from '../../../../common/types/notifications';
@ -47,7 +51,10 @@ export const MlNotificationsContextProvider: FC = ({ children }) => {
const canGetNotifications = canGetJobs && canGetDataFrameAnalytics && canGetTrainedModels;
const [lastCheckedAt, setLastCheckedAt] = useStorage(ML_NOTIFICATIONS_LAST_CHECKED_AT);
const [lastCheckedAt, setLastCheckedAt] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_NOTIFICATIONS_LAST_CHECKED_AT>
>(ML_NOTIFICATIONS_LAST_CHECKED_AT);
const lastCheckedAt$ = useAsObservable(lastCheckedAt);
/** Holds the value used for the actual request */

View file

@ -1,140 +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, useEffect, useMemo, useCallback, useState, useContext } from 'react';
import { omit } from 'lodash';
import { isDefined } from '../../../../common/types/guards';
import { useMlKibana } from '../kibana';
import { MlStorage, ML_STORAGE_KEYS, isMlStorageKey } from '../../../../common/types/storage';
import { MlStorageKey, TMlStorageMapped } from '../../../../common/types/storage';
interface StorageAPI {
value: MlStorage;
setValue: <K extends MlStorageKey, T extends TMlStorageMapped<K>>(key: K, value: T) => void;
removeValue: <K extends MlStorageKey>(key: K) => void;
}
export const MlStorageContext = React.createContext<StorageAPI>({
value: null,
setValue() {
throw new Error('MlStorageContext set method is not implemented');
},
removeValue() {
throw new Error('MlStorageContext remove method is not implemented');
},
});
export const MlStorageContextProvider: FC = ({ children }) => {
const {
services: { storage },
} = useMlKibana();
const initialValue = useMemo(() => {
return ML_STORAGE_KEYS.reduce((acc, curr) => {
acc[curr as MlStorageKey] = storage.get(curr);
return acc;
}, {} as Exclude<MlStorage, null>);
}, [storage]);
const [state, setState] = useState<MlStorage>(initialValue);
const setStorageValue = useCallback(
<K extends MlStorageKey, T extends TMlStorageMapped<K>>(key: K, value: T) => {
storage.set(key, value);
setState((prevState) => ({
...prevState,
[key]: value,
}));
},
[storage]
);
const removeStorageValue = useCallback(
(key: MlStorageKey) => {
storage.remove(key);
setState((prevState) => omit(prevState, key));
},
[storage]
);
useEffect(function updateStorageOnExternalChange() {
const eventListener = (event: StorageEvent) => {
if (!isMlStorageKey(event.key)) return;
if (isDefined(event.newValue)) {
setState((prev) => {
return {
...prev,
[event.key as MlStorageKey]:
typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue,
};
});
} else {
setState((prev) => {
return omit(prev, event.key as MlStorageKey);
});
}
};
/**
* This event listener is only invoked when
* the change happens in another browser's tab.
*/
window.addEventListener('storage', eventListener);
return () => {
window.removeEventListener('storage', eventListener);
};
}, []);
const value: StorageAPI = useMemo(() => {
return {
value: state,
setValue: setStorageValue,
removeValue: removeStorageValue,
};
}, [state, setStorageValue, removeStorageValue]);
return <MlStorageContext.Provider value={value}>{children}</MlStorageContext.Provider>;
};
/**
* Hook for consuming a storage value
* @param key
* @param initValue
*/
export function useStorage<K extends MlStorageKey, T extends TMlStorageMapped<K>>(
key: K,
initValue?: T
): [
typeof initValue extends undefined
? TMlStorageMapped<K> | undefined
: Exclude<TMlStorageMapped<K>, undefined>,
(value: TMlStorageMapped<K>) => void
] {
const { value, setValue, removeValue } = useContext(MlStorageContext);
const resultValue = useMemo(() => {
return (value?.[key] ?? initValue) as typeof initValue extends undefined
? TMlStorageMapped<K> | undefined
: Exclude<TMlStorageMapped<K>, undefined>;
}, [value, key, initValue]);
const setVal = useCallback(
(v: TMlStorageMapped<K>) => {
if (isDefined(v)) {
setValue(key, v);
} else {
removeValue(key);
}
},
[setValue, removeValue, key]
);
return [resultValue, setVal];
}

View file

@ -25,8 +25,8 @@ import {
VectorLayerDescriptor,
} from '@kbn/maps-plugin/common';
import { EMSTermJoinConfig } from '@kbn/maps-plugin/public';
import { isDefined } from '@kbn/ml-is-defined';
import { useMlKibana } from '../contexts/kibana';
import { isDefined } from '../../../common/types/guards';
import { MlEmbeddedMapComponent } from '../components/ml_embedded_map';
import { AnomaliesTableRecord } from '../../../common/types/anomalies';

View file

@ -19,7 +19,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import useObservable from 'react-use/lib/useObservable';
import type { Query, TimeRange } from '@kbn/es-query';
import { isDefined } from '../../../common/types/guards';
import { isDefined } from '@kbn/ml-is-defined';
import { useAnomalyExplorerContext } from './anomaly_explorer_context';
import { escapeKueryForFieldValuePair } from '../util/string_utils';
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';

View file

@ -27,6 +27,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import useDebounce from 'react-use/lib/useDebounce';
import useObservable from 'react-use/lib/useObservable';
import type { Query } from '@kbn/es-query';
import { isDefined } from '@kbn/ml-is-defined';
import { SEARCH_QUERY_LANGUAGE } from '../../../common/constants/search';
import { useCasesModal } from '../contexts/kibana/use_cases_modal';
import { useTimeRangeUpdates } from '../contexts/kibana/use_timefilter';
@ -46,7 +47,6 @@ import { AppStateSelectedCells, OverallSwimlaneData, ViewBySwimLaneData } from '
import { NoOverallData } from './components/no_overall_data';
import { SeverityControl } from '../components/severity_control';
import { AnomalyTimelineHelpPopover } from './anomaly_timeline_help_popover';
import { isDefined } from '../../../common/types/guards';
import { MlTooltipComponent } from '../components/chart_tooltip';
import { SwimlaneAnnotationContainer, Y_AXIS_LABEL_WIDTH } from './swimlane_annotation_container';
import { AnomalyTimelineService } from '../services/anomaly_timeline_service';

View file

@ -9,8 +9,8 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFieldNumber, EuiFormRow, htmlIdGenerator } from '@elastic/eui';
import type { Query } from '@kbn/es-query';
import useObservable from 'react-use/lib/useObservable';
import { isDefined } from '@kbn/ml-is-defined';
import { getSelectionInfluencers } from '../explorer_utils';
import { isDefined } from '../../../../common/types/guards';
import { useAnomalyExplorerContext } from '../anomaly_explorer_context';
import { escapeKueryForFieldValuePair } from '../../util/string_utils';
import { SEARCH_QUERY_LANGUAGE } from '../../../../common/constants/search';

View file

@ -31,6 +31,8 @@ import { css } from '@emotion/react';
import useObservable from 'react-use/lib/useObservable';
import type { DataView } from '@kbn/data-views-plugin/common';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import { isDefined } from '@kbn/ml-is-defined';
import { HelpPopover } from '../components/help_popover';
import { AnnotationFlyout } from '../components/annotations/annotation_flyout';
// @ts-ignore
@ -69,7 +71,6 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table';
import { AnomaliesMap } from './anomalies_map';
import { ANOMALY_DETECTION_DEFAULT_TIME_RANGE } from '../../../common/constants/settings';
import { AnomalyContextMenu } from './anomaly_context_menu';
import { isDefined } from '../../../common/types/guards';
import type { JobSelectorProps } from '../components/job_selector/job_selector';
import type { ExplorerState } from './reducers';
import type { TimeBuckets } from '../util/time_buckets';
@ -78,7 +79,6 @@ import { useMlKibana, useMlLocator } from '../contexts/kibana';
import { useMlContext } from '../contexts/ml';
import { useAnomalyExplorerContext } from './anomaly_explorer_context';
import { ML_ANOMALY_EXPLORER_PANELS } from '../../../common/types/storage';
import { useStorage } from '../contexts/storage';
interface ExplorerPageProps {
jobSelectorProps: JobSelectorProps;

View file

@ -8,8 +8,8 @@
import React, { FC } from 'react';
import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { useStorage } from '@kbn/ml-local-storage';
import { useMlKibana } from '../../contexts/kibana';
import { useStorage } from '../../contexts/storage';
import { ML_GETTING_STARTED_CALLOUT_DISMISSED } from '../../../../common/types/storage';
const feedbackLink = 'https://www.elastic.co/community/';

View file

@ -10,6 +10,7 @@ import { map as mapObservable } from 'rxjs/operators';
import type { TimeRange } from '@kbn/es-query';
import type { TimefilterContract } from '@kbn/data-plugin/public';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import type { RecordForInfluencer } from './results_service/results_service';
import type { EntityField } from '../../../common/util/anomaly_utils';
import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs';
@ -17,7 +18,6 @@ import type { MlApiServices } from './ml_api_service';
import type { MlResultsService } from './results_service';
import { ExplorerChartsData } from '../explorer/explorer_charts/explorer_charts_container_service';
import type { TimeRangeBounds } from '../util/time_buckets';
import { isDefined } from '../../../common/types/guards';
import type { AppStateSelectedCells } from '../explorer/explorer_utils';
import type { InfluencersFilterQuery } from '../../../common/types/es_client';
import type { SeriesConfigWithMetadata } from '../../../common/types/results';

View file

@ -6,7 +6,7 @@
*/
import { omitBy } from 'lodash';
import { isDefined } from '../../../../common/types/guards';
import { isDefined } from '@kbn/ml-is-defined';
import type {
NotificationsQueryParams,
NotificationsSearchResponse,

View file

@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSelect, EuiSelectProps } from '@elastic/eui';
import { debounce } from 'lodash';
import { lastValueFrom } from 'rxjs';
import { useStorage } from '@kbn/ml-local-storage';
import { EntityControl } from '../entity_control';
import { mlJobService } from '../../../services/job_service';
import { Detector, JobId } from '../../../../../common/types/anomaly_detection_jobs';
@ -23,10 +24,11 @@ import {
import { getControlsForDetector } from '../../get_controls_for_detector';
import {
ML_ENTITY_FIELDS_CONFIG,
PartitionFieldConfig,
PartitionFieldsConfig,
type PartitionFieldConfig,
type PartitionFieldsConfig,
type MlStorageKey,
type TMlStorageMapped,
} from '../../../../../common/types/storage';
import { useStorage } from '../../../contexts/storage';
import { EntityFieldType } from '../../../../../common/types/anomalies';
import { FieldDefinition } from '../../../services/results_service/result_service_rx';
import { getViewableDetectors } from '../../timeseriesexplorer_utils/get_viewable_detectors';
@ -113,7 +115,10 @@ export const SeriesControls: FC<SeriesControlsProps> = ({
return getControlsForDetector(selectedDetectorIndex, selectedEntities, selectedJobId);
}, [selectedDetectorIndex, selectedEntities, selectedJobId]);
const [storageFieldsConfig, setStorageFieldsConfig] = useStorage(ML_ENTITY_FIELDS_CONFIG);
const [storageFieldsConfig, setStorageFieldsConfig] = useStorage<
MlStorageKey,
TMlStorageMapped<typeof ML_ENTITY_FIELDS_CONFIG>
>(ML_ENTITY_FIELDS_CONFIG);
// Merge the default config with the one from the local storage
const resultFieldsConfig = useMemo(() => {

View file

@ -24,8 +24,8 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { FIELD_FORMAT_IDS } from '@kbn/field-formats-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import type { ModelItemFull } from './models_list';
import { isDefined } from '../../../../common/types/guards';
import { ModelPipelines } from './pipelines';
import { AllocatedModels } from '../nodes_overview/allocated_models';
import type { AllocatedModel } from '../../../../common/types/trained_models';

View file

@ -12,7 +12,7 @@ import d3 from 'd3';
import he from 'he';
import { escapeKuery } from '@kbn/es-query';
import { isDefined } from '../../../common/types/guards';
import { isDefined } from '@kbn/ml-is-defined';
import { CustomUrlAnomalyRecordDoc } from '../../../common/types/custom_urls';
import { Detector } from '../../../common/types/anomaly_detection_jobs';

View file

@ -15,6 +15,7 @@ import {
IFieldFormat,
SerializedFieldFormat,
} from '@kbn/field-formats-plugin/common';
import { isDefined } from '@kbn/ml-is-defined';
import { MlClient } from '../ml_client';
import {
MlAnomalyDetectionAlertParams,
@ -32,7 +33,6 @@ import {
} from '../../../common/types/alerts';
import { AnomalyDetectionAlertContext } from './register_anomaly_detection_alert_type';
import { resolveMaxTimeInterval } from '../../../common/util/job_utils';
import { isDefined } from '../../../common/types/guards';
import { getTopNBuckets, resolveLookbackInterval } from '../../../common/util/alerts';
import type { DatafeedsService } from '../../models/job_service/datafeeds';
import { getEntityFieldName, getEntityFieldValue } from '../../../common/util/anomaly_utils';

View file

@ -9,6 +9,7 @@ import { groupBy, keyBy, memoize, partition } from 'lodash';
import { KibanaRequest, Logger, SavedObjectsClientContract } from '@kbn/core/server';
import { i18n } from '@kbn/i18n';
import { MlJob } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isDefined } from '@kbn/ml-is-defined';
import { MlClient } from '../ml_client';
import { JobSelection } from '../../routes/schemas/alerting_schema';
import { datafeedsProvider, DatafeedsService } from '../../models/job_service/datafeeds';
@ -30,7 +31,6 @@ import {
import { AnnotationService } from '../../models/annotation_service/annotation';
import { annotationServiceProvider } from '../../models/annotation_service';
import { parseInterval } from '../../../common/util/parse_interval';
import { isDefined } from '../../../common/types/guards';
import {
jobAuditMessagesProvider,
JobAuditMessagesService,

View file

@ -11,6 +11,7 @@ import {
MlTrainedModelStats,
NodesInfoNodeInfo,
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { isDefined } from '@kbn/ml-is-defined';
import type {
NodeDeploymentStatsResponse,
PipelineDefinition,
@ -21,7 +22,6 @@ import {
TrainedModelDeploymentStatsResponse,
TrainedModelModelSizeStats,
} from '../../../common/types/trained_models';
import { isDefined } from '../../../common/types/guards';
export type ModelService = ReturnType<typeof modelsProvider>;

View file

@ -18,6 +18,7 @@ import moment from 'moment';
import { merge } from 'lodash';
import type { DataViewsService } from '@kbn/data-views-plugin/common';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import type { AnalysisLimits } from '../../../common/types/anomaly_detection_jobs';
import { getAuthorizationHeader } from '../../lib/request_authorization';
import type { MlClient } from '../../lib/ml_client';
@ -54,7 +55,6 @@ import { resultsServiceProvider } from '../results_service';
import type { JobExistResult, JobStat } from '../../../common/types/data_recognizer';
import type { Datafeed } from '../../../common/types/anomaly_detection_jobs';
import type { MLSavedObjectService } from '../../saved_objects';
import { isDefined } from '../../../common/types/guards';
const ML_DIR = 'ml';
const KIBANA_DIR = 'kibana';

View file

@ -11,6 +11,7 @@ import { each, find, get, keyBy, map, reduce, sortBy } from 'lodash';
import type * as estypes from '@elastic/elasticsearch/lib/api/types';
import { extent, max, min } from 'd3';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { isDefined } from '@kbn/ml-is-defined';
import type { MlClient } from '../../lib/ml_client';
import { isRuntimeMappings } from '../../../common';
import type {
@ -40,7 +41,6 @@ import {
isMultiBucketAnomaly,
} from '../../../common/util/anomaly_utils';
import { InfluencersFilterQuery } from '../../../common/types/es_client';
import { isDefined } from '../../../common/types/guards';
import { AnomalyRecordDoc, CombinedJob, Datafeed, RecordForInfluencer } from '../../shared';
import { ES_AGGREGATION, ML_JOB_AGGREGATION } from '../../../common/constants/aggregation_types';
import { parseInterval } from '../../../common/util/parse_interval';

View file

@ -68,6 +68,8 @@
"@kbn/repo-info",
"@kbn/ml-url-state",
"@kbn/ml-nested-property",
"@kbn/ml-local-storage",
"@kbn/ml-is-defined",
],
"exclude": [
"target/**/*",

View file

@ -20,7 +20,3 @@ export function dictionaryToArray<TValue>(dict: Dictionary<TValue>): TValue[] {
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
export function isDefined<T>(argument: T | undefined | null): argument is T {
return argument !== undefined && argument !== null;
}

View file

@ -7,8 +7,8 @@
import { EuiComboBox, EuiComboBoxProps, EuiFormRow } from '@elastic/eui';
import React, { FC, useMemo } from 'react';
import { isDefined } from '@kbn/ml-is-defined';
import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants';
import { isDefined } from '../../../common/types/common';
export interface TransformSelectorControlProps {
label?: string | JSX.Element;

View file

@ -6,6 +6,7 @@
*/
import type { IHttpFetchError } from '@kbn/core-http-browser';
import { isDefined } from '@kbn/ml-is-defined';
import {
isGetTransformNodesResponseSchema,
isGetTransformsResponseSchema,
@ -22,7 +23,6 @@ import {
import { useApi } from './use_api';
import { TRANSFORM_ERROR_TYPE } from '../common/transform';
import { isDefined } from '../../../common/types/common';
export type GetTransforms = (forceRefresh?: boolean) => void;

View file

@ -18,7 +18,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { isDefined } from '../../../../../../common/types/common';
import { isDefined } from '@kbn/ml-is-defined';
import { StepDefineFormHook } from '../step_define';
import { AdvancedRuntimeMappingsEditor } from '../advanced_runtime_mappings_editor/advanced_runtime_mappings_editor';
import { AdvancedRuntimeMappingsEditorSwitch } from '../advanced_runtime_mappings_editor_switch';

View file

@ -9,8 +9,9 @@ import { useCallback, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { KBN_FIELD_TYPES } from '@kbn/field-types';
import { isDefined } from '@kbn/ml-is-defined';
import { AggName } from '../../../../../../../common/types/aggregations';
import { dictionaryToArray, isDefined } from '../../../../../../../common/types/common';
import { dictionaryToArray } from '../../../../../../../common/types/common';
import { useToastNotifications } from '../../../../../app_dependencies';
import {

View file

@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n';
import { stringHash } from '@kbn/ml-string-hash';
import moment from 'moment-timezone';
import { isDefined } from '../../../../../../common/types/common';
import { isDefined } from '@kbn/ml-is-defined';
import { TransformListRow } from '../../../../common';
import { useAppDependencies } from '../../../../app_dependencies';
import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane';

View file

@ -45,6 +45,7 @@
"@kbn/ui-theme",
"@kbn/field-types",
"@kbn/ml-nested-property",
"@kbn/ml-is-defined",
],
"exclude": [
"target/**/*",

View file

@ -3773,10 +3773,18 @@
version "0.0.0"
uid ""
"@kbn/ml-is-defined@link:x-pack/packages/ml/is_defined":
version "0.0.0"
uid ""
"@kbn/ml-is-populated-object@link:x-pack/packages/ml/is_populated_object":
version "0.0.0"
uid ""
"@kbn/ml-local-storage@link:x-pack/packages/ml/local_storage":
version "0.0.0"
uid ""
"@kbn/ml-nested-property@link:x-pack/packages/ml/nested_property":
version "0.0.0"
uid ""