Procházet zdrojové kódy

Merge branch 'develop' into feature/enterprise#431-remove-production-version

Jean-Robin před 9 měsíci
rodič
revize
c46bb2e46e
100 změnil soubory, kde provedl 2251 přidání a 1669 odebrání
  1. 1 1
      .github/actions/gui-test/pyi/action.yml
  2. 7 2
      .github/workflows/build-and-release-single-package.yml
  3. 14 9
      .github/workflows/build-and-release.yml
  4. 1 1
      .github/workflows/check-config-pyi.yml
  5. 1 1
      .github/workflows/overall-tests.yml
  6. 18 1
      .github/workflows/packaging.yml
  7. 0 3
      doc/gui/examples/charts/advanced-python-lib.py
  8. binární
      doc/gui/examples/controls/alice-avatar.png
  9. binární
      doc/gui/examples/controls/beatrix-avatar.png
  10. binární
      doc/gui/examples/controls/charles-avatar.png
  11. 48 0
      doc/gui/examples/controls/chat-calculator.py
  12. 85 0
      doc/gui/examples/controls/chat-discuss.py
  13. 15 15
      doc/gui/examples/controls/metric-color-map.py
  14. 28 0
      doc/gui/examples/controls/metric-delta-color.py
  15. 3 6
      doc/gui/examples/controls/metric-formats.py
  16. 6 14
      doc/gui/examples/controls/metric-layout.py
  17. 2 4
      doc/gui/examples/controls/metric-range.py
  18. 4 5
      doc/gui/examples/controls/metric-simple.py
  19. 4 3
      doc/gui/examples/controls/metric-type.py
  20. 0 1
      doc/gui/examples/controls/number-min-max.py
  21. 0 1
      doc/gui/examples/controls/number-step.py
  22. 40 0
      doc/gui/examples/controls/table-formatting.py
  23. 0 3
      doc/gui/examples/controls/text-format.py
  24. 1 4
      doc/gui/examples/controls/text-md.py
  25. 0 3
      doc/gui/examples/controls/text-pre.py
  26. 0 3
      doc/gui/examples/controls/text-simple.py
  27. 8 3
      frontend/taipy-gui/base/src/app.ts
  28. 3 0
      frontend/taipy-gui/base/src/utils.ts
  29. 265 321
      frontend/taipy-gui/package-lock.json
  30. 20 18
      frontend/taipy-gui/src/components/Taipy/Chat.tsx
  31. 87 92
      frontend/taipy-gui/src/components/Taipy/Metric.tsx
  32. 4 2
      frontend/taipy-gui/src/components/Taipy/Navigate.tsx
  33. 139 10
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx
  34. 8 1
      frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx
  35. 1 1
      frontend/taipy-gui/src/components/Taipy/Progress.tsx
  36. 196 4
      frontend/taipy-gui/src/components/Taipy/Slider.spec.tsx
  37. 10 4
      frontend/taipy-gui/src/utils/hooks.ts
  38. 177 280
      frontend/taipy/package-lock.json
  39. 10 8
      frontend/taipy/src/CoreSelector.tsx
  40. 3 3
      frontend/taipy/src/DataNodeTable.tsx
  41. 13 13
      frontend/taipy/src/DataNodeViewer.tsx
  42. 4 4
      frontend/taipy/src/PropertiesEditor.tsx
  43. 1 0
      frontend/taipy/src/ScenarioSelector.tsx
  44. 19 19
      frontend/taipy/src/ScenarioViewer.tsx
  45. 5 5
      frontend/taipy/src/utils.ts
  46. 64 0
      pyproject.toml
  47. 0 121
      setup.py
  48. 8 5
      taipy/_cli/_run_cli.py
  49. 58 0
      taipy/config/pyproject.toml
  50. 0 80
      taipy/config/setup.py
  51. 1 1
      taipy/core/_entity/submittable.py
  52. 14 7
      taipy/core/_manager/_manager.py
  53. 3 0
      taipy/core/_orchestrator/_dispatcher/_development_job_dispatcher.py
  54. 4 0
      taipy/core/_orchestrator/_dispatcher/_standalone_job_dispatcher.py
  55. 3 0
      taipy/core/_version/_version_manager_factory.py
  56. 24 1
      taipy/core/config/checkers/_scenario_config_checker.py
  57. 11 0
      taipy/core/config/checkers/_task_config_checker.py
  58. 3 0
      taipy/core/cycle/_cycle_manager_factory.py
  59. 3 1
      taipy/core/data/_data_manager_factory.py
  60. 6 0
      taipy/core/job/_job_converter.py
  61. 9 2
      taipy/core/job/_job_manager.py
  62. 3 1
      taipy/core/job/_job_manager_factory.py
  63. 7 1
      taipy/core/job/_job_model.py
  64. 39 2
      taipy/core/job/job.py
  65. 62 0
      taipy/core/pyproject.toml
  66. 6 0
      taipy/core/reason/__init__.py
  67. 73 0
      taipy/core/reason/reason.py
  68. 33 12
      taipy/core/scenario/_scenario_manager.py
  69. 3 1
      taipy/core/scenario/_scenario_manager_factory.py
  70. 2 1
      taipy/core/scenario/scenario.py
  71. 8 3
      taipy/core/sequence/_sequence_manager.py
  72. 2 1
      taipy/core/sequence/_sequence_manager_factory.py
  73. 0 102
      taipy/core/setup.py
  74. 9 2
      taipy/core/submission/_submission_manager.py
  75. 3 1
      taipy/core/submission/_submission_manager_factory.py
  76. 32 3
      taipy/core/submission/submission.py
  77. 29 22
      taipy/core/taipy.py
  78. 3 1
      taipy/core/task/_task_manager_factory.py
  79. 2 1
      taipy/gui/_default_config.py
  80. 1 0
      taipy/gui/_renderers/factory.py
  81. 1 0
      taipy/gui/_renderers/json.py
  82. 6 4
      taipy/gui/config.py
  83. 1 1
      taipy/gui/custom/_page.py
  84. 17 0
      taipy/gui/gui.py
  85. 8 5
      taipy/gui/gui_actions.py
  86. 6 1
      taipy/gui/hook.py
  87. 58 0
      taipy/gui/pyproject.toml
  88. 8 4
      taipy/gui/server.py
  89. 0 118
      taipy/gui/setup.py
  90. 4 2
      taipy/gui/state.py
  91. 3 0
      taipy/gui/utils/_bindings.py
  92. 2 0
      taipy/gui/utils/types.py
  93. 128 119
      taipy/gui/viselements.json
  94. 13 9
      taipy/gui_core/_adapters.py
  95. 40 4
      taipy/gui_core/_context.py
  96. 50 0
      taipy/rest/pyproject.toml
  97. 0 87
      taipy/rest/setup.py
  98. 53 0
      taipy/templates/pyproject.toml
  99. 0 74
      taipy/templates/setup.py
  100. 74 1
      tests/core/_orchestrator/test_orchestrator__submit.py

+ 1 - 1
.github/actions/gui-test/pyi/action.yml

@@ -8,7 +8,7 @@ runs:
       run: pipenv run pip install mypy black isort
     - name: Generate pyi
       shell: bash
-      run: cp tools/gui/generate_pyi.py pyi_temp.py && pipenv run python pyi_temp.py && rm pyi_temp.py
+      run: pipenv run python tools/gui/generate_pyi.py
     - name: Cleanup any untracked files
       shell: bash
       run: git clean -f

+ 7 - 2
.github/workflows/build-and-release-single-package.yml

@@ -128,7 +128,7 @@ jobs:
       - name: Generate GUI pyi file
         if: github.event.inputs.target_package == 'gui'
         run: |
-          cp tools/gui/generate_pyi.py pyi_temp.py && pipenv run python pyi_temp.py && rm pyi_temp.py
+          pipenv run python tools/gui/generate_pyi.py
 
       - name: Build frontends
         if: github.event.inputs.target_package == 'gui'
@@ -153,10 +153,15 @@ jobs:
         run: |
           cp -r taipy/_cli/. ${{ steps.set-variables.outputs.package_dir }}/taipy/_cli
 
+      - name: Update pyproject.toml
+        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        run: |
+          python tools/release/setup_project.py . prod
+
       - name: Build package
         working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
-          python setup.py build_py && python -m build
+          python -m build
 
       - name: Rename files
         run: |

+ 14 - 9
.github/workflows/build-and-release.yml

@@ -126,7 +126,7 @@ jobs:
       - name: Generate GUI pyi file
         if: matrix.package == 'gui'
         run: |
-          cp tools/gui/generate_pyi.py pyi_temp.py && pipenv run python pyi_temp.py && rm pyi_temp.py
+          pipenv run python tools/gui/generate_pyi.py
 
       - name: Build frontends
         if: matrix.package == 'gui'
@@ -151,10 +151,15 @@ jobs:
         run: |
           cp -r taipy/_cli/. ${{ steps.set-variables.outputs.package_dir }}/taipy/_cli
 
+      - name: Update pyproject.toml
+        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        run: |
+          python tools/release/setup_project.py . prod
+
       - name: Build package
         working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
-          python setup.py build_py && python -m build
+          python -m build
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
       - name: Create tag and release
@@ -204,18 +209,18 @@ jobs:
           python -m pip install --upgrade pip
           pip install build wheel
 
-
-      - name: Backup setup.py
-        run: |
-          mv setup.py setup.old.py
-
       - name: Copy files from tools
         run: |
           cp -r tools/packages/taipy/. .
 
+      - name: Update pyproject.toml
+        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        run: |
+          python tools/release/setup_project.py . prod
+
       - name: Build Taipy package
         run: |
-          python setup.py build_py && python -m build
+          python -m build
 
       - name: Create tag and release Taipy
         run: |
@@ -244,7 +249,7 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - uses: stefanzweifel/git-auto-commit-action@v4
+      - uses: stefanzweifel/git-auto-commit-action@v5
         with:
           file_pattern: '*/version.json'
           commit_message: Update version to ${{ needs.fetch-versions.outputs.NEW_VERSION }}

+ 1 - 1
.github/workflows/check-config-pyi.yml

@@ -18,6 +18,6 @@ jobs:
           python-version: '3.11'
       - name: Update config.pyi
         run: python taipy/config/stubs/generate_pyi.py
-      - uses: stefanzweifel/git-auto-commit-action@v4
+      - uses: stefanzweifel/git-auto-commit-action@v5
         with:
           commit_message: "Update config.pyi"

+ 1 - 1
.github/workflows/overall-tests.yml

@@ -18,7 +18,7 @@ jobs:
     uses: ./.github/workflows/partial-tests.yml
 
   coverage:
-    timeout-minutes: 40
+    timeout-minutes: 50
     runs-on: ubuntu-latest
     if: ${{ github.event_name == 'pull_request' }}
     steps:

+ 18 - 1
.github/workflows/packaging.yml

@@ -31,11 +31,28 @@ jobs:
         with:
           python-version: ${{ matrix.python-versions }}
 
+      - name: Install Dependencies
+        run: |
+          pip install toml
+
       - name: Build frontends
         run: |
           python tools/frontend/bundle_build.py
 
-      - name: Install Taipy without dependencies
+      - name: Update pyproject.toml
+        run: |
+          python tools/release/setup_project.py taipy/config
+          python tools/release/setup_project.py taipy/core
+          python tools/release/setup_project.py taipy/gui
+          python tools/release/setup_project.py taipy/rest
+          python tools/release/setup_project.py taipy/templates
+          python tools/release/setup_project.py .
+
+      - name: Install Taipy Subpackages
+        run: |
+          pip install taipy/config taipy/core taipy/gui taipy/rest taipy/templates
+
+      - name: Install Taipy
         run: |
           pip install .
 

+ 0 - 3
doc/gui/examples/charts/advanced-python-lib.py

