-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[ML] Move local storage utilities to package. #148049
Changes from all commits
6113e98
ff7b2eb
ade605d
20a58e3
6be2cde
ef2454e
98c2732
e3f0d73
ec91b4c
eea9870
45bf5fa
92fb231
995ae43
183e884
915e6b2
ad5cfc9
40e18a1
5fa22db
ae7842f
286a971
6794796
b4d0bfd
928a2d6
c0e5b87
834f71f
64a4aa0
2bd1942
543387f
6c50789
469fa97
b41dacf
58fbb87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"type": "shared-common", | ||
"id": "@kbn/ml-is-defined", | ||
"owner": "@elastic/ml-ui" | ||
} |
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 |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"extends": "../../../../tsconfig.base.json", | ||
"compilerOptions": { | ||
"outDir": "target/types", | ||
"types": [ | ||
"jest", | ||
"node", | ||
"react" | ||
] | ||
}, | ||
"include": [ | ||
"**/*.ts", | ||
"**/*.tsx", | ||
], | ||
"exclude": [ | ||
"target/**/*" | ||
], | ||
"kbn_references": [] | ||
} |
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. |
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'; |
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'], | ||
}; |
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" | ||
} |
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" | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a comment to this to keep the 'public APIs without comments' count at 0 for this package. |
||
return storageKeys.includes(key as T); | ||
} | ||
|
||
/** | ||
* React context to hold storage API. | ||
*/ | ||
export const MlStorageContext = React.createContext<StorageAPI>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a comment to this to keep the 'public APIs without comments' count at 0 for this package. |
||
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>({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add a comment to this to keep the 'public APIs without comments' count at 0 for this package. |
||
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]; | ||
} |
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", | ||
] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a comment to this exported function to keep the 'public APIs without comments' count at 0?