Răsfoiți Sursa

Merge branch 'metric-example' of github.com:Avaiga/taipy into metric-example

namnguyen 11 luni în urmă
părinte
comite
4401e3ef47

+ 1 - 0
contributors.txt

@@ -15,3 +15,4 @@ bobbyshermi
 Forchapeatl
 yarikoptic
 Luke-0162
+Satoshi-Sh

+ 16 - 7
frontend/taipy-gui/base/src/app.ts

@@ -6,29 +6,31 @@ import { Socket, io } from "socket.io-client";
 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 type OnInitHandler = (taipyApp: TaipyApp) => void;
+export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
+export type OnNotifyHandler = (taipyApp: TaipyApp, type: string, message: string) => void;
+export type OnReloadHandler = (taipyApp: TaipyApp, removedChanges: ModuleData) => void;
+type Route = [string, string];
 
 export class TaipyApp {
     socket: Socket;
     _onInit: OnInitHandler | undefined;
     _onChange: OnChangeHandler | undefined;
     _onNotify: OnNotifyHandler | undefined;
-    _onReload: onReloadHandler | undefined;
+    _onReload: OnReloadHandler | undefined;
     variableData: DataManager | undefined;
     functionData: DataManager | undefined;
     appId: string;
     clientId: string;
     context: string;
     path: string | undefined;
+    routes: Route[] | undefined;
 
     constructor(
         onInit: OnInitHandler | undefined = undefined,
         onChange: OnChangeHandler | undefined = undefined,
         path: string | undefined = undefined,
-        socket: Socket | undefined = undefined
+        socket: Socket | undefined = undefined,
     ) {
         socket = socket || io("/", { autoConnect: false });
         this.onInit = onInit;
@@ -38,6 +40,7 @@ export class TaipyApp {
         this.clientId = "";
         this.context = "";
         this.appId = "";
+        this.routes = undefined;
         this.path = path;
         this.socket = socket;
         initSocket(socket, this);
@@ -77,7 +80,7 @@ export class TaipyApp {
     get onReload() {
         return this._onReload;
     }
-    set onReload(handler: onReloadHandler | undefined) {
+    set onReload(handler: OnReloadHandler | undefined) {
         if (handler !== undefined && handler?.length !== 2) {
             throw new Error("_onReload() requires two parameters");
         }
@@ -89,9 +92,11 @@ export class TaipyApp {
         this.clientId = "";
         this.context = "";
         this.appId = "";
+        this.routes = undefined;
         const id = getLocalStorageValue(TAIPY_CLIENT_ID, "");
         sendWsMessage(this.socket, "ID", TAIPY_CLIENT_ID, id, id, undefined, false);
         sendWsMessage(this.socket, "AID", "connect", "", id, undefined, false);
+        sendWsMessage(this.socket, "GR", "", "", id, undefined, false);
         if (id !== "") {
             this.clientId = id;
             this.updateContext(this.path);
@@ -128,6 +133,10 @@ export class TaipyApp {
         return Object.keys(functionData || {});
     }
 
+    getRoutes() {
+        return this.routes;
+    }
+
     // This update will only send the request to Taipy Gui backend
     // the actual update will be handled when the backend responds
     update(encodedName: string, value: unknown) {

+ 32 - 28
frontend/taipy-gui/base/src/utils.ts

@@ -17,16 +17,16 @@ interface AlertMessage extends WsMessage {
 
 const initWsMessageTypes = ["ID", "AID", "GMC"];
 
-export const initSocket = (socket: Socket, appManager: TaipyApp) => {
+export const initSocket = (socket: Socket, taipyApp: TaipyApp) => {
     socket.on("connect", () => {
-        if (appManager.clientId === "" || appManager.appId === "") {
-            appManager.init();
+        if (taipyApp.clientId === "" || taipyApp.appId === "") {
+            taipyApp.init();
         }
     });
     // Send a request to get App ID to verify that the app has not been reloaded
     socket.io.on("reconnect", () => {
         console.log("WebSocket reconnected")
-        sendWsMessage(socket, "AID", "reconnect", appManager.appId, appManager.clientId, appManager.context);
+        sendWsMessage(socket, "AID", "reconnect", taipyApp.appId, taipyApp.clientId, taipyApp.context);
     });
     // try to reconnect on connect_error
     socket.on("connect_error", (err) => {
@@ -44,7 +44,7 @@ export const initSocket = (socket: Socket, appManager: TaipyApp) => {
     });
     // handle message data from backend
     socket.on("message", (message: WsMessage) => {
-        processWsMessage(message, appManager);
+        processWsMessage(message, taipyApp);
     });
     // only now does the socket tries to open/connect
     if (!socket.connected) {
@@ -52,62 +52,66 @@ export const initSocket = (socket: Socket, appManager: TaipyApp) => {
     }
 };
 
-const processWsMessage = (message: WsMessage, appManager: TaipyApp) => {
+const processWsMessage = (message: WsMessage, taipyApp: TaipyApp) => {
     if (message.type) {
         if (message.type === "MU" && Array.isArray(message.payload)) {
             for (const muPayload of message.payload as [MultipleUpdatePayload]) {
                 const encodedName = muPayload.name;
                 const { value } = muPayload.payload;
-                appManager.variableData?.update(encodedName, value);
-                appManager.onChange && appManager.onChange(appManager, encodedName, value);
+                taipyApp.variableData?.update(encodedName, value);
+                taipyApp.onChange && taipyApp.onChange(taipyApp, encodedName, value);
             }
         } else if (message.type === "ID") {
             const { id } = message as unknown as IdMessage;
             storeClientId(id);
-            appManager.clientId = id;
-            appManager.updateContext(appManager.path);
+            taipyApp.clientId = id;
+            taipyApp.updateContext(taipyApp.path);
         } else if (message.type === "GMC") {
             const mc = (message.payload as Record<string, unknown>).data as string;
             window.localStorage.setItem("ModuleContext", mc);
-            appManager.context = mc;
+            taipyApp.context = mc;
         } else if (message.type === "GDT") {
             const payload = message.payload as Record<string, ModuleData>;
             const variableData = payload.variable;
             const functionData = payload.function;
-            if (appManager.variableData && appManager.functionData) {
-                const varChanges = appManager.variableData.init(variableData);
-                const functionChanges = appManager.functionData.init(functionData);
+            if (taipyApp.variableData && taipyApp.functionData) {
+                const varChanges = taipyApp.variableData.init(variableData);
+                const functionChanges = taipyApp.functionData.init(functionData);
                 const changes = merge(varChanges, functionChanges);
                 if (varChanges || functionChanges) {
-                    appManager.onReload && appManager.onReload(appManager, changes);
+                    taipyApp.onReload && taipyApp.onReload(taipyApp, changes);
                 }
             } else {
-                appManager.variableData = new DataManager(variableData);
-                appManager.functionData = new DataManager(functionData);
-                appManager.onInit && appManager.onInit(appManager);
+                taipyApp.variableData = new DataManager(variableData);
+                taipyApp.functionData = new DataManager(functionData);
+                taipyApp.onInit && taipyApp.onInit(taipyApp);
             }
         } else if (message.type === "AID") {
             const payload = message.payload as Record<string, unknown>;
             if (payload.name === "reconnect") {
-                return appManager.init();
+                return taipyApp.init();
             }
-            appManager.appId = payload.id as string;
-        } else if (message.type === "AL" && appManager.onNotify) {
+            taipyApp.appId = payload.id as string;
+        } else if (message.type === "GR") {
+            const payload = message.payload as [string, string][];
+            taipyApp.routes = payload;
+        } else if (message.type === "AL" && taipyApp.onNotify) {
             const payload = message as AlertMessage;
-            appManager.onNotify(appManager, payload.atype, payload.message);
+            taipyApp.onNotify(taipyApp, payload.atype, payload.message);
         }
-        postWsMessageProcessing(message, appManager);
+        postWsMessageProcessing(message, taipyApp);
     }
 };
 
-const postWsMessageProcessing = (message: WsMessage, appManager: TaipyApp) => {
+const postWsMessageProcessing = (message: WsMessage, taipyApp: TaipyApp) => {
     // perform data population only when all necessary metadata is ready
     if (
         initWsMessageTypes.includes(message.type) &&
-        appManager.clientId !== "" &&
-        appManager.appId !== "" &&
-        appManager.context !== ""
+        taipyApp.clientId !== "" &&
+        taipyApp.appId !== "" &&
+        taipyApp.context !== "" &&
+        taipyApp.routes !== undefined
     ) {
-        sendWsMessage(appManager.socket, "GDT", "get_data_tree", {}, appManager.clientId, appManager.context);
+        sendWsMessage(taipyApp.socket, "GDT", "get_data_tree", {}, taipyApp.clientId, taipyApp.context);
     }
 };

+ 3 - 7
frontend/taipy-gui/src/components/Taipy/Indicator.tsx

@@ -13,13 +13,12 @@
 
 import React, { useCallback, useMemo } from "react";
 import Slider from "@mui/material/Slider";
-import Tooltip from "@mui/material/Tooltip";
 import { sprintf } from "sprintf-js";
 
-import { TaipyBaseProps, TaipyHoverProps } from "./utils";
-import { useClassNames, useDynamicProperty } from "../../utils/hooks";
+import { TaipyBaseProps } from "./utils";
+import { useClassNames } from "../../utils/hooks";
 
-interface IndicatorProps extends TaipyBaseProps, TaipyHoverProps {
+interface IndicatorProps extends TaipyBaseProps {
     min?: number;
     max?: number;
     value?: number;
@@ -43,7 +42,6 @@ const Indicator = (props: IndicatorProps) => {
 
     const horizontalOrientation = props.orientation ? props.orientation.charAt(0).toLowerCase() !== "v" : true;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
-    const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
 
     const getLabel = useCallback(() => {
         const dsp = display === undefined ? (defaultDisplay === undefined ? "" : defaultDisplay) : display;
@@ -95,7 +93,6 @@ const Indicator = (props: IndicatorProps) => {
     );
 
     return (
-        <Tooltip title={hover || ""}>
             <Slider
                 id={props.id}
                 className={className}
@@ -109,7 +106,6 @@ const Indicator = (props: IndicatorProps) => {
                 orientation={horizontalOrientation ? undefined : "vertical"}
                 sx={sliderSx}
             ></Slider>
-        </Tooltip>
     );
 };
 

+ 10 - 7
frontend/taipy-gui/src/components/Taipy/TableFilter.tsx

@@ -101,7 +101,7 @@ const getActionsByType = (colType?: string) =>
 const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?: string, val?: string) => {
     if (colId && act && val !== undefined) {
         const colType = getTypeFromDf(columns[colId].type);
-        if (!val && (colType === "date" || colType === "number" || colType === "boolean")) {
+        if (val === "" && (colType === "date" || colType === "number" || colType === "boolean")) {
             return;
         }
         try {
@@ -109,12 +109,14 @@ const getFilterDesc = (columns: Record<string, ColumnDesc>, colId?: string, act?
                 col: columns[colId].dfid,
                 action: act,
                 value:
-                    colType === "number"
-                        ? parseFloat(val)
-                        : colType === "boolean"
-                        ? val === "1"
-                        : colType === "date"
-                        ? getDateTime(val)
+                    typeof val === "string"
+                        ? colType === "number"
+                            ? parseFloat(val)
+                            : colType === "boolean"
+                            ? val === "1"
+                            : colType === "date"
+                            ? getDateTime(val)
+                            : val
                         : val,
             } as FilterDesc;
         } catch (e) {
@@ -268,6 +270,7 @@ const FilterRow = (props: FilterRowProps) => {
                 ) : colLov ? (
                     <Autocomplete
                         freeSolo
+                        autoSelect
                         disableClearable
                         options={colLov}
                         value={val || ""}

+ 2 - 1
frontend/taipy-gui/src/context/wsUtils.ts

@@ -19,7 +19,8 @@ export type WsMessageType =
     | "ACK"
     | "GMC"
     | "GDT"
-    | "AID";
+    | "AID"
+    | "GR";
 
 export interface WsMessage {
     type: WsMessageType;

+ 1 - 2
taipy/gui/_renderers/factory.py

@@ -57,7 +57,7 @@ class _Factory:
         "text": "value",
         "toggle": "value",
         "tree": "value",
-        "metric": "value"
+        "metric": "value",
     }
 
     _TEXT_ATTRIBUTES = ["format", "id", "hover_text", "raw"]
@@ -280,7 +280,6 @@ class _Factory:
                 ("value", PropertyType.dynamic_number),
                 ("format",),
                 ("orientation"),
-                ("hover_text", PropertyType.dynamic_string),
                 ("width",),
                 ("height",),
             ]

+ 20 - 0
taipy/gui/gui.py

@@ -630,6 +630,8 @@ class Gui:
                         self.__handle_ws_get_data_tree()
                     elif msg_type == _WsType.APP_ID.value:
                         self.__handle_ws_app_id(message)
+                    elif msg_type == _WsType.GET_ROUTES.value:
+                        self.__handle_ws_get_routes()
                 self.__send_ack(message.get("ack_id"))
         except Exception as e:  # pragma: no cover
             if isinstance(e, AttributeError) and (name := message.get("name")):
@@ -1167,6 +1169,24 @@ class Gui:
             }
         )
 
+    def __handle_ws_get_routes(self):
+        routes = (
+            [[self._config.root_page._route, self._config.root_page._renderer.page_type]]
+            if self._config.root_page
+            else []
+        )
+        routes += [
+            [page._route, page._renderer.page_type]
+            for page in self._config.pages
+            if page._route != Gui.__root_page_name
+        ]
+        self.__send_ws(
+            {
+                "type": _WsType.GET_ROUTES.value,
+                "payload": routes,
+            }
+        )
+
     def __send_ws(self, payload: dict, allow_grouping=True, send_back_only=False) -> None:
         grouping_message = self.__get_message_grouping() if allow_grouping else None
         if grouping_message is None:

+ 2 - 0
taipy/gui/page.py

@@ -34,6 +34,8 @@ class Page:
     your application variables and interact with them.
     """
 
+    page_type: str = "Taipy"
+
     def __init__(self, **kwargs) -> None:
         from .custom import Page as CustomPage
 

+ 1 - 0
taipy/gui/types.py

@@ -49,6 +49,7 @@ class _WsType(Enum):
     ACKNOWLEDGEMENT = "ACK"
     GET_MODULE_CONTEXT = "GMC"
     GET_DATA_TREE = "GDT"
+    GET_ROUTES = "GR"
 
 
 NumberTypes = {"int", "int64", "float", "float64"}

+ 5 - 0
taipy/gui/viselements.json

@@ -833,6 +833,11 @@
                         "type": "str",
                         "default_value": "None",
                         "doc": "The height, in CSS units, of the indicator (used when orientation is vertical)."
+                    },
+                    {
+                        "name": "hover_text",
+                        "doc": "TODO not implemented",
+                        "hide": true
                     }
                 ]
             }

+ 14 - 2
taipy/gui_core/_adapters.py

@@ -15,6 +15,7 @@ import math
 import sys
 import typing as t
 from abc import ABC, abstractmethod
+from collections.abc import Iterable
 from dataclasses import dataclass
 from datetime import date, datetime
 from enum import Enum
@@ -246,6 +247,8 @@ _operators: t.Dict[str, t.Callable] = {
     "contains": contains,
 }
 
+def _filter_iterable(list_val: Iterable, operator: t.Callable, val: t.Any):
+    return next(filter(lambda v: operator(v, val), list_val), None) is not None
 
 def _invoke_action(
     ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn: t.Optional[str]
@@ -260,6 +263,8 @@ def _invoke_action(
         if op := _operators.get(action):
             cur_val = attrgetter(col_fn or col)(ent)
             cur_val = cur_val() if col_fn else cur_val
+            if isinstance(cur_val, Iterable):
+                return _filter_iterable(cur_val, op, val)
             return op(cur_val.isoformat() if isinstance(cur_val, (datetime, date)) else cur_val, val)
     except Exception as e:
         if _is_debugging():
@@ -401,9 +406,16 @@ class _GuiCoreScenarioProperties(_GuiCoreProperties):
     def get_enums(self):
         if _GuiCoreScenarioProperties.__ENUMS is None:
             _GuiCoreScenarioProperties.__ENUMS = {
-                "Config id": [c for c in Config.scenarios.keys() if c != "default"],
-                "Tags": list({t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])}),
+                k: v
+                for k, v in {
+                    "Config id": [c for c in Config.scenarios.keys() if c != "default"],
+                    "Tags": list(
+                        {t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])}
+                    ),
+                }.items()
+                if len(v)
             }
+
         return _GuiCoreScenarioProperties.__ENUMS if self.full_desc() else {}
 
     @staticmethod