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

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
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
54 changes: 38 additions & 16 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,8 @@ export enum Types {
Partial = "PARTIAL",
Acknowledgement = "ACKNOWLEDGEMENT",
Broadcast = "BROADCAST",
LocalStorage = "LOCAL_STORAGE",
LocalStorageUpdate = "LOCAL_STORAGE_UPDATE",
}

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

Expand Down Expand Up @@ -225,7 +227,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 +269,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 @@ -393,7 +396,7 @@ export const taipyReducer = (state: TaipyState, baseAction: TaipyBaseAction): Ta
const deleteAlertAction = action as unknown as TaipyAlertAction;
return {
...state,
alerts: state.alerts.filter(alert => alert.notificationId !== deleteAlertAction.notificationId),
alerts: state.alerts.filter((alert) => alert.notificationId !== deleteAlertAction.notificationId),
};
case Types.SetBlock:
const blockAction = action as unknown as TaipyBlockAction;
Expand Down Expand Up @@ -495,7 +498,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 +510,10 @@ 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:
case Types.LocalStorageUpdate:
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 +552,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 +605,7 @@ export const createRequestChartUpdateAction = (
context: string | undefined,
columns: string[],
pageKey: string,
decimatorPayload: unknown | undefined
decimatorPayload: unknown | undefined,
): TaipyAction =>
createRequestDataUpdateAction(
name,
Expand All @@ -609,7 +616,7 @@ export const createRequestChartUpdateAction = (
{
decimatorPayload: decimatorPayload,
},
true
true,
);

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

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

/**
Expand Down Expand Up @@ -733,7 +740,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 +778,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 +853,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 +889,18 @@ export const createPartialAction = (name: string, create: boolean): TaipyPartial
name,
create,
});

export const createLocalStorageAction = (localStorageData: Record<string, string>): TaipyAction => ({
type: Types.LocalStorage,
name: "init",
payload: localStorageData,
});

export const createLocalStorageUpdateAction = (key: string, value: string | null): TaipyAction => ({
type: Types.LocalStorageUpdate,
name: "update",
payload: {
key: key,
value: value,
},
});
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
dinhlongviolin1 marked this conversation as resolved.
Show resolved Hide resolved
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 };
97 changes: 97 additions & 0 deletions frontend/taipy-gui/src/hooks/useLocalStorageWithEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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, createLocalStorageUpdateAction, TaipyBaseAction } from "../context/taipyReducers";

const STORAGE_EVENT = "storage";
const CUSTOM_LOCAL_STORAGE_EVENT = "local-storage";

export const useLocalStorageWithEvent = (dispatch: Dispatch<TaipyBaseAction>) => {
// Override the original setItem and removeItem behaviour for localStorage to dispatch a custom storage event for local tab
useEffect(() => {
// Preserve the original setItem and removeItem method
const _setItem = Storage.prototype.setItem;
const _removeItem = Storage.prototype.removeItem;

Storage.prototype.setItem = function (key, value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit invasive ...
Any other way ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried multiple ways but to be able to catch an event on the local tab, I still need to modify the original implementation

if (this === window.localStorage) {
const oldValue = localStorage.getItem(key);
_setItem.call(this, key, value);

const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, {
detail: { key, oldValue, newValue: value },
});
window.dispatchEvent(customEvent);
} else {
_setItem.call(this, key, value);
}
};

Storage.prototype.removeItem = function (key: string) {
if (this === window.localStorage) {
const oldValue = localStorage.getItem(key);
_removeItem.call(this, key);

const customEvent = new CustomEvent(CUSTOM_LOCAL_STORAGE_EVENT, {
detail: { key, oldValue, newValue: null },
});
window.dispatchEvent(customEvent);
} else {
_removeItem.call(this, key);
}
};

// Cleanup the override on unmount
return () => {
Storage.prototype.setItem = _setItem;
Storage.prototype.removeItem = _removeItem;
};
}, []);

// addEventListener for storage and custom storage event
useEffect(() => {
const handleStorageEvent = (
event: StorageEvent | CustomEvent<{ key: string; oldValue: string | null; newValue: string | null }>,
) => {
const isCustomEvent = event instanceof CustomEvent;
const key = isCustomEvent ? event.detail.key : event.key;
const newValue = isCustomEvent ? event.detail.newValue : event.newValue;
if (!key) {
return;
}
dispatch(createLocalStorageUpdateAction(key, newValue));
};

window.addEventListener(STORAGE_EVENT, handleStorageEvent as EventListener);
window.addEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener);

// Cleanup event listener on unmount
return () => {
window.removeEventListener(STORAGE_EVENT, handleStorageEvent as EventListener);
window.removeEventListener(CUSTOM_LOCAL_STORAGE_EVENT, handleStorageEvent as EventListener);
};
}, [dispatch]); // Not necessary to add dispatch to the dependency array but comply with eslint warning anyway

// 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
};
1 change: 1 addition & 0 deletions taipy/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
from .gui_actions import (
broadcast_callback,
download,
get_local_storage,
get_module_context,
get_module_name_from_state,
get_state_id,
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
Loading
Loading