Skip to content

Commit

Permalink
get localstorage support (#2190) (#2234)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
dinhlongviolin1 and FabienLelaquais authored Jan 17, 2025
1 parent 9db15e4 commit f64ab3c
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 28 deletions.
3 changes: 2 additions & 1 deletion frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export type WsMessageType =
| "AID"
| "GR"
| "FV"
| "BC";
| "BC"
| "LS"
export interface WsMessage {
type: WsMessageType | string;
name: string;
Expand Down
5 changes: 4 additions & 1 deletion frontend/taipy-gui/src/components/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -125,7 +128,7 @@ const Router = () => {
<MainPage
path={routes["/"]}
route={Object.keys(routes).find(
(path) => path !== "/"
(path) => path !== "/",
)}
/>
}
Expand Down
41 changes: 26 additions & 15 deletions frontend/taipy-gui/src/context/taipyReducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -48,6 +48,7 @@ export enum Types {
Partial = "PARTIAL",
Acknowledgement = "ACKNOWLEDGEMENT",
Broadcast = "BROADCAST",
LocalStorage = "LOCAL_STORAGE",
}

/**
Expand Down Expand Up @@ -180,7 +181,7 @@ const getUserTheme = (mode: PaletteMode) => {
},
},
},
})
}),
);
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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:
Expand All @@ -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;
Expand Down Expand Up @@ -545,7 +550,7 @@ export const createSendUpdateAction = (
context: string | undefined,
onChange?: string,
propagate = true,
relName?: string
relName?: string,
): TaipyAction => ({
type: Types.SendUpdate,
name: name,
Expand Down Expand Up @@ -598,7 +603,7 @@ export const createRequestChartUpdateAction = (
context: string | undefined,
columns: string[],
pageKey: string,
decimatorPayload: unknown | undefined
decimatorPayload: unknown | undefined,
): TaipyAction =>
createRequestDataUpdateAction(
name,
Expand All @@ -609,7 +614,7 @@ export const createRequestChartUpdateAction = (
{
decimatorPayload: decimatorPayload,
},
true
true,
);

export const createRequestTableUpdateAction = (
Expand All @@ -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,
Expand All @@ -654,7 +659,7 @@ export const createRequestTableUpdateAction = (
compare,
compare_datas: compareDatas,
state_context: stateContext,
})
}),
);

export const createRequestInfiniteTableUpdateAction = (
Expand All @@ -677,7 +682,7 @@ export const createRequestInfiniteTableUpdateAction = (
compare?: string,
compareDatas?: string,
stateContext?: Record<string, unknown>,
reverse?: boolean
reverse?: boolean,
): TaipyAction =>
createRequestDataUpdateAction(
name,
Expand All @@ -702,7 +707,7 @@ export const createRequestInfiniteTableUpdateAction = (
compare_datas: compareDatas,
state_context: stateContext,
reverse: !!reverse,
})
}),
);

/**
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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: "",
Expand Down Expand Up @@ -846,7 +851,7 @@ export const createNavigateAction = (
to?: string,
params?: Record<string, string>,
tab?: string,
force?: boolean
force?: boolean,
): TaipyNavigateAction => ({
type: Types.Navigate,
to,
Expand Down Expand Up @@ -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,
});
3 changes: 2 additions & 1 deletion frontend/taipy-gui/src/context/wsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export type WsMessageType =
| "AID"
| "GR"
| "FV"
| "BC";
| "BC"
| "LS";

export interface WsMessage {
type: WsMessageType;
Expand Down
16 changes: 16 additions & 0 deletions frontend/taipy-gui/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions taipy/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
from ._renderers.json import JsonAdapter
from .gui_actions import (
broadcast_callback,
close_notification,
download,
get_module_context,
get_module_name_from_state,
Expand All @@ -87,6 +88,7 @@
invoke_long_callback,
navigate,
notify,
query_local_storage,
resume_control,
)
from .icon import Icon
Expand Down
13 changes: 10 additions & 3 deletions taipy/gui/data/data_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@
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

Expand Down Expand Up @@ -66,7 +73,7 @@ def create_scope(self, id: str) -> None:
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):
Expand Down
27 changes: 27 additions & 0 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,8 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None:
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"))
Expand Down Expand Up @@ -1368,6 +1370,31 @@ def __handle_ws_get_routes(self):
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:
Expand Down
32 changes: 29 additions & 3 deletions taipy/gui/gui_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -442,3 +442,29 @@ def thread_status(name: str, period_s: float, count: int):
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
Loading

0 comments on commit f64ab3c

Please sign in to comment.