@@ -43,9 +43,6 @@ figure.add_trace(
 figure.update_layout(title="Different Probability Distributions")
 
 page = """
-# Plotly Python
-<|toggle|theme|>
-
 <|chart|figure={figure}|>
 """
 

binární
doc/gui/examples/controls/alice-avatar.png


binární
doc/gui/examples/controls/beatrix-avatar.png


binární
doc/gui/examples/controls/charles-avatar.png


+ 48 - 0
doc/gui/examples/controls/chat-calculator.py

@@ -0,0 +1,48 @@
+# Copyright 2021-2024 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Human-computer dialog UI based on the chat control.
+# -----------------------------------------------------------------------------------------
+from math import cos, pi, sin, sqrt, tan  # noqa: F401
+
+from taipy.gui import Gui
+
+# The user interacts with the Python interpreter
+users = ["human", "Result"]
+messages: list[tuple[str, str, str]] = []
+
+
+def evaluate(state, var_name: str, payload: dict):
+    # Retrieve the callback parameters
+    (_, _, expression, sender_id) = payload.get("args", [])
+    # Add the input content as a sent message
+    messages.append((f"{len(messages)}", expression, sender_id))
+    # Default message used if evaluation fails
+    result = "Invalid expression"
+    try:
+        # Evaluate the expression and store the result
+        result = f"= {eval(expression)}"
+    except Exception:
+        pass
+    # Add the result as an incoming message
+    messages.append((f"{len(messages)}", result, users[1]))
+    state.messages = messages
+
+
+page = """
+<|{messages}|chat|users={users}|sender_id={users[0]}|on_action=evaluate|>
+"""
+
+Gui(page).run()

+ 85 - 0
doc/gui/examples/controls/chat-discuss.py

@@ -0,0 +1,85 @@
+# Copyright 2021-2024 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# A chatting application based on the chat control.
+# In order to see the users' avatars, the image files must be stored next to this script.
+# If you want to test this application locally, you need to use several browsers and/or
+# incognito windows so a given user's context is not reused.
+# -----------------------------------------------------------------------------------------
+from os import path
+from typing import Union
+
+from taipy.gui import Gui, Icon
+from taipy.gui.gui_actions import navigate, notify
+
+username = ""
+users: list[Union[str, Icon]] = []
+messages: list[tuple[str, str, str]] = []
+
+Gui.add_shared_variables("messages", "users")
+
+
+def on_init(state):
+    # Copy the global variables users and messages to this user's state
+    state.users = users
+    state.messages = messages
+
+
+def on_navigate(state, path: str):
+    # Navigate to the 'register' page if the user is not registered
+    if path == "discuss" and state.username == "":
+        return "register"
+    return path
+
+
+def register(state):
+    # Check that the user is not already registered
+    for user in users:
+        if state.username == user or (isinstance(user, (list, tuple)) and state.username == user[0]):
+            notify(state, "error", "User already registered.")
+            return
+    # Use the avatar image if we can find it
+    avatar_image_file = f"{state.username.lower()}-avatar.png"
+    if path.isfile(avatar_image_file):
+        users.append((state.username, Icon(avatar_image_file, state.username)))
+    else:
+        users.append(state.username)
+    # Because users is a shared variable, this propagates to every client
+    state.users = users
+    navigate(state, "discuss")
+
+
+def send(state, _: str, payload: dict):
+    (_, _, message, sender_id) = payload.get("args", [])
+    messages.append((f"{len(messages)}", message, sender_id))
+    state.messages = messages
+
+
+register_page = """
+Please enter your user name:
+
+<|{username}|input|>
+
+<|Submit|button|on_action=register|>
+"""
+
+discuss_page = """
+<|### Let's discuss, {username}|text|mode=markdown|>
+
+<|{messages}|chat|users={users}|sender_id={username}|on_action=send|>
+"""
+
+pages = {"register": register_page, "discuss": discuss_page}
+gui = Gui(pages=pages).run()

+ 15 - 15
doc/gui/examples/controls/metric-color-map.py

@@ -15,23 +15,23 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-# color_map = {
-#     # 0-20 - Let Taipy decide
-#     # 20-40 - red
-#     20: "red",
-#     # 40-60 - Let Taipy decide
-#     40: None,
-#     # 60-80 - blue
-#     60: "blue",
-#     # 80-100 - Let Taipy decide
-#     80: None
-# }
-
-value = 50
-color_map = {20: "red", 40: None, 60: "blue", 80: None}
+# Color wavelength
+color_wl = 530
+# Color ranges by wavelength
+color_map = {
+    200: None,
+    380: "violet",
+    435: "blue",
+    500: "cyan",
+    520: "green",
+    565: "yellow",
+    590: "orange",
+    625: "red",
+    740: None,
+}
 
 page = """
-<|{value}|metric|color_map={color_map}|>
+<|{color_wl}|metric|color_map={color_map}|format=%d nm|min=200|max=800|bar_color=gray|>
 """
 
 Gui(page).run()

+ 28 - 0
doc/gui/examples/controls/metric-delta-color.py

@@ -0,0 +1,28 @@
+# Copyright 2021-2024 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+from taipy.gui import Gui
+
+# Source: https://gml.noaa.gov/ccgg/trends/gl_gr.html
+# Estimated Global Trend on january 1st:
+co2_2014 = 396.37
+co2_2024 = 421.13
+delta = co2_2024 - co2_2014
+
+page = """
+<|{co2_2024}|metric|delta={delta}|delta_color=invert|format=%.1f ppm|delta_format=%.1f ppm|min=300|max=500|>
+"""
+
+Gui(page).run()

+ 3 - 6
doc/gui/examples/controls/metric-value-format.py → doc/gui/examples/controls/metric-formats.py

@@ -15,14 +15,11 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-value = 50
-delta_value = 20
-
-# format & delta_format are used to format the value and delta value respectively.
-# They use the printf syntax.
+speed = 60
+variation = 15
 
 page = """
-<|{value}|metric|delta={delta_value}|format=%d km/h|delta_format=%d km/h|>
+<|{speed}|metric|format=%d km/h|delta={variation}|delta_format=%d %%|>
 """
 
 

+ 6 - 14
doc/gui/examples/controls/metric-layout.py

@@ -15,22 +15,14 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-# Layout reference can be found in the documentation: https://plotly.com/python/reference/layout/
-
-value = 50
+value = 45
+# The layout object reference can be found in Plotly's documentation:
+#         https://plotly.com/python/reference/layout/
 layout = {
-    "width": "1000",
-    "height": "500",
-    "paper_bgcolor": "lightgray",
-    "margin": {
-        "l": 100,
-        "r": 100,
-        "b": 100,
-        "t": 100,
-    },
+    "paper_bgcolor": "lightblue",
     "font": {
-        "size": 20,
-        "color": "black",
+        "size": 30,
+        "color": "blue",
         "family": "Arial",
     },
 }

+ 2 - 4
doc/gui/examples/controls/metric-range.py

@@ -15,12 +15,10 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-value = 50
-min_value = 50
-max_value = 150
+value = 120
 
 page = """
-<|{value}|metric|min={min_value}|max={max_value}|>
+<|{value}|metric|min=50|max=150|>
 """
 
 

+ 4 - 5
doc/gui/examples/controls/metric-simple.py

@@ -15,13 +15,12 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-value = 50
-max_value = 150
-delta_value = 20
-threshold = 100
+value = 72
+delta = 15
+threshold = 60
 
 page = """
-<|{value}|metric|max={max_value}|delta={delta_value}|threshold={threshold}|>
+<|{value}|metric|delta={delta}|threshold={threshold}|>
 """
 
 Gui(page).run()

+ 4 - 3
doc/gui/examples/controls/metric-type.py

@@ -15,11 +15,12 @@
 # -----------------------------------------------------------------------------------------
 from taipy.gui import Gui
 
-value = 50
+value = 72
+delta = 15
+threshold = 60
 
 page = """
-<|{value}|metric|type=linear|>
-<|{value}|metric|type=circular|>
+<|{value}|metric|threshold={threshold}|type=linear|>
 """
 
 Gui(page).run()

+ 0 - 1
doc/gui/examples/controls/number-min-max.py

@@ -22,4 +22,3 @@ page = """
 """
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/number-step.py

@@ -22,4 +22,3 @@ page = """
 """
 
 Gui(page).run()
-

+ 40 - 0
doc/gui/examples/controls/table-formatting.py

@@ -0,0 +1,40 @@
+# Copyright 2021-2024 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+import datetime
+
+from taipy.gui import Gui
+
+stock = {
+    "date": [datetime.datetime(year=2000, month=12, day=d) for d in range(20, 30)],
+    "price": [119.88, 112.657, 164.5, 105.42, 188.36, 103.9, 143.97, 160.11, 136.3, 174.06],
+    "change": [7.814, -5.952, 0.01, 8.781, 7.335, 6.623, -6.635, -6.9, 0.327, -0.089],
+    "volume": [773, 2622, 2751, 1108, 7400, 3772, 9398, 4444, 9264, 1108],
+}
+
+columns = {
+    "date": {"title": "Data", "format": "MMM d"},
+    "price": {"title": "Price", "format": "$%.02f"},
+    "change": {"title": "% change", "format": "%.01f"},
+    "volume": {"title": "Volume"},
+}
+
+page = """
+# Formatting cells in a table
+
+<|{stock}|table|columns={columns}|>
+"""
+
+Gui(page).run()

+ 0 - 3
doc/gui/examples/controls/text-format.py

@@ -18,9 +18,6 @@ from taipy.gui import Gui
 pi = 3.14159265358979
 
 page = """
-# Text - Formatting
-<|toggle|theme|>
-
 π≈<|{pi}|text|format=%.3f|>
 """
 

+ 1 - 4
doc/gui/examples/controls/text-md.py

@@ -23,12 +23,9 @@ add style to the text.
 
 If a line ends with two white spaces, such as here
 then you can create line skips.
-""" # noqa W291
+"""  # noqa W291
 
 page = """
-# Text - Markdown
-<|toggle|theme|>
-
 <|{markdown}|text|mode=markdown|>
 """
 

+ 0 - 3
doc/gui/examples/controls/text-pre.py

@@ -24,9 +24,6 @@ if __name__ == "__main__":
 """
 
 page = """
-# Text - pre
-<|toggle|theme|>
-
 <|{code}|text|mode=pre|>
 """
 

+ 0 - 3
doc/gui/examples/controls/text-simple.py

@@ -18,9 +18,6 @@ from taipy.gui import Gui
 name = "Taipy"
 
 page = """
-# Text - simple
-<|toggle|theme|>
-
 <|Hello {name}!|>
 """
 

+ 8 - 3
frontend/taipy-gui/base/src/app.ts

@@ -7,6 +7,7 @@ import { DataManager, ModuleData } from "./dataManager";
 import { initSocket } from "./socket";
 import { TaipyWsAdapter, WsAdapter } from "./wsAdapter";
 import { WsMessageType } from "../../src/context/wsUtils";
+import { getBase } from "./utils";
 
 export type OnInitHandler = (taipyApp: TaipyApp) => void;
 export type OnChangeHandler = (taipyApp: TaipyApp, encodedName: string, value: unknown) => void;
@@ -46,9 +47,9 @@ export class TaipyApp {
         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 });
+        socket = socket || io("/", { autoConnect: false, path: `${this.getBaseUrl()}socket.io` });
         this.onInit = onInit;
         this.onChange = onChange;
         this.variableData = undefined;
@@ -231,7 +232,7 @@ export class TaipyApp {
 
     updateContext(path: string | undefined = "") {
         if (!path || path === "") {
-            path = window.location.pathname.slice(1);
+            path = window.location.pathname.replace(this.getBaseUrl(), "") || "/"
         }
         this.sendWsMessage("GMC", "get_module_context", { path: path || "/" });
     }
@@ -252,6 +253,10 @@ export class TaipyApp {
     getWsStatus() {
         return this._ackList;
     }
+
+    getBaseUrl() {
+        return getBase();
+    }
 }
 
 export const createApp = (onInit?: OnInitHandler, onChange?: OnChangeHandler, path?: string, socket?: Socket) => {

+ 3 - 0
frontend/taipy-gui/base/src/utils.ts

@@ -0,0 +1,3 @@
+export const getBase = () => {
+    return document.getElementsByTagName("base")[0].getAttribute("href") || "/";
+};

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 265 - 321
frontend/taipy-gui/package-lock.json


+ 20 - 18
frontend/taipy-gui/src/components/Taipy/Chat.tsx

@@ -35,7 +35,7 @@ import { TaipyActiveProps, disableColor, getSuffixedClassNames } from "./utils";
 import { useClassNames, useDispatch, useDynamicProperty, useElementVisible, useModule } from "../../utils/hooks";
 import { LoVElt, useLovListMemo } from "./lovUtils";
 import { IconAvatar, avatarSx } from "../../utils/icon";
-import { getInitials } from "../../utils";
+import { emptyArray, getInitials } from "../../utils";
 import { RowType, TableValueType } from "./tableUtils";
 
 interface ChatProps extends TaipyActiveProps {
@@ -167,7 +167,6 @@ const Chat = (props: ChatProps) => {
 
     const [rows, setRows] = useState<RowType[]>([]);
     const page = useRef<key2Rows>({ key: defaultKey });
-    const [rowCount, setRowCount] = useState(0);
     const [columns, setColumns] = useState<Array<string>>([]);
     const scrollDivRef = useRef<HTMLDivElement>(null);
     const anchorDivRef = useRef<HTMLElement>(null);
@@ -291,21 +290,24 @@ const Chat = (props: ChatProps) => {
     useEffect(() => {
         if (!refresh && props.messages && page.current.key && props.messages[page.current.key] !== undefined) {
             const newValue = props.messages[page.current.key];
-            setRowCount(newValue.rowcount);
-            const nr = newValue.data as RowType[];
-            if (Array.isArray(nr) && nr.length > newValue.start && nr[newValue.start]) {
-                setRows((old) => {
-                    old.length && nr.length > old.length && setShowMessage(true);
-                    if (nr.length < old.length) {
-                        return nr.concat(old.slice(nr.length))
-                    }
-                    if (old.length > newValue.start) {
-                        return old.slice(0, newValue.start).concat(nr.slice(newValue.start));
-                    }
-                    return nr;
-                });
-                const cols = Object.keys(nr[newValue.start]);
-                setColumns(cols.length > 2 ? cols : cols.length == 2 ? [...cols, ""] : ["", ...cols, "", ""]);
+            if (newValue.rowcount == 0) {
+                setRows(emptyArray)
+            } else {
+                const nr = newValue.data as RowType[];
+                if (Array.isArray(nr) && nr.length > newValue.start && nr[newValue.start]) {
+                    setRows((old) => {
+                        old.length && nr.length > old.length && setShowMessage(true);
+                        if (nr.length < old.length) {
+                            return nr.concat(old.slice(nr.length))
+                        }
+                        if (old.length > newValue.start) {
+                            return old.slice(0, newValue.start).concat(nr.slice(newValue.start));
+                        }
+                        return nr;
+                    });
+                    const cols = Object.keys(nr[newValue.start]);
+                    setColumns(cols.length > 2 ? cols : cols.length == 2 ? [...cols, ""] : ["", ...cols, "", ""]);
+                }
             }
             page.current.key = getChatKey(0, pageSize);
         }
@@ -341,7 +343,7 @@ const Chat = (props: ChatProps) => {
     );
 
     return (
-        <Tooltip title={hover || "" || `rowCount: ${rowCount}`}>
+        <Tooltip title={hover || ""}>
             <Paper className={className} sx={boxSx} id={id}>
                 <Grid container rowSpacing={2} sx={gridSx} ref={scrollDivRef}>
                     {rows.length && !rows[0] ? (

+ 87 - 92
frontend/taipy-gui/src/components/Taipy/Metric.tsx

@@ -11,62 +11,57 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, {CSSProperties, lazy, Suspense, useMemo} from 'react';
-import {Data, Delta, Layout} from "plotly.js";
+import React, { CSSProperties, lazy, Suspense, useMemo } from "react";
+import { Data, Delta, Layout } from "plotly.js";
 import Box from "@mui/material/Box";
 import Skeleton from "@mui/material/Skeleton";
 import Tooltip from "@mui/material/Tooltip";
-import {useTheme} from "@mui/material";
-import {useClassNames, useDynamicJsonProperty, useDynamicProperty} from "../../utils/hooks";
-import {extractPrefix, extractSuffix, sprintfToD3Converter} from "../../utils/formatConversion";
-import {TaipyBaseProps, TaipyHoverProps} from "./utils";
-import {darkThemeTemplate} from "../../themes/darkThemeTemplate";
+import { useTheme } from "@mui/material";
+import { useClassNames, useDynamicJsonProperty, useDynamicProperty } from "../../utils/hooks";
+import { extractPrefix, extractSuffix, sprintfToD3Converter } from "../../utils/formatConversion";
+import { TaipyBaseProps, TaipyHoverProps } from "./utils";
+import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
 
 const Plot = lazy(() => import("react-plotly.js"));
 
 interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
-    title?: string
-    type?: string
-    min?: number
-    max?: number
-    value?: number
-    defaultValue?: number
-    delta?: number
-    defaultDelta?: number
-    deltaColor?: string
-    negativeDeltaColor?: string
-    threshold?: number
-    defaultThreshold?: number
-    testId?: string
-    defaultLayout?: string;
+    value?: number;
+    defaultValue?: number;
+    delta?: number;
+    defaultDelta?: number;
+    type?: string;
+    min?: number;
+    max?: number;
+    deltaColor?: string;
+    negativeDeltaColor?: string;
+    threshold?: number;
+    defaultThreshold?: number;
+    format?: string;
+    deltaFormat?: string;
+    barColor?: string;
+    showValue?: boolean;
+    colorMap?: string;
+    title?: string;
+    testId?: string;
     layout?: string;
-    defaultStyle?: string;
+    defaultLayout?: string;
     style?: string;
+    defaultStyle?: string;
     width?: string | number;
     height?: string | number;
-    showValue?: boolean;
-    format?: string;
-    deltaFormat?: string;
-    colorMap?: string;
     template?: string;
     template_Dark_?: string;
     template_Light_?: string;
 }
 
 const emptyLayout = {} as Partial<Layout>;
-const defaultStyle = {position: "relative", display: "inline-block"};
+const defaultStyle = { position: "relative", display: "inline-block" };
 
 const Metric = (props: MetricProps) => {
-    const {
-        width = "100%",
-        height,
-        showValue = true,
-        deltaColor,
-        negativeDeltaColor
-    } = props;
-    const value = useDynamicProperty(props.value, props.defaultValue, 0)
-    const threshold = useDynamicProperty(props.threshold, props.defaultThreshold, undefined)
-    const delta = useDynamicProperty(props.delta, props.defaultDelta, undefined)
+    const { width = "100%", height, showValue = true, deltaColor, negativeDeltaColor } = props;
+    const value = useDynamicProperty(props.value, props.defaultValue, 0);
+    const threshold = useDynamicProperty(props.threshold, props.defaultThreshold, undefined);
+    const delta = useDynamicProperty(props.delta, props.defaultDelta, undefined);
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     const baseLayout = useDynamicJsonProperty(props.layout, props.defaultLayout || "", emptyLayout);
     const hover = useDynamicProperty(props.hoverText, props.defaultHoverText, undefined);
@@ -75,31 +70,42 @@ const Metric = (props: MetricProps) => {
     const colorMap = useMemo(() => {
         try {
             const obj = props.colorMap ? JSON.parse(props.colorMap) : null;
-            if (obj && typeof obj === 'object') {
+            if (obj && typeof obj === "object") {
                 const keys = Object.keys(obj);
-                return keys.sort((a, b) => Number(a) - Number(b)).map((key, index) => {
-                    const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
-                    return {range: [Number(key), nextKey], color: obj[key]};
-                }).filter(item => item.color !== null)
+                return keys
+                    .sort((a, b) => Number(a) - Number(b))
+                    .map((key, index) => {
+                        const nextKey = keys[index + 1] !== undefined ? Number(keys[index + 1]) : props.max || 100;
+                        return { range: [Number(key), nextKey], color: obj[key] };
+                    })
+                    .filter((item) => item.color !== null);
             }
         } catch (e) {
             console.info(`Error parsing color_map value (metric).\n${(e as Error).message || e}`);
         }
         return undefined;
-    }, [props.colorMap, props.max])
+    }, [props.colorMap, props.max]);
 
     const data = useMemo(() => {
-        const mode = (props.type === "none") ? [] : ["gauge"];
+        const mode = props.type === "none" ? [] : ["gauge"];
         showValue && mode.push("number");
-        (delta !== undefined) && mode.push("delta");
-        const deltaIncreasing = deltaColor ? {
-            color: deltaColor == "invert" ? "#FF4136" : deltaColor } : undefined
-        const deltaDecreasing = deltaColor == "invert" ? {
-                color: "#3D9970"
-            } : negativeDeltaColor ? { color: negativeDeltaColor } : undefined;
+        delta !== undefined && mode.push("delta");
+        const deltaIncreasing = deltaColor
+            ? {
+                  color: deltaColor == "invert" ? "#FF4136" : deltaColor,
+              }
+            : undefined;
+        const deltaDecreasing =
+            deltaColor == "invert"
+                ? {
+                      color: "#3D9970",
+                  }
+                : negativeDeltaColor
+                  ? { color: negativeDeltaColor }
+                  : undefined;
         return [
             {
-                domain: {x: [0, 1], y: [0, 1]},
+                domain: { x: [0, 1], y: [0, 1] },
                 value: value,
                 type: "indicator",
                 mode: mode.join("+"),
@@ -109,58 +115,58 @@ const Metric = (props: MetricProps) => {
                     valueformat: sprintfToD3Converter(props.format),
                 },
                 delta: {
-                    reference: typeof value === 'number' && typeof delta === 'number' ? value - delta : undefined,
+                    reference: typeof value === "number" && typeof delta === "number" ? value - delta : undefined,
                     prefix: extractPrefix(props.deltaFormat),
                     suffix: extractSuffix(props.deltaFormat),
                     valueformat: sprintfToD3Converter(props.deltaFormat),
                     increasing: deltaIncreasing,
-                    decreasing: deltaDecreasing
-
+                    decreasing: deltaDecreasing,
                 } as Partial<Delta>,
                 gauge: {
                     axis: {
-                        range: [
-                            props.min || 0,
-                            props.max || 100
-                        ]
+                        range: [props.min || 0, props.max || 100],
+                    },
+                    bar: {
+                        color: props.barColor,
                     },
                     steps: colorMap,
                     shape: props.type === "linear" ? "bullet" : "angular",
                     threshold: {
-                        line: {color: "red", width: 4},
+                        line: { color: "red", width: 4 },
                         thickness: 0.75,
-                        value: threshold
-                    }
+                        value: threshold,
+                    },
                 },
-            }
+            },
         ] as Data[];
     }, [
-        props.format,
-        props.deltaFormat,
+        value,
+        delta,
+        props.type,
         props.min,
         props.max,
-        props.type,
-        value,
-        showValue,
         deltaColor,
         negativeDeltaColor,
-        delta,
         threshold,
-        colorMap
+        props.format,
+        props.deltaFormat,
+        props.barColor,
+        showValue,
+        colorMap,
     ]);
 
     const style = useMemo(
         () =>
             height === undefined
-                ? ({...defaultStyle, width: width} as CSSProperties)
-                : ({...defaultStyle, width: width, height: height} as CSSProperties),
+                ? ({ ...defaultStyle, width: width } as CSSProperties)
+                : ({ ...defaultStyle, width: width, height: height } as CSSProperties),
         [height, width]
     );
 
-    const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]);
+    const skelStyle = useMemo(() => ({ ...style, minHeight: "7em" }), [style]);
 
     const layout = useMemo(() => {
-        const layout = {...baseLayout};
+        const layout = { ...baseLayout };
         let template = undefined;
         try {
             const tpl = props.template && JSON.parse(props.template);
@@ -170,7 +176,7 @@ const Metric = (props: MetricProps) => {
                         ? JSON.parse(props.template_Dark_)
                         : darkTemplate
                     : props.template_Light_ && JSON.parse(props.template_Light_);
-            template = tpl ? (tplTheme ? {...tpl, ...tplTheme} : tpl) : tplTheme ? tplTheme : undefined;
+            template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
         } catch (e) {
             console.info(`Error while parsing Metric.template\n${(e as Error).message || e}`);
         }
@@ -183,34 +189,23 @@ const Metric = (props: MetricProps) => {
         }
 
         return layout as Partial<Layout>;
-    }, [
-        props.title,
-        props.template,
-        props.template_Dark_,
-        props.template_Light_,
-        theme.palette.mode,
-        baseLayout,
-    ])
+    }, [props.title, props.template, props.template_Dark_, props.template_Light_, theme.palette.mode, baseLayout]);
 
+    const plotConfig = {displaylogo: false}
     return (
         <Tooltip title={hover || ""}>
             <Box data-testid={props.testId} className={className}>
-                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
-                    <Plot
-                        data={data}
-                        layout={layout}
-                        style={style}
-                        useResizeHandler
-                    />
+                <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle} />}>
+                    <Plot data={data} layout={layout} style={style} config={plotConfig} useResizeHandler />
                 </Suspense>
             </Box>
         </Tooltip>
     );
-}
+};
 
 export default Metric;
 
-const {colorscale, colorway, font} = darkThemeTemplate.layout;
+const { colorscale, colorway, font } = darkThemeTemplate.layout;
 const darkTemplate = {
     layout: {
         colorscale,
@@ -218,4 +213,4 @@ const darkTemplate = {
         font,
         paper_bgcolor: "rgb(31,47,68)",
     },
-}
+};

+ 4 - 2
frontend/taipy-gui/src/components/Taipy/Navigate.tsx

@@ -15,6 +15,7 @@ import { useContext, useEffect } from "react";
 import { useLocation, useNavigate } from "react-router-dom";
 import { TaipyContext } from "../../context/taipyContext";
 import { createNavigateAction } from "../../context/taipyReducers";
+import { getBaseURL } from "../../utils";
 
 interface NavigateProps {
     to?: string;
@@ -32,6 +33,7 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
     useEffect(() => {
         if (to) {
             const tos = to === "/" ? to : "/" + to;
+            const navigatePath = getBaseURL() + tos.slice(1)
             const filteredParams = params
                 ? Object.keys(params).reduce((acc, key) => {
                       if (!SPECIAL_PARAMS.includes(key)) {
@@ -56,10 +58,10 @@ const Navigate = ({ to, params, tab, force }: NavigateProps) => {
             // 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()) {
+                if (force && location.pathname === navigatePath  && searchParamsLocation.toString() === searchParams.toString()) {
                     navigate(0);
                 } else {
-                    navigate({ pathname: to, search: `?${searchParams.toString()}` });
+                    navigate({ pathname: navigatePath, search: `?${searchParams.toString()}` });
                     // Handle Resource Handler Id
                     const tprh = params?.tprh;
                     if (tprh !== undefined) {

+ 139 - 10
frontend/taipy-gui/src/components/Taipy/PaginatedTable.spec.tsx

@@ -11,8 +11,8 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React from "react";
-import { render, waitFor } from "@testing-library/react";
+import React, { act } from "react";
+import { fireEvent, render, waitFor } from "@testing-library/react";
 import "@testing-library/jest-dom";
 import userEvent from "@testing-library/user-event";
 
@@ -163,6 +163,20 @@ const buttonColumns = JSON.stringify({
     Code: { dfid: "Code", type: "str", index: 3 },
 });
 
+const styledColumns = JSON.stringify({
+    Entity: { dfid: "Entity" },
+    "Daily hospital occupancy": {
+        dfid: "Daily hospital occupancy",
+        type: "int64",
+        style: "some style function",
+        tooltip: "some tooltip",
+    },
+});
+
+const invalidColumns = JSON.stringify({
+    invalid: true,
+});
+
 describe("PaginatedTable Component", () => {
     it("renders", async () => {
         const { getByText } = render(<PaginatedTable data={undefined} defaultColumns={tableColumns} />);
@@ -187,13 +201,17 @@ describe("PaginatedTable Component", () => {
         expect(elt.parentElement).not.toHaveClass("Mui-disabled");
     });
     it("is enabled by active", async () => {
-        const { getByText, getAllByTestId } = render(<PaginatedTable data={undefined} defaultColumns={tableColumns} active={true} />);
+        const { getByText, getAllByTestId } = render(
+            <PaginatedTable data={undefined} defaultColumns={tableColumns} active={true} />
+        );
         const elt = getByText("Entity");
         expect(elt.parentElement).not.toHaveClass("Mui-disabled");
         expect(getAllByTestId("ArrowDownwardIcon").length).toBeGreaterThan(0);
     });
     it("Hides sort icons when not active", async () => {
-        const { queryByTestId } = render(<PaginatedTable data={undefined} defaultColumns={tableColumns} active={false} />);
+        const { queryByTestId } = render(
+            <PaginatedTable data={undefined} defaultColumns={tableColumns} active={false} />
+        );
         expect(queryByTestId("ArrowDownwardIcon")).toBeNull();
     });
     it("dispatch 2 well formed messages at first render", async () => {
@@ -201,7 +219,12 @@ describe("PaginatedTable Component", () => {
         const state: TaipyState = INITIAL_STATE;
         render(
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <PaginatedTable id="table" data={undefined} defaultColumns={tableColumns} updateVars="varname=varname" />
+                <PaginatedTable
+                    id="table"
+                    data={undefined}
+                    defaultColumns={tableColumns}
+                    updateVars="varname=varname"
+                />
             </TaipyContext.Provider>
         );
         expect(dispatch).toHaveBeenCalledWith({
@@ -337,7 +360,7 @@ describe("PaginatedTable Component", () => {
                 <PaginatedTable data={tableValue as TableValueType} defaultColumns={tableColumns} />
             </TaipyContext.Provider>
         );
-        expect(getAllByText("Austria").length).toBeGreaterThan(1)
+        expect(getAllByText("Austria").length).toBeGreaterThan(1);
 
         rerender(
             <TaipyContext.Provider value={{ state, dispatch }}>
@@ -375,7 +398,13 @@ describe("PaginatedTable Component", () => {
             const state: TaipyState = INITIAL_STATE;
             const { getAllByTestId, queryAllByTestId, rerender } = render(
                 <TaipyContext.Provider value={{ state, dispatch }}>
-                    <PaginatedTable data={undefined} defaultColumns={editableColumns} editable={true} onEdit="onEdit" showAll={true} />
+                    <PaginatedTable
+                        data={undefined}
+                        defaultColumns={editableColumns}
+                        editable={true}
+                        onEdit="onEdit"
+                        showAll={true}
+                    />
                 </TaipyContext.Provider>
             );
 
@@ -401,7 +430,13 @@ describe("PaginatedTable Component", () => {
             const state: TaipyState = INITIAL_STATE;
             const { getByTestId, queryAllByTestId, getAllByTestId, rerender } = render(
                 <TaipyContext.Provider value={{ state, dispatch }}>
-                    <PaginatedTable data={undefined} defaultColumns={editableColumns} editable={true} onEdit="onEdit" showAll={true} />
+                    <PaginatedTable
+                        data={undefined}
+                        defaultColumns={editableColumns}
+                        editable={true}
+                        onEdit="onEdit"
+                        showAll={true}
+                    />
                 </TaipyContext.Provider>
             );
 
@@ -499,7 +534,13 @@ describe("PaginatedTable Component", () => {
         const state: TaipyState = INITIAL_STATE;
         const { getByTestId } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <PaginatedTable data={undefined} defaultColumns={editableColumns} showAll={true} editable={true} onAdd="onAdd" />
+                <PaginatedTable
+                    data={undefined}
+                    defaultColumns={editableColumns}
+                    showAll={true}
+                    editable={true}
+                    onAdd="onAdd"
+                />
             </TaipyContext.Provider>
         );
 
@@ -521,7 +562,13 @@ describe("PaginatedTable Component", () => {
         const state: TaipyState = INITIAL_STATE;
         const { getAllByTestId, getByTestId, queryAllByTestId, rerender } = render(
             <TaipyContext.Provider value={{ state, dispatch }}>
-                <PaginatedTable data={undefined} defaultColumns={editableColumns} showAll={true} editable={true} onDelete="onDelete" />
+                <PaginatedTable
+                    data={undefined}
+                    defaultColumns={editableColumns}
+                    showAll={true}
+                    editable={true}
+                    onDelete="onDelete"
+                />
             </TaipyContext.Provider>
         );
 
@@ -650,4 +697,86 @@ describe("PaginatedTable Component", () => {
             type: "SEND_ACTION_ACTION",
         });
     });
+    it("should render correctly when style is applied to columns", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        await waitFor(() => {
+            render(
+                <TaipyContext.Provider value={{ state, dispatch }}>
+                    <PaginatedTable
+                        data={tableValue}
+                        defaultColumns={styledColumns}
+                        lineStyle={"class_name=rows-bordered"}
+                    />
+                </TaipyContext.Provider>
+            );
+        });
+        const elt = document.querySelector('table[aria-labelledby="tableTitle"]');
+        expect(elt).toBeInTheDocument();
+    });
+    it("logs error when baseColumns prop is invalid", () => {
+        // Mock console.info to check if it gets called
+        console.info = jest.fn();
+        // Render the component with invalid baseColumns prop
+        render(<PaginatedTable defaultColumns={invalidColumns} />);
+        // Check if console.info was called
+        expect(console.info).toHaveBeenCalled();
+    });
+    it("should sort the table in ascending order", async () => {
+        await waitFor(() => {
+            render(<PaginatedTable data={tableValue} defaultColumns={tableColumns} />);
+        });
+        const elt = document.querySelector('svg[data-testid="ArrowDownwardIcon"]');
+        act(() => {
+            fireEvent.click(elt as Element);
+        });
+        expect(document.querySelector('th[aria-sort="ascending"]')).toBeInTheDocument();
+    });
+    it("should handle rows per page change", async () => {
+        const { getByRole, queryByRole } = render(<PaginatedTable data={tableValue} defaultColumns={tableColumns} />);
+        const rowsPerPageDropdown = getByRole("combobox");
+        fireEvent.mouseDown(rowsPerPageDropdown);
+        const option = queryByRole("option", { selected: false, name: "50" });
+        fireEvent.click(option as Element);
+        const table = document.querySelector(
+            'table[aria-labelledby="tableTitle"].MuiTable-root.MuiTable-stickyHeader.css-cz602z-MuiTable-root'
+        );
+        expect(table).toBeInTheDocument();
+    });
+    it("should allow all rows", async () => {
+        const { getByRole, queryByRole } = render(
+            <PaginatedTable data={tableValue} defaultColumns={tableColumns} allowAllRows={true} />
+        );
+        const rowsPerPageDropdown = getByRole("combobox");
+        fireEvent.mouseDown(rowsPerPageDropdown);
+        const option = queryByRole("option", { selected: false, name: "All" });
+        expect(option).toBeInTheDocument();
+    });
+    it("should display row per page correctly", async () => {
+        const { getByRole, queryByRole } = render(
+            <PaginatedTable
+                data={tableValue}
+                defaultColumns={tableColumns}
+                pageSizeOptions={JSON.stringify([10, 20, 30])}
+            />
+        );
+        const rowsPerPageDropdown = getByRole("combobox");
+        fireEvent.mouseDown(rowsPerPageDropdown);
+        const option = queryByRole("option", { selected: false, name: "10" });
+        expect(option).toBeInTheDocument();
+    });
+    it("logs error when pageSizeOptions prop is invalid", () => {
+        // Create a spy on console.log
+        const logSpy = jest.spyOn(console, "log");
+        // Render the component with invalid pageSizeOptions prop
+        render(<PaginatedTable data={tableValue} defaultColumns={tableColumns} pageSizeOptions={"not a valid json"} />);
+        // Check if console.log was called with the expected arguments
+        expect(logSpy).toHaveBeenCalledWith(
+            "PaginatedTable pageSizeOptions is wrong ",
+            "not a valid json",
+            expect.any(Error)
+        );
+        // Clean up the spy
+        logSpy.mockRestore();
+    });
 });

+ 8 - 1
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -199,6 +199,11 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
 
     useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
 
+    /*
+    TODO: If the 'selected' value is a negative number, it will lead to unexpected pagination behavior.
+    For instance, if 'selected' is -1, the pagination will display from -99 to 0 and no data will be selected.
+    Need to fix this issue.
+    */
     useEffect(() => {
         if (selected.length) {
             if (selected[0] < startIndex || selected[0] > startIndex + rowsPerPage) {
@@ -259,7 +264,9 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                     afs,
                     compare ? onCompare : undefined,
                     updateVars && getUpdateVar(updateVars, "comparedatas"),
-                    typeof userData == "object" ? (userData as Record<string, Record<string, unknown>>).context : undefined
+                    typeof userData == "object"
+                        ? (userData as Record<string, Record<string, unknown>>).context
+                        : undefined
                 )
             );
         } else {

+ 1 - 1
frontend/taipy-gui/src/components/Taipy/Progress.tsx

@@ -49,7 +49,7 @@ const Progress = (props: ProgressBarProps) => {
     const { linear = false, showValue = false } = props;
 
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
-    const value = useDynamicProperty(props.value, props.defaultValue, undefined, "number");
+    const value = useDynamicProperty(props.value, props.defaultValue, undefined, "number", true);
     const render = useDynamicProperty(props.render, props.defaultRender, true);
 
     if (!render) {

+ 196 - 4
frontend/taipy-gui/src/components/Taipy/Slider.spec.tsx

@@ -18,6 +18,7 @@ import "@testing-library/jest-dom";
 import Slider from "./Slider";
 import { TaipyContext } from "../../context/taipyContext";
 import { TaipyState, INITIAL_STATE } from "../../context/taipyReducers";
+import { LoVElt } from "./lovUtils";
 
 describe("Slider Component", () => {
     it("renders", async () => {
@@ -83,7 +84,9 @@ describe("Slider Component", () => {
         });
     });
     it("holds a numeric range", async () => {
-        const { getByDisplayValue } = render(<Slider defaultValue={"[10,90]"} value={undefined as unknown as number[]} />);
+        const { getByDisplayValue } = render(
+            <Slider defaultValue={"[10,90]"} value={undefined as unknown as number[]} />
+        );
         const elt1 = getByDisplayValue("10");
         expect(elt1.tagName).toBe("INPUT");
         const elt2 = getByDisplayValue("90");
@@ -99,8 +102,8 @@ describe("Slider Component", () => {
         );
         const elts = getAllByText("Item 1");
         expect(elts).toHaveLength(3);
-        expect(elts[0].tagName).toBe("P")
-        expect(elts[1].tagName).toBe("P")
+        expect(elts[0].tagName).toBe("P");
+        expect(elts[1].tagName).toBe("P");
     });
     it("doesn't show text when_text_anchor is none", async () => {
         const { getAllByText } = render(
@@ -115,7 +118,9 @@ describe("Slider Component", () => {
         expect(elts[0].tagName).toBe("P");
     });
     it("holds a lov range", async () => {
-        const { getByDisplayValue } = render(<Slider value={["B", "C"]} defaultLov={'[["A", "A"], ["B", "B"], ["C", "C"], ["D", "D"]]'} />);
+        const { getByDisplayValue } = render(
+            <Slider value={["B", "C"]} defaultLov={'[["A", "A"], ["B", "B"], ["C", "C"], ["D", "D"]]'} />
+        );
         const elt1 = getByDisplayValue("1");
         expect(elt1.tagName).toBe("INPUT");
         const elt2 = getByDisplayValue("2");
@@ -150,4 +155,191 @@ describe("Slider Component", () => {
             type: "SEND_UPDATE_ACTION",
         });
     });
+    it("calls props.onChange when update is true and changeDelay is set", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+
+        const { getByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Slider value={33} changeDelay={100} />
+            </TaipyContext.Provider>
+        );
+
+        const slider = getByRole("slider");
+        fireEvent.change(slider, { target: { value: 50 } });
+
+        // Wait for the changeDelay timeout
+        await new Promise((r) => setTimeout(r, 150));
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "",
+            payload: { value: 50 },
+            propagate: true,
+            type: "SEND_UPDATE_ACTION",
+        });
+    });
+    it("should handle change when continuous is set to true", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const lovArray: [string, string][] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+        const { getByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Slider value={33} continuous={false} lov={lovArray} />
+            </TaipyContext.Provider>
+        );
+        const slider = getByRole("slider");
+        fireEvent.change(slider, { target: { value: 50 } });
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "",
+            payload: { value: "Item 3" },
+            propagate: true,
+            type: "SEND_UPDATE_ACTION",
+        });
+    });
+    it("returns correct text position and style when textAnchor is set to top", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+        const { container } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Slider changeDelay={0} lov={lovList} textAnchor="top" />
+            </TaipyContext.Provider>
+        );
+        expect(container).toHaveTextContent("Description 1");
+        const sliderContainer = container.querySelector("div");
+        expect(sliderContainer).toHaveStyle("display: inline-grid");
+        expect(sliderContainer).toHaveStyle("text-align: center");
+    });
+    it("returns correct style for textAnchor 'left'", () => {
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+        const { container } = render(<Slider lov={lovList} textAnchor="left" />);
+        const slider = container.querySelector("div");
+        expect(slider).toBeInTheDocument();
+        expect(slider).toHaveStyle("display: inline-grid");
+        expect(slider).toHaveStyle("grid-template-columns: auto 1fr");
+        expect(slider).toHaveStyle("align-items: center");
+    });
+    it("should change the orientation of the slider to horizontal", async () => {
+        render(<Slider value={5} orientation="horizontal" />);
+        const horizontalInputs = document.querySelectorAll('input[aria-orientation="horizontal"]');
+        expect(horizontalInputs).toHaveLength(1);
+    });
+    it("should change the orientation of the slider to vertical", async () => {
+        render(<Slider value={5} orientation="vertical" />);
+        const verticalInputs = document.querySelectorAll('input[aria-orientation="vertical"]');
+        expect(verticalInputs).toHaveLength(1);
+    });
+    it("should change the orientation of the slider to vertical when default value is an array", async () => {
+        render(<Slider orientation="vertical" defaultValue={[1, 2]} />);
+        const verticalInputs = document.querySelectorAll('input[aria-orientation="vertical"]');
+        expect(verticalInputs).toHaveLength(2);
+    });
+    it("should return an array of number when value is an array of number and no lov is defined", async () => {
+        const { getAllByRole } = render(<Slider value={[1, 2]} />);
+        const sliders = getAllByRole("slider");
+        expect(sliders).toHaveLength(2);
+    });
+    it("handles case when label is out of range", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+        const labels = {
+            "Item 4": "Label for Item 4",
+        };
+        const { container } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Slider changeDelay={0} lov={lovList} labels={JSON.stringify(labels)} />
+            </TaipyContext.Provider>
+        );
+        expect(container).not.toHaveTextContent("Label for Item 4");
+    });
+    it("should parse lov when default value is greater than 2 values", async () => {
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+        const { container } = render(<Slider defaultValue='["Item 2", "Item 3"]' lov={lovList} />);
+        const slider = container.querySelector("div");
+        expect(slider).toBeInTheDocument();
+    });
+    it("should parse lov when default value is less than 2 values", async () => {
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+
+        const { container } = render(<Slider defaultValue='["Item 3"]' lov={lovList} />);
+        const slider = container.querySelector("div");
+        expect(slider).toBeInTheDocument();
+    });
+    it("throws an error when defaultValue is an invalid JSON string", () => {
+        const lovList: LoVElt[] = [
+            ["Item 1", "Description 1"],
+            ["Item 2", "Description 2"],
+            ["Item 3", "Description 3"],
+        ];
+
+        const errorSpy = jest.spyOn(global, "Error");
+
+        expect(() => {
+            render(<Slider defaultValue="invalid-json" lov={lovList} />);
+        }).toThrow("Slider lov value couldn't be parsed");
+        expect(errorSpy).toHaveBeenCalledWith("Slider lov value couldn't be parsed");
+        errorSpy.mockRestore();
+    });
+    it("throws an error when defaultValue contains non-numeric values", () => {
+        const errorSpy = jest.spyOn(global, "Error");
+        render(<Slider defaultValue='["Item 1", "Item 2"]' />);
+        expect(errorSpy).toHaveBeenCalledWith("Slider values should all be numbers");
+    });
+    it("should return number when default value is a number", async () => {
+        const { container } = render(<Slider defaultValue={1} />);
+        const slider = container.querySelector("div");
+        expect(slider).toBeInTheDocument();
+    });
+    it("should return number when default value is a number", async () => {
+        const { getByRole } = render(<Slider defaultValue={"10"} />);
+        const inputElement = getByRole("slider", { hidden: true }) as HTMLInputElement;
+        expect(inputElement).toHaveValue("10");
+    });
+    it("should return an array numbers when value is an array of number", async () => {
+        const dispatch = jest.fn();
+        const state: TaipyState = INITIAL_STATE;
+        const lovList: LoVElt[] = [
+            ["1", "Description 1"],
+            ["2", "Description 2"],
+            ["3", "Description 3"],
+        ];
+
+        const { getAllByRole } = render(
+            <TaipyContext.Provider value={{ state, dispatch }}>
+                <Slider value={["1", "2"]} lov={lovList} />
+            </TaipyContext.Provider>
+        );
+        const sliders = getAllByRole("slider");
+        fireEvent.change(sliders[0], { target: { value: "2" } });
+        expect(dispatch).toHaveBeenCalledWith({
+            name: "",
+            payload: { value: ["2", "3"] },
+            propagate: true,
+            type: "SEND_UPDATE_ACTION",
+        });
+    });
 });

+ 10 - 4
frontend/taipy-gui/src/utils/hooks.ts

@@ -29,16 +29,22 @@ import { TIMEZONE_CLIENT } from "../utils";
  * @param defaultStatic - The default static value.
  * @returns The latest updated value.
  */
-export const useDynamicProperty = <T>(value: T, defaultValue: T, defaultStatic: T, check_type?: string): T => {
+export const useDynamicProperty = <T>(value: T, defaultValue: T, defaultStatic: T, checkType?: string, nullToDefault?: boolean): T => {
     return useMemo(() => {
-        if (value !== undefined && (!check_type || typeof value === check_type)) {
+        if (nullToDefault && value === null) {
+            return defaultStatic;
+        }
+        if (value !== undefined && (!checkType || typeof value === checkType)) {
             return value;
         }
-        if (defaultValue !== undefined && (!check_type || typeof value === check_type)) {
+        if (nullToDefault && defaultValue === null) {
+            return defaultStatic;
+        }
+        if (defaultValue !== undefined && (!checkType || typeof defaultValue === checkType)) {
             return defaultValue;
         }
         return defaultStatic;
-    }, [value, defaultValue, defaultStatic, check_type]);
+    }, [value, defaultValue, defaultStatic, checkType, nullToDefault]);
 };
 
 /**

+ 177 - 280
frontend/taipy/package-lock.json

@@ -56,11 +56,11 @@
       }
     },
     "node_modules/@babel/generator": {
-      "version": "7.24.10",
-      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.10.tgz",
-      "integrity": "sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
+      "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
       "dependencies": {
-        "@babel/types": "^7.24.9",
+        "@babel/types": "^7.25.0",
         "@jridgewell/gen-mapping": "^0.3.5",
         "@jridgewell/trace-mapping": "^0.3.25",
         "jsesc": "^2.5.1"
@@ -69,40 +69,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/helper-environment-visitor": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
-      "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-function-name": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
-      "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
-      "dependencies": {
-        "@babel/template": "^7.24.7",
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
-    "node_modules/@babel/helper-hoist-variables": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
-      "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-module-imports": {
       "version": "7.24.7",
       "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz",
@@ -115,17 +81,6 @@
         "node": ">=6.9.0"
       }
     },
-    "node_modules/@babel/helper-split-export-declaration": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
-      "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
-      "dependencies": {
-        "@babel/types": "^7.24.7"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@babel/helper-string-parser": {
       "version": "7.24.8",
       "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
@@ -157,9 +112,12 @@
       }
     },
     "node_modules/@babel/parser": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz",
-      "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==",
+      "version": "7.25.3",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
+      "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+      "dependencies": {
+        "@babel/types": "^7.25.2"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -168,9 +126,9 @@
       }
     },
     "node_modules/@babel/runtime": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
-      "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
+      "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
       "dependencies": {
         "regenerator-runtime": "^0.14.0"
       },
@@ -179,31 +137,28 @@
       }
     },
     "node_modules/@babel/template": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
-      "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
+      "version": "7.25.0",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
+      "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/parser": "^7.24.7",
-        "@babel/types": "^7.24.7"
+        "@babel/parser": "^7.25.0",
+        "@babel/types": "^7.25.0"
       },
       "engines": {
         "node": ">=6.9.0"
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.24.8",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz",
-      "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==",
+      "version": "7.25.3",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
+      "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
       "dependencies": {
         "@babel/code-frame": "^7.24.7",
-        "@babel/generator": "^7.24.8",
-        "@babel/helper-environment-visitor": "^7.24.7",
-        "@babel/helper-function-name": "^7.24.7",
-        "@babel/helper-hoist-variables": "^7.24.7",
-        "@babel/helper-split-export-declaration": "^7.24.7",
-        "@babel/parser": "^7.24.8",
-        "@babel/types": "^7.24.8",
+        "@babel/generator": "^7.25.0",
+        "@babel/parser": "^7.25.3",
+        "@babel/template": "^7.25.0",
+        "@babel/types": "^7.25.2",
         "debug": "^4.3.1",
         "globals": "^11.1.0"
       },
@@ -212,9 +167,9 @@
       }
     },
     "node_modules/@babel/types": {
-      "version": "7.24.9",
-      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.9.tgz",
-      "integrity": "sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ==",
+      "version": "7.25.2",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
+      "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
       "dependencies": {
         "@babel/helper-string-parser": "^7.24.8",
         "@babel/helper-validator-identifier": "^7.24.7",
@@ -252,9 +207,9 @@
       }
     },
     "node_modules/@emotion/cache": {
-      "version": "11.13.0",
-      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.0.tgz",
-      "integrity": "sha512-hPV345J/tH0Cwk2wnU/3PBzORQ9HeX+kQSbwI+jslzpRCHE6fSGTohswksA/Ensr8znPzwfzKZCmAM9Lmlhp7g==",
+      "version": "11.13.1",
+      "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
+      "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==",
       "dependencies": {
         "@emotion/memoize": "^0.9.0",
         "@emotion/sheet": "^1.4.0",
@@ -459,40 +414,6 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
-    "node_modules/@floating-ui/core": {
-      "version": "1.6.5",
-      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz",
-      "integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==",
-      "dependencies": {
-        "@floating-ui/utils": "^0.2.5"
-      }
-    },
-    "node_modules/@floating-ui/dom": {
-      "version": "1.6.8",
-      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz",
-      "integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==",
-      "dependencies": {
-        "@floating-ui/core": "^1.6.0",
-        "@floating-ui/utils": "^0.2.5"
-      }
-    },
-    "node_modules/@floating-ui/react-dom": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz",
-      "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==",
-      "dependencies": {
-        "@floating-ui/dom": "^1.0.0"
-      },
-      "peerDependencies": {
-        "react": ">=16.8.0",
-        "react-dom": ">=16.8.0"
-      }
-    },
-    "node_modules/@floating-ui/utils": {
-      "version": "0.2.5",
-      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz",
-      "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ=="
-    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.11.14",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -742,50 +663,19 @@
       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
       "dev": true
     },
-    "node_modules/@mui/base": {
-      "version": "5.0.0-beta.40",
-      "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
-      "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==",
-      "dependencies": {
-        "@babel/runtime": "^7.23.9",
-        "@floating-ui/react-dom": "^2.0.8",
-        "@mui/types": "^7.2.14",
-        "@mui/utils": "^5.15.14",
-        "@popperjs/core": "^2.11.8",
-        "clsx": "^2.1.0",
-        "prop-types": "^15.8.1"
-      },
-      "engines": {
-        "node": ">=12.0.0"
-      },
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/mui-org"
-      },
-      "peerDependencies": {
-        "@types/react": "^17.0.0 || ^18.0.0",
-        "react": "^17.0.0 || ^18.0.0",
-        "react-dom": "^17.0.0 || ^18.0.0"
-      },
-      "peerDependenciesMeta": {
-        "@types/react": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/@mui/core-downloads-tracker": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.4.tgz",
-      "integrity": "sha512-rNdHXhclwjEZnK+//3SR43YRx0VtjdHnUFhMSGYmAMJve+KiwEja/41EYh8V3pZKqF2geKyfcFUenTfDTYUR4w==",
+      "version": "5.16.7",
+      "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz",
+      "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==",
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mui-org"
       }
     },
     "node_modules/@mui/icons-material": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.4.tgz",
-      "integrity": "sha512-j9/CWctv6TH6Dou2uR2EH7UOgu79CW/YcozxCYVLJ7l03pCsiOlJ5sBArnWJxJ+nGkFwyL/1d1k8JEPMDR125A==",
+      "version": "5.16.7",
+      "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz",
+      "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==",
       "dependencies": {
         "@babel/runtime": "^7.23.9"
       },
@@ -808,15 +698,15 @@
       }
     },
     "node_modules/@mui/material": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.4.tgz",
-      "integrity": "sha512-dBnh3/zRYgEVIS3OE4oTbujse3gifA0qLMmuUk13ywsDCbngJsdgwW5LuYeiT5pfA8PGPGSqM7mxNytYXgiMCw==",
+      "version": "5.16.7",
+      "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz",
+      "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==",
       "dependencies": {
         "@babel/runtime": "^7.23.9",
-        "@mui/core-downloads-tracker": "^5.16.4",
-        "@mui/system": "^5.16.4",
+        "@mui/core-downloads-tracker": "^5.16.7",
+        "@mui/system": "^5.16.7",
         "@mui/types": "^7.2.15",
-        "@mui/utils": "^5.16.4",
+        "@mui/utils": "^5.16.6",
         "@popperjs/core": "^2.11.8",
         "@types/react-transition-group": "^4.4.10",
         "clsx": "^2.1.0",
@@ -852,12 +742,12 @@
       }
     },
     "node_modules/@mui/private-theming": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.4.tgz",
-      "integrity": "sha512-ZsAm8cq31SJ37SVWLRlu02v9SRthxnfQofaiv14L5Bht51B0dz6yQEoVU/V8UduZDCCIrWkBHuReVfKhE/UuXA==",
+      "version": "5.16.6",
+      "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz",
+      "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==",
       "dependencies": {
         "@babel/runtime": "^7.23.9",
-        "@mui/utils": "^5.16.4",
+        "@mui/utils": "^5.16.6",
         "prop-types": "^15.8.1"
       },
       "engines": {
@@ -878,9 +768,9 @@
       }
     },
     "node_modules/@mui/styled-engine": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.4.tgz",
-      "integrity": "sha512-0+mnkf+UiAmTVB8PZFqOhqf729Yh0Cxq29/5cA3VAyDVTRIUUQ8FXQhiAhUIbijFmM72rY80ahFPXIm4WDbzcA==",
+      "version": "5.16.6",
+      "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz",
+      "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==",
       "dependencies": {
         "@babel/runtime": "^7.23.9",
         "@emotion/cache": "^11.11.0",
@@ -909,15 +799,15 @@
       }
     },
     "node_modules/@mui/system": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.4.tgz",
-      "integrity": "sha512-ET1Ujl2/8hbsD611/mqUuNArMCGv/fIWO/f8B3ZqF5iyPHM2aS74vhTNyjytncc4i6dYwGxNk+tLa7GwjNS0/w==",
+      "version": "5.16.7",
+      "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz",
+      "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==",
       "dependencies": {
         "@babel/runtime": "^7.23.9",
-        "@mui/private-theming": "^5.16.4",
-        "@mui/styled-engine": "^5.16.4",
+        "@mui/private-theming": "^5.16.6",
+        "@mui/styled-engine": "^5.16.6",
         "@mui/types": "^7.2.15",
-        "@mui/utils": "^5.16.4",
+        "@mui/utils": "^5.16.6",
         "clsx": "^2.1.0",
         "csstype": "^3.1.3",
         "prop-types": "^15.8.1"
@@ -961,11 +851,12 @@
       }
     },
     "node_modules/@mui/utils": {
-      "version": "5.16.4",
-      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.4.tgz",
-      "integrity": "sha512-nlppYwq10TBIFqp7qxY0SvbACOXeOjeVL3pOcDsK0FT8XjrEXh9/+lkg8AEIzD16z7YfiJDQjaJG2OLkE7BxNg==",
+      "version": "5.16.6",
+      "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz",
+      "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==",
       "dependencies": {
         "@babel/runtime": "^7.23.9",
+        "@mui/types": "^7.2.15",
         "@types/prop-types": "^15.7.12",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
@@ -989,14 +880,13 @@
       }
     },
     "node_modules/@mui/x-date-pickers": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.11.0.tgz",
-      "integrity": "sha512-+zPWs1dwe7J1nZ2iFhTgCae31BLMYMQ2VtQfHxx21Dh6gbBRy/U7YJZg1LdhfQyE093S3e4A5uMZ6PUWdne7iA==",
-      "dependencies": {
-        "@babel/runtime": "^7.24.8",
-        "@mui/base": "^5.0.0-beta.40",
-        "@mui/system": "^5.16.2",
-        "@mui/utils": "^5.16.2",
+      "version": "7.12.1",
+      "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.12.1.tgz",
+      "integrity": "sha512-Zj8kt3SCQbJp1qhMi+A3I4KqB8i5OY2Q11mdOEathFhqN/SQm1sUjIa1G09cGP1dPDgK1a6KM6qJGNtcw/nuWA==",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "@mui/system": "^5.16.5",
+        "@mui/utils": "^5.16.5",
         "@types/react-transition-group": "^4.4.10",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
@@ -1054,12 +944,12 @@
       }
     },
     "node_modules/@mui/x-internals": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.11.0.tgz",
-      "integrity": "sha512-GqCYylKiB4cLH9tK4JweJlT2JvPjnpXjS3TEIqtHB4BcSsezhdRrMGzHOO5zCJqkasqTirJh2t6X16Qw1llr4Q==",
+      "version": "7.12.0",
+      "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.12.0.tgz",
+      "integrity": "sha512-zgu/JqSXBflSvtzfFN8lNi5Wxw79czBv6V/crOrXqCCOzxAIsrcup2FZlwvXlzetm3otS7o/Tzfo/O5dE68NkA==",
       "dependencies": {
-        "@babel/runtime": "^7.24.8",
-        "@mui/utils": "^5.16.2"
+        "@babel/runtime": "^7.25.0",
+        "@mui/utils": "^5.16.5"
       },
       "engines": {
         "node": ">=14.0.0"
@@ -1073,15 +963,14 @@
       }
     },
     "node_modules/@mui/x-tree-view": {
-      "version": "7.11.0",
-      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.11.0.tgz",
-      "integrity": "sha512-/nk3hhTW5c4Uk2MIcIujC6w5/e5m8RbfWY0YTfRdHApmcFjeEZDX7O5pky5DojhaALopDuNebr9PlE8QYloaiw==",
-      "dependencies": {
-        "@babel/runtime": "^7.24.8",
-        "@mui/base": "^5.0.0-beta.40",
-        "@mui/system": "^5.16.2",
-        "@mui/utils": "^5.16.2",
-        "@mui/x-internals": "7.11.0",
+      "version": "7.12.1",
+      "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-7.12.1.tgz",
+      "integrity": "sha512-WEejS6mzKQzwm0vKT5W1XqlHxqIFv0AV/MYDgvru39WwaCUCyip32sjvl7cDNwrsC8CkwyBCaEvNDEE9Jx0BkA==",
+      "dependencies": {
+        "@babel/runtime": "^7.25.0",
+        "@mui/system": "^5.16.5",
+        "@mui/utils": "^5.16.5",
+        "@mui/x-internals": "7.12.0",
         "@types/react-transition-group": "^4.4.10",
         "clsx": "^2.1.1",
         "prop-types": "^15.8.1",
@@ -1100,6 +989,14 @@
         "@mui/material": "^5.15.14",
         "react": "^17.0.0 || ^18.0.0",
         "react-dom": "^17.0.0 || ^18.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@emotion/react": {
+          "optional": true
+        },
+        "@emotion/styled": {
+          "optional": true
+        }
       }
     },
     "node_modules/@nodelib/fs.scandir": {
@@ -1292,12 +1189,12 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "20.14.11",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz",
-      "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==",
+      "version": "22.3.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz",
+      "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==",
       "dev": true,
       "dependencies": {
-        "undici-types": "~5.26.4"
+        "undici-types": "~6.18.2"
       }
     },
     "node_modules/@types/parse-json": {
@@ -1320,17 +1217,17 @@
       }
     },
     "node_modules/@types/react-transition-group": {
-      "version": "4.4.10",
-      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
-      "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
+      "version": "4.4.11",
+      "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz",
+      "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==",
       "dependencies": {
         "@types/react": "*"
       }
     },
     "node_modules/@types/yargs": {
-      "version": "17.0.32",
-      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
-      "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
+      "version": "17.0.33",
+      "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz",
+      "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==",
       "dev": true,
       "dependencies": {
         "@types/yargs-parser": "*"
@@ -1343,16 +1240,16 @@
       "dev": true
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz",
-      "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+      "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "7.17.0",
-        "@typescript-eslint/type-utils": "7.17.0",
-        "@typescript-eslint/utils": "7.17.0",
-        "@typescript-eslint/visitor-keys": "7.17.0",
+        "@typescript-eslint/scope-manager": "7.18.0",
+        "@typescript-eslint/type-utils": "7.18.0",
+        "@typescript-eslint/utils": "7.18.0",
+        "@typescript-eslint/visitor-keys": "7.18.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
@@ -1376,15 +1273,15 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz",
-      "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+      "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/scope-manager": "7.17.0",
-        "@typescript-eslint/types": "7.17.0",
-        "@typescript-eslint/typescript-estree": "7.17.0",
-        "@typescript-eslint/visitor-keys": "7.17.0",
+        "@typescript-eslint/scope-manager": "7.18.0",
+        "@typescript-eslint/types": "7.18.0",
+        "@typescript-eslint/typescript-estree": "7.18.0",
+        "@typescript-eslint/visitor-keys": "7.18.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -1404,13 +1301,13 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz",
-      "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+      "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.17.0",
-        "@typescript-eslint/visitor-keys": "7.17.0"
+        "@typescript-eslint/types": "7.18.0",
+        "@typescript-eslint/visitor-keys": "7.18.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1421,13 +1318,13 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz",
-      "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+      "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "7.17.0",
-        "@typescript-eslint/utils": "7.17.0",
+        "@typescript-eslint/typescript-estree": "7.18.0",
+        "@typescript-eslint/utils": "7.18.0",
         "debug": "^4.3.4",
         "ts-api-utils": "^1.3.0"
       },
@@ -1448,9 +1345,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz",
-      "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+      "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
       "dev": true,
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1461,13 +1358,13 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz",
-      "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+      "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.17.0",
-        "@typescript-eslint/visitor-keys": "7.17.0",
+        "@typescript-eslint/types": "7.18.0",
+        "@typescript-eslint/visitor-keys": "7.18.0",
         "debug": "^4.3.4",
         "globby": "^11.1.0",
         "is-glob": "^4.0.3",
@@ -1489,15 +1386,15 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz",
-      "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+      "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "7.17.0",
-        "@typescript-eslint/types": "7.17.0",
-        "@typescript-eslint/typescript-estree": "7.17.0"
+        "@typescript-eslint/scope-manager": "7.18.0",
+        "@typescript-eslint/types": "7.18.0",
+        "@typescript-eslint/typescript-estree": "7.18.0"
       },
       "engines": {
         "node": "^18.18.0 || >=20.0.0"
@@ -1511,12 +1408,12 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "7.17.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz",
-      "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==",
+      "version": "7.18.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+      "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
       "dev": true,
       "dependencies": {
-        "@typescript-eslint/types": "7.17.0",
+        "@typescript-eslint/types": "7.18.0",
         "eslint-visitor-keys": "^3.4.3"
       },
       "engines": {
@@ -2051,9 +1948,9 @@
       }
     },
     "node_modules/browserslist": {
-      "version": "4.23.2",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
-      "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
+      "version": "4.23.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+      "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
       "dev": true,
       "funding": [
         {
@@ -2070,9 +1967,9 @@
         }
       ],
       "dependencies": {
-        "caniuse-lite": "^1.0.30001640",
-        "electron-to-chromium": "^1.4.820",
-        "node-releases": "^2.0.14",
+        "caniuse-lite": "^1.0.30001646",
+        "electron-to-chromium": "^1.5.4",
+        "node-releases": "^2.0.18",
         "update-browserslist-db": "^1.1.0"
       },
       "bin": {
@@ -2116,9 +2013,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001643",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001643.tgz",
-      "integrity": "sha512-ERgWGNleEilSrHM6iUz/zJNSQTP8Mr21wDWpdgvRwcTXGAq6jMtOUPP4dqFPTdKqZ2wKTdtB+uucZ3MRpAUSmg==",
+      "version": "1.0.30001651",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
+      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
       "dev": true,
       "funding": [
         {
@@ -2348,9 +2245,9 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.5",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
-      "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+      "version": "4.3.6",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
+      "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
       "dependencies": {
         "ms": "2.1.2"
       },
@@ -2457,15 +2354,15 @@
       }
     },
     "node_modules/electron-to-chromium": {
-      "version": "1.5.0",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.0.tgz",
-      "integrity": "sha512-Vb3xHHYnLseK8vlMJQKJYXJ++t4u1/qJ3vykuVrVjvdiOEhYyT1AuP4x03G8EnPmYvYOhe9T+dADTmthjRQMkA==",
+      "version": "1.5.7",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.7.tgz",
+      "integrity": "sha512-6FTNWIWMxMy/ZY6799nBlPtF1DFDQ6VQJ7yyDP27SJNt5lwtQ5ufqVvHylb3fdQefvRcgA3fKcFMJi9OLwBRNw==",
       "dev": true
     },
     "node_modules/enhanced-resolve": {
-      "version": "5.17.0",
-      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz",
-      "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==",
+      "version": "5.17.1",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
+      "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
       "dev": true,
       "dependencies": {
         "graceful-fs": "^4.2.4",
@@ -3563,9 +3460,9 @@
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
     "node_modules/ignore": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
-      "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "dev": true,
       "engines": {
         "node": ">= 4"
@@ -5449,9 +5346,9 @@
       }
     },
     "node_modules/terser": {
-      "version": "5.31.3",
-      "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.3.tgz",
-      "integrity": "sha512-pAfYn3NIZLyZpa83ZKigvj6Rn9c/vd5KfYGX7cN1mnzqgDcxWvrU5ZtAfIKhEXz9nRecw4z3LXkjaq96/qZqAA==",
+      "version": "5.31.6",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
+      "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
       "dev": true,
       "dependencies": {
         "@jridgewell/source-map": "^0.3.3",
@@ -5829,9 +5726,9 @@
       }
     },
     "node_modules/undici-types": {
-      "version": "5.26.5",
-      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
-      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "version": "6.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz",
+      "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==",
       "dev": true
     },
     "node_modules/update-browserslist-db": {
@@ -5874,9 +5771,9 @@
       }
     },
     "node_modules/watchpack": {
-      "version": "2.4.1",
-      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
-      "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
+      "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
       "dev": true,
       "dependencies": {
         "glob-to-regexp": "^0.4.1",
@@ -6082,13 +5979,13 @@
       }
     },
     "node_modules/which-builtin-type": {
-      "version": "1.1.3",
-      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
-      "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz",
+      "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==",
       "dev": true,
       "dependencies": {
-        "function.prototype.name": "^1.1.5",
-        "has-tostringtag": "^1.0.0",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
         "is-async-function": "^2.0.0",
         "is-date-object": "^1.0.5",
         "is-finalizationregistry": "^1.0.2",
@@ -6097,8 +5994,8 @@
         "is-weakref": "^1.0.2",
         "isarray": "^2.0.5",
         "which-boxed-primitive": "^1.0.2",
-        "which-collection": "^1.0.1",
-        "which-typed-array": "^1.1.9"
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.15"
       },
       "engines": {
         "node": ">= 0.4"

+ 10 - 8
frontend/taipy/src/CoreSelector.tsx

@@ -375,14 +375,16 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 return;
             }
             setSelectedItems(() => {
-                const lovVar = getUpdateVar(updateVars, lovPropertyName);
-                const val = multiple ? nodeId : isSelectable ? nodeId : "";
-                setTimeout(
-                    // to avoid set state while render react errors
-                    () => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, lovVar)),
-                    1
-                );
-                onSelect && isSelectable && onSelect(val);
+                if (isSelectable) {
+                    const lovVar = getUpdateVar(updateVars, lovPropertyName);
+                    const val = nodeId;
+                    setTimeout(
+                        // to avoid set state while render react errors
+                        () => dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, lovVar)),
+                        1
+                    );
+                    onSelect && onSelect(val);
+                }
                 return Array.isArray(nodeId) ? nodeId : nodeId ? [nodeId] : [];
             });
         },

+ 3 - 3
frontend/taipy/src/DataNodeTable.tsx

@@ -58,14 +58,14 @@ interface DataNodeTableProps {
     onLock?: string;
     editInProgress?: boolean;
     editLock: MutableRefObject<boolean>;
-    editable: boolean;
+    notEditableReason: string;
     updateDnVars?: string;
 }
 
 const pushRightSx = { ml: "auto" };
 
 const DataNodeTable = (props: DataNodeTableProps) => {
-    const { uniqid, configId, nodeId, columns = "", onViewTypeChange, editable, updateDnVars = "" } = props;
+    const { uniqid, configId, nodeId, columns = "", onViewTypeChange, notEditableReason, updateDnVars = "" } = props;
 
     const dispatch = useDispatch();
     const module = useModule();
@@ -202,7 +202,7 @@ const DataNodeTable = (props: DataNodeTableProps) => {
                 ) : null}
                 <Grid item sx={tableEdit ? undefined : pushRightSx}>
                     <FormControlLabel
-                        disabled={!props.active || !editable || !!props.editInProgress}
+                        disabled={!props.active || !!notEditableReason || !!props.editInProgress}
                         control={<Switch color="primary" checked={tableEdit} onChange={toggleTableEdit} />}
                         label="Edit data"
                         labelPlacement="start"

+ 13 - 13
frontend/taipy/src/DataNodeViewer.tsx

@@ -114,8 +114,8 @@ type DataNodeFull = [
     DatanodeData, // data
     boolean, // editInProgress
     string, // editorId
-    boolean, // readable
-    boolean // editable
+    string, // notReadableReason
+    string // notEditableReason
 ];
 
 enum DataNodeFullProps {
@@ -131,8 +131,8 @@ enum DataNodeFullProps {
     data,
     editInProgress,
     editorId,
-    readable,
-    editable,
+    notReadableReason,
+    notEditableReason,
 }
 const DataNodeFullLength = Object.keys(DataNodeFullProps).length / 2;
 
@@ -206,8 +206,8 @@ const invalidDatanode: DataNodeFull = [
     [null, null, null, null],
     false,
     "",
-    false,
-    false,
+    "invalid",
+    "invalid",
 ];
 
 enum TabValues {
@@ -253,8 +253,8 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         dnData,
         dnEditInProgress,
         dnEditorId,
-        dnReadable,
-        dnEditable,
+        dnNotReadableReason,
+        dnNotEditableReason,
     ] = datanode;
     const dtType = dnData[DatanodeDataProps.type];
     const dtValue = dnData[DatanodeDataProps.value] ?? (dtType == "float" ? null : undefined);
@@ -454,7 +454,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
         [dnId, id, dispatch, module, props.onLock, updateDnVars]
     );
 
-    const active = useDynamicProperty(props.active, props.defaultActive, true) && dnReadable;
+    const active = useDynamicProperty(props.active, props.defaultActive, true) && !dnNotReadableReason;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
 
     // history & data
@@ -715,7 +715,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                         onClick={onFocus}
                                         sx={hoverSx}
                                     >
-                                        {active && dnEditable && focusName === "label" ? (
+                                        {active && !dnNotEditableReason && focusName === "label" ? (
                                             <TextField
                                                 label="Label"
                                                 variant="outlined"
@@ -859,7 +859,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                     setFocusName={setFocusName}
                                     onFocus={onFocus}
                                     onEdit={props.onEdit}
-                                    editable={dnEditable}
+                                    notEditableReason={dnNotEditableReason}
                                     updatePropVars={updateDnVars}
                                 />
                             </Grid>
@@ -929,7 +929,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                         sx={hoverSx}
                                     >
                                         {active &&
-                                        dnEditable &&
+                                        !dnNotEditableReason &&
                                         dnEditInProgress &&
                                         dnEditorId === editorId &&
                                         focusName === dataValueFocus ? (
@@ -1088,7 +1088,7 @@ const DataNodeViewer = (props: DataNodeViewerProps) => {
                                                 onLock={props.onLock}
                                                 editInProgress={dnEditInProgress && dnEditorId !== editorId}
                                                 editLock={editLock}
-                                                editable={dnEditable}
+                                                notEditableReason={dnNotEditableReason}
                                                 updateDnVars={updateDnVars}
                                             />
                                         ) : (

+ 4 - 4
frontend/taipy/src/PropertiesEditor.tsx

@@ -50,7 +50,7 @@ interface PropertiesEditorProps {
     setFocusName: (name: string) => void;
     isDefined: boolean;
     onEdit?: string;
-    editable: boolean;
+    notEditableReason: string;
     updatePropVars?: string;
 }
 
@@ -65,7 +65,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
         focusName,
         setFocusName,
         entProperties,
-        editable,
+        notEditableReason,
         updatePropVars = "",
     } = props;
 
@@ -195,7 +195,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                   onClick={onFocus}
                                   sx={hoverSx}
                               >
-                                  {active && editable && focusName === propName ? (
+                                  {active && !notEditableReason && focusName === propName ? (
                                       <>
                                           <Grid item xs={4}>
                                               <TextField
@@ -284,7 +284,7 @@ const PropertiesEditor = (props: PropertiesEditorProps) => {
                                           </Grid>
                                           <Grid item xs={5}>
                                               <Typography variant="subtitle2">{property.value}</Typography>
-                                          </Grid>{" "}
+                                          </Grid>
                                           <Grid item xs={3} />
                                       </>
                                   )}

+ 1 - 0
frontend/taipy/src/ScenarioSelector.tsx

@@ -457,6 +457,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                     { action: props.onScenarioCrud, error_id: getUpdateVar(updateScVars, "error_id") },
                     props.onCreation,
                     props.updateVarName,
+                    props.onChange,
                     ...values
                 )
             );

+ 19 - 19
frontend/taipy/src/ScenarioViewer.tsx

@@ -106,7 +106,7 @@ interface SequencesRowProps {
     focusName: string;
     setFocusName: (name: string) => void;
     notSubmittableReason: string;
-    editable: boolean;
+    notEditableReason: string;
     isValid: (sLabel: string, label: string) => boolean;
 }
 
@@ -119,12 +119,12 @@ const tagsAutocompleteSx = {
     maxWidth: "none",
 };
 
-type SequenceFull = [string, string[], string, boolean];
+type SequenceFull = [string, string[], string, string];
 // enum SeFProps {
 //     label,
 //     tasks,
-//     submittable,
-//     editable,
+//     notSubmittableReason,
+//     notEditablereason,
 // }
 
 const SequenceRow = ({
@@ -141,7 +141,7 @@ const SequenceRow = ({
     focusName,
     setFocusName,
     notSubmittableReason,
-    editable,
+    notEditableReason,
     isValid,
 }: SequencesRowProps) => {
     const [label, setLabel] = useState("");
@@ -202,7 +202,7 @@ const SequenceRow = ({
 
     return (
         <Grid item xs={12} container justifyContent="space-between" data-focus={name} onClick={onFocus} sx={hoverSx}>
-            {active && editable && focusName === name ? (
+            {active && !notEditableReason && focusName === name ? (
                 <>
                     <Grid item xs={4}>
                         <TextField
@@ -324,11 +324,11 @@ const invalidScenario: ScenarioFull = [
     [],
     {},
     [],
-    false,
-    false,
     "invalid",
-    false,
-    false,
+    "invalid",
+    "invalid",
+    "invalid",
+    "invalid",
 ];
 
 const ScenarioViewer = (props: ScenarioViewerProps) => {
@@ -390,11 +390,11 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         scDeletable,
         scPromotable,
         scNotSubmittableReason,
-        scReadable,
-        scEditable,
+        scNotReadableReason,
+        scNotEditableReason,
     ] = scenario || invalidScenario;
 
-    const active = useDynamicProperty(props.active, props.defaultActive, true) && scReadable;
+    const active = useDynamicProperty(props.active, props.defaultActive, true) && !scNotReadableReason;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
 
     const [deleteDialog, setDeleteDialogOpen] = useState(false);
@@ -595,7 +595,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         [sequences]
     );
 
-    const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", true]]), []);
+    const addSequenceHandler = useCallback(() => setSequences((seq) => [...seq, ["", [], "", ""]]), []);
 
     // on scenario change
     useEffect(() => {
@@ -714,7 +714,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                     onClick={onFocus}
                                     sx={hoverSx}
                                 >
-                                    {active && scEditable && focusName === "label" ? (
+                                    {active && !scNotEditableReason && focusName === "label" ? (
                                         <TextField
                                             label="Label"
                                             variant="outlined"
@@ -770,7 +770,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         onClick={onFocus}
                                         sx={hoverSx}
                                     >
-                                        {active && scEditable && focusName === "tags" ? (
+                                        {active && !scNotEditableReason && focusName === "tags" ? (
                                             <Autocomplete
                                                 multiple
                                                 options={scAuthorizedTags}
@@ -857,7 +857,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 setFocusName={setFocusName}
                                 onFocus={onFocus}
                                 onEdit={props.onEdit}
-                                editable={scEditable}
+                                notEditableReason={scNotEditableReason}
                                 updatePropVars={updateScVars}
                             />
                             {showSequences ? (
@@ -874,7 +874,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                     </Grid>
 
                                     {sequences.map((item, index) => {
-                                        const [label, taskIds, notSubmittableReason, editable] = item;
+                                        const [label, taskIds, notSubmittableReason, notEditableReason] = item;
                                         return (
                                             <SequenceRow
                                                 active={active}
@@ -891,7 +891,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                                 focusName={focusName}
                                                 setFocusName={setFocusName}
                                                 notSubmittableReason={notSubmittableReason}
-                                                editable={editable}
+                                                notEditableReason={notEditableReason}
                                                 isValid={isValidSequence}
                                             />
                                         );

+ 5 - 5
frontend/taipy/src/utils.ts

@@ -24,14 +24,14 @@ export type ScenarioFull = [
     string,     // label
     string[],   // tags
     Array<[string, string]>,    // properties
-    Array<[string, string[], string, boolean]>,   // sequences (label, task ids, notSubmittableReason, editable)
+    Array<[string, string[], string, string]>,   // sequences (label, task ids, notSubmittableReason, notEditableReason)
     Record<string, string>, // tasks (id: label)
     string[],   // authorized_tags
-    boolean,    // deletable
-    boolean,    // promotable
+    string,    // notDeletableReason
+    string,    // notPromotableReason
     string,     // notSubmittableReason
-    boolean,    // readable
-    boolean     // editable
+    string,     // notReadableReason
+    string      // notEditableReason
 ];
 
 export enum ScFProps {

+ 64 - 0
pyproject.toml

@@ -1,3 +1,67 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy"
+version = "0.0.0"  # will be dynamically set
+description = "A 360° open-source platform from Python pilots to production-ready web apps."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+license = {text = "Apache License 2.0"}
+keywords = ["taipy"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+dependencies = []  # will be dynamically set
+
+[project.optional-dependencies]
+test = ["pytest>=3.8"]
+ngrok = ["pyngrok>=5.1,<6.0"]
+image = [
+    "python-magic>=0.4.24,<0.5; platform_system!='Windows'",
+    "python-magic-bin>=0.4.14,<0.5; platform_system=='Windows'"
+]
+rdp = ["rdp>=0.8"]
+arrow = ["pyarrow>=14.0.2,<15.0"]
+mssql = ["pyodbc>=4"]
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-0.0.0/relnotes/"  # version will be dynamically set
+
+[tool.setuptools.packages.find]
+include = ["taipy", "taipy.*"]
+
+[tool.setuptools.package-data]
+"taipy" = ["version.json"]
+
+[tool.setuptools]
+zip-safe = false
+
+[project.scripts]
+taipy = "taipy._entrypoint:_entrypoint"
+
 [tool.ruff]
 exclude = [
     ".git",

+ 0 - 121
setup.py

@@ -1,121 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy package"""
-
-import os
-import json
-import platform
-import subprocess
-from pathlib import Path
-
-from setuptools import find_packages, setup
-from setuptools.command.build_py import build_py
-
-root_folder = Path(__file__).parent
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-# get current version
-with open(os.path.join("taipy", "version.json")) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-
-def get_requirements():
-    # get requirements from the different setups in tools/packages (removing taipy packages)
-    reqs = set()
-    for pkg in (root_folder / "tools" / "packages").iterdir():
-        requirements_file = pkg / "setup.requirements.txt"
-        if requirements_file.exists():
-            reqs.update(requirements_file.read_text("UTF-8").splitlines())
-
-    return [r for r in reqs if r and not r.startswith("taipy")]
-
-
-test_requirements = ["pytest>=3.8"]
-
-extras_require = {
-    "ngrok": ["pyngrok>=5.1,<6.0"],
-    "image": [
-        "python-magic>=0.4.24,<0.5;platform_system!='Windows'",
-        "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
-    ],
-    "rdp": ["rdp>=0.8"],
-    "arrow": ["pyarrow>=14.0.2,<15.0"],
-    "mssql": ["pyodbc>=4"],
-}
-
-
-class NPMInstall(build_py):
-    def run(self):
-        subprocess.run(
-            ["python", "bundle_build.py"],
-            cwd=root_folder / "tools" / "frontend",
-            check=True,
-            shell=platform.system() == "Windows",
-        )
-        build_py.run(self)
-
-
-setup(
-    author="Avaiga",
-    author_email="dev@taipy.io",
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    description="A 360° open-source platform from Python pilots to production-ready web apps.",
-    install_requires=get_requirements(),
-    entry_points={
-        "console_scripts": [
-            "taipy = taipy._entrypoint:_entrypoint",
-        ]
-    },
-    license="Apache License 2.0",
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    keywords="taipy",
-    name="taipy",
-    packages=find_packages(include=["taipy", "taipy.*"]),
-    include_package_data=True,
-    test_suite="tests",
-    version=version_string,
-    zip_safe=False,
-    extras_require=extras_require,
-    cmdclass={"build_py": NPMInstall},
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 8 - 5
taipy/_cli/_run_cli.py

@@ -54,10 +54,13 @@ class _RunCLI(_AbstractCLI):
 
         taipy_args = [f"--taipy-{arg[2:]}" if arg.startswith("--") else arg for arg in all_args]
 
-        subprocess.run(
-            [sys.executable, args.application_main_file, *(external_args + taipy_args)],
-            stdout=sys.stdout,
-            stderr=sys.stdout,
-        )
+        try:
+            subprocess.run(
+                [sys.executable, args.application_main_file, *(external_args + taipy_args)],
+                stdout=sys.stdout,
+                stderr=sys.stdout,
+            )
+        except KeyboardInterrupt:
+            pass
 
         sys.exit(0)

+ 58 - 0
taipy/config/pyproject.toml

@@ -0,0 +1,58 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy-config"
+version = "0.0.0"  # will be dynamically set
+description = "A Taipy package dedicated to easily configure a Taipy application."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+license = {text = "Apache License 2.0"}
+keywords = ["taipy-config"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+
+dependencies = [
+    "toml>=0.10,<0.11",
+    "deepdiff>=6.7,<6.8"
+]
+
+[project.optional-dependencies]
+test = [
+    "pytest>=3.8"
+]
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-0.0.0/relnotes/"  # version will be dynamically set
+
+[tool.setuptools.packages]
+find = {where = ["."], include = ["taipy", "taipy.config", "taipy.config.*", "taipy.logger", "taipy.logger.*"]}
+
+[tool.setuptools.package-data]
+"version" = ["version.json"]
+
+[tool.setuptools]
+zip-safe = false

+ 0 - 80
taipy/config/setup.py

@@ -1,80 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy-config package"""
-
-import json
-import os
-from pathlib import Path
-
-from setuptools import find_namespace_packages, find_packages, setup
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.json")
-
-with open(version_path) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-requirements = ["toml>=0.10,<0.11", "deepdiff>=6.7,<6.8"]
-
-test_requirements = ["pytest>=3.8"]
-
-setup(
-    author="Avaiga",
-    author_email="dev@taipy.io",
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    description="A Taipy package dedicated to easily configure a Taipy application.",
-    install_requires=requirements,
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    license="Apache License 2.0",
-    keywords="taipy-config",
-    name="taipy-config",
-    packages=find_namespace_packages(where=".")
-    + find_packages(include=["taipy", "taipy.config", "taipy.config.*", "taipy.logger", "taipy.logger.*"]),
-    include_package_data=True,
-    data_files=[('version', ['version.json'])],
-    test_suite="tests",
-    tests_require=test_requirements,
-    version=version_string,
-    zip_safe=False,
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 1 - 1
taipy/core/_entity/submittable.py

@@ -86,7 +86,7 @@ class Submittable:
         """Indicate if the entity is ready to be run.
 
         Returns:
-            A Reason object that can function as a Boolean value.
+            A ReasonCollection object that can function as a Boolean value,
             which is True if the given entity is ready to be run or there is no reason to be blocked, False otherwise.
         """
         reason_collection = ReasonCollection()

+ 14 - 7
taipy/core/_manager/_manager.py

@@ -17,6 +17,7 @@ from .._entity._entity_ids import _EntityIds
 from .._repository._abstract_repository import _AbstractRepository
 from ..exceptions.exceptions import ModelNotFound
 from ..notification import Event, EventOperation, Notifier
+from ..reason import EntityDoesNotExist, ReasonCollection
 
 EntityType = TypeVar("EntityType")
 
@@ -125,11 +126,17 @@ class _Manager(Generic[EntityType]):
             return default
 
     @classmethod
-    def _exists(cls, entity_id: str) -> bool:
+    def _exists(cls, entity_id: str) -> ReasonCollection:
         """
-        Returns True if the entity id exists.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the entity id exists.
         """
-        return cls._repository._exists(entity_id)
+        reason_collector = ReasonCollection()
+
+        if not cls._repository._exists(entity_id):
+            reason_collector._add_reason(entity_id, EntityDoesNotExist(entity_id))
+
+        return reason_collector
 
     @classmethod
     def _delete_entities_of_multiple_types(cls, _entity_ids: _EntityIds):
@@ -153,9 +160,9 @@ class _Manager(Generic[EntityType]):
         _SubmissionManagerFactory._build_manager()._delete_many(_entity_ids.submission_ids)
 
     @classmethod
-    def _is_editable(cls, entity: Union[EntityType, str]) -> bool:
-        return True
+    def _is_editable(cls, entity: Union[EntityType, str]) -> ReasonCollection:
+        return ReasonCollection()
 
     @classmethod
-    def _is_readable(cls, entity: Union[EntityType, str]) -> bool:
-        return True
+    def _is_readable(cls, entity: Union[EntityType, str]) -> ReasonCollection:
+        return ReasonCollection()

+ 3 - 0
taipy/core/_orchestrator/_dispatcher/_development_job_dispatcher.py

@@ -9,6 +9,7 @@
 # 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.
 
+import datetime
 from typing import Optional
 
 from ...job.job import Job
@@ -44,5 +45,7 @@ class _DevelopmentJobDispatcher(_JobDispatcher):
         Parameters:
             job (Job^): The job to submit on an executor with an available worker.
         """
+        job.execution_started_at = datetime.datetime.now()
         rs = _TaskFunctionWrapper(job.id, job.task).execute()
         self._update_job_status(job, rs)
+        job.execution_ended_at = datetime.datetime.now()

+ 4 - 0
taipy/core/_orchestrator/_dispatcher/_standalone_job_dispatcher.py

@@ -9,6 +9,7 @@
 # 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.
 
+import datetime
 import multiprocessing as mp
 from concurrent.futures import Executor, ProcessPoolExecutor
 from functools import partial
@@ -59,6 +60,8 @@ class _StandaloneJobDispatcher(_JobDispatcher):
             self._nb_available_workers -= 1
             self._logger.debug(f"Setting nb_available_workers to {self._nb_available_workers} in the dispatch method.")
         config_as_string = _TomlSerializer()._serialize(Config._applied_config)  # type: ignore[attr-defined]
+
+        job.execution_started_at = datetime.datetime.now()
         future = self._executor.submit(_TaskFunctionWrapper(job.id, job.task), config_as_string=config_as_string)
         future.add_done_callback(partial(self._update_job_status_from_future, job))
 
@@ -67,3 +70,4 @@ class _StandaloneJobDispatcher(_JobDispatcher):
             self._nb_available_workers += 1
             self._logger.debug(f"Setting nb_available_workers to {self._nb_available_workers} in the callback method.")
         self._update_job_status(job, ft.result())
+        job.execution_ended_at = datetime.datetime.now()

+ 3 - 0
taipy/core/_version/_version_manager_factory.py

@@ -9,6 +9,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +22,7 @@ class _VersionManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _VersionFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_VersionManager]:
         if cls._using_enterprise():
             version_manager = _utils._load_fct(
@@ -36,5 +38,6 @@ class _VersionManagerFactory(_ManagerFactory):
         return version_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 24 - 1
taipy/core/config/checkers/_scenario_config_checker.py

@@ -38,10 +38,33 @@ class _ScenarioConfigChecker(_ConfigChecker):
                 self._check_addition_data_node_configs(scenario_config_id, scenario_config)
                 self._check_additional_dns_not_overlapping_tasks_dns(scenario_config_id, scenario_config)
                 self._check_tasks_in_sequences_exist_in_scenario_tasks(scenario_config_id, scenario_config)
+                self._check_if_children_config_id_is_overlapping_with_properties(scenario_config_id, scenario_config)
                 self._check_comparators(scenario_config_id, scenario_config)
 
         return self._collector
 
+    def _check_if_children_config_id_is_overlapping_with_properties(
+        self, scenario_config_id: str, scenario_config: ScenarioConfig
+    ):
+        if scenario_config.tasks:
+            for task in scenario_config.tasks:
+                if isinstance(task, TaskConfig) and task.id in scenario_config.properties:
+                    self._error(
+                        TaskConfig._ID_KEY,
+                        task.id,
+                        f"The id of the TaskConfig `{task.id}` is overlapping with the "
+                        f"property `{task.id}` of ScenarioConfig `{scenario_config_id}`.",
+                    )
+        if scenario_config.data_nodes:
+            for data_node in scenario_config.data_nodes:
+                if isinstance(data_node, DataNodeConfig) and data_node.id in scenario_config.properties:
+                    self._error(
+                        DataNodeConfig._ID_KEY,
+                        data_node.id,
+                        f"The id of the DataNodeConfig `{data_node.id}` is overlapping with the "
+                        f"property `{data_node.id}` of ScenarioConfig `{scenario_config_id}`.",
+                    )
+
     def _check_task_configs(self, scenario_config_id: str, scenario_config: ScenarioConfig):
         self._check_children(
             ScenarioConfig,
@@ -78,7 +101,7 @@ class _ScenarioConfigChecker(_ConfigChecker):
                 f"{ScenarioConfig._COMPARATOR_KEY} field of ScenarioConfig"
                 f" `{scenario_config_id}` must be populated with a dictionary value.",
             )
-        else:
+        elif scenario_config.comparators is not None:
             for data_node_id, comparator in scenario_config.comparators.items():
                 if data_node_id not in Config.data_nodes:
                     self._error(

+ 11 - 0
taipy/core/config/checkers/_task_config_checker.py

@@ -40,8 +40,19 @@ class _TaskConfigChecker(_ConfigChecker):
                 self._check_existing_function(task_config_id, task_config)
                 self._check_inputs(task_config_id, task_config)
                 self._check_outputs(task_config_id, task_config)
+                self._check_if_children_config_id_is_overlapping_with_properties(task_config_id, task_config)
         return self._collector
 
+    def _check_if_children_config_id_is_overlapping_with_properties(self, task_config_id: str, task_config: TaskConfig):
+        for data_node in task_config.input_configs + task_config.output_configs:
+            if isinstance(data_node, DataNodeConfig) and data_node.id in task_config.properties:
+                self._error(
+                    DataNodeConfig._ID_KEY,
+                    data_node.id,
+                    f"The id of the DataNodeConfig `{data_node.id}` is overlapping with the "
+                    f"property `{data_node.id}` of TaskConfig `{task_config_id}`.",
+                )
+
     def _check_if_config_id_is_overlapping_with_scenario_attributes(
         self, task_config_id: str, task_config: TaskConfig, scenario_attributes: List[str]
     ):

+ 3 - 0
taipy/core/cycle/_cycle_manager_factory.py

@@ -9,6 +9,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +22,7 @@ class _CycleManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _CycleFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_CycleManager]:
         if cls._using_enterprise():
             cycle_manager = _load_fct(cls._TAIPY_ENTERPRISE_CORE_MODULE + ".cycle._cycle_manager", "_CycleManager")  # type: ignore
@@ -34,5 +36,6 @@ class _CycleManagerFactory(_ManagerFactory):
         return cycle_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 3 - 1
taipy/core/data/_data_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +21,7 @@ class _DataManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _DataFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_DataManager]:
         if cls._using_enterprise():
             data_manager = _load_fct(cls._TAIPY_ENTERPRISE_CORE_MODULE + ".data._data_manager", "_DataManager")  # type: ignore
@@ -34,5 +35,6 @@ class _DataManagerFactory(_ManagerFactory):
         return data_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 6 - 0
taipy/core/job/_job_converter.py

@@ -31,6 +31,8 @@ class _JobConverter(_AbstractConverter):
             job.submit_id,
             job.submit_entity_id,
             job._creation_date.isoformat(),
+            job._execution_started_at.isoformat() if job._execution_started_at else None,
+            job._execution_ended_at.isoformat() if job._execution_ended_at else None,
             cls.__serialize_subscribers(job._subscribers),
             job._stacktrace,
             version=job._version,
@@ -52,6 +54,10 @@ class _JobConverter(_AbstractConverter):
         job._status = model.status  # type: ignore
         job._force = model.force  # type: ignore
         job._creation_date = datetime.fromisoformat(model.creation_date)  # type: ignore
+        job._execution_started_at = (
+            datetime.fromisoformat(model.execution_started_at) if model.execution_started_at else None
+        )
+        job._execution_ended_at = datetime.fromisoformat(model.execution_ended_at) if model.execution_ended_at else None
         for it in model.subscribers:
             try:
                 fct_module, fct_name = it.get("fct_module"), it.get("fct_name")

+ 9 - 2
taipy/core/job/_job_manager.py

@@ -18,6 +18,7 @@ from .._version._version_manager_factory import _VersionManagerFactory
 from .._version._version_mixin import _VersionMixin
 from ..exceptions.exceptions import JobNotDeletedException
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
+from ..reason import JobIsNotFinished, ReasonCollection
 from ..task.task import Task
 from .job import Job
 from .job_id import JobId
@@ -87,7 +88,13 @@ class _JobManager(_Manager[Job], _VersionMixin):
             return max(jobs_of_task)
 
     @classmethod
-    def _is_deletable(cls, job: Union[Job, JobId]) -> bool:
+    def _is_deletable(cls, job: Union[Job, JobId]) -> ReasonCollection:
+        reason_collector = ReasonCollection()
+
         if isinstance(job, str):
             job = cls._get(job)
-        return job.is_finished()
+
+        if not job.is_finished():
+            reason_collector._add_reason(job.id, JobIsNotFinished(job.id))
+
+        return reason_collector

+ 3 - 1
taipy/core/job/_job_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +21,7 @@ class _JobManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _JobFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_JobManager]:
         if cls._using_enterprise():
             job_manager = _load_fct(cls._TAIPY_ENTERPRISE_CORE_MODULE + ".job._job_manager", "_JobManager")  # type: ignore
@@ -34,5 +35,6 @@ class _JobManagerFactory(_ManagerFactory):
         return job_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 7 - 1
taipy/core/job/_job_model.py

@@ -10,7 +10,7 @@
 # specific language governing permissions and limitations under the License.
 
 from dataclasses import dataclass
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 from .._repository._base_taipy_model import _BaseModel
 from .job_id import JobId
@@ -26,6 +26,8 @@ class _JobModel(_BaseModel):
     submit_id: str
     submit_entity_id: str
     creation_date: str
+    execution_started_at: Optional[str]
+    execution_ended_at: Optional[str]
     subscribers: List[Dict]
     stacktrace: List[str]
     version: str
@@ -40,6 +42,8 @@ class _JobModel(_BaseModel):
             submit_id=data["submit_id"],
             submit_entity_id=data["submit_entity_id"],
             creation_date=data["creation_date"],
+            execution_started_at=data["execution_started_at"],
+            execution_ended_at=data["execution_ended_at"],
             subscribers=_BaseModel._deserialize_attribute(data["subscribers"]),
             stacktrace=_BaseModel._deserialize_attribute(data["stacktrace"]),
             version=data["version"],
@@ -54,6 +58,8 @@ class _JobModel(_BaseModel):
             self.submit_id,
             self.submit_entity_id,
             self.creation_date,
+            self.execution_started_at,
+            self.execution_ended_at,
             _BaseModel._serialize_attribute(self.subscribers),
             _BaseModel._serialize_attribute(self.stacktrace),
             self.version,

+ 39 - 2
taipy/core/job/job.py

@@ -22,6 +22,7 @@ from .._entity._reload import _self_reload, _self_setter
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..common._utils import _fcts_to_dict
 from ..notification.event import Event, EventEntityType, EventOperation, _make_event
+from ..reason import ReasonCollection
 from .job_id import JobId
 from .status import Status
 
@@ -77,6 +78,8 @@ class Job(_Entity, _Labeled):
         self._creation_date = datetime.now()
         self._submit_id: str = submit_id
         self._submit_entity_id: str = submit_entity_id
+        self._execution_started_at: Optional[datetime] = None
+        self._execution_ended_at: Optional[datetime] = None
         self._subscribers: List[Callable] = []
         self._stacktrace: List[str] = []
         self.__logger = _TaipyLogger._get_logger()
@@ -143,6 +146,39 @@ class Job(_Entity, _Labeled):
     def creation_date(self, val):
         self._creation_date = val
 
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_started_at(self) -> Optional[datetime]:
+        return self._execution_started_at
+
+    @execution_started_at.setter
+    @_self_setter(_MANAGER_NAME)
+    def execution_started_at(self, val):
+        self._execution_started_at = val
+
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_ended_at(self) -> Optional[datetime]:
+        return self._execution_ended_at
+
+    @execution_ended_at.setter
+    @_self_setter(_MANAGER_NAME)
+    def execution_ended_at(self, val):
+        self._execution_ended_at = val
+
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_duration(self) -> Optional[float]:
+        """Get the duration of the job execution in seconds.
+
+        Returns:
+            Optional[float]: The duration of the job execution in seconds. If the job is not
+            completed, None is returned.
+        """
+        if self._execution_started_at and self._execution_ended_at:
+            return (self._execution_ended_at - self._execution_started_at).total_seconds()
+        return None
+
     @property  # type: ignore
     @_self_reload(_MANAGER_NAME)
     def stacktrace(self) -> List[str]:
@@ -348,11 +384,12 @@ class Job(_Entity, _Labeled):
         """
         return self._get_simple_label()
 
-    def is_deletable(self) -> bool:
+    def is_deletable(self) -> ReasonCollection:
         """Indicate if the job can be deleted.
 
         Returns:
-            True if the job can be deleted. False otherwise.
+            A ReasonCollection object that can function as a Boolean value,
+            which is True if the job can be deleted. False otherwise.
         """
         from ... import core as tp
 

+ 62 - 0
taipy/core/pyproject.toml

@@ -0,0 +1,62 @@
+[build-system]
+requires = ["setuptools>=42", "wheel", ]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy-core"
+version = "0.0.0"   # will be dynamically set
+description = "A Python library to build powerful and customized data-driven back-end applications."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+keywords = ["taipy-core", ]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+
+dependencies = []   # will be dynamically set
+
+[project.license]
+text = "Apache License 2.0"
+
+[project.optional-dependencies]
+test = ["pytest>=3.8", ]
+mssql = ["pyodbc>=4,<4.1", ]
+mysql = ["pymysql>1,<1.1", ]
+postgresql = ["psycopg2>2.9,<2.10", ]
+parquet = ["fastparquet==2022.11.0", "pyarrow>=14.0.2,<15.0", ]
+s3 = ["boto3==1.29.1", ]
+mongo = ["pymongo[srv]>=4.2.0,<5.0", ]
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-4.0.0.dev0/relnotes/"
+
+[tool.setuptools]
+zip-safe = false
+
+[tool.setuptools.package-data]
+taipy = ["version.json", ]
+
+[tool.setuptools.packages.find]
+where = [".", ]
+include = ["taipy", "taipy.core", "taipy.core.*", ]

+ 6 - 0
taipy/core/reason/__init__.py

@@ -12,10 +12,16 @@
 from .reason import (
     DataNodeEditInProgress,
     DataNodeIsNotWritten,
+    EntityDoesNotExist,
     EntityIsNotSubmittableEntity,
     InvalidUploadFile,
+    JobIsNotFinished,
     NotGlobalScope,
     Reason,
+    ScenarioDoesNotBelongToACycle,
+    ScenarioIsThePrimaryScenario,
+    SubmissionIsNotFinished,
+    SubmissionStatusIsUndefined,
     UploadFileCanNotBeRead,
     WrongConfigType,
 )

+ 73 - 0
taipy/core/reason/reason.py

@@ -155,3 +155,76 @@ class InvalidUploadFile(Reason, _DataNodeReasonMixin):
     def __init__(self, file_name: str, datanode_id: str):
         Reason.__init__(self, f'The uploaded file {file_name} has invalid data for data node "{datanode_id}"')
         _DataNodeReasonMixin.__init__(self, datanode_id)
+
+
+class EntityDoesNotExist(Reason, _DataNodeReasonMixin):
+    """
+    The entity id provided does not exist in the repository.
+
+    Attributes:
+        entity_id (str): The entity identifier.
+    """
+
+    def __init__(self, entity_id: str):
+        Reason.__init__(self, f"Entity {entity_id} does not exist in the repository.")
+
+
+class JobIsNotFinished(Reason, _DataNodeReasonMixin):
+    """
+    As the job is not finished yet, it prevents specific actions from being performed.
+
+    Attributes:
+        job_id (str): The job identifier.
+    """
+
+    def __init__(self, job_id: str):
+        Reason.__init__(self, f"The job {job_id} is not finished yet.")
+
+
+class ScenarioIsThePrimaryScenario(Reason, _DataNodeReasonMixin):
+    """
+    The scenario is the primary scenario of a cycle, which prevents specific actions from being performed.
+
+    Attributes:
+        scenario_id (str): The scenario identifier.
+        cycle_id (str): The cycle identifier.
+    """
+
+    def __init__(self, scenario_id: str, cycle: str):
+        Reason.__init__(self, f"The scenario {scenario_id} is the primary scenario of cycle {cycle}.")
+
+
+class ScenarioDoesNotBelongToACycle(Reason, _DataNodeReasonMixin):
+    """
+    The scenario does not belong to any cycle, which prevents specific actions from being performed.
+
+    Attributes:
+        scenario_id (str): The scenario identifier.
+    """
+
+    def __init__(self, scenario_id: str):
+        Reason.__init__(self, f"The scenario {scenario_id} does not belong to any cycle.")
+
+
+class SubmissionIsNotFinished(Reason, _DataNodeReasonMixin):
+    """
+    The submission is not finished yet.
+
+    Attributes:
+        submission_id (str): The submission identifier.
+    """
+
+    def __init__(self, submission_id: str):
+        Reason.__init__(self, f"The submission {submission_id} is not finished yet.")
+
+
+class SubmissionStatusIsUndefined(Reason, _DataNodeReasonMixin):
+    """
+    The submission status is undefined.
+
+    Attributes:
+        submission_id (str): The submission identifier.
+    """
+
+    def __init__(self, submission_id: str):
+        Reason.__init__(self, f"The status of submission {submission_id} is undefined.")

+ 33 - 12
taipy/core/scenario/_scenario_manager.py

@@ -39,7 +39,14 @@ from ..exceptions.exceptions import (
 from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
-from ..reason import EntityIsNotSubmittableEntity, ReasonCollection, WrongConfigType
+from ..reason import (
+    EntityDoesNotExist,
+    EntityIsNotSubmittableEntity,
+    ReasonCollection,
+    ScenarioDoesNotBelongToACycle,
+    ScenarioIsThePrimaryScenario,
+    WrongConfigType,
+)
 from ..submission._submission_manager_factory import _SubmissionManagerFactory
 from ..submission.submission import Submission
 from ..task._task_manager_factory import _TaskManagerFactory
@@ -111,9 +118,8 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         config_id = getattr(config, "id", None) or str(config)
         reason_collector = ReasonCollection()
 
-        if config is not None:
-            if not isinstance(config, ScenarioConfig):
-                reason_collector._add_reason(config_id, WrongConfigType(config_id, ScenarioConfig.__name__))
+        if config is not None and not isinstance(config, ScenarioConfig):
+            reason_collector._add_reason(config_id, WrongConfigType(config_id, ScenarioConfig.__name__))
 
         return reason_collector
 
@@ -332,12 +338,25 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return [scenario for scenario in scenarios if created_start_time <= scenario.creation_date < created_end_time]
 
     @classmethod
-    def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
+    def _is_promotable_to_primary(cls, scenario: Union[Scenario, ScenarioId]) -> ReasonCollection:
+        reason_collection = ReasonCollection()
+
         if isinstance(scenario, str):
-            scenario = cls._get(scenario)
-        if scenario and not scenario.is_primary and scenario.cycle:
-            return True
-        return False
+            scenario_id = scenario
+            scenario = cls._get(scenario_id)
+        else:
+            scenario_id = scenario.id
+
+        if not scenario:
+            reason_collection._add_reason(scenario_id, EntityDoesNotExist(scenario_id))
+        else:
+            if scenario.is_primary:
+                reason_collection._add_reason(scenario_id, ScenarioIsThePrimaryScenario(scenario_id, scenario.cycle.id))
+
+            if not scenario.cycle:
+                reason_collection._add_reason(scenario_id, ScenarioDoesNotBelongToACycle(scenario_id))
+
+        return reason_collection
 
     @classmethod
     def _set_primary(cls, scenario: Scenario) -> None:
@@ -409,13 +428,15 @@ class _ScenarioManager(_Manager[Scenario], _VersionMixin):
         return Config.scenarios.get(scenario.config_id, None)
 
     @classmethod
-    def _is_deletable(cls, scenario: Union[Scenario, ScenarioId]) -> bool:
+    def _is_deletable(cls, scenario: Union[Scenario, ScenarioId]) -> ReasonCollection:
+        reason_collection = ReasonCollection()
+
         if isinstance(scenario, str):
             scenario = cls._get(scenario)
         if scenario.is_primary:
             if len(cls._get_all_by_cycle(scenario.cycle)) > 1:
-                return False
-        return True
+                reason_collection._add_reason(scenario.id, ScenarioIsThePrimaryScenario(scenario.id, scenario.cycle.id))
+        return reason_collection
 
     @classmethod
     def _delete(cls, scenario_id: ScenarioId) -> None:

+ 3 - 1
taipy/core/scenario/_scenario_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +21,7 @@ class _ScenarioManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _ScenarioFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_ScenarioManager]:
         if cls._using_enterprise():
             scenario_manager = _load_fct(
@@ -36,5 +37,6 @@ class _ScenarioManagerFactory(_ManagerFactory):
         return scenario_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 2 - 1
taipy/core/scenario/scenario.py

@@ -40,6 +40,7 @@ from ..exceptions.exceptions import (
 )
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier, _make_event
+from ..reason import ReasonCollection
 from ..sequence.sequence import Sequence
 from ..submission.submission import Submission
 from ..task.task import Task
@@ -651,7 +652,7 @@ class Scenario(_Entity, Submittable, _Labeled):
 
         return tp.untag(self, tag)
 
-    def is_deletable(self) -> bool:
+    def is_deletable(self) -> ReasonCollection:
         """Indicate if the scenario can be deleted.
 
         Returns:

+ 8 - 3
taipy/core/sequence/_sequence_manager.py

@@ -29,7 +29,7 @@ from ..job._job_manager_factory import _JobManagerFactory
 from ..job.job import Job
 from ..notification import Event, EventEntityType, EventOperation, Notifier
 from ..notification.event import _make_event
-from ..reason import EntityIsNotSubmittableEntity, ReasonCollection
+from ..reason import EntityDoesNotExist, EntityIsNotSubmittableEntity, ReasonCollection
 from ..scenario._scenario_manager_factory import _ScenarioManagerFactory
 from ..scenario.scenario import Scenario
 from ..scenario.scenario_id import ScenarioId
@@ -389,11 +389,16 @@ class _SequenceManager(_Manager[Sequence], _VersionMixin):
         return submission
 
     @classmethod
-    def _exists(cls, entity_id: str) -> bool:
+    def _exists(cls, entity_id: str) -> ReasonCollection:
         """
         Returns True if the entity id exists.
         """
-        return True if cls._get(entity_id) else False
+        reason_collector = ReasonCollection()
+
+        if cls._get(entity_id) is None:
+            reason_collector._add_reason(entity_id, EntityDoesNotExist(entity_id))
+
+        return reason_collector
 
     @classmethod
     def __log_error_entity_not_found(cls, sequence_id: Union[SequenceId, str]):

+ 2 - 1
taipy/core/sequence/_sequence_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -18,6 +18,7 @@ from ._sequence_manager import _SequenceManager
 
 class _SequenceManagerFactory(_ManagerFactory):
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_SequenceManager]:  # type: ignore
         if cls._using_enterprise():
             sequence_manager = _load_fct(

+ 0 - 102
taipy/core/setup.py

@@ -1,102 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy-core package"""
-
-import json
-import os
-from pathlib import Path
-
-from setuptools import find_namespace_packages, find_packages, setup
-
-root_folder = Path(__file__).parent
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.json")
-with open(version_path) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-
-def get_requirements():
-    # get requirements from the different setups in tools/packages (removing taipy packages)
-    reqs = set()
-    for pkg in (root_folder / "tools" / "packages").iterdir():
-        if "taipy-core" not in str(pkg):
-            continue
-        requirements_file = pkg / "setup.requirements.txt"
-        if requirements_file.exists():
-            reqs.update(requirements_file.read_text("UTF-8").splitlines())
-
-    return [r for r in reqs if r and not r.startswith("taipy")]
-
-
-test_requirements = ["pytest>=3.8"]
-
-extras_require = {
-    "mssql": ["pyodbc>=4,<4.1"],
-    "mysql": ["pymysql>1,<1.1"],
-    "postgresql": ["psycopg2>2.9,<2.10"],
-    "parquet": ["fastparquet==2022.11.0", "pyarrow>=14.0.2,<15.0"],
-    "s3": ["boto3==1.29.1"],
-    "mongo": ["pymongo[srv]>=4.2.0,<5.0"],
-}
-
-setup(
-    author="Avaiga",
-    author_email="dev@taipy.io",
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    description="A Python library to build powerful and customized data-driven back-end applications.",
-    install_requires=get_requirements(),
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    license="Apache License 2.0",
-    keywords="taipy-core",
-    name="taipy-core",
-    packages=find_namespace_packages(where=".") + find_packages(include=["taipy", "taipy.core", "taipy.core.*"]),
-    include_package_data=True,
-    data_files=[('version', ['version.json'])],
-    test_suite="tests",
-    tests_require=test_requirements,
-    version=version_string,
-    zip_safe=False,
-    extras_require=extras_require,
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 9 - 2
taipy/core/submission/_submission_manager.py

@@ -21,6 +21,7 @@ from .._version._version_mixin import _VersionMixin
 from ..exceptions.exceptions import SubmissionNotDeletedException
 from ..job.job import Job, Status
 from ..notification import EventEntityType, EventOperation, Notifier, _make_event
+from ..reason import ReasonCollection, SubmissionIsNotFinished
 from ..scenario.scenario import Scenario
 from ..sequence.sequence import Sequence
 from ..submission.submission import Submission, SubmissionId, SubmissionStatus
@@ -174,7 +175,13 @@ class _SubmissionManager(_Manager[Submission], _VersionMixin):
         return entity_ids
 
     @classmethod
-    def _is_deletable(cls, submission: Union[Submission, SubmissionId]) -> bool:
+    def _is_deletable(cls, submission: Union[Submission, SubmissionId]) -> ReasonCollection:
+        reason_collector = ReasonCollection()
+
         if isinstance(submission, str):
             submission = cls._get(submission)
-        return submission.is_finished() or submission.submission_status == SubmissionStatus.UNDEFINED
+
+        if not submission.is_finished() and submission.submission_status != SubmissionStatus.UNDEFINED:
+            reason_collector._add_reason(submission.id, SubmissionIsNotFinished(submission.id))
+
+        return reason_collector

+ 3 - 1
taipy/core/submission/_submission_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +21,7 @@ class _SubmissionManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _SubmissionFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_SubmissionManager]:
         if cls._using_enterprise():
             submission_manager = _load_fct(
@@ -37,5 +38,6 @@ class _SubmissionManagerFactory(_ManagerFactory):
         return submission_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 32 - 3
taipy/core/submission/submission.py

@@ -21,12 +21,13 @@ from .._entity._reload import _Reloader, _self_reload, _self_setter
 from .._version._version_manager_factory import _VersionManagerFactory
 from ..job.job import Job, JobId
 from ..notification import Event, EventEntityType, EventOperation, _make_event
+from ..reason.reason_collection import ReasonCollection
 from .submission_id import SubmissionId
 from .submission_status import SubmissionStatus
 
 
 class Submission(_Entity, _Labeled):
-    """ Submission of a submittable entity: `Task^`, a `Sequence^` or a `Scenario^`.
+    """Submission of a submittable entity: `Task^`, a `Sequence^` or a `Scenario^`.
 
     Task, Sequence, and Scenario entities can be submitted for execution. The submission
     represents the unique request to execute a submittable entity. The submission is created
@@ -137,6 +138,33 @@ class Submission(_Entity, _Labeled):
     def creation_date(self):
         return self._creation_date
 
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_started_at(self) -> Optional[datetime]:
+        if all(job.execution_started_at is not None for job in self.jobs):
+            return min(job.execution_started_at for job in self.jobs)
+        return None
+
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_ended_at(self) -> Optional[datetime]:
+        if all(job.execution_ended_at is not None for job in self.jobs):
+            return max(job.execution_ended_at for job in self.jobs)
+        return None
+
+    @property
+    @_self_reload(_MANAGER_NAME)
+    def execution_duration(self) -> Optional[float]:
+        """Get the duration of the submission in seconds.
+
+        Returns:
+            Optional[float]: The duration of the submission in seconds. If the job is not
+            completed, None is returned.
+        """
+        if self.execution_started_at and self.execution_ended_at:
+            return (self.execution_ended_at - self.execution_started_at).total_seconds()
+        return None
+
     def get_label(self) -> str:
         """Returns the submission simple label prefixed by its owner label.
 
@@ -236,11 +264,12 @@ class Submission(_Entity, _Labeled):
             SubmissionStatus.CANCELED,
         ]
 
-    def is_deletable(self) -> bool:
+    def is_deletable(self) -> ReasonCollection:
         """Indicate if the submission can be deleted.
 
         Returns:
-            True if the submission can be deleted. False otherwise.
+            A ReasonCollection object that can function as a Boolean value,
+            which is True if the submission can be deleted. False otherwise.
         """
         from ... import core as tp
 

+ 29 - 22
taipy/core/taipy.py

@@ -40,7 +40,7 @@ from .exceptions.exceptions import DataNodeConfigIsNotGlobal, ModelNotFound, Non
 from .job._job_manager_factory import _JobManagerFactory
 from .job.job import Job
 from .job.job_id import JobId
-from .reason import EntityIsNotSubmittableEntity, ReasonCollection
+from .reason import EntityDoesNotExist, EntityIsNotSubmittableEntity, ReasonCollection
 from .scenario._scenario_manager_factory import _ScenarioManagerFactory
 from .scenario.scenario import Scenario
 from .scenario.scenario_id import ScenarioId
@@ -119,13 +119,14 @@ def is_editable(
         CycleId,
         SubmissionId,
     ],
-) -> bool:
+) -> ReasonCollection:
     """Indicate if an entity can be edited.
 
     This function checks if the given entity can be edited.
 
     Returns:
-        True if the given entity can be edited. False otherwise.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the given entity can be edited. False otherwise.
     """
     if isinstance(entity, Cycle):
         return _CycleManagerFactory._build_manager()._is_editable(entity)
@@ -155,7 +156,7 @@ def is_editable(
         return _SubmissionManagerFactory._build_manager()._is_editable(entity)
     if isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX):
         return _SubmissionManagerFactory._build_manager()._is_editable(SequenceId(entity))
-    return False
+    return ReasonCollection()._add_reason(str(entity), EntityDoesNotExist(str(entity)))
 
 
 def is_readable(
@@ -175,13 +176,14 @@ def is_readable(
         CycleId,
         SubmissionId,
     ],
-) -> bool:
+) -> ReasonCollection:
     """Indicate if an entity can be read.
 
     This function checks if the given entity can be read.
 
     Returns:
-        True if the given entity can be read. False otherwise.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the given entity can be read. False otherwise.
     """
     if isinstance(entity, Cycle):
         return _CycleManagerFactory._build_manager()._is_readable(entity)
@@ -211,7 +213,7 @@ def is_readable(
         return _SubmissionManagerFactory._build_manager()._is_readable(entity)
     if isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX):
         return _SubmissionManagerFactory._build_manager()._is_readable(SequenceId(entity))
-    return False
+    return ReasonCollection()._add_reason(str(entity), EntityDoesNotExist(str(entity)))
 
 
 @_warn_no_core_service("The submitted entity will not be executed until the Core service is running.")
@@ -259,46 +261,48 @@ def submit(
 
 
 @overload
-def exists(entity_id: TaskId) -> bool:
+def exists(entity_id: TaskId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: DataNodeId) -> bool:
+def exists(entity_id: DataNodeId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: SequenceId) -> bool:
+def exists(entity_id: SequenceId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: ScenarioId) -> bool:
+def exists(entity_id: ScenarioId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: CycleId) -> bool:
+def exists(entity_id: CycleId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: JobId) -> bool:
+def exists(entity_id: JobId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: SubmissionId) -> bool:
+def exists(entity_id: SubmissionId) -> ReasonCollection:
     ...
 
 
 @overload
-def exists(entity_id: str) -> bool:
+def exists(entity_id: str) -> ReasonCollection:
     ...
 
 
-def exists(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId, str]) -> bool:
+def exists(
+    entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId, str],
+) -> ReasonCollection:
     """Check if an entity with the specified identifier exists.
 
     This function checks if an entity with the given identifier exists.
@@ -311,7 +315,8 @@ def exists(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, C
             identifier of the entity to check for existence.
 
     Returns:
-        True if the given entity exists. False otherwise.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the given entity exists. False otherwise.
 
     Raises:
         ModelNotFound: If the entity's type cannot be determined.
@@ -429,7 +434,7 @@ def get_tasks() -> List[Task]:
     return _TaskManagerFactory._build_manager()._get_all()
 
 
-def is_deletable(entity: Union[Scenario, Job, Submission, ScenarioId, JobId, SubmissionId]) -> bool:
+def is_deletable(entity: Union[Scenario, Job, Submission, ScenarioId, JobId, SubmissionId]) -> ReasonCollection:
     """Check if a `Scenario^`, a `Job^` or a `Submission^` can be deleted.
 
     This function determines whether a scenario or a job can be safely
@@ -440,7 +445,8 @@ def is_deletable(entity: Union[Scenario, Job, Submission, ScenarioId, JobId, Sub
             job or submission to check.
 
     Returns:
-        True if the given scenario, job or submission can be deleted. False otherwise.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the given scenario, job or submission can be deleted. False otherwise.
     """
     if isinstance(entity, Job):
         return _JobManagerFactory._build_manager()._is_deletable(entity)
@@ -454,7 +460,7 @@ def is_deletable(entity: Union[Scenario, Job, Submission, ScenarioId, JobId, Sub
         return _SubmissionManagerFactory._build_manager()._is_deletable(entity)
     if isinstance(entity, str) and entity.startswith(Submission._ID_PREFIX):
         return _SubmissionManagerFactory._build_manager()._is_deletable(SubmissionId(entity))
-    return True
+    return ReasonCollection()._add_reason(str(entity), EntityDoesNotExist(str(entity)))
 
 
 def delete(entity_id: Union[TaskId, DataNodeId, SequenceId, ScenarioId, JobId, CycleId, SubmissionId]):
@@ -602,7 +608,7 @@ def get_primary_scenarios(
     return scenarios
 
 
-def is_promotable(scenario: Union[Scenario, ScenarioId]) -> bool:
+def is_promotable(scenario: Union[Scenario, ScenarioId]) -> ReasonCollection:
     """Determine if a scenario can be promoted to become a primary scenario.
 
     This function checks whether the given scenario is eligible to be promoted
@@ -612,7 +618,8 @@ def is_promotable(scenario: Union[Scenario, ScenarioId]) -> bool:
         scenario (Union[Scenario, ScenarioId]): The scenario to be evaluated for promotion.
 
     Returns:
-        True if the given scenario can be promoted to be a primary scenario. False otherwise.
+        A ReasonCollection object that can function as a Boolean value,
+        which is True if the given scenario can be promoted to be a primary scenario. False otherwise.
     """
     return _ScenarioManagerFactory._build_manager()._is_promotable_to_primary(scenario)
 

+ 3 - 1
taipy/core/task/_task_manager_factory.py

@@ -8,7 +8,7 @@
 # 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 functools import lru_cache
 from typing import Type
 
 from .._manager._manager_factory import _ManagerFactory
@@ -21,6 +21,7 @@ class _TaskManagerFactory(_ManagerFactory):
     __REPOSITORY_MAP = {"default": _TaskFSRepository}
 
     @classmethod
+    @lru_cache
     def _build_manager(cls) -> Type[_TaskManager]:
         if cls._using_enterprise():
             task_manager = _load_fct(cls._TAIPY_ENTERPRISE_CORE_MODULE + ".task._task_manager", "_TaskManager")  # type: ignore
@@ -34,5 +35,6 @@ class _TaskManagerFactory(_ManagerFactory):
         return task_manager  # type: ignore
 
     @classmethod
+    @lru_cache
     def _build_repository(cls):
         return cls._get_repository_with_repo_map(cls.__REPOSITORY_MAP)()

+ 2 - 1
taipy/gui/_default_config.py

@@ -58,12 +58,14 @@ default_config: Config = {
     "ngrok_token": "",
     "notebook_proxy": True,
     "notification_duration": 3000,
+    "port": 5000,
     "propagate": True,
     "run_browser": True,
     "run_in_thread": False,
     "run_server": True,
     "server_config": None,
     "single_client": False,
+    "state_retention_period": 0,
     "system_notification": False,
     "theme": None,
     "time_zone": None,
@@ -74,5 +76,4 @@ default_config: Config = {
     "use_reloader": False,
     "watermark": "Taipy inside",
     "webapp_path": None,
-    "port": 5000,
 }

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

@@ -361,6 +361,7 @@ class _Factory:
                 ("show_value", PropertyType.boolean, True),
                 ("format", PropertyType.string),
                 ("delta_format", PropertyType.string),
+                ("bar_color", PropertyType.string),
                 ("color_map", PropertyType.dict),
                 ("hover_text", PropertyType.dynamic_string),
                 ("template", PropertyType.dict),

+ 1 - 0
taipy/gui/_renderers/json.py

@@ -27,6 +27,7 @@ from ..utils.singleton import _Singleton
 
 
 class JsonAdapter(ABC):
+    """NOT DOCUMENTED"""
     def register(self):
         _TaipyJsonAdapter().register(self)
 

+ 6 - 4
taipy/gui/config.py

@@ -46,6 +46,7 @@ ConfigParameter = t.Literal[
     "ngrok_token",
     "notebook_proxy",
     "notification_duration",
+    "port",
     "propagate",
     "run_browser",
     "run_in_thread",
@@ -56,13 +57,13 @@ ConfigParameter = t.Literal[
     "theme",
     "time_zone",
     "title",
+    "state_retention_period",
     "stylekit",
     "upload_folder",
     "use_arrow",
     "use_reloader",
     "watermark",
     "webapp_path",
-    "port",
 ]
 
 Stylekit = t.TypedDict(
@@ -117,23 +118,24 @@ Config = t.TypedDict(
         "ngrok_token": str,
         "notebook_proxy": bool,
         "notification_duration": int,
+        "port": t.Union[t.Literal["auto"], int],
         "propagate": bool,
         "run_browser": bool,
         "run_in_thread": bool,
         "run_server": bool,
         "server_config": t.Optional[ServerConfig],
         "single_client": bool,
+        "state_retention_period": int,
+        "stylekit": t.Union[bool, Stylekit],
         "system_notification": bool,
         "theme": t.Optional[t.Dict[str, t.Any]],
         "time_zone": t.Optional[str],
         "title": t.Optional[str],
-        "stylekit": t.Union[bool, Stylekit],
         "upload_folder": t.Optional[str],
         "use_arrow": bool,
         "use_reloader": bool,
         "watermark": t.Optional[str],
         "webapp_path": t.Optional[str],
-        "port": t.Union[t.Literal["auto"], int],
     },
     total=False,
 )
@@ -235,7 +237,7 @@ class _Config(object):
                     elif key == "port" and str(value).strip() == "auto":
                         config["port"] = "auto"
                     else:
-                        config[key] = value if config[key] is None else type(config[key])(value)  # type: ignore
+                        config[key] = value if config[key] is None else type(config[key])(value)
                 except Exception as e:
                     _warn(
                         f"Invalid keyword arguments value in Gui.run {key} - {value}. Unable to parse value to the correct type",  # noqa: E501

+ 1 - 1
taipy/gui/custom/_page.py

@@ -55,7 +55,7 @@ class ResourceHandler(ABC):
         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:
+    def get_resources(self, path: str, taipy_resource_path: str, base_url: str) -> t.Any:
         raise NotImplementedError
 
 

+ 17 - 0
taipy/gui/gui.py

@@ -26,6 +26,7 @@ import warnings
 from importlib import metadata, util
 from importlib.util import find_spec
 from pathlib import Path
+from threading import Timer
 from types import FrameType, FunctionType, LambdaType, ModuleType, SimpleNamespace
 from urllib.parse import unquote, urlencode, urlparse
 
@@ -611,6 +612,22 @@ class Gui:
 
     def _handle_disconnect(self):
         Hooks()._handle_disconnect(self)
+        if (sid := getattr(request, "sid", None)) and (st_to := self._get_config("state_retention_period", 0)) > 0:
+            for cl_id, sids in self.__client_id_2_sid.items():
+                if sid in sids:
+                    if len(sids) == 1:
+                        Timer(st_to, self._remove_state, [cl_id]).start()
+                    else:
+                        sids.remove(sid)
+                    return
+
+    def _remove_state(self, client_id: str):
+        if (sids := self.__client_id_2_sid.get(client_id, None)) and len(sids) == 1:
+            try:
+                del self.__client_id_2_sid[client_id]
+                self._bindings()._delete_scope(client_id)
+            except Exception as e:
+                _warn(f"Unexpected error removing state {client_id}", e)
 
     def _manage_message(self, msg_type: _WsType, message: dict) -> None:
         try:

+ 8 - 5
taipy/gui/gui_actions.py

@@ -28,7 +28,7 @@ def download(
         name: File name for the content on the client browser (defaults to content name).
         on_action: Callback function (or callback name) to call when the download ends. See below.
 
-    ## Notes:
+    <h4>Notes:</h4>
 
     - *content*: this parameter can hold several values depending on your use case:
         - a string: the value must be an existing path name to the file that gets downloaded or
@@ -122,9 +122,12 @@ def hold_control(
             chooses to cancel.<br/>
             If empty or None, no cancel action is provided to the user.<br/>
             The signature of this function is:
-            - state (State^): The user state;
+
+            - state (`State^`): The user state;
             - id (str): the id of the button that triggered the callback. That will always be
               "UIBlocker" since it is created and managed internally;
+
+            If this parameter is None, no "Cancel" button is displayed.
         message: The message to show. The default value is the string "Work in Progress...".
     """
     if state and isinstance(state._gui, Gui):
@@ -212,8 +215,8 @@ def get_state_id(state: State) -> t.Optional[str]:
         state (State^): The current user state as received in any callback.
 
     Returns:
-        A string that uniquely identifies the state.<br/>
-        If None, then **state** was not handled by a `Gui^` instance.
+        A string that uniquely identifies the state. If this value None, it indicates that *state* is not
+        handled by a `Gui^` instance.
     """
     if state and isinstance(state._gui, Gui):
         return state._gui._get_client_id()
@@ -273,7 +276,7 @@ def invoke_callback(
     """Invoke a user callback for a given state.
 
     Calling this function is equivalent to calling
-    *gui*.[Gui.]invoke_callback(state_id, callback, args, module_context)^`.
+    *gui*.`(Gui.)invoke_callback(state_id, callback, args, module_context)^`.
 
     Arguments:
         gui (Gui^): The current Gui instance.

+ 6 - 1
taipy/gui/hook.py

@@ -11,9 +11,14 @@ class Hook:
 
 class Hooks(object, metaclass=_Singleton):
     def __init__(self):
-        self.__hooks: t.List[Hook] = []
+        self.__hooks: t.List[Hook] = []  # type: ignore[annotation-unchecked]
 
     def _register_hook(self, hook: Hook):
+        # Prevent duplicated hooks
+        for h in self.__hooks:
+            if type(hook) is type(h):
+                _TaipyLogger._get_logger().info(f"Failed to register duplicated hook of type '{type(h)}'")
+                return
         self.__hooks.append(hook)
 
     def __getattr__(self, name: str):

+ 58 - 0
taipy/gui/pyproject.toml

@@ -0,0 +1,58 @@
+[build-system]
+requires = [ "setuptools>=42", "wheel", "setuptools_scm",]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy-gui"
+version = "0.0.0"  # will be set dynamically
+description = "Low-code library to create graphical user interfaces on the Web for your Python applications."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+keywords = [ "taipy-gui",]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+dependencies = []  # will be set dynamically
+
+[project.license]
+text = "Apache License 2.0"
+
+[project.optional-dependencies]
+test = [ "pytest>=3.8",]
+ngrok = [ "pyngrok>=5.1,<6.0",]
+image = [ "python-magic>=0.4.24,<0.5; platform_system!='Windows'", "python-magic-bin>=0.4.14,<0.5; platform_system=='Windows'",]
+arrow = [ "pyarrow>=14.0.2,<15.0",]
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-0.0.0/relnotes/"  # will be set dynamically
+
+[tool.setuptools]
+zip-safe = false
+
+[tool.setuptools.package-data]
+taipy = [ "version.json",]
+
+[tool.setuptools.packages.find]
+where = [ ".",]
+include = [ "taipy", "taipy.gui", "taipy.gui.*",]

+ 8 - 4
taipy/gui/server.py

@@ -161,7 +161,7 @@ class _Server:
                 if resource_handler is None:
                     return (f"Invalid value for query {_Server._RESOURCE_HANDLER_ARG}", 404)
                 try:
-                    return resource_handler.get_resources(path, static_folder)
+                    return resource_handler.get_resources(path, static_folder, base_url)
                 except Exception as e:
                     raise RuntimeError("Can't get resources from custom resource handler") from e
             if path == "" or path == "index.html" or "." not in path:
@@ -256,8 +256,9 @@ class _Server:
 
     def _apply_patch(self):
         if self._get_async_mode() == "gevent" and util.find_spec("gevent"):
-            from gevent import monkey
+            from gevent import get_hub, monkey
 
+            get_hub().NOT_ERROR += (KeyboardInterrupt, )
             if not monkey.is_module_patched("time"):
                 monkey.patch_time()
         if self._get_async_mode() == "eventlet" and util.find_spec("eventlet"):
@@ -290,7 +291,7 @@ class _Server:
             runtime_manager.add_gui(self._gui, port)
         if debug and not is_running_from_reloader() and _is_port_open(host_value, port):
             raise ConnectionError(
-                "Port {port} is already opened on {host} because another application is running on the same port. Please pick another port number and rerun with the 'port=<new_port>' option. You can also let Taipy choose a port number for you by running with the 'port=\"auto\"' option."  # noqa: E501
+                f"Port {port} is already opened on {host} because another application is running on the same port.\nPlease pick another port number and rerun with the 'port=<new_port>' setting.\nYou can also let Taipy choose a port number for you by running with the 'port=\"auto\"' setting."  # noqa: E501
             )
         if not flask_log:
             log = logging.getLogger("werkzeug")
@@ -318,7 +319,10 @@ class _Server:
         # flask-socketio specific conditions for 'allow_unsafe_werkzeug' parameters to be popped out of kwargs
         if self._get_async_mode() == "threading" and (not sys.stdin or not sys.stdin.isatty()):
             run_config = {**run_config, "allow_unsafe_werkzeug": allow_unsafe_werkzeug}
-        self._ws.run(**run_config)
+        try:
+            self._ws.run(**run_config)
+        except KeyboardInterrupt:
+            pass
 
     def stop_thread(self):
         if hasattr(self, "_thread") and self._thread.is_alive() and self._is_running:

+ 0 - 118
taipy/gui/setup.py

@@ -1,118 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy-gui package"""
-
-import json
-import os
-from pathlib import Path
-
-from setuptools import find_namespace_packages, find_packages, setup
-from setuptools.command.build_py import build_py
-
-root_folder = Path(__file__).parent
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.json")
-with open(version_path) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-
-def get_requirements():
-    # get requirements from the different setups in tools/packages (removing taipy packages)
-    reqs = set()
-    for pkg in (root_folder / "tools" / "packages").iterdir():
-        if "taipy-gui" not in str(pkg):
-            continue
-        requirements_file = pkg / "setup.requirements.txt"
-        if requirements_file.exists():
-            reqs.update(requirements_file.read_text("UTF-8").splitlines())
-
-    return [r for r in reqs if r and not r.startswith("taipy")]
-
-
-test_requirements = ["pytest>=3.8"]
-
-extras_require = {
-    "ngrok": ["pyngrok>=5.1,<6.0"],
-    "image": [
-        "python-magic>=0.4.24,<0.5;platform_system!='Windows'",
-        "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
-    ],
-    "arrow": ["pyarrow>=14.0.2,<15.0"],
-}
-
-
-def _build_webapp():
-    already_exists = Path("./taipy/gui/webapp/index.html").exists()
-    if not already_exists:
-        os.system("cd ../../frontend/taipy-gui/dom && npm ci")
-        os.system("cd ../../frontend/taipy-gui && npm ci --omit=optional && npm run build")
-
-
-class NPMInstall(build_py):
-    def run(self):
-        _build_webapp()
-        build_py.run(self)
-
-
-setup(
-    author="Avaiga",
-    author_email="dev@taipy.io",
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    description="Low-code library to create graphical user interfaces on the Web for your Python applications.",
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    install_requires=get_requirements(),
-    license="Apache License 2.0",
-    include_package_data=True,
-    data_files=[("version", ["version.json"])],
-    keywords="taipy-gui",
-    name="taipy-gui",
-    packages=find_namespace_packages(where=".") + find_packages(include=["taipy", "taipy.gui", "taipy.gui.*"]),
-    test_suite="tests",
-    tests_require=test_requirements,
-    version=version_string,
-    zip_safe=False,
-    extras_require=extras_require,
-    cmdclass={"build_py": NPMInstall},
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 4 - 2
taipy/gui/state.py

@@ -117,6 +117,8 @@ class State:
         return filter(lambda n: n not in excluded_attrs, var_list)
 
     def __getattribute__(self, name: str) -> t.Any:
+        if name == "__class__":
+            return State
         if name in State.__methods:
             return super().__getattribute__(name)
         gui: "Gui" = self.get_gui()
@@ -128,7 +130,7 @@ class State:
             name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
         ):
             raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
-        if name not in super().__getattribute__(State.__attrs[1]):
+        if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
             raise AttributeError(f"Variable '{name}' is not defined.")
         with self._notebook_context(gui), self._set_context(gui):
             encoded_name = gui._bind_var(name)
@@ -140,7 +142,7 @@ class State:
             name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
         ):
             raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
-        if name not in super().__getattribute__(State.__attrs[1]):
+        if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
             raise AttributeError(f"Variable '{name}' is not accessible.")
         with self._notebook_context(gui), self._set_context(gui):
             encoded_name = gui._bind_var(name)

+ 3 - 0
taipy/gui/utils/_bindings.py

@@ -68,6 +68,9 @@ class _Bindings:
         self.__scopes.create_scope(id)
         return id, create
 
+    def _delete_scope(self, id: str):
+        self.__scopes.delete_scope(id)
+
     def _new_scopes(self):
         self.__scopes = _DataScopes(self.__gui)
 

+ 2 - 0
taipy/gui/utils/types.py

@@ -86,6 +86,8 @@ class _TaipyBool(_TaipyBase):
 
 class _TaipyNumber(_TaipyBase):
     def get(self):
+        if super().get() is None:
+            return None
         try:
             return float(super().get())
         except Exception as e:

+ 128 - 119
taipy/gui/viselements.json

@@ -23,7 +23,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the text is processed:<ul><li>&quot;raw&quot;: synonym for setting the *raw* property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
+                        "doc": "Define the way the text is processed:\n<ul><li>&quot;raw&quot;: synonym for setting the *raw* property to True</li><li>&quot;pre&quot;: keeps spaces and new lines</li><li>&quot;markdown&quot; or &quot;md&quot;: basic support for Markdown."
                     },
                     {
                         "name": "format",
@@ -44,14 +44,14 @@
                     {
                         "name": "label",
                         "default_property": true,
-                        "type": "dynamic(str|Icon)",
+                        "type": "dynamic(Union[str,Icon])",
                         "default_value": "\"\"",
                         "doc": "The label displayed in the button."
                     },
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when the button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the button.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
+                        "doc": "The name of a function that is triggered when the button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button it it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -108,7 +108,7 @@
                         "name": "lines_shown",
                         "type": "int",
                         "default_value": "5",
-                        "doc": "The height of the displayed element if multiline is True."
+                        "doc": "The number of lines shown in the input control, when multiline is True."
                     },
                     {
                         "name": "type",
@@ -142,24 +142,24 @@
                     },
                     {
                         "name": "step",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "default_value": "1",
                         "doc": "The amount by which the value is incremented or decremented when the user clicks one of the arrow buttons."
                     },
                     {
                         "name": "step_multiplier",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "default_value": "10",
                         "doc": "A factor that multiplies <i>step</i> when the user presses the Shift key while clicking one of the arrow buttons."
                     },
                     {
                         "name": "min",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "doc": "The minimum value to accept for this input."
                     },
                     {
                         "name": "max",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "doc": "The maximum value to accept for this input."
                     }
                 ]
@@ -176,26 +176,26 @@
                     {
                         "name": "value",
                         "default_property": true,
-                        "type": "dynamic(int|float|int[]|float[]|str|str[])",
+                        "type": "dynamic(Union[int,float,str,list[int],list[float],list[str]])",
                         "doc": "The value that is set for this slider.<br/>If this slider is based on a <i>lov</i> then this property can be set to the lov element.<br/>This value can also hold an array of numbers to indicate that the slider reflects a range (within the [<i>min</i>,<i>max</i>] domain) defined by several knobs that the user can set independently.<br/>If this slider is based on a <i>lov</i> then this property can be set to an array of lov elements. The slider is then represented with several knobs, one for each lov value."
                     },
                     {
                         "name": "min",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "0",
                         "doc": "The minimum value.<br/>This is ignored when <i>lov</i> is defined."
                     },
                     {
                         "name": "max",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "100",
                         "doc": "The maximum value.<br/>This is ignored when <i>lov</i> is defined."
                     },
                     {
                         "name": "step",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "1",
-                        "doc": "The step value: the gap between two consecutive values the slider set. It is a good practice to have (<i>max</i>-<i>min</i>) being divisible by <i>step</i>.<br/>This property is ignored when <i>lov</i> is defined."
+                        "doc": "The step value, which is the gap between two consecutive values the slider set. It is a good practice to have (<i>max</i>-<i>min</i>) being divisible by <i>step</i>.<br/>This property is ignored when <i>lov</i> is defined."
                     },
                     {
                         "name": "text_anchor",
@@ -205,7 +205,7 @@
                     },
                     {
                         "name": "labels",
-                        "type": "bool|dict",
+                        "type": "Union[bool,dict[str,str]]",
                         "doc": "The labels for specific points of the slider.<br/>If set to True, this slider uses the labels of the <i>lov</i> if there are any.<br/>If set to a dictionary, the slider uses the dictionary keys as a <i>lov</i> key or index, and the associated value as the label."
                     },
                     {
@@ -224,12 +224,12 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "\"300px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this slider, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str",
-                        "doc": "The height, in CSS units, of this element.<br/>It defaults to the <i>width</i> value when using the vertical orientation."
+                        "doc": "The height of this slider, in CSS units.<br/>It defaults to the value of <i>width</i> when using the vertical orientation."
                     },
                     {
                         "name": "orientation",
@@ -272,7 +272,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the toggle is displayed:<ul><li>&quot;theme&quot;: synonym for setting the *theme* property to True</li></ul>"
+                        "doc": "Define the way the toggle is displayed:\n<ul><li>&quot;theme&quot;: synonym for setting the *theme* property to True</li></ul>"
                     }
                 ]
             }
@@ -501,7 +501,7 @@
                     {
                         "name": "on_range_change",
                         "type": "Callback",
-                        "doc": "The callback function that is invoked when the visible part of the x axis changes.<br/>The function receives three parameters:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the chart control.</li>\n<li>payload (dict[str, any]): the full details on this callback's invocation, as emitted by <a href=\"https://plotly.com/javascript/plotlyjs-events/#update-data\">Plotly</a>.</li>\n</ul>",
+                        "doc": "The callback function that is invoked when the visible part of the x axis changes.<br/>The function receives three parameters:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the chart control if it has one.</li>\n<li>payload (dict[str, any]): the full details on this callback's invocation, as emitted by <a href=\"https://plotly.com/javascript/plotlyjs-events/#update-data\">Plotly</a>.</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -519,7 +519,7 @@
                     },
                     {
                         "name": "columns",
-                        "type": "str|list[str]|dict[str, dict[str, str]]",
+                        "type": "Union[str,list[str],dict[str,dict[str,str]]]",
                         "default_value": "<i>All columns</i>",
                         "doc": "The list of column names\n<ul>\n<li>str: ;-separated list of column names</li>\n<li>list[str]: list of names</li>\n<li>dict: {\"column_name\": {format: \"format\", index: 1}} if index is specified, it represents the display order of the columns.\nIf not, the list order defines the index</li>\n</ul>"
                     },
@@ -535,7 +535,7 @@
                     },
                     {
                         "name": "selected",
-                        "type": "indexed(dynamic(list[int]|str))",
+                        "type": "indexed(dynamic(Union[list[int],str]))",
                         "doc": "The list of the selected point indices  ."
                     },
                     {
@@ -555,7 +555,7 @@
                     },
                     {
                         "name": "line",
-                        "type": "indexed(str|dict[str, any])",
+                        "type": "indexed(Union[str,dict[str,any]])",
                         "doc": "The configuration of the line used for the indicated trace.<br/>See <a href=\"https://plotly.com/javascript/reference/scatter/#scatter-line\">line</a> for details.<br/>If the value is a string, it must be a dash type or pattern (see <a href=\"https://plotly.com/python/reference/scatter/#scatter-line-dash\">dash style of lines</a> for details)."
                     },
                     {
@@ -600,14 +600,14 @@
                     },
                     {
                         "name": "width",
-                        "type": "str|int|float",
+                        "type": "Union[str,int,float]",
                         "default_value": "\"100%\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this chart, in CSS units."
                     },
                     {
                         "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this element."
+                        "type": "Union[str,int,float]",
+                        "doc": "The height of this chart, in CSS units."
                     },
                     {
                         "name": "template",
@@ -685,22 +685,22 @@
                     {
                         "name": "width[<i>column_name</i>]",
                         "type": "str",
-                        "doc": "The width, in CSS units, of the indicated column."
+                        "doc": "The width of the indicated column, in CSS units."
                     },
                     {
                         "name": "selected",
-                        "type": "dynamic(list[int]|str)",
+                        "type": "dynamic(Union[list[int],str])",
                         "doc": "The list of the indices of the rows to be displayed as selected."
                     },
                     {
                         "name": "page_size_options",
-                        "type": "list[int]|str",
-                        "default_value": "[50, 100, 500]",
+                        "type": "Union[list[int],str]",
+                        "default_value": "(50, 100, 500)",
                         "doc": "The list of available page sizes that users can choose from."
                     },
                     {
                         "name": "columns",
-                        "type": "str|list[str]|dict[str, dict[str, str|int]]",
+                        "type": "Union[str,list[str],dict[str,dict[str,Union[str,int]]]]",
                         "default_value": "<i>shows all columns when empty</i>",
                         "doc": "The list of the column names to display.\n<ul>\n<li>str: Semicolon (';')-separated list of column names.</li>\n<li>list[str]: The list of column names.</li>\n<li>dict: A dictionary with entries matching: {\"col name\": {format: \"format\", index: 1}}.<br/>\nif <i>index</i> is specified, it represents the display order of the columns.\nIf <i>index</i> is not specified, the list order defines the index.<br/>\nIf <i>format</i> is specified, it is used for numbers or dates.</li>\n</ul>"
                     },
@@ -751,13 +751,13 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "\"100%\"",
-                        "doc": "The width, in CSS units, of this table control."
+                        "doc": "The width of this table control, in CSS units."
                     },
                     {
                         "name": "height",
                         "type": "str",
                         "default_value": "\"80vh\"",
-                        "doc": "The height, in CSS units, of this table control."
+                        "doc": "The height of this table control, in CSS units."
                     },
                     {
                         "name": "filter",
@@ -885,12 +885,12 @@
                     },
                     {
                         "name": "lov[<i>column_name</i>]",
-                        "type": "list[str]|str",
+                        "type": "Union[list[str],str]",
                         "doc": "The list of values of the indicated column."
                     },
                     {
                         "name": "downloadable",
-                        "type": "boolean",
+                        "type": "bool",
                         "doc": "If True, a clickable icon is shown so the user can download the data as CSV."
                     },
                     {
@@ -932,7 +932,7 @@
                     {
                         "name": "mode",
                         "type": "str",
-                        "doc": "Define the way the selector is displayed:<ul><li>&quot;radio&quot;: list of radio buttons</li><li>&quot;check&quot;: list of check buttons</li><li>any other value: selector as usual."
+                        "doc": "Define the way the selector is displayed:\n<ul><li>&quot;radio&quot;: list of radio buttons</li><li>&quot;check&quot;: list of check buttons</li><li>any other value: selector as usual."
                     },
                     {
                         "name": "dropdown",
@@ -954,14 +954,14 @@
                     },
                     {
                         "name": "width",
-                        "type": "str|int",
+                        "type": "Union[str,int]",
                         "default_value": "\"360px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this selector, in CSS units."
                     },
                     {
                         "name": "height",
-                        "type": "str|int",
-                        "doc": "The height, in CSS units, of this element."
+                        "type": "Union[str,int]",
+                        "doc": "The height of this selector, in CSS units."
                     }
                 ]
             }
@@ -977,7 +977,7 @@
                     {
                         "name": "content",
                         "default_property": true,
-                        "type": "dynamic(path|file|URL|ReadableBuffer|None)",
+                        "type": "dynamic(Union[path,file,URL,ReadableBuffer,None])",
                         "doc": "The content to transfer.<br/>If this is a string, a URL, or a file, then the content is read from this source.<br/>If a readable buffer is provided (such as an array of bytes...), and to prevent the bandwidth from being consumed too much, the way the data is transferred depends on the <i>data_url_max_size</i> parameter of the application configuration (which is set to 50kB by default):\n<ul>\n<li>If the buffer size is smaller than this setting, then the raw content is generated as a data URL, encoded using base64 (i.e. <code>\"data:&lt;mimetype&gt;;base64,&lt;data&gt;\"</code>).</li>\n<li>If the buffer size exceeds this setting, then it is transferred through a temporary file.</li>\n</ul>If this property is set to None, that indicates that dynamic content is generated. Please take a look at the examples below for details on dynamic generation."
                     },
                     {
@@ -988,7 +988,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when the download is terminated (or on user action if <i>content</i> is None).<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has two keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: A list of two elements: <i>args[0]</i> reflects the <i>name</i> property and <i>args[1]</i> holds the file URL.</li>\n</ul>\n</li>\n</ul>",
+                        "doc": "The name of a function that is triggered when the download is terminated (or on user action if <i>content</i> is None).<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has two keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: A list of two elements: <i>args[0]</i> reflects the <i>name</i> property and <i>args[1]</i> holds the file URL.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -1052,7 +1052,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of the function that will be triggered.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the button.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
+                        "doc": "The name of the function that will be triggered.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -1106,7 +1106,7 @@
                     {
                         "name": "content",
                         "default_property": true,
-                        "type": "dynamic(path|URL|file|ReadableBuffer)",
+                        "type": "dynamic(Union[path,URL,file,ReadableBuffer])",
                         "doc": "The image source.<br/>If a buffer is provided (string, array of bytes...), and in order to prevent the bandwidth to be consumed too much, the way the image data is transferred depends on the <i>data_url_max_size</i> parameter of the application configuration (which is set to 50kB by default):\n<ul>\n<li>If the size of the buffer is smaller than this setting, then the raw content is generated as a\n  data URL, encoded using base64 (i.e. <code>\"data:&lt;mimetype&gt;;base64,&lt;data&gt;\"</code>).</li>\n<li>If the size of the buffer is greater than this setting, then it is transferred through a temporary\n  file.</li>\n</ul>"
                     },
                     {
@@ -1117,7 +1117,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when the user clicks on the image.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the button.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
+                        "doc": "The name of a function that is triggered when the user clicks on the image.<br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): a dictionary that contains the key \"action\" set to the name of the action that triggered this callback.</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -1135,14 +1135,14 @@
                     },
                     {
                         "name": "width",
-                        "type": "str|int|float",
+                        "type": "Union[str,int,float]",
                         "default_value": "\"300px\"",
-                        "doc": "The width, in CSS units, of this element."
+                        "doc": "The width of this image control, in CSS units."
                     },
                     {
                         "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this element."
+                        "type": "Union[str,int,float]",
+                        "doc": "The height of this image control, in CSS units."
                     }
                 ]
             }
@@ -1157,52 +1157,51 @@
                     {
                         "name": "value",
                         "default_property": true,
-                        "type": "dynamic(int|float)",
-                        "doc": "The value to display."
+                        "type": "dynamic(Union[int,float])",
+                        "doc": "The value to represent."
                     },
                     {
                         "name": "type",
                         "default_value": "\"circular\"",
                         "type": "str",
-                        "doc": "The type of the gauge.<br/>Possible values are:\n<ul>\n<li>\"none\"</li>\n<li>\"circular\"</li>\n<li>\"linear\"</li></ul>."
-                    },
-                    {
-                        "name": "title",
-                        "default_value": "None",
-                        "type": "str",
-                        "doc": "The title of the metric."
+                        "doc": "The type of the gauge.<br/>Possible values are:\n<ul>\n<li>\"none\"</li>\n<li>\"circular\"</li>\n<li>\"linear\"</li></ul>Setting this value to \"none\" remove the gauge."
                     },
                     {
                         "name": "min",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "0",
-                        "doc": "The minimum value of this metric control."
+                        "doc": "The minimum value of this metric control's gauge."
                     },
                     {
                         "name": "max",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "100",
-                        "doc": "The maximum value of this metric control."
+                        "doc": "The maximum value of this metric control's gauge."
                     },
                     {
                         "name": "delta",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "doc": "The delta value to display."
                     },
                     {
                         "name": "delta_color",
                         "type": "str",
-                        "doc": "The color that is used to display the value of the <i>delta</i> property. If negative_delta_color is set, then this property applies for positive values of delta only. If this property is set to \"invert\", then delta values are represented with the color used for negative values if delta is positive. The value for delta is also represented with the color used for positive values if delta is negative."
+                        "doc": "The color that is used to display the value of the <i>delta</i> property.<br/>If <i>negative_delta_color</i> is set, then this property applies for positive values of <i>delta</i> only.<br/>If this property is set to \"invert\", then values for <i>delta</i> are represented with the color used for negative values if delta is positive and <i>delta</i> is represented with the color used for positive values if it is negative."
+                    },
+                    {
+                        "name": "title",
+                        "default_value": "None",
+                        "type": "str",
+                        "doc": "The title of the metric."
                     },
                     {
                         "name": "negative_delta_color",
                         "type": "str",
-                        "doc": "If set, this represents the color to be used when the value of <i>delta</i> is negative (or positive if <i>delta_color</i> is set to \"invert\")"
-
+                        "doc": "If set, this represents the color to be used when the value of <i>delta</i> is negative (or positive if <i>delta_color</i> is set to \"invert\")."
                     },
                     {
                         "name": "threshold",
-                        "type": "dynamic(int|float)",
+                        "type": "dynamic(Union[int,float])",
                         "doc": "The threshold value to display."
                     },
                     {
@@ -1221,22 +1220,32 @@
                         "type": "str",
                         "doc": "The format to use when displaying the delta value.<br/>This uses the <code>printf</code> syntax."
                     },
+                    {
+                        "name": "bar_color",
+                        "type": "str",
+                        "doc": "The color of the bar in the gauge."
+                    },
                     {
                         "name": "color_map",
                         "type": "dict",
-                        "doc": "TODO The color_map is used to display different colors for different ranges of the metric. The color_map's keys represent the starting point of each range, which is a number, while the values represent the corresponding color for that range. If the value associated with a key is set to None, it implies that the corresponding range is not assigned any color."
+                        "doc": "Indicates what colors should be used for different ranges of the metric. The <i>color_map</i>'s keys represent the lower bound of each range, which is a number, while the values represent the color for that range.<br/>If the value associated with a key is set to None, the corresponding range is not assigned any color."
                     },
                     {
                         "name": "width",
-                        "type": "str|number",
+                        "type": "Union[str,number]",
                         "default_value": "None",
-                        "doc": "The width, in CSS units, of the metric."
+                        "doc": "The width of the metric control, in CSS units."
                     },
                     {
                         "name": "height",
-                        "type": "str|number",
+                        "type": "Union[str,number]",
                         "default_value": "None",
-                        "doc": "The height, in CSS units, of the metric."
+                        "doc": "The height of the metric control, in CSS units."
+                    },
+                    {
+                        "name": "layout",
+                        "type": "dynamic(dict[str, any])",
+                        "doc": "The <i>plotly.js</i> compatible <a href=\"https://plotly.com/javascript/reference/layout/\">layout object</a>."
                     },
                     {
                         "name": "template",
@@ -1264,7 +1273,7 @@
                         "name": "value",
                         "type": "dynamic(int)",
                         "doc": "If set, then the value represents the progress percentage that is shown.TODO - if unset?",
-                        "default_property": "true"
+                        "default_property": true
                     },
                     {
                         "name": "linear",
@@ -1281,8 +1290,8 @@
                     {
                         "name": "render",
                         "type": "dynamic(bool)",
-                        "doc": "If False, this progress indicator is hidden from the page.",
-                        "default_property": "true"
+                        "default_value": "True",
+                        "doc": "If False, this progress indicator is hidden from the page."
                     }
                 ]
             }
@@ -1308,13 +1317,13 @@
                     },
                     {
                         "name": "min",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "0",
                         "doc": "The minimum value of the range."
                     },
                     {
                         "name": "max",
-                        "type": "int|float",
+                        "type": "Union[int,float]",
                         "default_value": "100",
                         "doc": "The maximum value of the range."
                     },
@@ -1333,13 +1342,13 @@
                         "name": "width",
                         "type": "str",
                         "default_value": "None",
-                        "doc": "The width, in CSS units, of the indicator (used when orientation is horizontal)."
+                        "doc": "The width of the indicator, in CSS units (used when orientation is horizontal)."
                     },
                     {
                         "name": "height",
                         "type": "str",
                         "default_value": "None",
-                        "doc": "The height, in CSS units, of the indicator (used when orientation is vertical)."
+                        "doc": "The height of the indicator, in CSS units (used when orientation is vertical)."
                     },
                     {
                         "name": "hover_text",
@@ -1358,14 +1367,14 @@
                     {
                         "name": "lov",
                         "default_property": true,
-                        "type": "dynamic(str|list[str|Icon|any])",
+                        "type": "dynamic(Union[str,list[Union[str,Icon,any]]])",
                         "doc": "The list of menu option values."
                     },
                     {
                         "name": "adapter",
                         "type": "Function",
                         "default_value": "`\"lambda x: str(x)\"`",
-                        "doc": "The function that transforms an element of <i>lov</i> into a <i>tuple(id:str, label:str|Icon)</i>."
+                        "doc": "The function that transforms an element of <i>lov</i> into a <i>tuple(id:str, label:Union[str,Icon])</i>."
                     },
                     {
                         "name": "type",
@@ -1380,25 +1389,25 @@
                     },
                     {
                         "name": "inactive_ids",
-                        "type": "dynamic(str|list[str])",
+                        "type": "dynamic(Union[str,list[str]])",
                         "doc": "Semicolon (';')-separated list or a list of menu items identifiers that are disabled."
                     },
                     {
                         "name": "width",
                         "type": "str",
                         "default_value": "\"15vw\"",
-                        "doc": "The width, in CSS units, of the menu when unfolded.<br/>Note that when running on a mobile device, the property <i>width[active]</i> is used instead."
+                        "doc": "The width of the menu when unfolded, in CSS units.<br/>Note that when running on a mobile device, the property <i>width[active]</i> is used instead."
                     },
                     {
                         "name": "width[mobile]",
                         "type": "str",
                         "default_value": "\"85vw\"",
-                        "doc": "The width, in CSS units, of the menu when unfolded, on a mobile device."
+                        "doc": "The width of the menu when unfolded, in CSS units, when running on a mobile device."
                     },
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of the function that is triggered when a menu option is selected.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: List where the first element contains the id of the selected option.</li>\n</ul>\n</li>\n</ul>",
+                        "doc": "The name of the function that is triggered when a menu option is selected.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: List where the first element contains the id of the selected option.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -1444,7 +1453,7 @@
                     {
                         "name": "value",
                         "default_property": true,
-                        "type": "tuple|dict|list[dict]|list[tuple]",
+                        "type": "Union[tuple,dict,list[dict],list[tuple]]",
                         "doc": "The different status items to represent. See below."
                     },
                     {
@@ -1473,7 +1482,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
+                        "doc": "The name of the function that is triggered when the dialog button is pressed.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the button if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list with three elements:\n<ul><li>The first element is the username</li><li>The second element is the password</li><li>The third element is the current page name</li></ul></li></li>\n</ul>\n</li>\n</ul><br/>When the button is pressed, and if this property is not set, Taipy will try to find a callback function called <i>on_login()</i> and invoke it with the parameters listed above.",
                         "signature": [
                             [
                                 "state",
@@ -1510,17 +1519,29 @@
                         "default_property": true,
                         "required": true,
                         "type": "dynamic(list[str])",
-                        "doc": "The list of messages. Each element is a list composed of an id, a message and an user identifier."
+                        "doc": "The list of messages. Each item of this list must consist of a list of three strings: a message identifier, a message content, and a user identifier."
                     },
                     {
                         "name": "users",
-                        "type": "dynamic(list[str|Icon])",
+                        "type": "dynamic(list[Union[str,Icon]])",
                         "doc": "The list of users. See the <a href=\"../../binding/#list-of-values\">section on List of Values</a> for details."
                     },
+                    {
+                        "name": "sender_id",
+                        "type": "str",
+                        "default_value": "\"taipy\"",
+                        "doc": "The user identifier, as indicated in the <i>users</i> list, associated with all messages sent from the input."
+                    },
+                    {
+                        "name": "with_input",
+                        "type": "dynamic(bool)",
+                        "default_value": "True",
+                        "doc": "If False, the input field is not rendered."
+                    },
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when the user enters a new message.<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>var_name (str): the name of the messages variable.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args (list): A list composed of a reason (click or Enter), variable name, message, sender id.</li></ul></li></ul>.",
+                        "doc": "The name of a function that is triggered when the user enters a new message.<br/>All the parameters of that function are optional:\n<ul>\n<li><i>state</i> (<code>State^</code>): the state instance.</li>\n<li><i>var_name</i> (str): the name of the variable bound to the <i>messages</i> property.</li>\n<li><i>payload</i> (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li><i>action</i>: the name of the action that triggered this callback.</li>\n<li><i>args</i> (list): A list composed of a reason (\"click\" or \"Enter\"), the variable name, message, the user identifier of the sender.</li></ul></li></ul>",
                         "signature": [
                             [
                                 "state",
@@ -1536,28 +1557,16 @@
                             ]
                         ]
                     },
-                    {
-                        "name": "with_input",
-                        "type": "dynamic(bool)",
-                        "default_value": "True",
-                        "doc": "If True, the input field is visible."
-                    },
-                    {
-                        "name": "sender_id",
-                        "type": "str",
-                        "default_value": "\"taipy\"",
-                        "doc": "The user id associated with the message sent from the input"
-                    },
-                    {
-                        "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The maximum height, in CSS units, of this element."
-                    },
                     {
                         "name": "page_size",
                         "type": "int",
                         "default_value": "50",
-                        "doc": "The number of rows retrieved on the frontend."
+                        "doc": "The number of messages retrieved from the application and sent to the frontend. Larger values imply more potential latency."
+                    },
+                    {
+                        "name": "height",
+                        "type": "Union[str,int,float]",
+                        "doc": "The maximum height of this chat control, in CSS units."
                     }
                 ]
             }
