Skip to content

Commit 92b488b

Browse files
authored
feat(ws): Persist last used namespace across refreshes and tabs (#341)
Signed-off-by: Charles Thao <cthao@redhat.com>
1 parent db82bd5 commit 92b488b

File tree

4 files changed

+108
-16
lines changed

4 files changed

+108
-16
lines changed

workspaces/frontend/src/app/App.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import AppRoutes from './AppRoutes';
2323
import NavSidebar from './NavSidebar';
2424
import { NotebookContextProvider } from './context/NotebookContext';
2525
import { isMUITheme, Theme } from './const';
26+
import { BrowserStorageContextProvider } from './context/BrowserStorageContext';
2627

2728
const App: React.FC = () => {
2829
React.useEffect(() => {
@@ -66,17 +67,19 @@ const App: React.FC = () => {
6667
return (
6768
<ErrorBoundary>
6869
<NotebookContextProvider>
69-
<NamespaceContextProvider>
70-
<Page
71-
mainContainerId="primary-app-container"
72-
masthead={masthead}
73-
isContentFilled
74-
isManagedSidebar
75-
sidebar={<NavSidebar />}
76-
>
77-
<AppRoutes />
78-
</Page>
79-
</NamespaceContextProvider>
70+
<BrowserStorageContextProvider>
71+
<NamespaceContextProvider>
72+
<Page
73+
mainContainerId="primary-app-container"
74+
masthead={masthead}
75+
isContentFilled
76+
isManagedSidebar
77+
sidebar={<NavSidebar />}
78+
>
79+
<AppRoutes />
80+
</Page>
81+
</NamespaceContextProvider>
82+
</BrowserStorageContextProvider>
8083
</NotebookContextProvider>
8184
</ErrorBoundary>
8285
);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { createContext, useCallback, useContext, useEffect } from 'react';
2+
3+
export interface BrowserStorageContextType {
4+
getValue: (key: string) => unknown;
5+
setValue: (key: string, value: string) => void;
6+
}
7+
8+
export interface BrowserStorageContextProviderProps {
9+
children: React.ReactNode;
10+
}
11+
12+
const BrowserStorageContext = createContext<BrowserStorageContextType>({
13+
getValue: () => null,
14+
setValue: () => undefined,
15+
});
16+
17+
export const BrowserStorageContextProvider: React.FC<BrowserStorageContextProviderProps> = ({
18+
children,
19+
}) => {
20+
const [values, setValues] = React.useState<{ [key: string]: unknown }>({});
21+
const valuesRef = React.useRef(values);
22+
useEffect(() => {
23+
valuesRef.current = values;
24+
}, [values]);
25+
26+
const storageEventCb = useCallback(() => {
27+
const keys = Object.keys(values);
28+
setValues(Object.fromEntries(keys.map((k) => [k, localStorage.getItem(k)])));
29+
}, [values, setValues]);
30+
31+
useEffect(() => {
32+
window.addEventListener('storage', storageEventCb);
33+
return () => {
34+
window.removeEventListener('storage', storageEventCb);
35+
};
36+
}, [storageEventCb]);
37+
38+
const getValue = useCallback<BrowserStorageContextType['getValue']>(
39+
(key: string) => localStorage.getItem(key),
40+
[],
41+
);
42+
43+
const setValue = useCallback<BrowserStorageContextType['setValue']>(
44+
(key: string, value: string) => {
45+
localStorage.setItem(key, value);
46+
setValues((prev) => ({ ...prev, [key]: value }));
47+
},
48+
[],
49+
);
50+
51+
// eslint-disable-next-line react-hooks/exhaustive-deps
52+
const contextValue = React.useMemo(() => ({ getValue, setValue }), [getValue, setValue, values]);
53+
54+
return (
55+
<BrowserStorageContext.Provider value={contextValue}>{children}</BrowserStorageContext.Provider>
56+
);
57+
};
58+
59+
export const useStorage = <T,>(
60+
storageKey: string,
61+
defaultValue: T,
62+
): [T, (key: string, value: string) => void] => {
63+
const context = useContext(BrowserStorageContext);
64+
const { getValue, setValue } = context;
65+
const value = (getValue(storageKey) as T) ?? defaultValue;
66+
return [value, setValue];
67+
};

workspaces/frontend/src/app/context/NamespaceContextProvider.tsx

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import React, { useState, useContext, ReactNode, useMemo, useCallback } from 'react';
22
import useMount from '~/app/hooks/useMount';
33
import useNamespaces from '~/app/hooks/useNamespaces';
4+
import { useStorage } from './BrowserStorageContext';
5+
6+
const storageKey = 'kubeflow.notebooks.namespace.lastUsed';
47

58
interface NamespaceContextType {
69
namespaces: string[];
710
selectedNamespace: string;
811
setSelectedNamespace: (namespace: string) => void;
12+
lastUsedNamespace: string;
13+
updateLastUsedNamespace: (value: string) => void;
914
}
1015

1116
const NamespaceContext = React.createContext<NamespaceContextType | undefined>(undefined);
@@ -26,26 +31,41 @@ export const NamespaceContextProvider: React.FC<NamespaceContextProviderProps> =
2631
const [namespaces, setNamespaces] = useState<string[]>([]);
2732
const [selectedNamespace, setSelectedNamespace] = useState<string>('');
2833
const [namespacesData, loaded, loadError] = useNamespaces();
34+
const [lastUsedNamespace, setLastUsedNamespace] = useStorage<string>(storageKey, '');
2935

3036
const fetchNamespaces = useCallback(() => {
3137
if (loaded && namespacesData) {
3238
const namespaceNames = namespacesData.map((ns) => ns.name);
3339
setNamespaces(namespaceNames);
34-
setSelectedNamespace(namespaceNames.length > 0 ? namespaceNames[0] : '');
40+
setSelectedNamespace(lastUsedNamespace.length ? lastUsedNamespace : namespaceNames[0]);
41+
if (!lastUsedNamespace.length) {
42+
setLastUsedNamespace(storageKey, namespaceNames[0]);
43+
}
3544
} else {
3645
if (loadError) {
3746
console.error('Error loading namespaces: ', loadError);
3847
}
3948
setNamespaces([]);
4049
setSelectedNamespace('');
4150
}
42-
}, [loaded, namespacesData, loadError]);
51+
}, [loaded, namespacesData, lastUsedNamespace, setLastUsedNamespace, loadError]);
52+
53+
const updateLastUsedNamespace = useCallback(
54+
(value: string) => setLastUsedNamespace(storageKey, value),
55+
[setLastUsedNamespace],
56+
);
4357

4458
useMount(fetchNamespaces);
4559

4660
const namespacesContextValues = useMemo(
47-
() => ({ namespaces, selectedNamespace, setSelectedNamespace }),
48-
[namespaces, selectedNamespace],
61+
() => ({
62+
namespaces,
63+
selectedNamespace,
64+
setSelectedNamespace,
65+
lastUsedNamespace,
66+
updateLastUsedNamespace,
67+
}),
68+
[namespaces, selectedNamespace, lastUsedNamespace, updateLastUsedNamespace],
4969
);
5070

5171
return (

workspaces/frontend/src/shared/components/NamespaceSelector.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { SearchIcon } from '@patternfly/react-icons/dist/esm/icons/search-icon';
1818
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
1919

2020
const NamespaceSelector: FC = () => {
21-
const { namespaces, selectedNamespace, setSelectedNamespace } = useNamespaceContext();
21+
const { namespaces, selectedNamespace, setSelectedNamespace, updateLastUsedNamespace } =
22+
useNamespaceContext();
2223
const [isNamespaceDropdownOpen, setIsNamespaceDropdownOpen] = useState<boolean>(false);
2324
const [searchInputValue, setSearchInputValue] = useState<string>('');
2425
const [filteredNamespaces, setFilteredNamespaces] = useState<string[]>(namespaces);
@@ -54,6 +55,7 @@ const NamespaceSelector: FC = () => {
5455

5556
const onSelect: DropdownProps['onSelect'] = (_event, value) => {
5657
setSelectedNamespace(value as string);
58+
updateLastUsedNamespace(value as string);
5759
setIsNamespaceDropdownOpen(false);
5860
};
5961

0 commit comments

Comments
 (0)