Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use SessionStorage to refresh every hour on OAuth issues (#798)
- Loading branch information
1 parent
6fb9f0f
commit b18d47a
Showing
11 changed files
with
223 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import * as React from 'react'; | ||
import { useBrowserStorage } from '../components/browserStorage'; | ||
import { logout } from './appUtils'; | ||
|
||
export type SetTime = (refreshDateMarker: Date) => void; | ||
|
||
const useTimeBasedRefresh = (): SetTime => { | ||
const KEY_NAME = 'odh.dashboard.last.auto.refresh'; | ||
const [lastRefreshTimestamp, setLastRefreshTimestamp] = useBrowserStorage( | ||
KEY_NAME, | ||
'0', | ||
false, | ||
true, | ||
); | ||
const ref = React.useRef<{ | ||
lastRefreshTimestamp: string; | ||
setLastRefreshTimestamp: (newValue: string) => void; | ||
}>({ lastRefreshTimestamp, setLastRefreshTimestamp }); | ||
ref.current = { lastRefreshTimestamp, setLastRefreshTimestamp }; | ||
|
||
return React.useCallback<SetTime>((refreshDateMarker) => { | ||
// Intentionally avoid referential changes. We want the value at call time. | ||
// Recomputing the ref is not needed and will impact usage in hooks if it does. | ||
const lastDate = new Date(ref.current.lastRefreshTimestamp); | ||
const setNewDateString = ref.current.setLastRefreshTimestamp; | ||
|
||
// Print into the console in case we are not refreshing or the browser has preserve log enabled | ||
console.warn('Attempting to re-trigger an auto refresh'); | ||
console.log('Last refresh was on:', lastDate); | ||
console.log('Refreshing requested after:', refreshDateMarker); | ||
|
||
lastDate.setHours(lastDate.getHours() + 1); | ||
if (lastDate < refreshDateMarker) { | ||
setNewDateString(refreshDateMarker.toString()); | ||
console.log('Logging out and refreshing'); | ||
logout().then(() => window.location.reload()); | ||
} else { | ||
console.error( | ||
`We should have refreshed but it appears the last time we auto-refreshed was less than an hour ago. '${KEY_NAME}' session storage setting can be cleared for this to refresh again within the hour from the last refresh.`, | ||
); | ||
} | ||
}, []); | ||
}; | ||
|
||
export default useTimeBasedRefresh; |
140 changes: 140 additions & 0 deletions
140
frontend/src/components/browserStorage/BrowserStorageContext.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import * as React from 'react'; | ||
import { useEventListener } from '../../utilities/useEventListener'; | ||
|
||
type ValueMap = { [storageKey: string]: unknown }; | ||
export type BrowserStorageContext = { | ||
/** Based on parseJSON it can be any jsonify-able item */ | ||
getValue: (storageKey: string, parseJSON: boolean, isSessionStorage?: boolean) => unknown; | ||
/** Returns a boolean if it was able to json-ify it. */ | ||
setJSONValue: (storageKey: string, value: unknown, isSessionStorage?: boolean) => boolean; | ||
setStringValue: (storageKey: string, value: string, isSessionStorage?: boolean) => void; | ||
}; | ||
|
||
const BrowserStorageContext = React.createContext<BrowserStorageContext>({ | ||
getValue: () => null, | ||
setJSONValue: () => false, | ||
setStringValue: () => undefined, | ||
}); | ||
|
||
/** | ||
* @returns {boolean} if it was successful, false if it was not | ||
*/ | ||
export type SetBrowserStorageHook<T> = (value: T) => boolean; | ||
|
||
/** | ||
* useBrowserStorage will handle all the effort behind managing localStorage or sessionStorage. | ||
*/ | ||
export const useBrowserStorage = <T,>( | ||
storageKey: string, | ||
defaultValue: T, | ||
jsonify = true, | ||
isSessionStorage = false, | ||
): [T, SetBrowserStorageHook<T>] => { | ||
const { getValue, setJSONValue, setStringValue } = React.useContext(BrowserStorageContext); | ||
|
||
const setValue = React.useCallback<SetBrowserStorageHook<T>>( | ||
(value) => { | ||
if (jsonify) { | ||
return setJSONValue(storageKey, value, isSessionStorage); | ||
} else if (typeof value === 'string') { | ||
setStringValue(storageKey, value, isSessionStorage); | ||
return true; | ||
} else { | ||
console.error('Was not a string value provided, cannot stringify'); | ||
return false; | ||
} | ||
}, | ||
[isSessionStorage, jsonify, setJSONValue, setStringValue, storageKey], | ||
); | ||
|
||
return [(getValue(storageKey, jsonify, isSessionStorage) as T) ?? defaultValue, setValue]; | ||
}; | ||
|
||
type BrowserStorageContextProviderProps = { | ||
children: React.ReactNode; | ||
}; | ||
|
||
const getStorage = (isSessionStorage: boolean): Storage => { | ||
if (isSessionStorage) { | ||
return sessionStorage; | ||
} | ||
|
||
return localStorage; | ||
}; | ||
|
||
/** | ||
* @see useBrowserStorage | ||
*/ | ||
export const BrowserStorageContextProvider: React.FC<BrowserStorageContextProviderProps> = ({ | ||
children, | ||
}) => { | ||
const [values, setValues] = React.useState<ValueMap>({}); | ||
|
||
/** | ||
* Only listen to other storage changes (windows/tabs) -- which are localStorage. | ||
* Session storage does not have cross instance storages. | ||
* See MDN for more: https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage | ||
*/ | ||
useEventListener(window, 'storage', () => { | ||
// Another browser tab has updated storage, sync up the data | ||
const keys = Object.keys(values); | ||
setValues( | ||
keys.reduce((acc, key) => { | ||
const value = localStorage.getItem(key); | ||
return { ...acc, [key]: value }; | ||
}, {}), | ||
); | ||
}); | ||
|
||
const getValue = React.useCallback<BrowserStorageContext['getValue']>( | ||
(key, parseJSON, isSessionStorage = false) => { | ||
const value = getStorage(isSessionStorage).getItem(key); | ||
if (value === null) { | ||
return value; | ||
} | ||
|
||
if (parseJSON) { | ||
try { | ||
return JSON.parse(value); | ||
} catch (e) { | ||
console.warn(`Failed to parse storage value "${key}"`); | ||
return null; | ||
} | ||
} else { | ||
return value; | ||
} | ||
}, | ||
[], | ||
); | ||
|
||
const setJSONValue = React.useCallback<BrowserStorageContext['setJSONValue']>( | ||
(storageKey, value, isSessionStorage = false) => { | ||
try { | ||
const storageValue = JSON.stringify(value); | ||
getStorage(isSessionStorage).setItem(storageKey, storageValue); | ||
setValues((oldValues) => ({ ...oldValues, [storageKey]: storageValue })); | ||
|
||
return true; | ||
} catch (e) { | ||
console.warn( | ||
'Could not store a value because it was requested to be stringified but was an invalid value for stringification.', | ||
); | ||
return false; | ||
} | ||
}, | ||
[], | ||
); | ||
const setStringValue = React.useCallback<BrowserStorageContext['setStringValue']>( | ||
(storageKey, value, isSessionStorage = false) => { | ||
getStorage(isSessionStorage).setItem(storageKey, value); | ||
setValues((oldValues) => ({ ...oldValues, [storageKey]: value })); | ||
}, | ||
[], | ||
); | ||
|
||
return ( | ||
<BrowserStorageContext.Provider value={{ getValue, setJSONValue, setStringValue }}> | ||
{children} | ||
</BrowserStorageContext.Provider> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { useBrowserStorage } from './BrowserStorageContext'; |
Oops, something went wrong.