@@ -1571,7 +1580,7 @@
                 "properties": [
                     {
                         "name": "expanded",
-                        "type": "dynamic(bool|str[])",
+                        "type": "dynamic(Union[bool,list[str]])",
                         "default_value": "True",
                         "doc": "If Boolean and False, only one node can be expanded at one given level. Otherwise this should be set to an array of the node identifiers that need to be expanded."
                     },
@@ -1590,7 +1599,7 @@
                     {
                         "name": "row_height",
                         "type": "str",
-                        "doc": "The height, in CSS units, of each row."
+                        "doc": "The height of each row of this tree, in CSS units."
                     }
                 ]
             }
@@ -1678,7 +1687,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "Name of a function triggered when a button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the dialog.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list where the first element contains the index of the selected label.</li>\n</ul>\n</li>\n</ul>",
+                        "doc": "Name of a function triggered when a button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the dialog if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list where the first element contains the index of the selected label.</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
                                 "state",
@@ -1702,18 +1711,18 @@
                     },
                     {
                         "name": "labels",
-                        "type": " str|list[str]",
+                        "type": "Union[str,list[str]]",
                         "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the <tt>on_action</tt> callback (that index is -1 for the <i>close</i> icon)."
                     },
                     {
                         "name": "width",
-                        "type": "str|int|float",
-                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
+                        "type": "Union[str,int,float]",
+                        "doc": "The width of this dialog, in CSS units."
                     },
                     {
                         "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
+                        "type": "Union[str,int,float]",
+                        "doc": "The height of this dialog, in CSS units."
                     }
                 ]
             }
