Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get localstorage support (#2190) #2234

Merged
merged 13 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading