Skip to content

Commit

Permalink
Backport/custom-frontend (#1205)
Browse files Browse the repository at this point in the history
* Remove uploaded part file after merging (#1030)

* Decouple preview bundle (#1013)

* Decouple preview bundle

* per Fred

* Add name to shared bundle for easier import (#1027)

* Extensible CustomPage class (#962)

* GUI: Allow dict update on custom frontend (#932) (#935)

* Allow dict update on custom frontend

* fix codespell

* per Fred

* Trigger Build

* fix ruff

* Decouple onReload (#1203) (#1204)

* onReload

* update on reload

* per Fred
  • Loading branch information
dinhlongviolin1 authored Apr 19, 2024
1 parent b118b5c commit e02c591
Show file tree
Hide file tree
Showing 13 changed files with 118 additions and 29 deletions.
24 changes: 20 additions & 4 deletions frontend/taipy-gui/base/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ import { sendWsMessage, TAIPY_CLIENT_ID } from "../../src/context/wsUtils";
import { uploadFile } from "../../src/workers/fileupload";

import { Socket, io } from "socket.io-client";
import { DataManager } from "./dataManager";
import { DataManager, ModuleData } from "./dataManager";
import { initSocket } from "./utils";

export type OnInitHandler = (appManager: TaipyApp) => void;
export type OnChangeHandler = (appManager: TaipyApp, encodedName: string, value: unknown) => void;
export type OnNotifyHandler = (appManager: TaipyApp, type: string, message: string) => void;
export type onReloadHandler = (appManager: TaipyApp, removedChanges: ModuleData) => void;

export class TaipyApp {
socket: Socket;
_onInit: OnInitHandler | undefined;
_onChange: OnChangeHandler | undefined;
_onNotify: OnNotifyHandler | undefined;
_onReload: onReloadHandler | undefined;
variableData: DataManager | undefined;
functionData: DataManager | undefined;
appId: string;
Expand Down Expand Up @@ -46,7 +48,7 @@ export class TaipyApp {
return this._onInit;
}
set onInit(handler: OnInitHandler | undefined) {
if (handler !== undefined && handler?.length !== 1) {
if (handler !== undefined && handler.length !== 1) {
throw new Error("onInit() requires one parameter");
}
this._onInit = handler;
Expand All @@ -56,7 +58,7 @@ export class TaipyApp {
return this._onChange;
}
set onChange(handler: OnChangeHandler | undefined) {
if (handler !== undefined && handler?.length !== 3) {
if (handler !== undefined && handler.length !== 3) {
throw new Error("onChange() requires three parameters");
}
this._onChange = handler;
Expand All @@ -66,12 +68,22 @@ export class TaipyApp {
return this._onNotify;
}
set onNotify(handler: OnNotifyHandler | undefined) {
if (handler !== undefined && handler?.length !== 3) {
if (handler !== undefined && handler.length !== 3) {
throw new Error("onNotify() requires three parameters");
}
this._onNotify = handler;
}

get onReload() {
return this._onReload;
}
set onReload(handler: onReloadHandler | undefined) {
if (handler !== undefined && handler?.length !== 2) {
throw new Error("_onReload() requires two parameters");
}
this._onReload = handler;
}

// Utility methods
init() {
this.clientId = "";
Expand Down Expand Up @@ -141,6 +153,10 @@ export class TaipyApp {
upload(encodedName: string, files: FileList, progressCallback: (val: number) => void) {
return uploadFile(encodedName, files, progressCallback, this.clientId);
}

getPageMetadata() {
return JSON.parse(localStorage.getItem("tp_cp_meta") || "{}");
}
}

export const createApp = (onInit?: OnInitHandler, onChange?: OnChangeHandler, path?: string, socket?: Socket) => {
Expand Down
1 change: 1 addition & 0 deletions frontend/taipy-gui/base/src/dataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class DataManager {
this._data[vData["encoded_name"]] = vData.value;
}
}
return changes;
}

getEncodedName(varName: string, module: string): string | undefined {
Expand Down
6 changes: 6 additions & 0 deletions frontend/taipy-gui/base/src/index-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TaipyApp, createApp, OnChangeHandler, OnInitHandler } from "./app";
import { ModuleData } from "./dataManager";

export default TaipyApp;
export { TaipyApp, createApp };
export type { OnChangeHandler, OnInitHandler, ModuleData };
1 change: 1 addition & 0 deletions frontend/taipy-gui/base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export type { OnChangeHandler, OnInitHandler, ModuleData };

window.addEventListener("beforeunload", () => {
document.cookie = "tprh=;path=/;Max-Age=-99999999;";
localStorage.removeItem("tp_cp_meta");
});
9 changes: 7 additions & 2 deletions frontend/taipy-gui/base/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import merge from "lodash/merge";
import { Socket } from "socket.io-client";
import { IdMessage, storeClientId } from "../../src/context/utils";
import { WsMessage, sendWsMessage } from "../../src/context/wsUtils";
Expand Down Expand Up @@ -74,8 +75,12 @@ const processWsMessage = (message: WsMessage, appManager: TaipyApp) => {
const variableData = payload.variable;
const functionData = payload.function;
if (appManager.variableData && appManager.functionData) {
appManager.variableData.init(variableData);
appManager.functionData.init(functionData);
const varChanges = appManager.variableData.init(variableData);
const functionChanges = appManager.functionData.init(functionData);
const changes = merge(varChanges, functionChanges);
if (varChanges || functionChanges) {
appManager.onReload && appManager.onReload(appManager, changes);
}
} else {
appManager.variableData = new DataManager(variableData);
appManager.functionData = new DataManager(functionData);
Expand Down
24 changes: 17 additions & 7 deletions frontend/taipy-gui/base/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,31 @@ const webAppPath = resolveApp(basePath);

module.exports = {
target: "web",
entry: "./base/src/index.ts",
entry: {
"default": "./base/src/index.ts",
"preview": "./base/src/index-preview.ts",
},
output: {
filename: "taipy-gui-base.js",
filename: (arg) => {
if (arg.chunk.name === "default") {
return "taipy-gui-base.js";
}
return "[name].taipy-gui-base.js";
},
chunkFilename: "[name].taipy-gui-base.js",
path: webAppPath,
globalObject: "this",
library: {
name: moduleName,
type: "umd",
},
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: {
chunks: 'all',
name: "shared",
},
},
module: {
rules: [
{
Expand Down
8 changes: 8 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/Navigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
const searchParams = new URLSearchParams(params || "");
// Handle Resource Handler Id
let tprh: string | null = null;
let meta: string | null = null;
if (searchParams.has("tprh")) {
tprh = searchParams.get("tprh");
searchParams.delete("tprh");
if (searchParams.has("tp_cp_meta")) {
meta = searchParams.get("tp_cp_meta");
searchParams.delete("tp_cp_meta");
}
}
if (Object.keys(state.locations || {}).some((route) => tos === route)) {
const searchParamsLocation = new URLSearchParams(location.search);
Expand All @@ -47,6 +52,9 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
if (tprh !== null) {
// Add a session cookie for the resource handler id
document.cookie = `tprh=${tprh};path=/;`;
if (meta !== null) {
localStorage.setItem("tp_cp_meta", meta);
}
navigate(0);
}
}
Expand Down
25 changes: 18 additions & 7 deletions frontend/taipy-gui/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,21 +171,32 @@ module.exports = (env, options) => {
},
{
mode: options.mode,
entry: ["./base/src/index.ts"],
target: "web",
entry: {
"default": "./base/src/index.ts",
"preview": "./base/src/index-preview.ts",
},
output: {
filename: "taipy-gui-base.js",
filename: (arg) => {
if (arg.chunk.name === "default") {
return "taipy-gui-base.js";
}
return "[name].taipy-gui-base.js";
},
chunkFilename: "[name].taipy-gui-base.js",
path: webAppPath,
globalObject: "this",
library: {
name: taipyGuiBaseBundleName,
type: "umd",
},
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
optimization: {
splitChunks: {
chunks: 'all',
name: "shared",
},
},
module: {
rules: [
{
Expand Down
9 changes: 8 additions & 1 deletion taipy/gui/custom/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ class Page(BasePage):
A custom page for external application that can be added to Taipy GUI"""

def __init__(
self, resource_handler: ResourceHandler, binding_variables: t.Optional[t.List[str]] = None, **kwargs
self,
resource_handler: ResourceHandler,
binding_variables: t.Optional[t.List[str]] = None,
metadata: t.Optional[t.Dict[str, t.Any]] = None,
**kwargs,
) -> None:
if binding_variables is None:
binding_variables = []
if metadata is None:
metadata = {}
super().__init__(**kwargs)
self._resource_handler = resource_handler
self._binding_variables = binding_variables
self._metadata: t.Dict[str, t.Any] = metadata


class ResourceHandler(ABC):
Expand Down
32 changes: 28 additions & 4 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@

import markdown as md_lib
import tzlocal
from flask import Blueprint, Flask, g, has_app_context, jsonify, request, send_file, send_from_directory
from flask import (
Blueprint,
Flask,
g,
has_app_context,
has_request_context,
jsonify,
request,
send_file,
send_from_directory,
)
from werkzeug.utils import secure_filename

import __main__ # noqa: F401
Expand Down Expand Up @@ -732,7 +742,8 @@ def _get_real_var_name(self, var_name: str) -> t.Tuple[str, str]:
return f"{var_name_decode}.{suffix_var_name}" if suffix_var_name else var_name_decode, module_name
if module_name == current_context:
var_name = var_name_decode
else:
# only strict checking for cross-context linked variable when the context has been properly set
elif self._has_set_context():
if var_name not in self.__var_dir._var_head:
raise NameError(f"Can't find matching variable for {var_name} on context: {current_context}")
_found = False
Expand Down Expand Up @@ -940,8 +951,11 @@ def __upload_files(self):
try:
with open(file_path, "wb") as grouped_file:
for nb in range(part + 1):
with open(upload_path / f"{file_path.name}.part.{nb}", "rb") as part_file:
part_file_path = upload_path / f"{file_path.name}.part.{nb}"
with open(part_file_path, "rb") as part_file:
grouped_file.write(part_file.read())
# remove file_path after it is merged
part_file_path.unlink()
except EnvironmentError as ee: # pragma: no cover
_warn("Cannot group file after chunk upload", ee)
return
Expand Down Expand Up @@ -1002,7 +1016,13 @@ def __send_var_list_update( # noqa C901
elif isinstance(newvalue, _TaipyToJson):
newvalue = newvalue.get()
if isinstance(newvalue, (dict, _MapDict)):
continue # this var has no transformer
# Skip in taipy-gui, available in custom frontend
resource_handler_id = None
with contextlib.suppress(Exception):
if has_request_context():
resource_handler_id = request.cookies.get(_Server._RESOURCE_HANDLER_ARG, None)
if resource_handler_id is None:
continue # this var has no transformer
if isinstance(newvalue, float) and math.isnan(newvalue):
# do not let NaN go through json, it is not handle well (dies silently through websocket)
newvalue = None
Expand Down Expand Up @@ -1568,6 +1588,9 @@ def _get_locals_context(self) -> str:
def _set_locals_context(self, context: t.Optional[str]) -> t.ContextManager[None]:
return self.__locals_context.set_locals_context(context)

def _has_set_context(self):
return self.__locals_context.get_context() is not None

def _get_page_context(self, page_name: str) -> str | None:
if page_name not in self._config.routes:
return None
Expand Down Expand Up @@ -2062,6 +2085,7 @@ def __render_page(self, page_name: str) -> t.Any:
to=page_name,
params={
_Server._RESOURCE_HANDLER_ARG: pr._resource_handler.get_id(),
_Server._CUSTOM_PAGE_META_ARG: json.dumps(pr._metadata, cls=_TaipyJsonEncoder)
},
):
# Proactively handle the bindings of custom page variables
Expand Down
3 changes: 3 additions & 0 deletions taipy/gui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ def __init__(self, **kwargs) -> None:
self._frame = self._renderer._frame
elif isinstance(self, CustomPage):
self._frame = t.cast(FrameType, t.cast(FrameType, inspect.stack()[2].frame))
# Allow CustomPage class to be inherited
if len(inspect.stack()) > 3 and inspect.stack()[2].function != "<module>":
self._frame = t.cast(FrameType, t.cast(FrameType, inspect.stack()[3].frame))
elif len(inspect.stack()) < 4:
raise RuntimeError(f"Can't resolve module. Page '{type(self).__name__}' is not registered.")
else:
Expand Down
1 change: 1 addition & 0 deletions taipy/gui/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class _Server:
__OPENING_CURLY = r"\1&#x7B;"
__CLOSING_CURLY = r"&#x7D;\2"
_RESOURCE_HANDLER_ARG = "tprh"
_CUSTOM_PAGE_META_ARG = "tp_cp_meta"

def __init__(
self,
Expand Down
4 changes: 0 additions & 4 deletions tests/gui/server/http/test_file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,12 @@ def test_file_upload_multi_part(gui: Gui, helpers):
content_type="multipart/form-data",
)
assert ret.status_code == 200
file0_path = upload_path / f"{file_name}.part.0"
assert file0_path.exists()
ret = flask_client.post(
f"/taipy-uploads?client_id={sid}",
data={"var_name": "varname", "blob": file1, "total": "2", "part": "1"},
content_type="multipart/form-data",
)
assert ret.status_code == 200
file1_path = upload_path / f"{file_name}.part.1"
assert file1_path.exists()
file_path = upload_path / file_name
assert file_path.exists()

Expand Down

0 comments on commit e02c591

Please sign in to comment.