@@ -1767,7 +1776,7 @@
                     {
                         "name": "on_close",
                         "type": "Callback",
-                        "doc": "The name of a function that is triggered when this pane is closed (if the user clicks outside of it or presses the Esc key).<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the button.</li>\n</ul><br/>If this property is not set, no function is called when this pane is closed.",
+                        "doc": "The name of a function that is triggered when this pane is closed (if the user clicks outside of it or presses the Esc key).<br/>All parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (optional[str]): the identifier of the close button if it has one.</li>\n</ul><br/>If this property is not set, no function is called when this pane is closed.",
                         "signature": [
                             [
                                 "state",
@@ -1843,7 +1852,7 @@
                         "name": "adapter",
                         "type": "Function",
                         "default_value": "`lambda x: str(x)`",
-                        "doc": "The function that transforms an element of <i>lov</i> into a <i>tuple(id:str, label:str|Icon)</i>."
+                        "doc": "The function that transforms an element of <i>lov</i> into a <i>tuple(id:str, label:Union[str,Icon])</i>."
                     },
                     {
                         "name": "type",
@@ -1892,7 +1901,7 @@
                 "properties": [
                     {
                         "name": "partial",
-                        "type": "Partial",
+                        "type": "taipy.gui.Partial",
                         "doc": "A Partial object that holds the content of the block.<br/>This should not be defined if <i>page</i> is set."
                     },
                     {
@@ -1933,7 +1942,7 @@
                     {
                         "name": "on_action",
                         "type": "Callback",
-                        "doc": "Name of a function that is triggered when a specific key is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the input.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args (list):\n<ul><li>key name</li><li>variable name</li><li>current value</li></ul>\n</li>\n</ul>\n</li>\n</ul>",
+                        "doc": "Name of a function that is triggered when a specific key is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the control if it has one.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args (list):\n<ul><li>key name</li><li>variable name</li><li>current value</li></ul>\n</li>\n</ul>\n</li>\n</ul>",
                         "signature": [
                             [
                                 "state",

+ 13 - 9
taipy/gui_core/_adapters.py

@@ -37,6 +37,7 @@ from taipy.core import (
 from taipy.core import get as core_get
 from taipy.core.config import Config
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
+from taipy.core.reason import ReasonCollection
 from taipy.gui._warnings import _warn
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.utils import _is_boolean, _is_true, _TaipyBase
@@ -55,6 +56,9 @@ class _EntityType(Enum):
     DATANODE = 3
 
 
+def _get_reason(rc: ReasonCollection, message: str):
+    return "" if rc else f"{message}: {rc.reasons}"
+
 class _GuiCoreScenarioAdapter(_TaipyBase):
     __INNER_PROPS = ["name"]
 
@@ -84,8 +88,8 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                             (
                                 s.get_simple_label(),
                                 [t.id for t in s.tasks.values()] if hasattr(s, "tasks") else [],
-                                "" if (reason := is_submittable(s)) else f"Sequence not submittable: {reason.reasons}",
-                                is_editable(s),
+                                _get_reason(is_submittable(s), "Sequence not submittable"),
+                                _get_reason(is_editable(s), "Sequence not editable"),
                             )
                             for s in scenario.sequences.values()
                         ]
@@ -95,11 +99,11 @@ class _GuiCoreScenarioAdapter(_TaipyBase):
                         if hasattr(scenario, "tasks")
                         else {},
                         list(scenario.properties.get("authorized_tags", [])) if scenario.properties else [],
-                        is_deletable(scenario),
-                        is_promotable(scenario),
-                        "" if (reason := is_submittable(scenario)) else f"Scenario not submittable: {reason.reasons}",
-                        is_readable(scenario),
-                        is_editable(scenario),
+                        _get_reason(is_deletable(scenario), "Scenario not deletable"),
+                        _get_reason(is_promotable(scenario), "Scenario not promotable"),
+                        _get_reason(is_submittable(scenario), "Scenario not submittable"),
+                        _get_reason(is_readable(scenario), "Scenario not readable"),
+                        _get_reason(is_editable(scenario), "Scenario not editable"),
                     ]
             except Exception as e:
                 _warn(f"Access to scenario ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)
@@ -221,8 +225,8 @@ class _GuiCoreDatanodeAdapter(_TaipyBase):
                         self.__get_data(datanode),
                         datanode._edit_in_progress,
                         datanode._editor_id,
-                        is_readable(datanode),
-                        is_editable(datanode),
+                        _get_reason(is_readable(datanode), "Datanode not readable"),
+                        _get_reason(is_editable(datanode), "Datanode not editable"),
                     ]
             except Exception as e:
                 _warn(f"Access to datanode ({data.id if hasattr(data, 'id') else 'No_id'}) failed", e)

+ 40 - 4
taipy/gui_core/_context.py

@@ -94,11 +94,19 @@ class _GuiCoreContext(CoreEventConsumerBase):
         # locks
         self.lock = Lock()
         self.submissions_lock = Lock()
+        # lazy_start
+        self.__started = False
         # super
         super().__init__(reg_id, reg_queue)
+
+    def __lazy_start(self):
+        if self.__started:
+            return
+        self.__started = True
         self.start()
 
     def process_event(self, event: Event):
+        self.__lazy_start()
         if event.entity_type == EventEntityType.SCENARIO:
             with self.gui._get_autorization(system=True):
                 self.scenario_refresh(
@@ -221,6 +229,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return entity
 
     def cycle_adapter(self, cycle: Cycle, sorts: t.Optional[t.List[t.Dict[str, t.Any]]] = None):
+        self.__lazy_start()
         try:
             if (
                 isinstance(cycle, Cycle)
@@ -243,6 +252,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def scenario_adapter(self, scenario: Scenario):
+        self.__lazy_start()
         if isinstance(scenario, (tuple, list)):
             return scenario
         try:
@@ -342,6 +352,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         filters: t.Optional[t.List[t.Dict[str, t.Any]]],
         sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
     ):
+        self.__lazy_start()
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
         with self.lock:
             # always needed to get scenarios for a cycle in cycle_adapter
@@ -360,12 +371,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return adapted_list
 
     def select_scenario(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 2:
             return
         state.assign(args[0], args[1])
 
     def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
+        self.__lazy_start()
         if not id or not is_readable(t.cast(ScenarioId, id)):
             return None
         try:
@@ -374,6 +387,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return None
 
     def get_scenario_configs(self):
+        self.__lazy_start()
         with self.lock:
             if self.scenario_configs is None:
                 configs = Config.scenarios
@@ -382,8 +396,9 @@ class _GuiCoreContext(CoreEventConsumerBase):
             return self.scenario_configs
 
     def crud_scenario(self, state: State, id: str, payload: t.Dict[str, str]):  # noqa: C901
+        self.__lazy_start()
         args = payload.get("args")
-        start_idx = 2
+        start_idx = 3
         if (
             args is None
             or not isinstance(args, list)
@@ -399,6 +414,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         data = args[start_idx + 2]
         with_dialog = True if len(args) < start_idx + 4 else bool(args[start_idx + 3])
         scenario = None
+        user_scenario = None
 
         name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
         if update:
@@ -453,7 +469,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         )
                         if isinstance(res, Scenario):
                             # everything's fine
-                            scenario_id = res.id
+                            user_scenario = res
+                            scenario_id = user_scenario.id
                             state.assign(error_var, "")
                             return
                         if res:
@@ -487,10 +504,10 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 state.assign(error_var, f"Error creating Scenario. {e}")
             finally:
                 self.scenario_refresh(scenario_id)
-                if scenario and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
+                if (scenario or user_scenario) and (sel_scenario_var := args[1] if isinstance(args[1], str) else None):
                     try:
                         var_name, _ = gui._get_real_var_name(sel_scenario_var)
-                        state.assign(var_name, scenario)
+                        self.gui._update_var(var_name, scenario or user_scenario, on_change= args[2])
                     except Exception as e:  # pragma: no cover
                         _warn("Can't find value variable name in context", e)
         if scenario:
@@ -519,6 +536,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             state.assign(var_name, msg)
 
     def edit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -567,6 +585,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 _GuiCoreContext.__assign_var(state, error_var, f"Error updating {type(scenario).__name__}. {e}")
 
     def submit_entity(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -679,6 +698,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         filters: t.Optional[t.List[t.Dict[str, t.Any]]],
         sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
     ):
+        self.__lazy_start()
         base_list = []
         with self.lock:
             self.__do_datanodes_tree()
@@ -707,6 +727,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         sorts: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
         adapt_dn=True,
     ):
+        self.__lazy_start()
         if isinstance(data, tuple):
             raise NotImplementedError
         if isinstance(data, list):
@@ -767,12 +788,14 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def get_jobs_list(self):
+        self.__lazy_start()
         with self.lock:
             if self.jobs_list is None:
                 self.jobs_list = get_jobs()
             return self.jobs_list
 
     def job_adapter(self, job):
+        self.__lazy_start()
         try:
             if hasattr(job, "id") and is_readable(job.id) and core_get(job.id) is not None:
                 if isinstance(job, Job):
@@ -795,6 +818,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def act_on_jobs(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -830,6 +854,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             _GuiCoreContext.__assign_var(state, payload.get("error_id"), "<br/>".join(errs) if errs else "")
 
     def edit_data_node(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -847,6 +872,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 _GuiCoreContext.__assign_var(state, error_var, f"Error updating Datanode. {e}")
 
     def lock_datanode_for_edit(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -893,6 +919,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         ent.properties.pop(key, None)
 
     def get_scenarios_for_owner(self, owner_id: str):
+        self.__lazy_start()
         cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
         with self.lock:
             if self.scenario_by_cycle is None:
@@ -913,6 +940,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return sorted(cycles_scenarios, key=_get_entity_property("creation_date", Scenario))
 
     def get_data_node_history(self, id: str):
+        self.__lazy_start()
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):
             res = []
             for e in dn.edits:
@@ -945,6 +973,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return True
 
     def update_data(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
@@ -973,6 +1002,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             _GuiCoreContext.__assign_var(state, payload.get("data_id"), entity_id)  # this will update the data value
 
     def tabular_data_edit(self, state: State, var_name: str, payload: dict):
+        self.__lazy_start()
         error_var = payload.get("error_id")
         user_data = payload.get("user_data", {})
         dn_id = user_data.get("dn_id")
@@ -1039,6 +1069,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         _GuiCoreContext.__assign_var(state, payload.get("data_id"), dn_id)
 
     def get_data_node_properties(self, id: str):
+        self.__lazy_start()
         if id and is_readable(t.cast(DataNodeId, id)) and (dn := core_get(id)) and isinstance(dn, DataNode):
             try:
                 return (
@@ -1056,6 +1087,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return datanode.read()
 
     def get_data_node_tabular_data(self, id: str):
+        self.__lazy_start()
         if (
             id
             and is_readable(t.cast(DataNodeId, id))
@@ -1072,6 +1104,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def get_data_node_tabular_columns(self, id: str):
+        self.__lazy_start()
         if (
             id
             and is_readable(t.cast(DataNodeId, id))
@@ -1090,6 +1123,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def get_data_node_chart_config(self, id: str):
+        self.__lazy_start()
         if (
             id
             and is_readable(t.cast(DataNodeId, id))
@@ -1106,6 +1140,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         return None
 
     def on_dag_select(self, state: State, id: str, payload: t.Dict[str, str]):
+        self.__lazy_start()
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 2:
             return
@@ -1124,4 +1159,5 @@ class _GuiCoreContext(CoreEventConsumerBase):
             _warn(f"dag.on_action(): Invalid function '{args[1]}()'.")
 
     def get_creation_reason(self):
+        self.__lazy_start()
         return "" if (reason := can_create()) else f"Cannot create scenario: {reason.reasons}"

+ 50 - 0
taipy/rest/pyproject.toml

@@ -0,0 +1,50 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy-rest"
+version = "0.0.0"  # will be dynamically set
+description = "Library to expose taipy-core REST APIs."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+license = {text = "Apache License 2.0"}
+keywords = ["taipy-rest"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+dependencies = []  # will be dynamically set
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-0.0.0/relnotes/"  # version will be dynamically set
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["taipy", "taipy.rest"]
+
+[tool.setuptools.package-data]
+"taipy" = ["version.json"]
+
+[tool.setuptools]
+zip-safe = false

+ 0 - 87
taipy/rest/setup.py

@@ -1,87 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy-rest package"""
-
-import json
-import os
-from pathlib import Path
-
-from setuptools import find_namespace_packages, find_packages, setup
-
-root_folder = Path(__file__).parent
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.json")
-with open(version_path) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-
-def get_requirements():
-    # get requirements from the different setups in tools/packages (removing taipy packages)
-    reqs = set()
-    for pkg in (root_folder / "tools" / "packages").iterdir():
-        if "taipy-rest" not in str(pkg):
-            continue
-        requirements_file = pkg / "setup.requirements.txt"
-        if requirements_file.exists():
-            reqs.update(requirements_file.read_text("UTF-8").splitlines())
-
-    return [r for r in reqs if r and not r.startswith("taipy")]
-
-
-setup(
-    author="Avaiga",
-    name="taipy-rest",
-    keywords="taipy-rest",
-    python_requires=">=3.8",
-    version=version_string,
-    author_email="dev@taipy.io",
-    packages=find_namespace_packages(where=".") + find_packages(include=["taipy", "taipy.rest"]),
-    include_package_data=True,
-    data_files=[('version', ['version.json'])],
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    description="Library to expose taipy-core REST APIs.",
-    license="Apache License 2.0",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    install_requires=get_requirements(),
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 53 - 0
taipy/templates/pyproject.toml

@@ -0,0 +1,53 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "taipy-templates"
+version = "0.0.0"  # will be dynamically set
+description = "An open-source package holding Taipy application templates."
+readme = "package_desc.md"
+requires-python = ">=3.8"
+license = {text = "Apache License 2.0"}
+keywords = ["taipy-templates"]
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Intended Audience :: Developers",
+    "License :: OSI Approved :: Apache Software License",
+    "Natural Language :: English",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Topic :: Software Development",
+    "Topic :: Scientific/Engineering",
+    "Operating System :: Microsoft :: Windows",
+    "Operating System :: POSIX",
+    "Operating System :: Unix",
+    "Operating System :: MacOS",
+]
+dependencies = []  # version will be dynamically set
+
+[project.optional-dependencies]
+test = ["pytest>=3.8"]
+
+[project.urls]
+Homepage = "https://www.taipy.io"
+Documentation = "https://docs.taipy.io"
+Source = "https://github.com/Avaiga/taipy"
+Download = "https://pypi.org/project/taipy/#files"
+Tracker = "https://github.com/Avaiga/taipy/issues"
+Security = "https://github.com/Avaiga/taipy?tab=security-ov-file#readme"
+"Release notes" = "https://docs.taipy.io/en/release-0.0.0/relnotes/"  # version will be dynamically set
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["taipy"]
+
+[tool.setuptools.package-data]
+"taipy" = ["version.json"]
+
+[tool.setuptools]
+zip-safe = false

+ 0 - 74
taipy/templates/setup.py

@@ -1,74 +0,0 @@
-# Copyright 2021-2024 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.
-
-"""The setup script for taipy-templates package"""
-
-import json
-import os
-from pathlib import Path
-
-from setuptools import find_namespace_packages, find_packages, setup
-
-package_desc = Path("package_desc.md").read_text("UTF-8")
-
-version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "version.json")
-with open(version_path) as version_file:
-    version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-    if vext := version.get("ext"):
-        version_string = f"{version_string}.{vext}"
-
-test_requirements = ["pytest>=3.8"]
-
-setup(
-    author="Avaiga",
-    author_email="dev@taipy.io",
-    python_requires=">=3.8",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Intended Audience :: Developers",
-        "License :: OSI Approved :: Apache Software License",
-        "Natural Language :: English",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Topic :: Software Development",
-        "Topic :: Scientific/Engineering",
-        "Operating System :: Microsoft :: Windows",
-        "Operating System :: POSIX",
-        "Operating System :: Unix",
-        "Operating System :: MacOS",
-    ],
-    description="An open-source package holding Taipy application templates.",
-    license="Apache License 2.0",
-    long_description=package_desc,
-    long_description_content_type="text/markdown",
-    keywords="taipy-templates",
-    name="taipy-templates",
-    packages=find_namespace_packages(where=".") + find_packages(include=["taipy"]),
-    include_package_data=True,
-    data_files=[('version', ['version.json'])],
-    test_suite="tests",
-    version=version_string,
-    zip_safe=False,
-    project_urls={
-        "Homepage": "https://www.taipy.io",
-        "Documentation": "https://docs.taipy.io",
-        "Source": "https://github.com/Avaiga/taipy",
-        "Download": "https://pypi.org/project/taipy/#files",
-        "Tracker": "https://github.com/Avaiga/taipy/issues",
-        "Security": "https://github.com/Avaiga/taipy?tab=security-ov-file#readme",
-        f"Release notes": "https://docs.taipy.io/en/release-{version_string}/relnotes/",
-    },
-)

+ 74 - 1
tests/core/_orchestrator/test_orchestrator__submit.py

@@ -10,6 +10,7 @@
 # specific language governing permissions and limitations under the License.
 
 from datetime import datetime, timedelta
+from time import sleep
 from unittest import mock
 
 import freezegun
@@ -17,7 +18,7 @@ import pytest
 
 from taipy import Scenario, Scope, Task
 from taipy.config import Config
-from taipy.core import taipy
+from taipy.core import Core, taipy
 from taipy.core._orchestrator._orchestrator import _Orchestrator
 from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
 from taipy.core.config import JobConfig
@@ -27,6 +28,7 @@ from taipy.core.scenario._scenario_manager import _ScenarioManager
 from taipy.core.submission._submission_manager_factory import _SubmissionManagerFactory
 from taipy.core.submission.submission_status import SubmissionStatus
 from taipy.core.task._task_manager import _TaskManager
+from tests.core.utils import assert_true_after_time
 
 
 def nothing(*args, **kwargs):
@@ -53,6 +55,7 @@ def test_submit_scenario_development_mode():
     scenario = create_scenario()
     scenario.dn_0.write(0)  # input data is made ready
     orchestrator = _OrchestratorFactory._build_orchestrator()
+    _OrchestratorFactory._build_dispatcher()
 
     submit_time = datetime.now() + timedelta(seconds=1)  # +1 to ensure the edit time of dn_0 is before the submit time
     with freezegun.freeze_time(submit_time):
@@ -505,3 +508,73 @@ def test_submit_submittable_generate_unique_submit_id():
     assert jobs_1[0].submit_id == jobs_1[1].submit_id
     assert jobs_2[0].submit_id == jobs_2[1].submit_id
     assert jobs_1[0].submit_id != jobs_2[0].submit_id
+
+
+def task_sleep_1():
+    sleep(1)
+
+
+def task_sleep_2():
+    sleep(2)
+    return
+
+
+def test_submit_duration_development_mode():
+    core = Core()
+    core.run()
+
+    task_1 = Task("task_config_id_1", {}, task_sleep_1, [], [])
+    task_2 = Task("task_config_id_2", {}, task_sleep_2, [], [])
+
+    _TaskManager._set(task_1)
+    _TaskManager._set(task_2)
+
+    scenario = Scenario("scenario", {task_1, task_2}, {})
+    _ScenarioManager._set(scenario)
+    submission = taipy.submit(scenario)
+    jobs = submission.jobs
+    core.stop()
+
+    assert all(isinstance(job.execution_started_at, datetime) for job in jobs)
+    assert all(isinstance(job.execution_ended_at, datetime) for job in jobs)
+    jobs_1s = jobs[0] if jobs[0].task.config_id == "task_config_id_1" else jobs[1]
+    jobs_2s = jobs[0] if jobs[0].task.config_id == "task_config_id_2" else jobs[1]
+    assert jobs_1s.execution_duration >= 1
+    assert jobs_2s.execution_duration >= 2
+
+    assert submission.execution_duration >= 3
+    assert submission.execution_started_at == min(jobs_1s.execution_started_at, jobs_2s.execution_started_at)
+    assert submission.execution_ended_at == max(jobs_1s.execution_ended_at, jobs_2s.execution_ended_at)
+
+
+@pytest.mark.standalone
+def test_submit_duration_standalone_mode():
+    Config.configure_job_executions(mode=JobConfig._STANDALONE_MODE)
+    core = Core()
+    core.run()
+
+    task_1 = Task("task_config_id_1", {}, task_sleep_1, [], [])
+    task_2 = Task("task_config_id_2", {}, task_sleep_2, [], [])
+
+    _TaskManager._set(task_1)
+    _TaskManager._set(task_2)
+
+    scenario = Scenario("scenario", {task_1, task_2}, {})
+    _ScenarioManager._set(scenario)
+    submission = taipy.submit(scenario)
+    jobs = submission.jobs
+
+    assert_true_after_time(jobs[1].is_completed)
+
+    core.stop()
+
+    assert all(isinstance(job.execution_started_at, datetime) for job in jobs)
+    assert all(isinstance(job.execution_ended_at, datetime) for job in jobs)
+    jobs_1s = jobs[0] if jobs[0].task.config_id == "task_config_id_1" else jobs[1]
+    jobs_2s = jobs[0] if jobs[0].task.config_id == "task_config_id_2" else jobs[1]
+    assert jobs_1s.execution_duration >= 1
+    assert jobs_2s.execution_duration >= 2
+
+    assert submission.execution_duration >= 2  # Both tasks are executed in parallel so the duration may smaller than 3
+    assert submission.execution_started_at == min(jobs_1s.execution_started_at, jobs_2s.execution_started_at)
+    assert submission.execution_ended_at == max(jobs_1s.execution_ended_at, jobs_2s.execution_ended_at)

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů