Skip to content

Commit

Permalink
[feat] Sync board state with URL (#2960)
Browse files Browse the repository at this point in the history
  • Loading branch information
roubkar committed Sep 1, 2023
1 parent 590114e commit 34e5c2c
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 42 deletions.
42 changes: 21 additions & 21 deletions src/aimcore/web/ui/public/aim_ui_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 {}
)
Expand All @@ -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]
Expand All @@ -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 = {
Expand Down Expand Up @@ -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 = []
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/aimcore/web/ui/src/pages/App/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ export interface AppWrapperProps {
boardPath: string;
editMode: boolean;
boardList: string[];
stateStr?: str | null;
}
4 changes: 3 additions & 1 deletion src/aimcore/web/ui/src/pages/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ function App(): React.FunctionComponentElement<React.ReactNode> {
<ErrorBoundary>
{isLoading ? null : (
<Route path={[`${PathEnum.App}/*`, `${PathEnum.App}/*/edit`]} exact>
{(props) => <AppPage {...props} data={data} />}
{(props) => (
<AppPage key={props.location.pathname} {...props} data={data} />
)}
</Route>
)}
<ToastProvider
Expand Down
43 changes: 41 additions & 2 deletions src/aimcore/web/ui/src/pages/App/components/AppPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,60 @@ import * as React from 'react';

import AppWrapper from './AppWrapper';

function AppPage({ match, location, data }: any) {
function AppPage({ match, location, history, data }: any) {
let boardPath = '';
if (match?.params?.[0]) {
boardPath = match.params[0];
}

const editMode = location.pathname.endsWith('/edit');

const getStateFromURL = React.useCallback(() => {
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 (
<AppWrapper boardList={data} boardPath={boardPath} editMode={editMode!} />
<AppWrapper
boardList={data}
boardPath={boardPath}
editMode={editMode!}
stateStr={stateStr}
/>
);
}

Expand Down
8 changes: 7 additions & 1 deletion src/aimcore/web/ui/src/pages/App/components/AppWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -93,6 +98,7 @@ function AppWrapper({ boardPath, editMode, boardList }: AppWrapperProps) {
key={board.path + editMode}
data={board}
editMode={editMode}
stateStr={stateStr}
// saveBoard={saveBoard}
/>
)}
Expand Down
39 changes: 36 additions & 3 deletions src/aimcore/web/ui/src/pages/Board/Board.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ function Board({
newMode,
notifyData,
onNotificationDelete,
}: // saveBoard,
any): React.FunctionComponentElement<React.ReactNode> {
stateStr,
}: any): React.FunctionComponentElement<React.ReactNode> {
const [mounted, setMounted] = React.useState(false);
const {
isLoading: pyodideIsLoading,
Expand Down Expand Up @@ -146,8 +146,11 @@ any): React.FunctionComponentElement<React.ReactNode> {
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)
Expand Down Expand Up @@ -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) {
Expand All @@ -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) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ function BoardLinkVizElement(props: any) {
size='md'
variant='ghost'
color='secondary'
onClick={() => props.callbacks.on_navigation()}
>
<Text>{props.options.text}</Text>
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ function BoardVizElement(props: any) {
}
}, [code, boards]);

React.useEffect(() => {
props.callbacks.on_mount();
}, []);

return (
<div
className='VizComponentContainer'
Expand All @@ -40,6 +36,7 @@ function BoardVizElement(props: any) {
}}
editMode={false}
previewMode
stateStr={props.options.state_str}
/>
)}
</div>
Expand Down

0 comments on commit 34e5c2c

Please sign in to comment.