diff --git a/frontend/taipy-gui/base/src/app.ts b/frontend/taipy-gui/base/src/app.ts index 59c789281d..9a9447960b 100644 --- a/frontend/taipy-gui/base/src/app.ts +++ b/frontend/taipy-gui/base/src/app.ts @@ -4,31 +4,36 @@ import { uploadFile } from "../../src/workers/fileupload"; import { Socket, io } from "socket.io-client"; import { DataManager, ModuleData } from "./dataManager"; -import { initSocket } from "./utils"; +import { initSocket } from "./socket"; +import { TaipyWsAdapter, WsAdapter } from "./wsAdapter"; -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; + metadata: Record; path: string | undefined; + routes: Route[] | undefined; + wsAdapters: WsAdapter[]; 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; @@ -37,9 +42,13 @@ export class TaipyApp { this.functionData = undefined; this.clientId = ""; this.context = ""; + this.metadata = {}; this.appId = ""; + this.routes = undefined; this.path = path; this.socket = socket; + this.wsAdapters = [new TaipyWsAdapter()]; + // Init socket io connection initSocket(socket, this); } @@ -47,6 +56,7 @@ export class TaipyApp { get onInit() { return this._onInit; } + set onInit(handler: OnInitHandler | undefined) { if (handler !== undefined && handler.length !== 1) { throw new Error("onInit() requires one parameter"); @@ -57,6 +67,7 @@ export class TaipyApp { get onChange() { return this._onChange; } + set onChange(handler: OnChangeHandler | undefined) { if (handler !== undefined && handler.length !== 3) { throw new Error("onChange() requires three parameters"); @@ -67,6 +78,7 @@ export class TaipyApp { get onNotify() { return this._onNotify; } + set onNotify(handler: OnNotifyHandler | undefined) { if (handler !== undefined && handler.length !== 3) { throw new Error("onNotify() requires three parameters"); @@ -77,7 +89,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 +101,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); @@ -99,6 +113,10 @@ export class TaipyApp { } // Public methods + registerWsAdapter(wsAdapter: WsAdapter) { + this.wsAdapters.unshift(wsAdapter); + } + getEncodedName(varName: string, module: string) { return this.variableData?.getEncodedName(varName, module); } @@ -128,6 +146,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) { @@ -142,7 +164,7 @@ export class TaipyApp { if (!path || path === "") { path = window.location.pathname.slice(1); } - sendWsMessage(this.socket, "GMC", "get_module_context", { path: path }, this.clientId); + sendWsMessage(this.socket, "GMC", "get_module_context", { path: path || "/" }, this.clientId); } trigger(actionName: string, triggerId: string, payload: Record = {}) { @@ -155,7 +177,7 @@ export class TaipyApp { } getPageMetadata() { - return JSON.parse(localStorage.getItem("tp_cp_meta") || "{}"); + return this.metadata; } } diff --git a/frontend/taipy-gui/base/src/dataManager.ts b/frontend/taipy-gui/base/src/dataManager.ts index b16775df5b..c445b91a93 100644 --- a/frontend/taipy-gui/base/src/dataManager.ts +++ b/frontend/taipy-gui/base/src/dataManager.ts @@ -2,7 +2,7 @@ export type ModuleData = Record; export type VarName = Record; -interface VarData { +export interface VarData { type: string; value: unknown; encoded_name: string; diff --git a/frontend/taipy-gui/base/src/exports.ts b/frontend/taipy-gui/base/src/exports.ts new file mode 100644 index 0000000000..3343eb83ae --- /dev/null +++ b/frontend/taipy-gui/base/src/exports.ts @@ -0,0 +1,9 @@ +import { WsAdapter } from "./wsAdapter"; +import { sendWsMessage } from "../../src/context/wsUtils"; +// import { TaipyApp } from "./app"; + +export { + WsAdapter, + sendWsMessage, + // TaipyApp, +}; diff --git a/frontend/taipy-gui/base/src/index.ts b/frontend/taipy-gui/base/src/index.ts index 6e814f0693..a0c111a622 100644 --- a/frontend/taipy-gui/base/src/index.ts +++ b/frontend/taipy-gui/base/src/index.ts @@ -7,5 +7,4 @@ export type { OnChangeHandler, OnInitHandler, ModuleData }; window.addEventListener("beforeunload", () => { document.cookie = "tprh=;path=/;Max-Age=-99999999;"; - localStorage.removeItem("tp_cp_meta"); }); diff --git a/frontend/taipy-gui/base/src/packaging/package.json b/frontend/taipy-gui/base/src/packaging/package.json new file mode 100644 index 0000000000..96039a7e7a --- /dev/null +++ b/frontend/taipy-gui/base/src/packaging/package.json @@ -0,0 +1,7 @@ +{ + "name": "taipy-gui-base", + "version": "3.2.0", + "private": true, + "main": "./taipy-gui-base.js", + "types": "./taipy-gui-base.d.ts" +} diff --git a/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts new file mode 100644 index 0000000000..4568e91566 --- /dev/null +++ b/frontend/taipy-gui/base/src/packaging/taipy-gui-base.d.ts @@ -0,0 +1,113 @@ +import { Socket } from "socket.io-client"; + +export type ModuleData = Record; +export type VarName = Record; +export interface VarData { + type: string; + value: unknown; + encoded_name: string; +} +declare class DataManager { + _data: Record; + _init_data: ModuleData; + constructor(variableModuleData: ModuleData); + init(variableModuleData: ModuleData): ModuleData; + getEncodedName(varName: string, module: string): string | undefined; + getName(encodedName: string): [string, string] | undefined; + get(encodedName: string): unknown; + getInfo(encodedName: string): VarData | undefined; + getDataTree(): ModuleData; + getAllData(): Record; + update(encodedName: string, value: unknown): 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; +export type Route = [string, string]; +export declare class TaipyApp { + socket: Socket; + _onInit: OnInitHandler | undefined; + _onChange: OnChangeHandler | undefined; + _onNotify: OnNotifyHandler | undefined; + _onReload: OnReloadHandler | undefined; + variableData: DataManager | undefined; + functionData: DataManager | undefined; + appId: string; + clientId: string; + context: string; + path: string | undefined; + routes: Route[] | undefined; + wsAdapters: WsAdapter[]; + constructor( + onInit?: OnInitHandler | undefined, + onChange?: OnChangeHandler | undefined, + path?: string | undefined, + socket?: Socket | undefined + ); + get onInit(): OnInitHandler | undefined; + set onInit(handler: OnInitHandler | undefined); + get onChange(): OnChangeHandler | undefined; + set onChange(handler: OnChangeHandler | undefined); + get onNotify(): OnNotifyHandler | undefined; + set onNotify(handler: OnNotifyHandler | undefined); + get onReload(): OnReloadHandler | undefined; + set onReload(handler: OnReloadHandler | undefined); + init(): void; + registerWsAdapter(wsAdapter: WsAdapter): void; + getEncodedName(varName: string, module: string): string | undefined; + getName(encodedName: string): [string, string] | undefined; + get(encodedName: string): unknown; + getInfo(encodedName: string): VarData | undefined; + getDataTree(): ModuleData | undefined; + getAllData(): Record | undefined; + getFunctionList(): string[]; + getRoutes(): Route[] | undefined; + update(encodedName: string, value: unknown): void; + getContext(): string; + updateContext(path?: string | undefined): void; + trigger(actionName: string, triggerId: string, payload?: Record): void; + upload(encodedName: string, files: FileList, progressCallback: (val: number) => void): Promise; + getPageMetadata(): any; +} +export type WsMessageType = + | "A" + | "U" + | "DU" + | "MU" + | "RU" + | "AL" + | "BL" + | "NA" + | "ID" + | "MS" + | "DF" + | "PR" + | "ACK" + | "GMC" + | "GDT" + | "AID" + | "GR"; +export interface WsMessage { + type: WsMessageType | str; + name: string; + payload: Record | unknown; + propagate: boolean; + client_id: string; + module_context: string; + ack_id?: string; +} +export declare const sendWsMessage: ( + socket: Socket | undefined, + type: WsMessageType | str, + name: string, + payload: Record | unknown, + id: string, + moduleContext?: string, + propagate?: boolean, + serverAck?: (val: unknown) => void +) => string; +export declare abstract class WsAdapter { + abstract supportedMessageTypes: string[]; + abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean; +} diff --git a/frontend/taipy-gui/base/src/socket.ts b/frontend/taipy-gui/base/src/socket.ts new file mode 100644 index 0000000000..194f73e772 --- /dev/null +++ b/frontend/taipy-gui/base/src/socket.ts @@ -0,0 +1,46 @@ +import { Socket } from "socket.io-client"; +import { WsMessage, sendWsMessage } from "../../src/context/wsUtils"; +import { TaipyApp } from "./app"; + +export const initSocket = (socket: Socket, taipyApp: TaipyApp) => { + socket.on("connect", () => { + 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", taipyApp.appId, taipyApp.clientId, taipyApp.context); + }); + // try to reconnect on connect_error + socket.on("connect_error", (err) => { + console.log("Error connecting WebSocket: ", err); + setTimeout(() => { + socket && socket.connect(); + }, 500); + }); + // try to reconnect on server disconnection + socket.on("disconnect", (reason, details) => { + console.log("WebSocket disconnected due to: ", reason, details); + if (reason === "io server disconnect") { + socket && socket.connect(); + } + }); + // handle message data from backend + socket.on("message", (message: WsMessage) => { + // handle messages with registered websocket adapters + for (const adapter of taipyApp.wsAdapters) { + if (adapter.supportedMessageTypes.includes(message.type)) { + const messageResolved = adapter.handleWsMessage(message, taipyApp); + if (messageResolved) { + return; + } + } + } + }); + // only now does the socket tries to open/connect + if (!socket.connected) { + socket.connect(); + } +}; diff --git a/frontend/taipy-gui/base/src/utils.ts b/frontend/taipy-gui/base/src/utils.ts deleted file mode 100644 index 836bda06e2..0000000000 --- a/frontend/taipy-gui/base/src/utils.ts +++ /dev/null @@ -1,113 +0,0 @@ -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"; -import { TaipyApp } from "./app"; -import { DataManager, ModuleData } from "./dataManager"; - -interface MultipleUpdatePayload { - name: string; - payload: { value: unknown }; -} - -interface AlertMessage extends WsMessage { - atype: string; - message: string; -} - -const initWsMessageTypes = ["ID", "AID", "GMC"]; - -export const initSocket = (socket: Socket, appManager: TaipyApp) => { - socket.on("connect", () => { - if (appManager.clientId === "" || appManager.appId === "") { - appManager.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); - }); - // try to reconnect on connect_error - socket.on("connect_error", (err) => { - console.log("Error connecting WebSocket: ", err); - setTimeout(() => { - socket && socket.connect(); - }, 500); - }); - // try to reconnect on server disconnection - socket.on("disconnect", (reason, details) => { - console.log("WebSocket disconnected due to: ", reason, details); - if (reason === "io server disconnect") { - socket && socket.connect(); - } - }); - // handle message data from backend - socket.on("message", (message: WsMessage) => { - processWsMessage(message, appManager); - }); - // only now does the socket tries to open/connect - if (!socket.connected) { - socket.connect(); - } -}; - -const processWsMessage = (message: WsMessage, appManager: 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); - } - } else if (message.type === "ID") { - const { id } = message as unknown as IdMessage; - storeClientId(id); - appManager.clientId = id; - appManager.updateContext(appManager.path); - } else if (message.type === "GMC") { - const mc = (message.payload as Record).data as string; - window.localStorage.setItem("ModuleContext", mc); - appManager.context = mc; - } else if (message.type === "GDT") { - const payload = message.payload as Record; - 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); - 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); - appManager.onInit && appManager.onInit(appManager); - } - } else if (message.type === "AID") { - const payload = message.payload as Record; - if (payload.name === "reconnect") { - return appManager.init(); - } - appManager.appId = payload.id as string; - } else if (message.type === "AL" && appManager.onNotify) { - const payload = message as AlertMessage; - appManager.onNotify(appManager, payload.atype, payload.message); - } - postWsMessageProcessing(message, appManager); - } -}; - -const postWsMessageProcessing = (message: WsMessage, appManager: TaipyApp) => { - // perform data population only when all necessary metadata is ready - if ( - initWsMessageTypes.includes(message.type) && - appManager.clientId !== "" && - appManager.appId !== "" && - appManager.context !== "" - ) { - sendWsMessage(appManager.socket, "GDT", "get_data_tree", {}, appManager.clientId, appManager.context); - } -}; diff --git a/frontend/taipy-gui/base/src/wsAdapter.ts b/frontend/taipy-gui/base/src/wsAdapter.ts new file mode 100644 index 0000000000..09b446c525 --- /dev/null +++ b/frontend/taipy-gui/base/src/wsAdapter.ts @@ -0,0 +1,102 @@ +import merge from "lodash/merge"; +import { TaipyApp } from "./app"; +import { IdMessage, storeClientId } from "../../src/context/utils"; +import { WsMessage, sendWsMessage } from "../../src/context/wsUtils"; +import { DataManager, ModuleData } from "./dataManager"; + +export abstract class WsAdapter { + abstract supportedMessageTypes: string[]; + + abstract handleWsMessage(message: WsMessage, app: TaipyApp): boolean; +} + +interface MultipleUpdatePayload { + name: string; + payload: { value: unknown }; +} + +interface AlertMessage extends WsMessage { + atype: string; + message: string; +} + +export class TaipyWsAdapter extends WsAdapter { + supportedMessageTypes: string[]; + initWsMessageTypes: string[]; + constructor() { + super(); + this.supportedMessageTypes = ["MU", "ID", "GMC", "GDT", "AID", "GR", "AL"]; + this.initWsMessageTypes = ["ID", "AID", "GMC"]; + } + handleWsMessage(message: WsMessage, taipyApp: TaipyApp): boolean { + 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; + 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); + taipyApp.clientId = id; + taipyApp.updateContext(taipyApp.path); + } else if (message.type === "GMC") { + const payload = message.payload as Record; + taipyApp.context = payload.context as string; + if (payload?.metadata) { + try { + taipyApp.metadata = JSON.parse((payload.metadata as string) || "{}"); + } catch (e) { + console.error("Error parsing metadata from Taipy Designer", e); + } + } + } else if (message.type === "GDT") { + const payload = message.payload as Record; + const variableData = payload.variable; + const functionData = payload.function; + 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) { + taipyApp.onReload && taipyApp.onReload(taipyApp, changes); + } + } else { + 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; + if (payload.name === "reconnect") { + taipyApp.init(); + return true; + } + 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; + taipyApp.onNotify(taipyApp, payload.atype, payload.message); + } + this.postWsMessageProcessing(message, taipyApp); + return true; + } + return false; + } + postWsMessageProcessing(message: WsMessage, taipyApp: TaipyApp) { + // perform data population only when all necessary metadata is ready + if ( + this.initWsMessageTypes.includes(message.type) && + taipyApp.clientId !== "" && + taipyApp.appId !== "" && + taipyApp.context !== "" && + taipyApp.routes !== undefined + ) { + sendWsMessage(taipyApp.socket, "GDT", "get_data_tree", {}, taipyApp.clientId, taipyApp.context); + } + } +} diff --git a/frontend/taipy-gui/base/webpack.config.js b/frontend/taipy-gui/base/webpack.config.js index 20ac795672..6c4c509c5c 100644 --- a/frontend/taipy-gui/base/webpack.config.js +++ b/frontend/taipy-gui/base/webpack.config.js @@ -1,56 +1,89 @@ const path = require("path"); const webpack = require("webpack"); -const resolveApp = relativePath => path.resolve(__dirname, relativePath); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const resolveApp = (relativePath) => path.resolve(__dirname, relativePath); const moduleName = "TaipyGuiBase"; const basePath = "../../../taipy/gui/webapp"; const webAppPath = resolveApp(basePath); +const taipyGuiBaseExportPath = resolveApp(basePath + "/taipy-gui-base-export"); -module.exports = { - target: "web", - entry: { - "default": "./base/src/index.ts", - "preview": "./base/src/index-preview.ts", - }, - output: { - 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", +module.exports = [ + { + target: "web", + entry: { + default: "./base/src/index.ts", + preview: "./base/src/index-preview.ts", }, - }, - optimization: { - splitChunks: { - chunks: 'all', - name: "shared", + output: { + 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", + }, + }, + optimization: { + splitChunks: { + chunks: "all", + name: "shared", + }, }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js", ".tsx"], + }, + // externals: { + // "socket.io-client": { + // commonjs: "socket.io-client", + // commonjs2: "socket.io-client", + // amd: "socket.io-client", + // root: "_", + // }, + // }, }, - module: { - rules: [ - { - test: /\.tsx?$/, - use: "ts-loader", - exclude: /node_modules/, + { + entry: "./base/src/exports.ts", + output: { + filename: "taipy-gui-base.js", + path: taipyGuiBaseExportPath, + library: { + name: moduleName, + type: "umd", }, + publicPath: "", + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js", ".tsx"], + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [{ from: "./base/src/packaging", to: taipyGuiBaseExportPath }], + }), ], }, - resolve: { - extensions: [".tsx", ".ts", ".js", ".tsx"], - }, - // externals: { - // "socket.io-client": { - // commonjs: "socket.io-client", - // commonjs2: "socket.io-client", - // amd: "socket.io-client", - // root: "_", - // }, - // }, -}; +]; diff --git a/frontend/taipy-gui/package-lock.json b/frontend/taipy-gui/package-lock.json index 461cb864a2..9363465fd1 100644 --- a/frontend/taipy-gui/package-lock.json +++ b/frontend/taipy-gui/package-lock.json @@ -61,8 +61,8 @@ "css-loader": "^6.5.0", "css-mediaquery": "^0.1.2", "dotenv-webpack": "^8.0.0", - "dts-bundle-generator": "^9.2.1", - "eslint": "^8.3.0", + "dts-bundle-generator": "^7.2.0", + "eslint": "^8.57.0", "eslint-plugin-react": "^7.26.1", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-tsdoc": "^0.2.16", @@ -5347,12 +5347,12 @@ } }, "node_modules/dts-bundle-generator": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.3.1.tgz", - "integrity": "sha512-1/nMT7LFOkXbrL1ZvLpzrjNbfX090LZ64nLIXVmet557mshFCGP/oTiQiZenafJZ6GsmRQLTYKSlQnkxK8tsTw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-7.2.0.tgz", + "integrity": "sha512-pHjRo52hvvLDRijzIYRTS9eJR7vAOs3gd/7jx+7YVnLU8ay3yPUWGtHXPtuMBSlJYk/s4nq1SvXObDCZVguYMg==", "dev": true, "dependencies": { - "typescript": ">=5.0.2", + "typescript": ">=4.5.2", "yargs": "^17.6.0" }, "bin": { diff --git a/frontend/taipy-gui/package.json b/frontend/taipy-gui/package.json index b822e418f2..5187f05af9 100644 --- a/frontend/taipy-gui/package.json +++ b/frontend/taipy-gui/package.json @@ -46,6 +46,7 @@ "lint:fix": "npm run lint -- --fix", "coverage": "npm test -- --coverage", "types": "dts-bundle-generator -o packaging/taipy-gui.gen.d.ts src/extensions/exports.ts", + "types-base": "dts-bundle-generator -o base/src/packaging/taipy-gui-base.gen.d.ts base/src/exports.ts", "doc": "typedoc --plugin typedoc-plugin-markdown --excludeNotDocumented --disableSources src/extensions/exports.ts", "doc.json": "typedoc --json docs/taipy-gui.json src/extensions/exports.ts", "mkdocs": "typedoc --options typedoc-mkdocs.json" @@ -97,8 +98,8 @@ "css-loader": "^6.5.0", "css-mediaquery": "^0.1.2", "dotenv-webpack": "^8.0.0", - "dts-bundle-generator": "^9.2.1", - "eslint": "^8.3.0", + "dts-bundle-generator": "^7.2.0", + "eslint": "^8.57.0", "eslint-plugin-react": "^7.26.1", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-tsdoc": "^0.2.16", diff --git a/frontend/taipy-gui/src/components/Taipy/Navigate.tsx b/frontend/taipy-gui/src/components/Taipy/Navigate.tsx index fa9c3d692d..dc8a946365 100644 --- a/frontend/taipy-gui/src/components/Taipy/Navigate.tsx +++ b/frontend/taipy-gui/src/components/Taipy/Navigate.tsx @@ -27,34 +27,44 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => { const { dispatch, state } = useContext(TaipyContext); const navigate = useNavigate(); const location = useLocation(); + const SPECIAL_PARAMS = ["tp_reload_all", "tp_reload_same_route_only", "tprh"]; useEffect(() => { if (to) { const tos = to === "/" ? to : "/" + to; - 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"); + const filteredParams = params + ? Object.keys(params).reduce((acc, key) => { + if (!SPECIAL_PARAMS.includes(key)) { + acc[key] = params[key]; + } + return acc; + }, {} as Record) + : {}; + const searchParams = new URLSearchParams(filteredParams); + // Special case for notebook reload + const reloadAll = params?.tp_reload_all === "true"; + const reloadSameRouteOnly = params?.tp_reload_same_route_only === "true"; + if (reloadAll) { + return navigate(0); + } + if (reloadSameRouteOnly) { + if (location.pathname === tos) { + navigate(0); } + return; } + // Regular navigate cases if (Object.keys(state.locations || {}).some((route) => tos === route)) { const searchParamsLocation = new URLSearchParams(location.search); if (force && location.pathname === tos && searchParamsLocation.toString() === searchParams.toString()) { navigate(0); } else { navigate({ pathname: to, search: `?${searchParams.toString()}` }); - if (tprh !== null) { + // Handle Resource Handler Id + const tprh = params?.tprh; + if (tprh !== undefined) { // 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); } } diff --git a/frontend/taipy-gui/src/context/wsUtils.ts b/frontend/taipy-gui/src/context/wsUtils.ts index 3c56a5bf79..854f5d72a3 100644 --- a/frontend/taipy-gui/src/context/wsUtils.ts +++ b/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; diff --git a/frontend/taipy-gui/webpack.config.js b/frontend/taipy-gui/webpack.config.js index b986bd542b..eaa49c601f 100644 --- a/frontend/taipy-gui/webpack.config.js +++ b/frontend/taipy-gui/webpack.config.js @@ -34,6 +34,7 @@ const webAppPath = resolveApp(basePath); const reactManifestPath = resolveApp(basePath + "/" + reactBundle + "-manifest.json"); const reactDllPath = resolveApp(basePath + "/" + reactBundle + ".dll.js") const taipyDllPath = resolveApp(basePath + "/" + taipyBundle + ".js") +const taipyGuiBaseExportPath = resolveApp(basePath + "/taipy-gui-base-export"); module.exports = (env, options) => { const envVariables = { @@ -217,5 +218,36 @@ module.exports = (env, options) => { // root: "_", // }, // }, + }, + { + entry: "./base/src/exports.ts", + output: { + filename: "taipy-gui-base.js", + path: taipyGuiBaseExportPath, + library: { + name: taipyGuiBaseBundleName, + type: "umd", + }, + publicPath: "", + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: "ts-loader", + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js", ".tsx"], + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { from: "./base/src/packaging", to: taipyGuiBaseExportPath }, + ], + }), + ], }]; }; diff --git a/taipy/__init__.py b/taipy/__init__.py index 75884936b5..474f3738bb 100644 --- a/taipy/__init__.py +++ b/taipy/__init__.py @@ -30,5 +30,8 @@ if find_spec("taipy.enterprise"): from taipy.enterprise._init import * + if find_spec("taipy.designer"): + from taipy.designer._init import * + if find_spec("taipy._run"): from taipy._run import _run as run diff --git a/taipy/gui/custom/_page.py b/taipy/gui/custom/_page.py index dcf62f4d60..58929ed330 100644 --- a/taipy/gui/custom/_page.py +++ b/taipy/gui/custom/_page.py @@ -46,13 +46,13 @@ class ResourceHandler(ABC): User can implement this class to provide custom resources for the custom pages """ - id: str = "" + rh_id: str = "" def __init__(self) -> None: _ExternalResourceHandlerManager().register(self) def get_id(self) -> str: - return self.id if id != "" else str(id(self)) + return self.rh_id if self.rh_id != "" else str(id(self)) @abstractmethod def get_resources(self, path: str, taipy_resource_path: str) -> t.Any: diff --git a/taipy/gui/gui.py b/taipy/gui/gui.py index 6e0e9419ad..e54d168cef 100644 --- a/taipy/gui/gui.py +++ b/taipy/gui/gui.py @@ -599,6 +599,12 @@ def __clean_vars_on_exit(self) -> t.Optional[t.Set[str]]: setattr(g, "update_count", update_count) # noqa: B010 return None + def _handle_connect(self): + pass + + def _handle_disconnect(self): + pass + def _manage_message(self, msg_type: _WsType, message: dict) -> None: try: client_id = None @@ -631,6 +637,10 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None: 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() + else: + self._manage_external_message(msg_type, message) self.__send_ack(message.get("ack_id")) except Exception as e: # pragma: no cover if isinstance(e, AttributeError) and (name := message.get("name")): @@ -651,6 +661,11 @@ def _manage_message(self, msg_type: _WsType, message: dict) -> None: else: _warn(f"Decoding Message has failed: {message}", e) + # To be expanded by inheriting classes + # this will be used to handle ws messages that is not handled by the base Gui class + def _manage_external_message(self, msg_type: _WsType, message: dict) -> None: + pass + def __front_end_update( self, var_name: str, @@ -1088,15 +1103,24 @@ def __request_var_update(self, payload: t.Any): def __handle_ws_get_module_context(self, payload: t.Any): if isinstance(payload, dict): + page_path = str(payload.get("path")) + if page_path in {"/", ""}: + page_path = Gui.__root_page_name # Get Module Context - if mc := self._get_page_context(str(payload.get("path"))): + if mc := self._get_page_context(page_path): + page_renderer = self._get_page(page_path)._renderer self._bind_custom_page_variables( - self._get_page(str(payload.get("path")))._renderer, self._get_client_id() + page_renderer, self._get_client_id() ) + # get metadata if there is one + metadata: t.Dict[str, t.Any] = {} + if hasattr(page_renderer, "_metadata"): + metadata = getattr(page_renderer, "_metadata", {}) + meta_return = json.dumps(metadata, cls=_TaipyJsonEncoder) if metadata else None self.__send_ws( { "type": _WsType.GET_MODULE_CONTEXT.value, - "payload": {"data": mc}, + "payload": {"context": mc, "metadata": meta_return}, } ) @@ -1159,14 +1183,32 @@ def __handle_ws_app_id(self, message: t.Any): } ) - def __send_ws(self, payload: dict, allow_grouping=True) -> None: + 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: try: self._server._ws.emit( "message", payload, - to=self.__get_ws_receiver(), + to=self.__get_ws_receiver(send_back_only), ) time.sleep(0.001) except Exception as e: # pragma: no cover @@ -1185,7 +1227,11 @@ def __broadcast_ws(self, payload: dict, client_id: t.Optional[str] = None): def __send_ack(self, ack_id: t.Optional[str]) -> None: if ack_id: try: - self._server._ws.emit("message", {"type": _WsType.ACKNOWLEDGEMENT.value, "id": ack_id}) + self._server._ws.emit( + "message", + {"type": _WsType.ACKNOWLEDGEMENT.value, "id": ack_id}, + to=self.__get_ws_receiver(True), + ) time.sleep(0.001) except Exception as e: # pragma: no cover _warn(f"Exception raised in WebSocket communication (send ack) in '{self.__frame.f_code.co_name}'", e) @@ -1200,7 +1246,10 @@ def _send_ws_id(self, id: str) -> None: ) def __send_ws_download(self, content: str, name: str, on_action: str) -> None: - self.__send_ws({"type": _WsType.DOWNLOAD_FILE.value, "content": content, "name": name, "onAction": on_action}) + self.__send_ws( + {"type": _WsType.DOWNLOAD_FILE.value, "content": content, "name": name, "onAction": on_action}, + send_back_only=True, + ) def __send_ws_alert(self, type: str, message: str, system_notification: bool, duration: int) -> None: self.__send_ws( @@ -1264,13 +1313,15 @@ def __send_ws_broadcast(self, var_name: str, var_value: t.Any, client_id: t.Opti client_id, ) - def __get_ws_receiver(self) -> t.Union[t.List[str], t.Any, None]: + def __get_ws_receiver(self, send_back_only=False) -> t.Union[t.List[str], t.Any, None]: if self._bindings()._is_single_client(): return None sid = getattr(request, "sid", None) if request else None sids = self.__get_sids(self._get_client_id()) if sid: sids.add(sid) + if send_back_only: + return sid return list(sids) def __get_sids(self, client_id: str) -> t.Set[str]: @@ -2056,6 +2107,8 @@ def _get_page(self, page_name: str): def _bind_custom_page_variables(self, page: CustomPage, client_id: t.Optional[str]): """Handle the bindings of custom page variables""" + if not isinstance(page, CustomPage): + return with self.get_flask_app().app_context() if has_app_context() else contextlib.nullcontext(): # type: ignore[attr-defined] self.__set_client_id_in_context(client_id) with self._set_locals_context(page._get_module_name()): @@ -2085,7 +2138,6 @@ 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 diff --git a/taipy/gui/page.py b/taipy/gui/page.py index 6a437872d5..4e685286e1 100644 --- a/taipy/gui/page.py +++ b/taipy/gui/page.py @@ -33,6 +33,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 diff --git a/taipy/gui/server.py b/taipy/gui/server.py index 3f9824328c..b6f4b40cbd 100644 --- a/taipy/gui/server.py +++ b/taipy/gui/server.py @@ -48,7 +48,6 @@ class _Server: __OPENING_CURLY = r"\1{" __CLOSING_CURLY = r"}\2" _RESOURCE_HANDLER_ARG = "tprh" - _CUSTOM_PAGE_META_ARG = "tp_cp_meta" def __init__( self, @@ -111,6 +110,14 @@ def handle_message(message, *args) -> None: elif "type" in message: gui._manage_message(message["type"], message) + @self._ws.on("connect") + def handle_connect(): + gui._handle_connect() + + @self._ws.on("disconnect") + def handle_disconnect(): + gui._handle_disconnect() + def __is_ignored(self, file_path: str) -> bool: if not hasattr(self, "_ignore_matches"): __IGNORE_FILE = ".taipyignore" diff --git a/taipy/gui/types.py b/taipy/gui/types.py index e418df9c03..0754d963f0 100644 --- a/taipy/gui/types.py +++ b/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"} diff --git a/taipy/gui/utils/types.py b/taipy/gui/utils/types.py index 8c3f4538c4..a21fc4db29 100644 --- a/taipy/gui/utils/types.py +++ b/taipy/gui/utils/types.py @@ -12,7 +12,6 @@ import json import typing as t -from abc import ABC from datetime import datetime from importlib.util import find_spec @@ -20,7 +19,7 @@ from . import _date_to_string, _MapDict, _string_to_date, _variable_decode -class _TaipyBase(ABC): +class _TaipyBase: __HOLDER_PREFIXES: t.Optional[t.List[str]] = None _HOLDER_PREFIX = "_Tp"