Skip to content

Commit

Permalink
[ML] Move local storage utilities to package. (#148049)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
walterra authored Jan 5, 2023
1 parent 9a0e692 commit dc1ae9e
Show file tree
Hide file tree
Showing 77 changed files with 597 additions and 377 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
3 changes: 3 additions & 0 deletions x-pack/packages/ml/is_defined/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/ml-is-defined

Utility function to determine if a value is not `undefined` and not `null`.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
* 2.0.
*/

export { MlStorageContextProvider, useStorage } from './storage_context';
export { isDefined } from './src/is_defined';
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
};
5 changes: 5 additions & 0 deletions x-pack/packages/ml/is_defined/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-is-defined",
"owner": "@elastic/ml-ui"
}
6 changes: 6 additions & 0 deletions x-pack/packages/ml/is_defined/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/ml-is-defined",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
19 changes: 19 additions & 0 deletions x-pack/packages/ml/is_defined/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}
3 changes: 3 additions & 0 deletions x-pack/packages/ml/local_storage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/ml-local-storage

Utilities to combine url state management with local storage.
8 changes: 8 additions & 0 deletions x-pack/packages/ml/local_storage/index.ts
Original file line number Diff line number Diff line change
@@ -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';
12 changes: 12 additions & 0 deletions x-pack/packages/ml/local_storage/jest.config.js
Original file line number Diff line number Diff line change
@@ -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'],
};
5 changes: 5 additions & 0 deletions x-pack/packages/ml/local_storage/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/ml-local-storage",
"owner": "@elastic/ml-ui"
}
9 changes: 9 additions & 0 deletions x-pack/packages/ml/local_storage/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
193 changes: 193 additions & 0 deletions x-pack/packages/ml/local_storage/src/storage_context.tsx
Original file line number Diff line number Diff line change
@@ -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];
}
22 changes: 22 additions & 0 deletions x-pack/packages/ml/local_storage/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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",
]
}
2 changes: 1 addition & 1 deletion x-pack/plugins/aiops/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"licensing"
],
"optionalPlugins": [],
"requiredBundles": ["fieldFormats", "kibanaReact"],
"requiredBundles": ["fieldFormats", "kibanaReact", "kibanaUtils"],
"extraPublicDirs": ["common"]
}
Loading

0 comments on commit dc1ae9e

Please sign in to comment.