diff --git a/src/aimcore/web/ui/public/aim_ui_core.py b/src/aimcore/web/ui/public/aim_ui_core.py index 994d76010f..191603189e 100644 --- a/src/aimcore/web/ui/public/aim_ui_core.py +++ b/src/aimcore/web/ui/public/aim_ui_core.py @@ -2,7 +2,7 @@ # Bindings for fetching Aim Objects #################### -from js import search, runFunction, findItem +from js import search, runFunction, findItem, encodeURIComponent import json import hashlib @@ -60,6 +60,10 @@ def query_filter(type_, query="", count=None, start=None, stop=None, is_sequence try: data = search(board_path, type_, query, count, start, stop, is_sequence) + + if data is None: + raise WaitForQueryError() + data = json.loads(data) query_results_cache[query_key] = data @@ -271,7 +275,7 @@ def group(name, data, options, key=None): state = {} -def set_state(update, board_path, persist=False): +def set_state(update, board_path, persist=True): from js import setState if board_path not in state: @@ -361,17 +365,18 @@ def __init__(self, key, type_, block): ) self.no_facet = True - def set_state(self, value): + def set_state(self, value, persist=True): should_batch = ( self.parent_block is not None and self.parent_block["type"] == "form" ) if should_batch: + state_key = f'__form__{self.parent_block["id"]}' state_slice = ( - state[self.board_path][self.parent_block["id"]] + state[self.board_path][state_key] if ( self.board_path in state - and self.parent_block["id"] in state[self.board_path] + and state_key in state[self.board_path] ) else {} ) @@ -384,7 +389,7 @@ def set_state(self, value): state_slice.update({self.key: component_state_slice}) - set_state({self.parent_block["id"]: state_slice}, self.board_path) + set_state({state_key: state_slice}, self.board_path, persist=False) else: state_slice = ( state[self.board_path][self.key] @@ -394,7 +399,7 @@ def set_state(self, value): state_slice.update(value) - set_state({self.key: state_slice}, self.board_path) + set_state({self.key: state_slice}, self.board_path, persist=persist) def render(self): component_data = { @@ -874,7 +879,7 @@ def on_row_select(self, val): selected_indices = val.to_py() if selected_indices is None: - self.set_state({"selected_rows": None, "selected_rows_indices": None}) + self.set_state({"selected_rows": None, "selected_rows_indices": None}, persist=False) return rows = [] @@ -887,7 +892,7 @@ def on_row_select(self, val): rows.append(row) - self.set_state({"selected_rows": rows, "selected_rows_indices": selected_indices}) + self.set_state({"selected_rows": rows, "selected_rows_indices": selected_indices}, persist=False) def on_row_focus(self, val): if val is None: @@ -1652,17 +1657,14 @@ def __init__(self, path, state={}, block=None, key=None): self.data = path - self.board_state = state - - self.callbacks = {"on_mount": self.on_mount} + self.state_str = json.dumps(state) + + self.options = {"state_str": self.state_str} self.render() def get_state(self): return state[self.data] if self.data in state else None - - def on_mount(self): - set_state(self.board_state or {}, self.data) class BoardLink(Component): @@ -1681,17 +1683,15 @@ def __init__( self.data = path + if state: + self.data += "?state=" + encodeURIComponent(json.dumps(state)) + self.board_state = state self.options = {"text": text, "new_tab": new_tab} - self.callbacks = {"on_navigation": self.on_navigation} - self.render() - def on_navigation(self): - if self.board_state is not None: - set_state(self.board_state, self.data) class UI: @@ -1906,7 +1906,7 @@ def __init__(self, submit_button_label="Submit", block=None): self.render() def submit(self): - batch_id = self.block_context["id"] + batch_id = f'__form__{self.block_context["id"]}' state_update = state[board_path][batch_id] set_state(state_update, board_path=self.board_path) diff --git a/src/aimcore/web/ui/src/pages/App/App.d.ts b/src/aimcore/web/ui/src/pages/App/App.d.ts index 1c992ad15f..37328c3d51 100644 --- a/src/aimcore/web/ui/src/pages/App/App.d.ts +++ b/src/aimcore/web/ui/src/pages/App/App.d.ts @@ -16,4 +16,5 @@ export interface AppWrapperProps { boardPath: string; editMode: boolean; boardList: string[]; + stateStr?: str | null; } diff --git a/src/aimcore/web/ui/src/pages/App/App.tsx b/src/aimcore/web/ui/src/pages/App/App.tsx index 6a53f9fdd3..341b604a6b 100644 --- a/src/aimcore/web/ui/src/pages/App/App.tsx +++ b/src/aimcore/web/ui/src/pages/App/App.tsx @@ -15,7 +15,9 @@ function App(): React.FunctionComponentElement { {isLoading ? null : ( - {(props) => } + {(props) => ( + + )} )} { + let searchParams = new URLSearchParams(window.location.search); + let stateParam = searchParams.get('state'); + + if (stateParam) { + return decodeURIComponent(stateParam); + } + + return ''; + }, []); + + let [stateStr, setStateStr] = React.useState(getStateFromURL()); + + const updateStateStr = React.useCallback(() => { + setStateStr(getStateFromURL()); + }, []); + React.useEffect(() => { if (boardPath) { document.title = `${boardPath} | Aim`; } + updateStateStr(); }, [boardPath]); + React.useEffect(() => { + (function (history: any) { + var pushState = history.pushState; + history.pushState = function (state: any) { + if (typeof history.onpushstate == 'function') { + history.onpushstate({ state: state }); + } + + return pushState.apply(history, arguments); + }; + })(window.history); + + window.onpopstate = history.onpushstate = updateStateStr; + }, []); + return ( - + ); } diff --git a/src/aimcore/web/ui/src/pages/App/components/AppWrapper.tsx b/src/aimcore/web/ui/src/pages/App/components/AppWrapper.tsx index 6dd47753b9..f12c4ab6c0 100644 --- a/src/aimcore/web/ui/src/pages/App/components/AppWrapper.tsx +++ b/src/aimcore/web/ui/src/pages/App/components/AppWrapper.tsx @@ -15,7 +15,12 @@ import { AppContainer, BoardWrapper } from '../App.style'; import AppSidebar from './AppSidebar'; -function AppWrapper({ boardPath, editMode, boardList }: AppWrapperProps) { +function AppWrapper({ + boardPath, + editMode, + boardList, + stateStr, +}: AppWrapperProps) { const path = boardPath?.replace('/edit', ''); const board = useBoardStore((state) => state.boards?.[path]); const fetchBoard = useBoardStore((state) => state.fetchBoard); @@ -93,6 +98,7 @@ function AppWrapper({ boardPath, editMode, boardList }: AppWrapperProps) { key={board.path + editMode} data={board} editMode={editMode} + stateStr={stateStr} // saveBoard={saveBoard} /> )} diff --git a/src/aimcore/web/ui/src/pages/Board/Board.tsx b/src/aimcore/web/ui/src/pages/Board/Board.tsx index 2d3c40dc70..e43eca791f 100644 --- a/src/aimcore/web/ui/src/pages/Board/Board.tsx +++ b/src/aimcore/web/ui/src/pages/Board/Board.tsx @@ -51,8 +51,8 @@ function Board({ newMode, notifyData, onNotificationDelete, -}: // saveBoard, -any): React.FunctionComponentElement { + stateStr, +}: any): React.FunctionComponentElement { const [mounted, setMounted] = React.useState(false); const { isLoading: pyodideIsLoading, @@ -146,8 +146,11 @@ any): React.FunctionComponentElement { block_context = { "current": 0, } + current_layout = [] + board_path = ${boardPath === undefined ? 'None' : `"${boardPath}"`} + session_state = state[board_path] if board_path in state else {} def set_session_state(state_slice): set_state(state_slice, board_path) @@ -186,7 +189,14 @@ def set_session_state(state_slice): })); } } - }, [pyodide, pyodideIsLoading, boardPath, state.execCode, namespace]); + }, [ + pyodide, + pyodideIsLoading, + boardPath, + state.execCode, + namespace, + stateStr, + ]); React.useEffect(() => { if (pyodide !== null && pyodideIsLoading === false) { @@ -206,6 +216,29 @@ def set_session_state(state_slice): } }, [state.executionCount]); + React.useEffect(() => { + if (pyodide && namespace) { + pyodide.runPython( + ` +board_path = ${boardPath === undefined ? 'None' : `"${boardPath}"`} + +if board_path not in state: + state[board_path] = {} + +state_str = ${JSON.stringify(stateStr)} + +if len(state_str) > 0: + state[board_path] = json.loads(state_str) +else: + state[board_path] = {} + `, + { globals: namespace }, + ); + + runParsedCode(); + } + }, [stateStr, pyodide, namespace]); + React.useEffect(() => { if (pyodideIsLoading) { setState((s: any) => ({ diff --git a/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardLinkVizElement.tsx b/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardLinkVizElement.tsx index 277249174b..7b14701b78 100644 --- a/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardLinkVizElement.tsx +++ b/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardLinkVizElement.tsx @@ -27,7 +27,6 @@ function BoardLinkVizElement(props: any) { size='md' variant='ghost' color='secondary' - onClick={() => props.callbacks.on_navigation()} > {props.options.text} diff --git a/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardVizElement.tsx b/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardVizElement.tsx index 199e20dbb1..f42032f3e4 100644 --- a/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardVizElement.tsx +++ b/src/aimcore/web/ui/src/pages/Board/components/VisualizationElements/BoardVizElement.tsx @@ -21,10 +21,6 @@ function BoardVizElement(props: any) { } }, [code, boards]); - React.useEffect(() => { - props.callbacks.on_mount(); - }, []); - return (
)}
diff --git a/src/aimcore/web/ui/src/services/pyodide/pyodide.ts b/src/aimcore/web/ui/src/services/pyodide/pyodide.ts index fcc56964ea..f888802bfb 100644 --- a/src/aimcore/web/ui/src/services/pyodide/pyodide.ts +++ b/src/aimcore/web/ui/src/services/pyodide/pyodide.ts @@ -1,3 +1,5 @@ +import * as _ from 'lodash-es'; + import { AIM_VERSION, getBasePath } from 'config/config'; import { fetchPackages } from 'modules/core/api/projectApi'; @@ -6,8 +8,6 @@ import { search } from 'pages/Board/serverAPI/search'; import { runFunction } from 'pages/Board/serverAPI/runFunction'; import { find } from 'pages/Board/serverAPI/find'; -import { getItem, setItem } from 'utils/storage'; - import pyodideEngine from './store'; declare global { @@ -150,15 +150,42 @@ window.setState = (update: any, boardPath: string, persist = false) => { // This section add persistence for state through saving it to URL and localStorage - if (persist) { - const stateStr = JSON.stringify(state); - const boardStateStr = JSON.stringify(state[boardPath]); - const prevStateStr = getItem('app_state'); + // TODO: remove hardcoded '/app/' from pathname + if (persist && boardPath === window.location.pathname.slice(5)) { + // Escape form state updates and unnecessary keys + + let boartState: Record = {}; + + for (let key in state[boardPath]) { + // Escape form state updates + if (key.startsWith('__form__')) { + continue; + } + + // Escape table selected and focused rows as only keeping indexes is enough + let item = _.omit(state[boardPath][key], [ + 'selected_rows', + 'focused_row', + ]); + + // Escape state fields which value is None (undefined in JS) + if (!_.isEmpty(JSON.parse(JSON.stringify(item)))) { + boartState[key] = item; + } + } + + const stateStr = encodeURIComponent(JSON.stringify(boartState)); + + const url = new URL(window.location as any); + + const prevStateStr = url.searchParams.get('state'); if (stateStr !== prevStateStr) { - setItem('app_state', stateStr); - const url = new URL(window.location as any); - url.searchParams.set('state', boardStateStr); + if (_.isEmpty(boartState)) { + url.searchParams.delete('state'); + } else { + url.searchParams.set('state', stateStr); + } window.history.pushState({}, '', url as any); } }