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

Gui Builder Indexed Property (#616) #633

Closed
wants to merge 5 commits into from
Closed
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
1 change: 1 addition & 0 deletions frontend/taipy-gui/dom/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/taipy-gui/src/components/Taipy/Navigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
navigate(0);
} else {
navigate({ pathname: to, search: `?${searchParams.toString()}` });
if (searchParams.has("tprh")) {
navigate(0);
}
}
} else {
window.open(`${to}?${searchParams.toString()}`, tab || "_blank")?.focus();
Expand Down
12 changes: 11 additions & 1 deletion taipy/gui/builder/_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from __future__ import annotations

import copy
import re
import typing as t
from abc import ABC, abstractmethod
from collections.abc import Iterable
Expand All @@ -28,6 +29,7 @@ class _Element(ABC):

_ELEMENT_NAME = ""
_DEFAULT_PROPERTY = ""
__RE_INDEXED_PROPERTY = re.compile(r"^(.*?)_([\d]+)$")

def __new__(cls, *args, **kwargs):
obj = super(_Element, cls).__new__(cls)
Expand All @@ -49,12 +51,20 @@ def update(self, **kwargs):

# Convert property value to string
def parse_properties(self):
self._properties = {k: _Element._parse_property(v) for k, v in self._properties.items()}
self._properties = {
_Element._parse_property_key(k): _Element._parse_property(v) for k, v in self._properties.items()
}

# Get a deepcopy version of the properties
def _deepcopy_properties(self):
return copy.deepcopy(self._properties)

@staticmethod
def _parse_property_key(key: str) -> str:
if match := _Element.__RE_INDEXED_PROPERTY.match(key):
return f"{match.group(1)}[{match.group(2)}]"
return key

@staticmethod
def _parse_property(value: t.Any) -> t.Any:
if isinstance(value, (str, dict, Iterable)):
Expand Down
12 changes: 12 additions & 0 deletions taipy/gui/external/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2023 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.

from ._custom_page import CustomPage, ResourceHandler
82 changes: 82 additions & 0 deletions taipy/gui/external/_custom_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# Copyright 2023 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.

from __future__ import annotations

import typing as t
from abc import ABC, abstractmethod

from ..page import Page
from ..utils.singleton import _Singleton

if t.TYPE_CHECKING:
from ..gui import Gui


class CustomPage(Page):
"""A custom page for external application that can be added to Taipy GUI"""

def __init__(self, resource_handler: ResourceHandler, binding_variables: t.List[str] = None, **kwargs) -> None:
if binding_variables is None:
binding_variables = []
super().__init__(**kwargs)
self._resource_handler = resource_handler
self._binding_variables = binding_variables


class ResourceHandler(ABC):
"""Resource handler for custom pages
User can implement this class to provide custom resources for the custom pages
"""

id: str = ""

def __init__(self) -> None:
_ExternalResourceHandlerManager().register(self)

def get_id(self) -> str:
return self.id if id != "" else str(id(self))

@abstractmethod
def get_resources(self, path: str) -> t.Any:
raise NotImplementedError


class _ExternalResourceHandlerManager(object, metaclass=_Singleton):
"""Manager for resource handlers
This class is used to manage resource handlers for custom pages
"""

def __init__(self) -> None:
self.__handlers: t.Dict[str, ResourceHandler] = {}

def register(self, handler: ResourceHandler) -> None:
"""Register a resource handler
Arguments:
handler (ResourceHandler): The resource handler to register
"""
self.__handlers[handler.get_id()] = handler

def get(self, id: str) -> t.Optional[ResourceHandler]:
"""Get a resource handler by its id
Arguments:
id (str): The id of the resource handler
Returns:
ResourceHandler: The resource handler
"""
return self.__handlers.get(id, None)

def get_all(self) -> t.List[ResourceHandler]:
"""Get all resource handlers
Returns:
List[ResourceHandler]: The list of resource handlers
"""
return list(self.__handlers.values())
106 changes: 98 additions & 8 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@
import warnings
from importlib import metadata, util
from importlib.util import find_spec
from types import FrameType, SimpleNamespace
from types import FrameType, FunctionType, LambdaType, ModuleType, SimpleNamespace
from urllib.parse import unquote, urlencode, urlparse

import __main__
import markdown as md_lib
import tzlocal
from flask import Blueprint, Flask, g, jsonify, request, send_file, send_from_directory
from flask import Blueprint, Flask, g, has_app_context, jsonify, request, send_file, send_from_directory
from taipy.logger._taipy_logger import _TaipyLogger
from werkzeug.utils import secure_filename

Expand All @@ -53,6 +53,7 @@
from .data.data_format import _DataFormat
from .data.data_scope import _DataScopes
from .extension.library import Element, ElementLibrary
from .external import CustomPage
from .gui_types import _WsType
from .page import Page
from .partial import Partial
Expand Down Expand Up @@ -537,6 +538,8 @@ def _get_client_id(self) -> str:
def __set_client_id_in_context(self, client_id: t.Optional[str] = None, force=False):
if not client_id and request:
client_id = request.args.get(Gui.__ARG_CLIENT_ID, "")
if not client_id and (ws_client_id := getattr(g, "ws_client_id", None)):
client_id = ws_client_id
if not client_id and force:
res = self._bindings()._get_or_create_scope("")
client_id = res[0] if res[1] else None
Expand Down Expand Up @@ -583,10 +586,12 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None:
if msg_type == _WsType.CLIENT_ID.value:
res = self._bindings()._get_or_create_scope(message.get("payload", ""))
client_id = res[0] if res[1] else None
self.__set_client_id_in_context(client_id or message.get(Gui.__ARG_CLIENT_ID))
expected_client_id = client_id or message.get(Gui.__ARG_CLIENT_ID)
self.__set_client_id_in_context(expected_client_id)
g.ws_client_id = expected_client_id
with self._set_locals_context(message.get("module_context") or None):
payload = message.get("payload", {})
if msg_type == _WsType.UPDATE.value:
payload = message.get("payload", {})
self.__front_end_update(
str(message.get("name")),
payload.get("value"),
Expand All @@ -600,6 +605,10 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None:
self.__request_data_update(str(message.get("name")), message.get("payload"))
elif msg_type == _WsType.REQUEST_UPDATE.value:
self.__request_var_update(message.get("payload"))
elif msg_type == _WsType.GET_MODULE_CONTEXT.value:
self.__handle_ws_get_module_context(payload)
elif msg_type == _WsType.GET_VARIABLES.value:
self.__handle_ws_get_variables()
self.__send_ack(message.get("ack_id"))
except Exception as e: # pragma: no cover
_warn(f"Decoding Message has failed: {message}", e)
Expand Down Expand Up @@ -1024,6 +1033,54 @@ def __request_var_update(self, payload: t.Any):
)
self.__send_var_list_update(payload["names"])

def __handle_ws_get_module_context(self, payload: t.Any):
if isinstance(payload, dict):
# Get Module Context
if mc := self._get_page_context(str(payload.get("path"))):
self._bind_custom_page_variables(
self._get_page(str(payload.get("path")))._renderer, self._get_client_id()
)
self.__send_ws(
{
"type": _WsType.GET_MODULE_CONTEXT.value,
"payload": {"data": mc},
}
)

def __handle_ws_get_variables(self):
# Get Variables
self.__pre_render_pages()
# Module Context -> Variable -> Variable data (name, type, initial_value)
variable_tree: t.Dict[str, t.Dict[str, t.Dict[str, t.Any]]] = {}
data = vars(self._bindings()._get_data_scope())
data = {
k: v
for k, v in data.items()
if not k.startswith("_")
and not callable(v)
and "TpExPr" not in k
and not isinstance(v, (ModuleType, FunctionType, LambdaType, type, Page))
}
for k, v in data.items():
if isinstance(v, _TaipyBase):
data[k] = v.get()
var_name, var_module_name = _variable_decode(k)
if var_module_name == "" or var_module_name is None:
var_module_name = "__main__"
if var_module_name not in variable_tree:
variable_tree[var_module_name] = {}
variable_tree[var_module_name][var_name] = {
"type": type(v).__name__,
"value": data[k],
"encoded_name": k,
}
self.__send_ws(
{
"type": _WsType.GET_VARIABLES.value,
"payload": {"data": variable_tree},
}
)

def __send_ws(self, payload: dict, allow_grouping=True) -> None:
grouping_message = self.__get_message_grouping() if allow_grouping else None
if grouping_message is None:
Expand Down Expand Up @@ -1865,10 +1922,12 @@ def __pre_render_pages(self) -> None:
for page in self._config.pages:
if page is not None:
with contextlib.suppress(Exception):
page.render(self)
if isinstance(page._renderer, CustomPage):
self._bind_custom_page_variables(page._renderer, self._get_client_id())
else:
page.render(self)

def __render_page(self, page_name: str) -> t.Any:
self.__set_client_id_in_context()
def _get_navigated_page(self, page_name: str) -> t.Any:
nav_page = page_name
if hasattr(self, "on_navigate") and callable(self.on_navigate):
try:
Expand All @@ -1889,8 +1948,26 @@ def __render_page(self, page_name: str) -> t.Any:
except Exception as e: # pragma: no cover
if not self._call_on_exception("on_navigate", e):
_warn("Exception raised in on_navigate()", e)
page = next((page_i for page_i in self._config.pages if page_i._route == nav_page), None)
return nav_page

def _get_page(self, page_name: str):
return next((page_i for page_i in self._config.pages if page_i._route == page_name), None)

def _bind_custom_page_variables(self, page: CustomPage, client_id: t.Optional[str]):
"""Handle the bindings of custom page variables"""
with self.get_flask_app().app_context() if has_app_context() else contextlib.nullcontext():
self.__set_client_id_in_context(client_id)
with self._set_locals_context(page._get_module_name()):
for k in self._get_locals_bind().keys():
if (not page._binding_variables or k in page._binding_variables) and not k.startswith("_"):
self._bind_var(k)

def __render_page(self, page_name: str) -> t.Any:
self.__set_client_id_in_context()
nav_page = self._get_navigated_page(page_name)
if not isinstance(nav_page, str):
return nav_page
page = self._get_page(nav_page)
# Try partials
if page is None:
page = self._get_partial(nav_page)
Expand All @@ -1901,6 +1978,19 @@ def __render_page(self, page_name: str) -> t.Any:
400,
{"Content-Type": "application/json; charset=utf-8"},
)
# Handle custom pages
if (pr := page._renderer) is not None and isinstance(pr, CustomPage):
if self._navigate(
to=page_name,
params={
_Server._RESOURCE_HANDLER_ARG: pr._resource_handler.get_id(),
},
):
# proactively handle the bindings of custom page variables
self._bind_custom_page_variables(pr, self._get_client_id())
return ("Successfully redirect to external resource handler", 200)
return ("Failed to navigate to external resource handler", 500)
# Handle page rendering
context = page.render(self)
if (
nav_page == Gui.__root_page_name
Expand Down
2 changes: 2 additions & 0 deletions taipy/gui/gui_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class _WsType(Enum):
DOWNLOAD_FILE = "DF"
PARTIAL = "PR"
ACKNOWLEDGEMENT = "ACK"
GET_MODULE_CONTEXT = "GMC"
GET_VARIABLES = "GVS"


NumberTypes = {"int", "int64", "float", "float64"}
Expand Down
4 changes: 4 additions & 0 deletions taipy/gui/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Page:
"""

def __init__(self, **kwargs) -> None:
from .external import CustomPage

self._class_module_name = ""
self._class_locals: t.Dict[str, t.Any] = {}
self._frame: t.Optional[FrameType] = None
Expand All @@ -42,6 +44,8 @@ def __init__(self, **kwargs) -> None:
self._frame = kwargs.get("frame")
elif self._renderer:
self._frame = self._renderer._frame
elif isinstance(self, CustomPage):
self._frame = t.cast(FrameType, t.cast(FrameType, inspect.stack()[2].frame))
elif len(inspect.stack()) < 4:
raise RuntimeError(f"Can't resolve module. Page '{type(self).__name__}' is not registered.")
else:
Expand Down
Loading