Line data Source code
1 : import * as React from 'react';
2 :
3 : import { useLocation, useNavigate } from 'react-router-dom';
4 :
5 : /// How a session is shown
6 1 : export enum SessionViewMode {
7 1 : Session = 0,
8 1 : Shell = 1,
9 1 : Dashboard = 2,
10 1 : Dataflow = 3,
11 1 : }
12 :
13 : /// The views that are part of the URL, as the value of the `view` query parameter
14 1 : const VIEW_PARAMETERS: [SessionViewMode, string][] = [
15 1 : [SessionViewMode.Dashboard, 'dashboard'],
16 1 : [SessionViewMode.Dataflow, 'dataflow'],
17 1 : ];
18 :
19 : export interface SessionViewModeContextValue {
20 : mode: SessionViewMode;
21 : setMode: React.Dispatch<React.SetStateAction<SessionViewMode>>;
22 : }
23 1 : const SESSION_VIEW_MODE_CTX = React.createContext<SessionViewModeContextValue | null>(null);
24 1 : export const useSessionViewMode = () => React.useContext(SESSION_VIEW_MODE_CTX)!;
25 :
26 : /// The view of the `view` query parameter, the session if the view is unknown
27 1 : export function sessionViewModeFromSearch(search: string): SessionViewMode {
28 1 : const value = new URLSearchParams(search).get('view');
29 1 : const view = VIEW_PARAMETERS.find(([, parameter]) => parameter === value);
30 1 : return view != null ? view[0] : SessionViewMode.Session;
31 1 : }
32 :
33 1 : export function sessionViewSearch(search: string, mode: SessionViewMode): string {
34 1 : const params = new URLSearchParams(search);
35 1 : const view = VIEW_PARAMETERS.find(([m]) => m === mode);
36 1 : if (view != null) params.set('view', view[1]);
37 1 : else params.delete('view');
38 1 : const next = params.toString();
39 1 : return next ? `?${next}` : '';
40 1 : }
41 :
42 1 : export function sessionViewNavigation(
43 1 : location: { pathname: string; search: string; hash: string; state: unknown },
44 1 : mode: SessionViewMode,
45 1 : ) {
46 1 : return {
47 1 : to: {
48 1 : pathname: location.pathname,
49 1 : search: sessionViewSearch(location.search, mode),
50 1 : hash: location.hash,
51 1 : },
52 1 : options: { replace: true as const, state: location.state },
53 1 : };
54 1 : }
55 :
56 : /// Is a view mode part of the URL?
57 1 : function inSearch(mode: SessionViewMode): boolean {
58 1 : return VIEW_PARAMETERS.some(([m]) => m === mode);
59 1 : }
60 :
61 : /// The view mode of the session. The dashboard and dataflow views are part of the URL.
62 1 : export const SessionViewModeProvider: React.FC<React.PropsWithChildren> = (props: React.PropsWithChildren) => {
63 1 : const location = useLocation();
64 1 : const navigate = useNavigate();
65 :
66 1 : const [sessionViewMode, setSessionViewModeState] = React.useState(() => sessionViewModeFromSearch(location.search));
67 1 : const sessionViewModeRef = React.useRef(sessionViewMode);
68 1 : const pendingViewSearchRef = React.useRef<string | null>(null);
69 1 : sessionViewModeRef.current = sessionViewMode;
70 1 : const setSessionViewMode = React.useCallback<React.Dispatch<React.SetStateAction<SessionViewMode>>>((update) => {
71 1 : const next = typeof update === 'function' ? update(sessionViewModeRef.current) : update;
72 1 : sessionViewModeRef.current = next;
73 1 : setSessionViewModeState(next);
74 1 : const navigation = sessionViewNavigation(location, next);
75 1 : if (navigation.to.search !== location.search) {
76 1 : pendingViewSearchRef.current = navigation.to.search;
77 1 : navigate(navigation.to, navigation.options);
78 1 : }
79 1 : }, [location.pathname, location.search, location.hash, location.state, navigate]);
80 1 : const sessionViewModeValue = React.useMemo<SessionViewModeContextValue>(
81 1 : () => ({ mode: sessionViewMode, setMode: setSessionViewMode }),
82 1 : [sessionViewMode, setSessionViewMode],
83 1 : );
84 :
85 1 : React.useEffect(() => {
86 1 : const requested = sessionViewModeFromSearch(location.search);
87 1 : const parameter = new URLSearchParams(location.search).get('view');
88 : // An unknown view falls back to the session, also in the URL
89 1 : if (parameter != null && !inSearch(requested)) {
90 1 : pendingViewSearchRef.current = null;
91 1 : const navigation = sessionViewNavigation(location, SessionViewMode.Session);
92 1 : navigate(navigation.to, navigation.options);
93 1 : return;
94 1 : }
95 1 : if (pendingViewSearchRef.current != null) {
96 1 : if (pendingViewSearchRef.current === location.search) pendingViewSearchRef.current = null;
97 1 : return;
98 1 : }
99 1 : if (inSearch(requested) && sessionViewModeRef.current !== requested) {
100 1 : sessionViewModeRef.current = requested;
101 1 : setSessionViewModeState(requested);
102 1 : } else if (!inSearch(requested) && inSearch(sessionViewModeRef.current)) {
103 1 : sessionViewModeRef.current = SessionViewMode.Session;
104 1 : setSessionViewModeState(SessionViewMode.Session);
105 1 : }
106 1 : }, [location.search, navigate]);
107 :
108 1 : return (
109 1 : <SESSION_VIEW_MODE_CTX.Provider value={sessionViewModeValue}>
110 1 : {props.children}
111 1 : </SESSION_VIEW_MODE_CTX.Provider>
112 : );
113 1 : };
|