Просмотр исходного кода

get localstorage support (#2190) (#2234)

* frontend done

* add support to get localStorage from backend (#2190)

* add on_loca_storage_change

* update get_local_storage function

* update local storage simplify

* Improve documentation

* Doc formatting

* Fix RefMan generation bug.

---------

Co-authored-by: Fabien Lelaquais <fabien.lelaquais@avaiga.com>
Dinh Long Nguyen 4 месяцев назад
Родитель
Сommit
f64ab3ca52

+ 2 - 1
frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts

@@ -76,7 +76,8 @@ export type WsMessageType =
     | "AID"
     | "GR"
     | "FV"
-    | "BC";
+    | "BC"
+    | "LS"
 export interface WsMessage {
     type: WsMessageType | string;
     name: string;

+ 4 - 1
frontend/taipy-gui/src/components/Router.tsx

@@ -44,6 +44,7 @@ import MainPage from "./pages/MainPage";
 import TaipyRendered from "./pages/TaipyRendered";
 import NotFound404 from "./pages/NotFound404";
 import { getBaseURL } from "../utils";
+import { useLocalStorageWithEvent } from "../hooks";
 
 interface AxiosRouter {
     router: string;
@@ -63,6 +64,8 @@ const Router = () => {
     const themeClass = "taipy-" + state.theme.palette.mode;
     const baseURL = getBaseURL();
 
+    useLocalStorageWithEvent(dispatch);
+
     useEffect(() => {
         if (refresh) {
             // no need to access the backend again, the routes are static
@@ -125,7 +128,7 @@ const Router = () => {
                                                                 <MainPage
                                                                     path={routes["/"]}
                                                                     route={Object.keys(routes).find(
-                                                                        (path) => path !== "/"
+                                                                        (path) => path !== "/",
                                                                     )}
                                                                 />
                                                             }

+ 26 - 15
frontend/taipy-gui/src/context/taipyReducers.ts

@@ -16,7 +16,7 @@ import { createTheme, Theme } from "@mui/material/styles";
 import merge from "lodash/merge";
 import { Dispatch } from "react";
 import { io, Socket } from "socket.io-client";
-import { nanoid } from 'nanoid';
+import { nanoid } from "nanoid";
 
 import { FilterDesc } from "../components/Taipy/tableUtils";
 import { stylekitModeThemes, stylekitTheme } from "../themes/stylekit";
@@ -48,6 +48,7 @@ export enum Types {
     Partial = "PARTIAL",
     Acknowledgement = "ACKNOWLEDGEMENT",
     Broadcast = "BROADCAST",
+    LocalStorage = "LOCAL_STORAGE",
 }
 
 /**
@@ -180,7 +181,7 @@ const getUserTheme = (mode: PaletteMode) => {
                     },
                 },
             },
-        })
+        }),
     );
 };
 
@@ -225,7 +226,7 @@ export const messageToAction = (message: WsMessage) => {
                 (message as unknown as NavigateMessage).to,
                 (message as unknown as NavigateMessage).params,
                 (message as unknown as NavigateMessage).tab,
-                (message as unknown as NavigateMessage).force
+                (message as unknown as NavigateMessage).force,
             );
         } else if (message.type === "ID") {
             return createIdAction((message as unknown as IdMessage).id);
@@ -267,7 +268,8 @@ export const getWsMessageListener = (dispatch: Dispatch<TaipyBaseAction>) => {
 // Broadcast
 const __BroadcastRepo: Record<string, Array<unknown>> = {};
 
-const stackBroadcast = (name: string, value: unknown) => (__BroadcastRepo[name] = __BroadcastRepo[name] || []).push(value);
+const stackBroadcast = (name: string, value: unknown) =>
+    (__BroadcastRepo[name] = __BroadcastRepo[name] || []).push(value);
 
 const broadcast_timeout = 250;
 
@@ -495,7 +497,7 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta
                 action.payload,
                 state.id,
                 action.context,
-                action.propagate
+                action.propagate,
             );
             break;
         case Types.Action:
@@ -507,6 +509,9 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta
         case Types.RequestUpdate:
             ackId = sendWsMessage(state.socket, "RU", action.name, action.payload, state.id, action.context);
             break;
+        case Types.LocalStorage:
+            ackId = sendWsMessage(state.socket, "LS", action.name, action.payload, state.id, action.context);
+            break;
     }
     if (ackId) return { ...state, ackList: [...state.ackList, ackId] };
     return state;
@@ -545,7 +550,7 @@ export const createSendUpdateAction = (
     context: string | undefined,
     onChange?: string,
     propagate = true,
-    relName?: string
+    relName?: string,
 ): TaipyAction => ({
     type: Types.SendUpdate,
     name: name,
@@ -598,7 +603,7 @@ export const createRequestChartUpdateAction = (
     context: string | undefined,
     columns: string[],
     pageKey: string,
-    decimatorPayload: unknown | undefined
+    decimatorPayload: unknown | undefined,
 ): TaipyAction =>
     createRequestDataUpdateAction(
         name,
@@ -609,7 +614,7 @@ export const createRequestChartUpdateAction = (
         {
             decimatorPayload: decimatorPayload,
         },
-        true
+        true,
     );
 
 export const createRequestTableUpdateAction = (
@@ -631,7 +636,7 @@ export const createRequestTableUpdateAction = (
     filters?: Array<FilterDesc>,
     compare?: string,
     compareDatas?: string,
-    stateContext?: Record<string, unknown>
+    stateContext?: Record<string, unknown>,
 ): TaipyAction =>
     createRequestDataUpdateAction(
         name,
@@ -654,7 +659,7 @@ export const createRequestTableUpdateAction = (
             compare,
             compare_datas: compareDatas,
             state_context: stateContext,
-        })
+        }),
     );
 
 export const createRequestInfiniteTableUpdateAction = (
@@ -677,7 +682,7 @@ export const createRequestInfiniteTableUpdateAction = (
     compare?: string,
     compareDatas?: string,
     stateContext?: Record<string, unknown>,
-    reverse?: boolean
+    reverse?: boolean,
 ): TaipyAction =>
     createRequestDataUpdateAction(
         name,
@@ -702,7 +707,7 @@ export const createRequestInfiniteTableUpdateAction = (
             compare_datas: compareDatas,
             state_context: stateContext,
             reverse: !!reverse,
-        })
+        }),
     );
 
 /**
@@ -733,7 +738,7 @@ export const createRequestDataUpdateAction = (
     pageKey: string,
     payload: Record<string, unknown>,
     allData = false,
-    library?: string
+    library?: string,
 ): TaipyAction => {
     payload = payload || {};
     if (id !== undefined) {
@@ -771,7 +776,7 @@ export const createRequestUpdateAction = (
     context: string | undefined,
     names: string[],
     forceRefresh = false,
-    stateContext?: Record<string, unknown>
+    stateContext?: Record<string, unknown>,
 ): TaipyAction => ({
     type: Types.RequestUpdate,
     name: "",
@@ -846,7 +851,7 @@ export const createNavigateAction = (
     to?: string,
     params?: Record<string, string>,
     tab?: string,
-    force?: boolean
+    force?: boolean,
 ): TaipyNavigateAction => ({
     type: Types.Navigate,
     to,
@@ -882,3 +887,9 @@ export const createPartialAction = (name: string, create: boolean): TaipyPartial
     name,
     create,
 });
+
+export const createLocalStorageAction = (localStorageData: Record<string, string>): TaipyAction => ({
+    type: Types.LocalStorage,
+    name: "",
+    payload: localStorageData,
+});

+ 2 - 1
frontend/taipy-gui/src/context/wsUtils.ts

@@ -22,7 +22,8 @@ export type WsMessageType =
     | "AID"
     | "GR"
     | "FV"
-    | "BC";
+    | "BC"
+    | "LS";
 
 export interface WsMessage {
     type: WsMessageType;

+ 16 - 0
frontend/taipy-gui/src/hooks/index.ts

@@ -0,0 +1,16 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import { useLocalStorageWithEvent } from "./useLocalStorageWithEvent";
+
+export { useLocalStorageWithEvent };

+ 29 - 0
frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021-2024 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import { Dispatch, useEffect } from "react";
+import { createLocalStorageAction, TaipyBaseAction } from "../context/taipyReducers";
+
+export const useLocalStorageWithEvent = (dispatch: Dispatch<TaipyBaseAction>) => {
+    // send all localStorage data to backend on init
+    useEffect(() => {
+        const localStorageData: Record<string, string> = {};
+        for (let i = 0; i < localStorage.length; i++) {
+            const key = localStorage.key(i);
+            if (key) {
+                localStorageData[key] = localStorage.getItem(key) || "";
+            }
+        }
+        dispatch(createLocalStorageAction(localStorageData));
+    }, [dispatch]); // Not necessary to add dispatch to the dependency array but comply with eslint warning anyway
+};

+ 2 - 0
taipy/gui/__init__.py

@@ -77,6 +77,7 @@ from ._renderers import Html, Markdown
 from ._renderers.json import JsonAdapter
 from .gui_actions import (
     broadcast_callback,
+    close_notification,
     download,
     get_module_context,
     get_module_name_from_state,
@@ -87,6 +88,7 @@ from .gui_actions import (
     invoke_long_callback,
     navigate,
     notify,
+    query_local_storage,
     resume_control,
 )
 from .icon import Icon

+ 10 - 3
taipy/gui/data/data_scope.py

@@ -23,17 +23,24 @@ if t.TYPE_CHECKING:
 class _DataScopes:
     _GLOBAL_ID = "global"
     _META_PRE_RENDER = "pre_render"
-    _DEFAULT_METADATA = {_META_PRE_RENDER: False}
+    _META_LOCAL_STORAGE = "local_storage"
+    _DEFAULT_METADATA = {_META_PRE_RENDER: False, _META_LOCAL_STORAGE: {}}
 
     def __init__(self, gui: "Gui") -> None:
         self.__gui = gui
         self.__scopes: t.Dict[str, SimpleNamespace] = {_DataScopes._GLOBAL_ID: SimpleNamespace()}
         # { scope_name: { metadata: value } }
         self.__scopes_metadata: t.Dict[str, t.Dict[str, t.Any]] = {
-            _DataScopes._GLOBAL_ID: _DataScopes._DEFAULT_METADATA.copy()
+            _DataScopes._GLOBAL_ID: _DataScopes._get_new_default_metadata()
         }
         self.__single_client = True
 
+    @staticmethod
+    def _get_new_default_metadata() -> t.Dict[str, t.Any]:
+        metadata = _DataScopes._DEFAULT_METADATA.copy()
+        metadata[_DataScopes._META_LOCAL_STORAGE] = {}
+        return metadata
+
     def set_single_client(self, value: bool) -> None:
         self.__single_client = value
 
@@ -66,7 +73,7 @@ class _DataScopes:
             return
         if id not in self.__scopes:
             self.__scopes[id] = SimpleNamespace()
-            self.__scopes_metadata[id] = _DataScopes._DEFAULT_METADATA.copy()
+            self.__scopes_metadata[id] = _DataScopes._get_new_default_metadata()
             # Propagate shared variables to the new scope from the global scope
             for var in self.__gui._get_shared_variables():
                 if hasattr(self.__scopes[_DataScopes._GLOBAL_ID], var):

+ 27 - 0
taipy/gui/gui.py

@@ -730,6 +730,8 @@ class Gui:
                         self.__handle_ws_app_id(message)
                     elif msg_type == _WsType.GET_ROUTES.value:
                         self.__handle_ws_get_routes()
+                    elif msg_type == _WsType.LOCAL_STORAGE.value:
+                        self.__handle_ws_local_storage(message)
                     else:
                         self._manage_external_message(msg_type, message)
                 self.__send_ack(message.get("ack_id"))
@@ -1368,6 +1370,31 @@ class Gui:
             send_back_only=True,
         )
 
+    def __handle_ws_local_storage(self, message: t.Any):
+        if not isinstance(message, dict):
+            return
+        payload = message.get("payload", None)
+        scope_meta_ls = self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE]
+        if payload is None:
+            return
+        for key, value in payload.items():
+            if value is not None and scope_meta_ls.get(key) != value:
+                scope_meta_ls[key] = value
+
+    def _query_local_storage(self, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]:
+        if not keys:
+            return None
+        if len(keys) == 1:
+            if keys[0] in self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE]:
+                return self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE][keys[0]]
+            return None
+        # case of multiple keys
+        ls_items = {}
+        for key in keys:
+            if key in self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE]:
+                ls_items[key] = self._get_data_scope_metadata()[_DataScopes._META_LOCAL_STORAGE][key]
+        return ls_items
+
     def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None:
         grouping_message = self.__get_message_grouping() if allow_grouping else None
         if grouping_message is None:

+ 29 - 3
taipy/gui/gui_actions.py

@@ -226,8 +226,8 @@ def get_state_id(state: State) -> t.Optional[str]:
         state (State^): The current user state as received in any callback.
 
     Returns:
-        A string that uniquely identifies the state. If this value None, it indicates that *state* is not
-        handled by a `Gui^` instance.
+        A string that uniquely identifies the state.<br/>
+            If this value None, it indicates that *state* is not handled by a `Gui^` instance.
     """
     if state and isinstance(state._gui, Gui):
         return state._gui._get_client_id()
@@ -241,7 +241,7 @@ def get_module_context(state: State) -> t.Optional[str]:
         state (State^): The current user state as received in any callback.
 
     Returns:
-        The name of the current module
+        The name of the current module.
     """
     if state and isinstance(state._gui, Gui):
         return state._gui._get_locals_context()
@@ -442,3 +442,29 @@ def invoke_long_callback(
     thread.start()
     if isinstance(period, int) and period >= 500 and _is_function(user_status_function):
         thread_status(thread.name, period / 1000.0, 0)
+
+
+def query_local_storage(state: State, *keys: str) -> t.Optional[t.Union[str, t.Dict[str, str]]]:
+    """Retrieve values from the browser's local storage.
+
+    This function queries the local storage of the client identified by *state* and returns the
+    values associated with the specified keys. Local storage is a key-value store available in the
+    user's browser, typically manipulated by client-side code.
+
+    Arguments:
+        state (State^): The current user state as received in any callback.
+        *keys (string): One or more keys to retrieve values for from the client's local storage.
+
+    Returns:
+        The requested values from the browser's local storage.
+
+            - If a single key is provided (*keys* has a single element), this function returns the
+              corresponding value as a string.
+            - If multiple keys are provided, this function returns a dictionary mapping each key to
+              its value in the client's local storage.
+            - If no value is found for a key, that key will not appear in the dictionary.
+    """
+    if state and isinstance(state._gui, Gui):
+        return state._gui._query_local_storage(*keys)
+    _warn("'query_local_storage()' must be called in the context of a callback.")
+    return None

+ 3 - 4
taipy/gui/types.py

@@ -53,6 +53,7 @@ class _WsType(Enum):
     GET_ROUTES = "GR"
     FAVICON = "FV"
     BROADCAST = "BC"
+    LOCAL_STORAGE = "LS"
 
 
 NumberTypes = {"int", "int64", "float", "float64"}
@@ -158,8 +159,7 @@ class PropertyType(Enum):
 
 
 @t.overload  # noqa: F811
-def _get_taipy_type(a_type: None) -> None:
-    ...
+def _get_taipy_type(a_type: None) -> None: ...
 
 
 @t.overload
@@ -175,8 +175,7 @@ def _get_taipy_type(a_type: PropertyType) -> t.Type[_TaipyBase]:  # noqa: F811
 @t.overload
 def _get_taipy_type(  # noqa: F811
     a_type: t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]],
-) -> t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]]:
-    ...
+) -> t.Optional[t.Union[t.Type[_TaipyBase], t.Type[Decimator], PropertyType]]: ...
 
 
 def _get_taipy_type(  # noqa: F811