Kaynağa Gözat

Merge branch 'develop' into metric-example

namnguyen 11 ay önce
ebeveyn
işleme
0199ac8b3c

+ 19 - 9
.github/ISSUE_TEMPLATE/bug-report.yml

@@ -11,6 +11,7 @@ body:
         - Take a look at our template and try to add as much detail as possible.
         - The more details we have, the easier it would be to fix it. 
         - If any heading is not applicable, please set it to `NA`.
+
   - type: textarea
     id: something_wrong
     attributes:
@@ -19,12 +20,14 @@ body:
       placeholder: The description of the issue you experienced in detail, including any relevant information or context.
     validations:
       required: true
+
   - type: textarea
     id: expected_behavior
     attributes:
       label: "Expected Behavior"
       description: Please describe how you expected the system to behave when you encountered the issue.
       placeholder: The description of the behavior that you expected to see when encountering the issue.
+
   - type: textarea
     id: reproduction_steps
     attributes:
@@ -34,12 +37,14 @@ body:
         1. A code fragment
         2. And/or configuration files or code
         3. And/or Taipy GUI Markdown or HTML files
+
   - type: textarea
     id: solution_proposed
     attributes:
       label: "Solution Proposed"
       description: Any ideas on how this should be solved
       placeholder: The potential solution to solve this issue
+
   - type: textarea
     id: screenshot
     attributes:
@@ -47,9 +52,9 @@ body:
       description: If applicable, add screenshots to help explain your problem.
       value: |
         ![DESCRIPTION](LINK.png)
-      render: bash
     validations:
       required: false
+
   - type: input
     id: environment
     attributes:
@@ -58,6 +63,7 @@ body:
       placeholder: ex. Windows 10, Chrome 91.0.4472.124
     validations:
       required: false
+
   - type: dropdown
     id: browsers
     attributes:
@@ -74,6 +80,7 @@ body:
         - Other
     validations:
       required: false
+
   - type: dropdown
     id: os
     attributes:
@@ -89,6 +96,7 @@ body:
         - Other
     validations:
       required: false
+
   - type: input
     id: version_taipy
     attributes:
@@ -97,6 +105,7 @@ body:
       placeholder: ex. 3.0.1
     validations:
       required: false
+
   - type: textarea
     id: additional_context
     attributes:
@@ -106,15 +115,15 @@ body:
       render: bash
     validations:
       required: false
-  - type: checkboxes
-    id: acceptance_criteria
+
+  - type: markdown
     attributes:
-      label: Acceptance Criteria
-      options:
-        - label: "Ensure new code is unit tested, and check code coverage is at least 90%."
-          required: true
-        - label: "Create a related issue in taipy-doc for documentation and Release Notes if relevant."
-          required: true
+      value: |
+        ### Acceptance Criteria
+        
+        - [ ] Ensure new code is unit tested, and check code coverage is at least 90%.
+        - [ ] Create related issue in taipy-doc for documentation and Release Notes.
+
   - type: checkboxes
     id: terms_checklist_bug
     attributes:
@@ -125,6 +134,7 @@ body:
           required: true
         - label: "I am willing to work on this issue (optional)"
           required: false
+
   - type: markdown
     attributes:
       value: Thank you for taking the time to report the issue! 😄

+ 2 - 0
.github/ISSUE_TEMPLATE/documentation.yml

@@ -9,6 +9,7 @@ body:
         - Thank you for using Taipy and taking the time to suggest improvements in documentation! 😄 
         - Take a look at our template and try to add as much detail as possible.
         - If any heading is not applicable, please set it to `NA`.
+        
   - type: textarea
     id: docs_description
     attributes:
@@ -39,6 +40,7 @@ body:
           required: true
         - label: "I am willing to work on this issue (optional)"
           required: false
+
   - type: markdown
     attributes:
       value: Thank you for taking the time to report the issue! 😄

+ 15 - 12
.github/ISSUE_TEMPLATE/feature-request.yml

@@ -9,6 +9,7 @@ body:
         - Thank you for using Taipy and taking the time to suggest a new feature! 😄 
         - Take a look at our template and try to add as much detail as possible. 
         - If any heading is not applicable, please set it to `NA`.
+
   - type: textarea
     id: description_feature
     attributes:
@@ -17,18 +18,21 @@ body:
       placeholder: A detailed description of the new feature request.
     validations:
       required: true
+
   - type: textarea
     id: feature_solution_proposed
     attributes:
       label: "Solution Proposed"
       description: A precise description of how you would like to see this functionality implemented.
       placeholder: Including any relevant materials such as sketches, wireframes, or flowcharts to illustrate your proposal is highly welcomed.
+
   - type: textarea
     id: feature_impact_solution
     attributes:
       label: "Impact of Solution"
       description: What impact could that feature have on the rest of the product, and should be taken special care of?
       placeholder: Including any relevant info to understand the impact of solution on the rest of the product.
+
   - type: textarea
     id: additional_context_feature
     attributes:
@@ -36,19 +40,17 @@ body:
       description: If you have any additional context or information that may help us to implement this feature, please provide it here. (workaround, third-party...)
     validations:
       required: false
-  - type: checkboxes
-    id: acceptance_criteria
+
+  - type: markdown
     attributes:
-      label: Acceptance Criteria
-      options:
-        - label: "Ensure new code is unit tested, and check code coverage is at least 90%."
-          required: true
-        - label: "Create related issue in taipy-doc for documentation and Release Notes."
-          required: true
-        - label: "Check if a new demo could be provided based on this, or if legacy demos could be benefit from it."
-          required: true
-        - label: "Ensure any change is well documented."
-          required: true
+      value: |
+        ### Acceptance Criteria
+        
+        - [ ] Ensure new code is unit tested, and check code coverage is at least 90%.
+        - [ ] Create related issue in taipy-doc for documentation and Release Notes.
+        - [ ] Check if a new demo could be provided based on this, or if legacy demos could be benefit from it.
+        - [ ] Ensure any change is well documented.      
+
   - type: checkboxes
     id: terms_checklist_feature
     attributes:
@@ -59,6 +61,7 @@ body:
           required: true
         - label: "I am willing to work on this issue (optional)"
           required: false
+
   - type: markdown
     attributes:
       value: Thank you for taking the time to suggest a feature request! 😄

+ 3 - 0
.github/ISSUE_TEMPLATE/other.yml

@@ -8,6 +8,7 @@ body:
       value: |
         - Thank you for using Taipy and taking the time to submit this issue! 😄 
         - This is for discussion or any questions you may have.
+
   - type: textarea
     id: issuedescription
     attributes:
@@ -15,6 +16,7 @@ body:
       description: Provide a clear and concise explanation of your issue.
     validations:
       required: true
+
   - type: checkboxes
     id: terms_checklist_discussion
     attributes:
@@ -25,6 +27,7 @@ body:
           required: true
         - label: "I am willing to work on this issue (optional)"
           required: false
+          
   - type: markdown
     attributes:
       value: Thank you for taking the time to report the issue! 😄

+ 11 - 10
.github/ISSUE_TEMPLATE/refactor-code.yml

@@ -7,6 +7,7 @@ body:
     attributes:
       value: |
         - Thank you for using Taipy and taking the time to suggest feature improvements! 😄
+
   - type: textarea
     id: refactor_description
     attributes:
@@ -14,17 +15,16 @@ body:
       description: "Describe what improvements can be made(performance, API...) in the codebase without introducing breaking changes."
     validations:
       required: true
-  - type: checkboxes
-    id: acceptance_criteria
+
+  - type: markdown
     attributes:
-      label: Acceptance Criteria
-      options:
-        - label: "Ensure new code is unit tested, and check code coverage is at least 90%."
-          required: true
-        - label: "Propagate any change on the demos and run all of them to ensure there is no breaking change."
-          required: true
-        - label: "Ensure any change is well documented."
-          required: true
+      value: |
+        ### Acceptance Criteria
+        
+        - [ ] Ensure new code is unit tested, and check code coverage is at least 90%.
+        - [ ] Propagate any change on the demos and run all of them to ensure there is no breaking change.
+        - [ ] Ensure any change is well documented.
+
   - type: checkboxes
     id: terms_checklist_refactor
     attributes:
@@ -35,6 +35,7 @@ body:
           required: true
         - label: "I am willing to work on this issue (optional)"
           required: false
+
   - type: markdown
     attributes:
       value: Thank you for taking the time to report the issue! 😄

Dosya farkı çok büyük olduğundan ihmal edildi
+ 521 - 221
frontend/taipy-gui/package-lock.json


+ 28 - 0
frontend/taipy-gui/public/stylekit/controls/file_download.css

@@ -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.
+ */
+
+/**************************************************************
+
+                   TAIPY FILE_DOWNLOAD
+
+***************************************************************/
+
+/*************************************************
+              MODIFIER CLASSES
+**************************************************/
+
+/* fullwidth :  */
+.taipy-file-download.fullwidth button {
+    display: flex;
+    width: 100%;
+}

+ 28 - 0
frontend/taipy-gui/public/stylekit/controls/file_selector.css

@@ -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.
+ */
+
+/**************************************************************
+
+                   TAIPY FILE_SELECTOR
+
+***************************************************************/
+
+/*************************************************
+              MODIFIER CLASSES
+**************************************************/
+
+/* fullwidth :  */
+.taipy-file-selector.fullwidth [role='button'] {
+    display: flex;
+    width: 100%;
+}

+ 2 - 0
frontend/taipy-gui/public/stylekit/stylekit.css

@@ -38,6 +38,8 @@
 @import 'controls/slider.css';
 @import 'controls/selector.css';
 @import 'controls/toggle.css';
+@import 'controls/file_download.css';
+@import 'controls/file_selector.css';
 
 /* Blocks */
 @import 'blocks/layout.css';

+ 23 - 361
frontend/taipy-gui/src/components/Taipy/Chart.tsx

@@ -43,6 +43,7 @@ import {
     useDynamicProperty,
     useModule,
 } from "../../utils/hooks";
+import { darkThemeTemplate } from "../../themes/darkThemeTemplate";
 
 const Plot = lazy(() => import("react-plotly.js"));
 
@@ -236,6 +237,14 @@ const TaipyPlotlyButtons: ModeBarButtonAny[] = [
     },
 ];
 
+const updateArrays = (sel: number[][], val: number[], idx: number) => {
+    if (idx >= sel.length || val.length !== sel[idx].length || val.some((v, i) => sel[idx][i] != v)) {
+        sel = sel.concat(); // shallow copy
+        sel[idx] = val;
+    }
+    return sel;
+};
+
 const Chart = (props: ChartProp) => {
     const {
         title = "",
@@ -286,13 +295,12 @@ const Chart = (props: ChartProp) => {
                         if (!Array.isArray(val)) {
                             val = [];
                         }
-                        if (
-                            idx >= sel.length ||
-                            val.length !== sel[idx].length ||
-                            val.some((v, i) => sel[idx][i] != v)
-                        ) {
-                            sel = sel.concat();
-                            sel[idx] = val;
+                        if (idx === 0 && val.length && Array.isArray(val[0])) {
+                            for (let i = 0; i < val.length; i++) {
+                                sel = updateArrays(sel, val[i] as unknown as number[], i);
+                            }
+                        } else {
+                            sel = updateArrays(sel, val, idx);
                         }
                     }
                 }
@@ -341,7 +349,7 @@ const Chart = (props: ChartProp) => {
                 theme.palette.mode === "dark"
                     ? props.template_Dark_
                         ? JSON.parse(props.template_Dark_)
-                        : darkTemplate
+                        : darkThemeTemplate
                     : props.template_Light_ && JSON.parse(props.template_Light_);
             template = tpl ? (tplTheme ? { ...tpl, ...tplTheme } : tpl) : tplTheme ? tplTheme : undefined;
         } catch (e) {
@@ -584,10 +592,14 @@ const Chart = (props: ChartProp) => {
                     return tr;
                 }, [] as number[][]);
                 if (traces.length) {
+                    const upvars = traces.map((_, idx) => getUpdateVar(updateVars, `selected${idx}`));
+                    if (traces.length > 1 && new Set(upvars).size === 1) {
+                        dispatch(createSendUpdateAction(upvars[0], traces, module, props.onChange, propagate));
+                        return;
+                    }
                     traces.forEach((tr, idx) => {
-                        const upvar = getUpdateVar(updateVars, `selected${idx}`);
-                        if (upvar && tr && tr.length) {
-                            dispatch(createSendUpdateAction(upvar, tr, module, props.onChange, propagate));
+                        if (upvars[idx] && tr && tr.length) {
+                            dispatch(createSendUpdateAction(upvars[idx], tr, module, props.onChange, propagate));
                         }
                     });
                 } else if (config.traces.length === 1) {
@@ -638,353 +650,3 @@ const Chart = (props: ChartProp) => {
 };
 
 export default Chart;
-
-const darkTemplate = {
-    data: {
-        barpolar: [
-            {
-                marker: {
-                    line: {
-                        color: "rgb(17,17,17)",
-                    },
-                    pattern: {
-                        solidity: 0.2,
-                    },
-                },
-                type: "barpolar",
-            },
-        ],
-        bar: [
-            {
-                error_x: {
-                    color: "#f2f5fa",
-                },
-                error_y: {
-                    color: "#f2f5fa",
-                },
-                marker: {
-                    line: {
-                        color: "rgb(17,17,17)",
-                    },
-                    pattern: {
-                        solidity: 0.2,
-                    },
-                },
-                type: "bar",
-            },
-        ],
-        carpet: [
-            {
-                aaxis: {
-                    endlinecolor: "#A2B1C6",
-                    gridcolor: "#506784",
-                    linecolor: "#506784",
-                    minorgridcolor: "#506784",
-                    startlinecolor: "#A2B1C6",
-                },
-                baxis: {
-                    endlinecolor: "#A2B1C6",
-                    gridcolor: "#506784",
-                    linecolor: "#506784",
-                    minorgridcolor: "#506784",
-                    startlinecolor: "#A2B1C6",
-                },
-                type: "carpet",
-            },
-        ],
-        contour: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "contour",
-            },
-        ],
-        heatmapgl: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "heatmapgl",
-            },
-        ],
-        heatmap: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "heatmap",
-            },
-        ],
-        histogram2dcontour: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "histogram2dcontour",
-            },
-        ],
-        histogram2d: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "histogram2d",
-            },
-        ],
-        histogram: [
-            {
-                marker: {
-                    pattern: {
-                        solidity: 0.2,
-                    },
-                },
-                type: "histogram",
-            },
-        ],
-        scatter: [
-            {
-                marker: {
-                    line: {
-                        color: "#283442",
-                    },
-                },
-                type: "scatter",
-            },
-        ],
-        scattergl: [
-            {
-                marker: {
-                    line: {
-                        color: "#283442",
-                    },
-                },
-                type: "scattergl",
-            },
-        ],
-        surface: [
-            {
-                colorscale: [
-                    [0.0, "#0d0887"],
-                    [0.1111111111111111, "#46039f"],
-                    [0.2222222222222222, "#7201a8"],
-                    [0.3333333333333333, "#9c179e"],
-                    [0.4444444444444444, "#bd3786"],
-                    [0.5555555555555556, "#d8576b"],
-                    [0.6666666666666666, "#ed7953"],
-                    [0.7777777777777778, "#fb9f3a"],
-                    [0.8888888888888888, "#fdca26"],
-                    [1.0, "#f0f921"],
-                ],
-                type: "surface",
-            },
-        ],
-        table: [
-            {
-                cells: {
-                    fill: {
-                        color: "#506784",
-                    },
-                    line: {
-                        color: "rgb(17,17,17)",
-                    },
-                },
-                header: {
-                    fill: {
-                        color: "#2a3f5f",
-                    },
-                    line: {
-                        color: "rgb(17,17,17)",
-                    },
-                },
-                type: "table",
-            },
-        ],
-    },
-    layout: {
-        annotationdefaults: {
-            arrowcolor: "#f2f5fa",
-        },
-        colorscale: {
-            diverging: [
-                [0, "#8e0152"],
-                [0.1, "#c51b7d"],
-                [0.2, "#de77ae"],
-                [0.3, "#f1b6da"],
-                [0.4, "#fde0ef"],
-                [0.5, "#f7f7f7"],
-                [0.6, "#e6f5d0"],
-                [0.7, "#b8e186"],
-                [0.8, "#7fbc41"],
-                [0.9, "#4d9221"],
-                [1, "#276419"],
-            ],
-            sequential: [
-                [0.0, "#0d0887"],
-                [0.1111111111111111, "#46039f"],
-                [0.2222222222222222, "#7201a8"],
-                [0.3333333333333333, "#9c179e"],
-                [0.4444444444444444, "#bd3786"],
-                [0.5555555555555556, "#d8576b"],
-                [0.6666666666666666, "#ed7953"],
-                [0.7777777777777778, "#fb9f3a"],
-                [0.8888888888888888, "#fdca26"],
-                [1.0, "#f0f921"],
-            ],
-            sequentialminus: [
-                [0.0, "#0d0887"],
-                [0.1111111111111111, "#46039f"],
-                [0.2222222222222222, "#7201a8"],
-                [0.3333333333333333, "#9c179e"],
-                [0.4444444444444444, "#bd3786"],
-                [0.5555555555555556, "#d8576b"],
-                [0.6666666666666666, "#ed7953"],
-                [0.7777777777777778, "#fb9f3a"],
-                [0.8888888888888888, "#fdca26"],
-                [1.0, "#f0f921"],
-            ],
-        },
-        colorway: [
-            "#636efa",
-            "#EF553B",
-            "#00cc96",
-            "#ab63fa",
-            "#FFA15A",
-            "#19d3f3",
-            "#FF6692",
-            "#B6E880",
-            "#FF97FF",
-            "#FECB52",
-        ],
-        font: {
-            color: "#f2f5fa",
-        },
-        geo: {
-            bgcolor: "rgb(17,17,17)",
-            lakecolor: "rgb(17,17,17)",
-            landcolor: "rgb(17,17,17)",
-            subunitcolor: "#506784",
-        },
-        mapbox: {
-            style: "dark",
-        },
-        paper_bgcolor: "rgb(17,17,17)",
-        plot_bgcolor: "rgb(17,17,17)",
-        polar: {
-            angularaxis: {
-                gridcolor: "#506784",
-                linecolor: "#506784",
-            },
-            bgcolor: "rgb(17,17,17)",
-            radialaxis: {
-                gridcolor: "#506784",
-                linecolor: "#506784",
-            },
-        },
-        scene: {
-            xaxis: {
-                backgroundcolor: "rgb(17,17,17)",
-                gridcolor: "#506784",
-                linecolor: "#506784",
-                zerolinecolor: "#C8D4E3",
-            },
-            yaxis: {
-                backgroundcolor: "rgb(17,17,17)",
-                gridcolor: "#506784",
-                linecolor: "#506784",
-                zerolinecolor: "#C8D4E3",
-            },
-            zaxis: {
-                backgroundcolor: "rgb(17,17,17)",
-                gridcolor: "#506784",
-                linecolor: "#506784",
-                showbackground: true,
-                zerolinecolor: "#C8D4E3",
-            },
-        },
-        shapedefaults: {
-            line: {
-                color: "#f2f5fa",
-            },
-        },
-        sliderdefaults: {
-            bgcolor: "#C8D4E3",
-            bordercolor: "rgb(17,17,17)",
-        },
-        ternary: {
-            aaxis: {
-                gridcolor: "#506784",
-                linecolor: "#506784",
-            },
-            baxis: {
-                gridcolor: "#506784",
-                linecolor: "#506784",
-            },
-            bgcolor: "rgb(17,17,17)",
-            caxis: {
-                gridcolor: "#506784",
-                linecolor: "#506784",
-            },
-        },
-        updatemenudefaults: {
-            bgcolor: "#506784",
-        },
-        xaxis: {
-            gridcolor: "#283442",
-            linecolor: "#506784",
-            tickcolor: "#506784",
-            zerolinecolor: "#283442",
-        },
-        yaxis: {
-            gridcolor: "#283442",
-            linecolor: "#506784",
-            tickcolor: "#506784",
-            zerolinecolor: "#283442",
-        },
-    },
-};

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

@@ -16,9 +16,11 @@ import {Data} 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";
 
 const Plot = lazy(() => import("react-plotly.js"));
 
@@ -42,6 +44,9 @@ interface MetricProps extends TaipyBaseProps, TaipyHoverProps {
     showValue?: boolean;
     format?: string;
     deltaFormat?: string;
+    template?: string;
+    template_Dark_?: string;
+    template_Light_?: string;
 }
 
 const emptyLayout = {} as Record<string, Record<string, unknown>>;
@@ -59,6 +64,7 @@ const Metric = (props: MetricProps) => {
     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);
+    const theme = useTheme();
 
     const data = useMemo(() => {
         return [
@@ -116,13 +122,41 @@ const Metric = (props: MetricProps) => {
 
     const skelStyle = useMemo(() => ({...style, minHeight: "7em"}), [style]);
 
+    const layout = useMemo(() => {
+        const layout = {...baseLayout};
+        let template = undefined;
+        try {
+            const tpl = props.template && JSON.parse(props.template);
+            const tplTheme =
+                theme.palette.mode === "dark"
+                    ? props.template_Dark_
+                        ? JSON.parse(props.template_Dark_)
+                        : darkTemplate
+                    : props.template_Light_ && JSON.parse(props.template_Light_);
+            template = tpl ? (tplTheme ? {...tpl, ...tplTheme} : tpl) : tplTheme ? tplTheme : undefined;
+        } catch (e) {
+            console.info(`Error while parsing Metric.template\n${(e as Error).message || e}`);
+        }
+        if (template) {
+            layout.template = template;
+        }
+
+        return layout
+    }, [
+        props.template,
+        props.template_Dark_,
+        props.template_Light_,
+        theme.palette.mode,
+        baseLayout
+    ])
+
     return (
         <Box data-testid={props.testId} className={className}>
             <Tooltip title={hover || ""}>
                 <Suspense fallback={<Skeleton key="skeleton" sx={skelStyle}/>}>
                     <Plot
                         data={data as Data[]}
-                        layout={baseLayout}
+                        layout={layout}
                         style={style}
                         useResizeHandler
                     />
@@ -133,3 +167,13 @@ const Metric = (props: MetricProps) => {
 }
 
 export default Metric;
+
+const { colorscale, colorway, font} = darkThemeTemplate.layout;
+const darkTemplate = {
+    layout: {
+        colorscale,
+        colorway,
+        font,
+        paper_bgcolor: "rgb(31,47,68)",
+    },
+}

+ 349 - 0
frontend/taipy-gui/src/themes/darkThemeTemplate.ts

@@ -0,0 +1,349 @@
+export const darkThemeTemplate = {
+    data: {
+        barpolar: [
+            {
+                marker: {
+                    line: {
+                        color: "rgb(17,17,17)",
+                    },
+                    pattern: {
+                        solidity: 0.2,
+                    },
+                },
+                type: "barpolar",
+            },
+        ],
+        bar: [
+            {
+                error_x: {
+                    color: "#f2f5fa",
+                },
+                error_y: {
+                    color: "#f2f5fa",
+                },
+                marker: {
+                    line: {
+                        color: "rgb(17,17,17)",
+                    },
+                    pattern: {
+                        solidity: 0.2,
+                    },
+                },
+                type: "bar",
+            },
+        ],
+        carpet: [
+            {
+                aaxis: {
+                    endlinecolor: "#A2B1C6",
+                    gridcolor: "#506784",
+                    linecolor: "#506784",
+                    minorgridcolor: "#506784",
+                    startlinecolor: "#A2B1C6",
+                },
+                baxis: {
+                    endlinecolor: "#A2B1C6",
+                    gridcolor: "#506784",
+                    linecolor: "#506784",
+                    minorgridcolor: "#506784",
+                    startlinecolor: "#A2B1C6",
+                },
+                type: "carpet",
+            },
+        ],
+        contour: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "contour",
+            },
+        ],
+        heatmapgl: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "heatmapgl",
+            },
+        ],
+        heatmap: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "heatmap",
+            },
+        ],
+        histogram2dcontour: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "histogram2dcontour",
+            },
+        ],
+        histogram2d: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "histogram2d",
+            },
+        ],
+        histogram: [
+            {
+                marker: {
+                    pattern: {
+                        solidity: 0.2,
+                    },
+                },
+                type: "histogram",
+            },
+        ],
+        scatter: [
+            {
+                marker: {
+                    line: {
+                        color: "#283442",
+                    },
+                },
+                type: "scatter",
+            },
+        ],
+        scattergl: [
+            {
+                marker: {
+                    line: {
+                        color: "#283442",
+                    },
+                },
+                type: "scattergl",
+            },
+        ],
+        surface: [
+            {
+                colorscale: [
+                    [0.0, "#0d0887"],
+                    [0.1111111111111111, "#46039f"],
+                    [0.2222222222222222, "#7201a8"],
+                    [0.3333333333333333, "#9c179e"],
+                    [0.4444444444444444, "#bd3786"],
+                    [0.5555555555555556, "#d8576b"],
+                    [0.6666666666666666, "#ed7953"],
+                    [0.7777777777777778, "#fb9f3a"],
+                    [0.8888888888888888, "#fdca26"],
+                    [1.0, "#f0f921"],
+                ],
+                type: "surface",
+            },
+        ],
+        table: [
+            {
+                cells: {
+                    fill: {
+                        color: "#506784",
+                    },
+                    line: {
+                        color: "rgb(17,17,17)",
+                    },
+                },
+                header: {
+                    fill: {
+                        color: "#2a3f5f",
+                    },
+                    line: {
+                        color: "rgb(17,17,17)",
+                    },
+                },
+                type: "table",
+            },
+        ],
+    },
+    layout: {
+        annotationdefaults: {
+            arrowcolor: "#f2f5fa",
+        },
+        colorscale: {
+            diverging: [
+                [0, "#8e0152"],
+                [0.1, "#c51b7d"],
+                [0.2, "#de77ae"],
+                [0.3, "#f1b6da"],
+                [0.4, "#fde0ef"],
+                [0.5, "#f7f7f7"],
+                [0.6, "#e6f5d0"],
+                [0.7, "#b8e186"],
+                [0.8, "#7fbc41"],
+                [0.9, "#4d9221"],
+                [1, "#276419"],
+            ],
+            sequential: [
+                [0.0, "#0d0887"],
+                [0.1111111111111111, "#46039f"],
+                [0.2222222222222222, "#7201a8"],
+                [0.3333333333333333, "#9c179e"],
+                [0.4444444444444444, "#bd3786"],
+                [0.5555555555555556, "#d8576b"],
+                [0.6666666666666666, "#ed7953"],
+                [0.7777777777777778, "#fb9f3a"],
+                [0.8888888888888888, "#fdca26"],
+                [1.0, "#f0f921"],
+            ],
+            sequentialminus: [
+                [0.0, "#0d0887"],
+                [0.1111111111111111, "#46039f"],
+                [0.2222222222222222, "#7201a8"],
+                [0.3333333333333333, "#9c179e"],
+                [0.4444444444444444, "#bd3786"],
+                [0.5555555555555556, "#d8576b"],
+                [0.6666666666666666, "#ed7953"],
+                [0.7777777777777778, "#fb9f3a"],
+                [0.8888888888888888, "#fdca26"],
+                [1.0, "#f0f921"],
+            ],
+        },
+        colorway: [
+            "#636efa",
+            "#EF553B",
+            "#00cc96",
+            "#ab63fa",
+            "#FFA15A",
+            "#19d3f3",
+            "#FF6692",
+            "#B6E880",
+            "#FF97FF",
+            "#FECB52",
+        ],
+        font: {
+            color: "#f2f5fa",
+        },
+        geo: {
+            bgcolor: "rgb(17,17,17)",
+            lakecolor: "rgb(17,17,17)",
+            landcolor: "rgb(17,17,17)",
+            subunitcolor: "#506784",
+        },
+        mapbox: {
+            style: "dark",
+        },
+        paper_bgcolor: "rgb(17,17,17)",
+        plot_bgcolor: "rgb(17,17,17)",
+        polar: {
+            angularaxis: {
+                gridcolor: "#506784",
+                linecolor: "#506784",
+            },
+            bgcolor: "rgb(17,17,17)",
+            radialaxis: {
+                gridcolor: "#506784",
+                linecolor: "#506784",
+            },
+        },
+        scene: {
+            xaxis: {
+                backgroundcolor: "rgb(17,17,17)",
+                gridcolor: "#506784",
+                linecolor: "#506784",
+                zerolinecolor: "#C8D4E3",
+            },
+            yaxis: {
+                backgroundcolor: "rgb(17,17,17)",
+                gridcolor: "#506784",
+                linecolor: "#506784",
+                zerolinecolor: "#C8D4E3",
+            },
+            zaxis: {
+                backgroundcolor: "rgb(17,17,17)",
+                gridcolor: "#506784",
+                linecolor: "#506784",
+                showbackground: true,
+                zerolinecolor: "#C8D4E3",
+            },
+        },
+        shapedefaults: {
+            line: {
+                color: "#f2f5fa",
+            },
+        },
+        sliderdefaults: {
+            bgcolor: "#C8D4E3",
+            bordercolor: "rgb(17,17,17)",
+        },
+        ternary: {
+            aaxis: {
+                gridcolor: "#506784",
+                linecolor: "#506784",
+            },
+            baxis: {
+                gridcolor: "#506784",
+                linecolor: "#506784",
+            },
+            bgcolor: "rgb(17,17,17)",
+            caxis: {
+                gridcolor: "#506784",
+                linecolor: "#506784",
+            },
+        },
+        updatemenudefaults: {
+            bgcolor: "#506784",
+        },
+        xaxis: {
+            gridcolor: "#283442",
+            linecolor: "#506784",
+            tickcolor: "#506784",
+            zerolinecolor: "#283442",
+        },
+        yaxis: {
+            gridcolor: "#283442",
+            linecolor: "#506784",
+            tickcolor: "#506784",
+            zerolinecolor: "#283442",
+        },
+    },
+};

Dosya farkı çok büyük olduğundan ihmal edildi
+ 473 - 209
frontend/taipy/package-lock.json


+ 29 - 27
frontend/taipy/src/CoreSelector.tsx

@@ -106,7 +106,7 @@ interface CoreSelectorProps {
     leafType: NodeType;
     editComponent?: ComponentType<EditProps>;
     showPins?: boolean;
-    onSelect?: (id: string | string[]) => void;
+    onSelect?: (id: string | string[] | null) => void;
     updateCoreVars: string;
     filter?: string;
     sort?: string;
@@ -126,6 +126,7 @@ const tinyPinIconButtonSx = (theme: Theme) => ({
 
 const switchBoxSx = { ml: 2, width: (theme: Theme) => `calc(100% - ${theme.spacing(2)})` };
 const iconInRowSx = { fontSize: "body2.fontSize" };
+const labelInRowSx = {"& .MuiFormControlLabel-label": iconInRowSx};
 
 const CoreItem = (props: {
     item: Entity;
@@ -294,7 +295,7 @@ const filterTree = (entities: Entities, search: string, leafType: NodeType, coun
 };
 
 const localStoreSet = (val: string, ...ids: string[]) => {
-    const id = ids.filter(i => !!i).join(" ");
+    const id = ids.filter((i) => !!i).join(" ");
     if (!id) {
         return;
     }
@@ -306,7 +307,7 @@ const localStoreSet = (val: string, ...ids: string[]) => {
 };
 
 const localStoreGet = (...ids: string[]) => {
-    const id = ids.filter(i => !!i).join(" ");
+    const id = ids.filter((i) => !!i).join(" ");
     if (!id) {
         return undefined;
     }
@@ -364,23 +365,21 @@ const CoreSelector = (props: CoreSelectorProps) => {
     }, []);
 
     const onNodeSelect = useCallback(
-        (e: SyntheticEvent, nodeId: string, isSelected: boolean) => {
+        (e: SyntheticEvent, nodeId: string | string[] | null) => {
             const { selectable = "false" } = e.currentTarget.parentElement?.dataset || {};
             const isSelectable = selectable === "true";
             if (!isSelectable && multiple) {
                 return;
             }
-            setSelectedItems((old) => {
-                const res = isSelected ? [...old, nodeId] : old.filter((id) => id !== nodeId);
-                const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
-                const val = multiple ? res : isSelectable ? nodeId : "";
-                setTimeout(
-                    () =>
-                        dispatch(createSendUpdateAction(updateVarName, val, module, onChange, propagate, scenariosVar)),
+            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);
-                return res;
+                return Array.isArray(nodeId) ? nodeId : nodeId ? [nodeId] : [];
             });
         },
         [updateVarName, updateVars, onChange, onSelect, multiple, propagate, dispatch, module, lovPropertyName]
@@ -526,15 +525,16 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 if (old.length != filters.length || JSON.stringify(old) != jsonFilters) {
                     localStoreSet(jsonFilters, id, lovPropertyName, "filter");
                     const filterVar = getUpdateVar(updateCoreVars, "filter");
-                    dispatch(
+                    const lovVar = getUpdateVarNames(updateVars, lovPropertyName);
+                    setTimeout(() => dispatch(
                         createRequestUpdateAction(
                             id,
                             module,
-                            getUpdateVarNames(updateVars, lovPropertyName),
+                            lovVar,
                             true,
                             filterVar ? { [filterVar]: filters } : undefined
                         )
-                    );
+                    ), 1);
                     return filters;
                 }
                 return old;
@@ -625,6 +625,17 @@ const CoreSelector = (props: CoreSelectorProps) => {
                         <TableSort columns={colSorts} appliedSorts={sorts} onValidate={applySorts}></TableSort>
                     </Grid>
                 ) : null}
+                {showSearch ? (
+                    <Grid item>
+                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
+                            {revealSearch ? (
+                                <SearchOffOutlined fontSize="inherit" />
+                            ) : (
+                                <SearchOutlined fontSize="inherit" />
+                            )}
+                        </IconButton>
+                    </Grid>
+                ) : null}
                 {showPins ? (
                     <Grid item>
                         <FormControlLabel
@@ -633,23 +644,14 @@ const CoreSelector = (props: CoreSelectorProps) => {
                                     onChange={onShowPinsChange}
                                     checked={hideNonPinned}
                                     disabled={!hideNonPinned && !Object.keys(pins[0]).length}
+                                    size="small"
                                 />
                             }
                             label="Pinned only"
+                            sx={labelInRowSx}
                         />
                     </Grid>
                 ) : null}
-                {showSearch ? (
-                    <Grid item>
-                        <IconButton onClick={onRevealSearch} size="small" sx={iconInRowSx}>
-                            {revealSearch ? (
-                                <SearchOffOutlined fontSize="inherit" />
-                            ) : (
-                                <SearchOutlined fontSize="inherit" />
-                            )}
-                        </IconButton>
-                    </Grid>
-                ) : null}
                 {showSearch && revealSearch ? (
                     <Grid item xs={12}>
                         <TextField
@@ -665,7 +667,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
             <SimpleTreeView
                 slots={treeSlots}
                 sx={treeViewSx}
-                onItemSelectionToggle={onNodeSelect}
+                onSelectedItemsChange={onNodeSelect}
                 selectedItems={selectedItems}
                 multiSelect={multiple}
                 expandedItems={expandedItems}

+ 10 - 6
frontend/taipy/src/NodeSelector.tsx

@@ -21,7 +21,7 @@ import CoreSelector from "./CoreSelector";
 interface NodeSelectorProps {
     id?: string;
     updateVarName?: string;
-    datanodes?: Cycles | Scenarios | DataNodes;
+    innerDatanodes?: Cycles | Scenarios | DataNodes;
     coreChanged?: Record<string, unknown>;
     updateVars: string;
     onChange?: string;
@@ -37,22 +37,26 @@ interface NodeSelectorProps {
     dynamicClassName?: string;
     showPins?: boolean;
     multiple?: boolean;
+    updateDnVars?: string;
+    filter?: string;
+    sort?: string;
+    showSearch?: boolean;
 }
 
 const NodeSelector = (props: NodeSelectorProps) => {
-    const { showPins = true, multiple = false } = props;
+    const { showPins = true, multiple = false, updateDnVars = "", showSearch = true } = props;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     return (
         <Box sx={MainTreeBoxSx} id={props.id} className={className}>
             <CoreSelector
                 {...props}
-                entities={props.datanodes}
+                entities={props.innerDatanodes}
                 leafType={NodeType.NODE}
-                lovPropertyName="datanodes"
+                lovPropertyName="innerDatanodes"
                 showPins={showPins}
                 multiple={multiple}
-                showSearch={false}
-                updateCoreVars=""
+                showSearch={showSearch}
+                updateCoreVars={updateDnVars}
             />
             <Box>{props.error}</Box>
         </Box>

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

@@ -96,6 +96,7 @@ interface ScenarioSelectorProps {
     showDialog?: boolean;
     multiple?: boolean;
     filter?: string;
+    sort?: string;
     updateScVars?: string;
     showSearch?: boolean;
 }

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

@@ -372,6 +372,9 @@ class _Factory:
                 ("show_value", PropertyType.boolean, True),
                 ("format", PropertyType.string),
                 ("delta_format", PropertyType.string),
+                ("template", PropertyType.dict),
+                ("template[dark]", PropertyType.dict),
+                ("template[light]", PropertyType.dict),
             ]
         ),
         "navbar": lambda gui, control_type, attrs: _Builder(

+ 18 - 3
taipy/gui/viselements.json

@@ -905,6 +905,21 @@
                         "type": "str|number",
                         "default_value": "None",
                         "doc": "The height, in CSS units, of the metric."
+                    },
+                    {
+                        "name": "template",
+                        "type": "dict",
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a>."
+                    },
+                    {
+                        "name": "template[dark]",
+                        "type": "dict",
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a> applied over the base template when theme is dark."
+                    },
+                    {
+                        "name": "template[light]",
+                        "type": "dict",
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a> applied over the base template when theme is not dark."
                     }
                 ]
             }
@@ -2375,17 +2390,17 @@
                     {
                         "name": "template",
                         "type": "dict",
-                        "doc": "The Plotly layout <a href=\"https://plotly.com/javascript/layout-template/\">template</a>."
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a>."
                     },
                     {
                         "name": "template[dark]",
                         "type": "dict",
-                        "doc": "The Plotly layout <a href=\"https://plotly.com/javascript/layout-template/\">template</a> applied over the base template when theme is dark."
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a> applied over the base template when theme is dark."
                     },
                     {
                         "name": "template[light]",
                         "type": "dict",
-                        "doc": "The Plotly layout <a href=\"https://plotly.com/javascript/layout-template/\">template</a> applied over the base template when theme is not dark."
+                        "doc": "The Plotly <a href=\"https://plotly.com/javascript/layout-template/\">layout template</a> applied over the base template when theme is not dark."
                     },
                     {
                         "name": "decimator",

+ 24 - 4
taipy/gui_core/_GuiCoreLib.py

@@ -19,6 +19,8 @@ from taipy.gui.extension import Element, ElementLibrary, ElementProperty, Proper
 from ..version import _get_version
 from ._adapters import (
     _GuiCoreDatanodeAdapter,
+    _GuiCoreDatanodeFilter,
+    _GuiCoreDatanodeSort,
     _GuiCoreDoNotUpdate,
     _GuiCoreScenarioAdapter,
     _GuiCoreScenarioDagAdapter,
@@ -57,6 +59,10 @@ class _GuiCore(ElementLibrary):
     __DATANODE_VIZ_DATA_NODE_PROP = "data_node"
     __DATANODE_SEL_SCENARIO_PROP = "scenario"
     __SEL_SCENARIOS_PROP = "scenarios"
+    __SEL_DATANODES_PROP = "datanodes"
+    __DATANODE_SELECTOR_FILTER_VAR = "__tpgc_dn_filter"
+    __DATANODE_SELECTOR_SORT_VAR = "__tpgc_dn_sort"
+    __DATANODE_SELECTOR_ERROR_VAR = "__tpgc_dn_error"
 
     __elts = {
         "scenario_selector": Element(
@@ -75,8 +81,8 @@ class _GuiCore(ElementLibrary):
                 "show_dialog": ElementProperty(PropertyType.boolean, True),
                 __SEL_SCENARIOS_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
-                "filter": ElementProperty(_GuiCoreScenarioFilter, _GuiCoreScenarioFilter.DEFAULT),
-                "sort": ElementProperty(_GuiCoreScenarioSort, _GuiCoreScenarioSort.DEFAULT),
+                "filter": ElementProperty(_GuiCoreScenarioFilter, "*"),
+                "sort": ElementProperty(_GuiCoreScenarioSort, "*"),
                 "show_search": ElementProperty(PropertyType.boolean, True),
             },
             inner_properties={
@@ -165,15 +171,29 @@ class _GuiCore(ElementLibrary):
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "show_pins": ElementProperty(PropertyType.boolean, True),
                 __DATANODE_SEL_SCENARIO_PROP: ElementProperty(PropertyType.dynamic_list),
+                __SEL_DATANODES_PROP: ElementProperty(PropertyType.dynamic_list),
                 "multiple": ElementProperty(PropertyType.boolean, False),
+                "filter": ElementProperty(_GuiCoreDatanodeFilter, "*"),
+                "sort": ElementProperty(_GuiCoreDatanodeSort, "*"),
+                "show_search": ElementProperty(PropertyType.boolean, True),
             },
             inner_properties={
-                "datanodes": ElementProperty(
+                "inner_datanodes": ElementProperty(
                     PropertyType.lov,
-                    f"{{{__CTX_VAR_NAME}.get_datanodes_tree(<tp:prop:{__DATANODE_SEL_SCENARIO_PROP}>)}}",
+                    f"{{{__CTX_VAR_NAME}.get_datanodes_tree(<tp:prop:{__DATANODE_SEL_SCENARIO_PROP}>, "
+                    + f"<tp:prop:{__SEL_DATANODES_PROP}>, "
+                    + f"{__DATANODE_SELECTOR_FILTER_VAR}<tp:uniq:dns>, "
+                    + f"{__DATANODE_SELECTOR_SORT_VAR}<tp:uniq:dns>)}}",
                 ),
                 "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
                 "type": ElementProperty(PropertyType.inner, __DATANODE_ADAPTER),
+                "error": ElementProperty(PropertyType.react, f"{{{__DATANODE_SELECTOR_ERROR_VAR}<tp:uniq:dns>}}"),
+                "update_dn_vars": ElementProperty(
+                    PropertyType.string,
+                    f"filter={__DATANODE_SELECTOR_FILTER_VAR}<tp:uniq:dns>;"
+                    + f"sort={__DATANODE_SELECTOR_SORT_VAR}<tp:uniq:dns>;"
+                    + f"error_id={__DATANODE_SELECTOR_ERROR_VAR}<tp:uniq:dns>",
+                ),
             },
         ),
         "data_node": Element(

+ 228 - 56
taipy/gui_core/_adapters.py

@@ -9,11 +9,13 @@
 # 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 inspect
 import json
 import math
 import sys
 import typing as t
-from abc import abstractmethod
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
 from datetime import date, datetime
 from enum import Enum
 from numbers import Number
@@ -21,7 +23,17 @@ from operator import attrgetter, contains, eq, ge, gt, le, lt, ne
 
 import pandas as pd
 
-from taipy.core import Cycle, DataNode, Scenario, is_deletable, is_editable, is_promotable, is_readable, is_submittable
+from taipy.core import (
+    Cycle,
+    DataNode,
+    Scenario,
+    Sequence,
+    is_deletable,
+    is_editable,
+    is_promotable,
+    is_readable,
+    is_submittable,
+)
 from taipy.core import get as core_get
 from taipy.core.config import Config
 from taipy.core.data._tabular_datanode_mixin import _TabularDataNodeMixin
@@ -235,67 +247,72 @@ _operators: t.Dict[str, t.Callable] = {
 }
 
 
-def _invoke_action(ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any) -> bool:
+def _invoke_action(
+    ent: t.Any, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn: t.Optional[str]
+) -> bool:
+    if ent is None:
+        return False
     try:
         if col_type == "any":
             # when a property is not found, return True only if action is not equals
-            entity = getattr(ent, col.split(".")[0]) if is_dn else ent
-            if not hasattr(entity, "properties") or not entity.properties.get(col):
+            if not is_dn and not hasattr(ent, "properties") or not ent.properties.get(col_fn or col):
                 return action == "!="
         if op := _operators.get(action):
-            cur_val = attrgetter(col)(ent)
+            cur_val = attrgetter(col_fn or col)(ent)
+            cur_val = cur_val() if col_fn else cur_val
             return op(cur_val.isoformat() if isinstance(cur_val, (datetime, date)) else cur_val, val)
     except Exception as e:
-        _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
+        if _is_debugging():
+            _warn(f"Error filtering with {col} {action} {val} on {ent}.", e)
+        return col_type == "any" and action == "!="
     return True
 
 
+def _get_entity_property(col: str, a_type: t.Type):
+    col_parts = col.split("(")  # handle the case where the col is a method (ie get_simple_label())
+    col_fn = (
+        next(
+            (col_parts[0] for i in inspect.getmembers(a_type, predicate=inspect.isfunction) if i[0] == col_parts[0]),
+            None,
+        )
+        if len(col_parts) > 1
+        else None
+    )
+
+    def sort_key(entity: t.Union[Scenario, Cycle, Sequence, DataNode]):
+        # we compare only strings
+        if isinstance(entity, a_type):
+            try:
+                val = attrgetter(col_fn or col)(entity)
+                if col_fn:
+                    val = val()
+            except AttributeError as e:
+                if _is_debugging():
+                    _warn("Attribute", e)
+                val = ""
+        else:
+            val = ""
+        return val.isoformat() if isinstance(val, (datetime, date)) else str(val)
+
+    return sort_key
+
+
 def _get_datanode_property(attr: str):
     if (parts := attr.split(".")) and len(parts) > 1:
         return parts[1]
     return None
 
 
-class _GuiCoreScenarioProperties(_TaipyBase):
-    _SC_TYPES = {
-        "Config id": "string",
-        "Label": "string",
-        "Creation date": "date",
-        "Cycle label": "string",
-        "Cycle start": "date",
-        "Cycle end": "date",
-        "Primary": "boolean",
-        "Tags": "string",
-    }
-    __SC_LABELS = {
-        "Config id": "config_id",
-        "Creation date": "creation_date",
-        "Label": "name",
-        "Cycle label": "cycle.name",
-        "Cycle start": "cycle.start",
-        "Cycle end": "cycle.end",
-        "Primary": "is_primary",
-        "Tags": "tags",
-    }
-    __DN_TYPES = {"Up to date": "boolean", "Valid": "boolean", "Last edit date": "date"}
-    __DN_LABELS = {"Up to date": "is_up_to_date", "Valid": "is_valid", "Last edit date": "last_edit_date"}
-    __ENUMS = None
-
-    @staticmethod
-    def get_hash():
-        return _TaipyBase._HOLDER_PREFIX + "ScP"
-
+class _GuiCoreProperties(ABC):
     @staticmethod
+    @abstractmethod
     def get_type(attr: str):
-        if prop := _get_datanode_property(attr):
-            return _GuiCoreScenarioProperties.__DN_TYPES.get(prop, "any")
-        return _GuiCoreScenarioProperties._SC_TYPES.get(attr, "any")
+        raise NotImplementedError
 
     @staticmethod
+    @abstractmethod
     def get_col_name(attr: str):
-        if prop := _get_datanode_property(attr):
-            return f'{attr.split(".")[0]}.{_GuiCoreScenarioProperties.__DN_LABELS.get(prop, prop)}'
-        return _GuiCoreScenarioProperties.__SC_LABELS.get(attr, attr)
+        raise NotImplementedError
 
     @staticmethod
     @abstractmethod
@@ -307,6 +324,9 @@ class _GuiCoreScenarioProperties(_TaipyBase):
     def full_desc():
         raise NotImplementedError
 
+    def get_enums(self):
+        return {}
+
     def get(self):
         data = super().get()
         if _is_boolean(data):
@@ -323,16 +343,9 @@ class _GuiCoreScenarioProperties(_TaipyBase):
                     flist.extend(self.get_default_list())
                 else:
                     flist.append(f)
-            if _GuiCoreScenarioProperties.__ENUMS is None and self.full_desc():
-                _GuiCoreScenarioProperties.__ENUMS = {
-                    "Config id": [c for c in Config.scenarios.keys() if c != "default"],
-                    "Tags": [t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])],
-                }
             return json.dumps(
                 [
-                    (attr, _GuiCoreScenarioProperties.get_type(attr), _GuiCoreScenarioProperties.__ENUMS.get(attr))
-                    if self.full_desc()
-                    else (attr,)
+                    (attr, self.get_type(attr), self.get_enums().get(attr)) if self.full_desc() else (attr,)
                     for attr in flist
                     if attr and isinstance(attr, str)
                 ]
@@ -340,8 +353,73 @@ class _GuiCoreScenarioProperties(_TaipyBase):
         return None
 
 
-class _GuiCoreScenarioFilter(_GuiCoreScenarioProperties):
-    DEFAULT = list(_GuiCoreScenarioProperties._SC_TYPES.keys())
+@dataclass(frozen=True)
+class _GuiCorePropDesc:
+    attr: str
+    type: str
+    extended: bool = False
+    for_sort: bool = False
+
+
+_EMPTY_PROP_DESC = _GuiCorePropDesc("", "any")
+
+
+class _GuiCoreScenarioProperties(_GuiCoreProperties):
+    _SC_PROPS: t.Dict[str, _GuiCorePropDesc] = {
+        "Config id": _GuiCorePropDesc("config_id", "string", for_sort=True),
+        "Label": _GuiCorePropDesc("get_simple_label()", "string", for_sort=True),
+        "Creation date": _GuiCorePropDesc("creation_date", "date", for_sort=True),
+        "Cycle label": _GuiCorePropDesc("cycle.name", "string", extended=True),
+        "Cycle start": _GuiCorePropDesc("cycle.start_date", "date", extended=True),
+        "Cycle end": _GuiCorePropDesc("cycle.end_date", "date", extended=True),
+        "Primary": _GuiCorePropDesc("is_primary", "boolean", extended=True),
+        "Tags": _GuiCorePropDesc("tags", "string"),
+    }
+    __DN_PROPS = {
+        "Up to date": _GuiCorePropDesc("is_up_to_date", "boolean"),
+        "Valid": _GuiCorePropDesc("is_valid", "boolean"),
+        "Last edit date": _GuiCorePropDesc("last_edit_date", "date"),
+    }
+    __ENUMS = None
+    __SC_CYCLE = None
+
+    @staticmethod
+    def get_type(attr: str):
+        if prop := _get_datanode_property(attr):
+            return _GuiCoreScenarioProperties.__DN_PROPS.get(prop, _EMPTY_PROP_DESC).type
+        return _GuiCoreScenarioProperties._SC_PROPS.get(attr, _EMPTY_PROP_DESC).type
+
+    @staticmethod
+    def get_col_name(attr: str):
+        if prop := _get_datanode_property(attr):
+            return (
+                attr.split(".")[0]
+                + f".{_GuiCoreScenarioProperties.__DN_PROPS.get(prop, _EMPTY_PROP_DESC).attr or prop}"
+            )
+        return _GuiCoreScenarioProperties._SC_PROPS.get(attr, _EMPTY_PROP_DESC).attr or attr
+
+    def get_enums(self):
+        if _GuiCoreScenarioProperties.__ENUMS is None:
+            _GuiCoreScenarioProperties.__ENUMS = {
+                "Config id": [c for c in Config.scenarios.keys() if c != "default"],
+                "Tags": list({t for s in Config.scenarios.values() for t in s.properties.get("authorized_tags", [])}),
+            }
+        return _GuiCoreScenarioProperties.__ENUMS if self.full_desc() else {}
+
+    @staticmethod
+    def has_cycle():
+        if _GuiCoreScenarioProperties.__SC_CYCLE is None:
+            _GuiCoreScenarioProperties.__SC_CYCLE = (
+                next(filter(lambda sc: sc.frequency is not None, Config.scenarios.values()), None) is not None
+            )
+        return _GuiCoreScenarioProperties.__SC_CYCLE
+
+
+class _GuiCoreScenarioFilter(_GuiCoreScenarioProperties, _TaipyBase):
+    DEFAULT = list(_GuiCoreScenarioProperties._SC_PROPS.keys())
+    DEFAULT_NO_CYCLE = [
+        p[0] for p in filter(lambda prop: not prop[1].extended, _GuiCoreScenarioProperties._SC_PROPS.items())
+    ]
 
     @staticmethod
     def full_desc():
@@ -353,11 +431,21 @@ class _GuiCoreScenarioFilter(_GuiCoreScenarioProperties):
 
     @staticmethod
     def get_default_list():
-        return _GuiCoreScenarioFilter.DEFAULT
+        return (
+            _GuiCoreScenarioFilter.DEFAULT
+            if _GuiCoreScenarioProperties.has_cycle()
+            else _GuiCoreScenarioFilter.DEFAULT_NO_CYCLE
+        )
 
 
-class _GuiCoreScenarioSort(_GuiCoreScenarioProperties):
-    DEFAULT = ["Config id", "Label", "Creation date"]
+class _GuiCoreScenarioSort(_GuiCoreScenarioProperties, _TaipyBase):
+    DEFAULT = [p[0] for p in filter(lambda prop: prop[1].for_sort, _GuiCoreScenarioProperties._SC_PROPS.items())]
+    DEFAULT_NO_CYCLE = [
+        p[0]
+        for p in filter(
+            lambda prop: prop[1].for_sort and not prop[1].extended, _GuiCoreScenarioProperties._SC_PROPS.items()
+        )
+    ]
 
     @staticmethod
     def full_desc():
@@ -369,7 +457,91 @@ class _GuiCoreScenarioSort(_GuiCoreScenarioProperties):
 
     @staticmethod
     def get_default_list():
-        return _GuiCoreScenarioSort.DEFAULT
+        return (
+            _GuiCoreScenarioSort.DEFAULT
+            if _GuiCoreScenarioProperties.has_cycle()
+            else _GuiCoreScenarioSort.DEFAULT_NO_CYCLE
+        )
+
+
+class _GuiCoreDatanodeProperties(_GuiCoreProperties):
+    _DN_PROPS: t.Dict[str, _GuiCorePropDesc] = {
+        "Config id": _GuiCorePropDesc("config_id", "string", for_sort=True),
+        "Label": _GuiCorePropDesc("get_simple_label()", "string", for_sort=True),
+        "Up to date": _GuiCorePropDesc("is_up_to_date", "boolean"),
+        "Last edit date": _GuiCorePropDesc("last_edit_date", "date", for_sort=True),
+        "Input": _GuiCorePropDesc("is_input", "boolean"),
+        "Output": _GuiCorePropDesc("is_output", "boolean"),
+        "Intermediate": _GuiCorePropDesc("is_intermediate", "boolean"),
+        "Expiration date": _GuiCorePropDesc("expiration_date", "date", extended=True, for_sort=True),
+        "Expired": _GuiCorePropDesc("is_expired", "boolean", extended=True),
+    }
+    __DN_VALIDITY = None
+
+    @staticmethod
+    def get_type(attr: str):
+        return _GuiCoreDatanodeProperties._DN_PROPS.get(attr, _EMPTY_PROP_DESC).type
+
+    @staticmethod
+    def get_col_name(attr: str):
+        return _GuiCoreDatanodeProperties._DN_PROPS.get(attr, _EMPTY_PROP_DESC).attr or attr
+
+    @staticmethod
+    def has_validity():
+        if _GuiCoreDatanodeProperties.__DN_VALIDITY is None:
+            _GuiCoreDatanodeProperties.__DN_VALIDITY = (
+                next(filter(lambda dn: dn.validity_period is not None, Config.data_nodes.values()), None) is not None
+            )
+        return _GuiCoreDatanodeProperties.__DN_VALIDITY
+
+
+class _GuiCoreDatanodeFilter(_GuiCoreDatanodeProperties, _TaipyBase):
+    DEFAULT = list(_GuiCoreDatanodeProperties._DN_PROPS.keys())
+    DEFAULT_NO_VALIDITY = [
+        p[0] for p in filter(lambda prop: not prop[1].extended, _GuiCoreDatanodeProperties._DN_PROPS.items())
+    ]
+
+    @staticmethod
+    def full_desc():
+        return True
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "DnF"
+
+    @staticmethod
+    def get_default_list():
+        return (
+            _GuiCoreDatanodeFilter.DEFAULT
+            if _GuiCoreDatanodeProperties.has_validity()
+            else _GuiCoreDatanodeFilter.DEFAULT_NO_VALIDITY
+        )
+
+
+class _GuiCoreDatanodeSort(_GuiCoreDatanodeProperties, _TaipyBase):
+    DEFAULT = [p[0] for p in filter(lambda prop: prop[1].for_sort, _GuiCoreDatanodeProperties._DN_PROPS.items())]
+    DEFAULT_NO_VALIDITY = [
+        p[0]
+        for p in filter(
+            lambda prop: prop[1].for_sort and not prop[1].extended, _GuiCoreDatanodeProperties._DN_PROPS.items()
+        )
+    ]
+
+    @staticmethod
+    def full_desc():
+        return False
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "DnS"
+
+    @staticmethod
+    def get_default_list():
+        return (
+            _GuiCoreDatanodeSort.DEFAULT
+            if _GuiCoreDatanodeProperties.has_validity()
+            else _GuiCoreDatanodeSort.DEFAULT_NO_VALIDITY
+        )
 
 
 def _is_debugging() -> bool:

+ 182 - 95
taipy/gui_core/_context.py

@@ -12,9 +12,8 @@
 import json
 import typing as t
 from collections import defaultdict
-from datetime import date, datetime
+from datetime import datetime
 from numbers import Number
-from operator import attrgetter
 from threading import Lock
 
 try:
@@ -64,10 +63,11 @@ from taipy.gui.gui import _DoNotUpdate
 from ._adapters import (
     _EntityType,
     _get_datanode_property,
+    _get_entity_property,
     _GuiCoreDatanodeAdapter,
+    _GuiCoreDatanodeProperties,
     _GuiCoreScenarioProperties,
     _invoke_action,
-    _is_debugging,
 )
 
 
@@ -223,7 +223,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 return [
                     cycle.id,
                     cycle.get_simple_label(),
-                    self.get_sorted_entity_list(self.scenario_by_cycle.get(cycle, []), sorts),
+                    self.get_sorted_scenario_list(self.scenario_by_cycle.get(cycle, []), sorts),
                     _EntityType.CYCLE.value,
                     False,
                 ]
@@ -255,15 +255,19 @@ class _GuiCoreContext(CoreEventConsumerBase):
             )
         return None
 
-    def filter_scenarios(self, cycle: t.List, col: str, col_type: str, is_dn: bool, action: str, val: t.Any):
-        cycle[2] = [e for e in cycle[2] if _invoke_action(e, col, col_type, is_dn, action, val)]
-        return cycle
+    def filter_entities(
+        self, cycle_scenario: t.List, col: str, col_type: str, is_dn: bool, action: str, val: t.Any, col_fn=None
+    ):
+        cycle_scenario[2] = [
+            e for e in cycle_scenario[2] if _invoke_action(e, col, col_type, is_dn, action, val, col_fn)
+        ]
+        return cycle_scenario
 
     def adapt_scenarios(self, cycle: t.List):
         cycle[2] = [self.scenario_adapter(e) for e in cycle[2]]
         return cycle
 
-    def get_sorted_entity_list(
+    def get_sorted_scenario_list(
         self,
         entities: t.Union[t.List[t.Union[Cycle, Scenario]], t.List[Scenario]],
         sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
@@ -274,11 +278,45 @@ class _GuiCoreContext(CoreEventConsumerBase):
                 col = sd.get("col", "")
                 col = _GuiCoreScenarioProperties.get_col_name(col)
                 order = sd.get("order", True)
-                sorted_list = sorted(sorted_list, key=_GuiCoreContext.get_entity_property(col), reverse=not order)
+                sorted_list = sorted(sorted_list, key=_get_entity_property(col, Scenario), reverse=not order)
         else:
-            sorted_list = sorted(entities, key=_GuiCoreContext.get_entity_property("creation_date"))
+            sorted_list = sorted(entities, key=_get_entity_property("creation_date", Scenario))
         return [self.cycle_adapter(e, sorts) if isinstance(e, Cycle) else e for e in sorted_list]
 
+    def get_filtered_scenario_list(
+        self,
+        entities: t.List[t.Union[t.List, Scenario]],
+        filters: t.Optional[t.List[t.Dict[str, t.Any]]],
+    ):
+        if not filters:
+            return entities
+        # filtering
+        filtered_list = list(entities)
+        for fd in filters:
+            col = fd.get("col", "")
+            is_datanode_prop = _get_datanode_property(col) is not None
+            col_type = _GuiCoreScenarioProperties.get_type(col)
+            col = _GuiCoreScenarioProperties.get_col_name(col)
+            col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
+            val = fd.get("value")
+            action = fd.get("action", "")
+            # level 1 filtering
+            filtered_list = [
+                e
+                for e in filtered_list
+                if not isinstance(e, Scenario)
+                or _invoke_action(e, col, col_type, is_datanode_prop, action, val, col_fn)
+            ]
+            # level 2 filtering
+            filtered_list = [
+                e
+                if isinstance(e, Scenario)
+                else self.filter_entities(e, col, col_type, is_datanode_prop, action, val, col_fn)
+                for e in filtered_list
+            ]
+        # remove empty cycles
+        return [e for e in filtered_list if isinstance(e, Scenario) or (isinstance(e, (tuple, list)) and len(e[2]))]
+
     def get_scenarios(
         self,
         scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]],
@@ -287,6 +325,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
     ):
         cycles_scenarios: t.List[t.Union[Cycle, Scenario]] = []
         with self.lock:
+            # always needed to get scenarios for a cycle in cycle_adapter
             if self.scenario_by_cycle is None:
                 self.scenario_by_cycle = get_cycles_scenarios()
             if scenarios is None:
@@ -297,36 +336,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.append(cycle)
         if scenarios is not None:
             cycles_scenarios = scenarios
-        adapted_list = self.get_sorted_entity_list(cycles_scenarios, sorts)
-        if filters:
-            # filtering
-            filtered_list = list(adapted_list)
-            for fd in filters:
-                col = fd.get("col", "")
-                is_datanode_prop = _get_datanode_property(col) is not None
-                col = _GuiCoreScenarioProperties.get_col_name(col)
-                col_type = _GuiCoreScenarioProperties.get_type(col)
-                val = fd.get("value")
-                action = fd.get("action", "")
-                if isinstance(val, str) and col_type == "date":
-                    val = datetime.fromisoformat(val[:-1])
-                # level 1 filtering
-                filtered_list = [
-                    e
-                    for e in filtered_list
-                    if not isinstance(e, Scenario) or _invoke_action(e, col, col_type, is_datanode_prop, action, val)
-                ]
-                # level 2 filtering
-                filtered_list = [
-                    self.filter_scenarios(e, col, col_type, is_datanode_prop, action, val)
-                    if not isinstance(e, Scenario)
-                    else e
-                    for e in filtered_list
-                ]
-            # remove empty cycles
-            adapted_list = [
-                e for e in filtered_list if isinstance(e, Scenario) or (isinstance(e, (tuple, list)) and len(e[2]))
-            ]
+        adapted_list = self.get_sorted_scenario_list(cycles_scenarios, sorts)
+        adapted_list = self.get_filtered_scenario_list(adapted_list, filters)
         return adapted_list
 
     def select_scenario(self, state: State, id: str, payload: t.Dict[str, str]):
@@ -575,62 +586,152 @@ class _GuiCoreContext(CoreEventConsumerBase):
         except Exception as e:
             _GuiCoreContext.__assign_var(state, error_var, f"Error submitting entity. {e}")
 
+    def get_filtered_datanode_list(
+        self,
+        entities: t.List[t.Union[t.List, DataNode]],
+        filters: t.Optional[t.List[t.Dict[str, t.Any]]],
+    ):
+        if not filters or not entities:
+            return entities
+        # filtering
+        filtered_list = list(entities)
+        for fd in filters:
+            col = fd.get("col", "")
+            col_type = _GuiCoreDatanodeProperties.get_type(col)
+            col = _GuiCoreDatanodeProperties.get_col_name(col)
+            col_fn = cp[0] if (cp := col.split("(")) and len(cp) > 1 else None
+            val = fd.get("value")
+            action = fd.get("action", "")
+            if isinstance(val, str) and col_type == "date":
+                val = datetime.fromisoformat(val[:-1])
+            # level 1 filtering
+            filtered_list = [
+                e
+                for e in filtered_list
+                if not isinstance(e, DataNode) or _invoke_action(e, col, col_type, False, action, val, col_fn)
+            ]
+            # level 3 filtering
+            filtered_list = [
+                e if isinstance(e, DataNode) else self.filter_entities(d, col, col_type, False, action, val, col_fn)
+                for e in filtered_list
+                for d in e[2]
+            ]
+        # remove empty cycles
+        return [e for e in filtered_list if isinstance(e, DataNode) or (isinstance(e, (tuple, list)) and len(e[2]))]
+
+    def get_sorted_datanode_list(
+        self,
+        entities: t.Union[
+            t.List[t.Union[Cycle, Scenario, DataNode]], t.List[t.Union[Scenario, DataNode]], t.List[DataNode]
+        ],
+        sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
+        adapt_dn=False,
+    ):
+        if not entities:
+            return entities
+        if sorts:
+            sorted_list = entities
+            for sd in reversed(sorts):
+                col = sd.get("col", "")
+                col = _GuiCoreDatanodeProperties.get_col_name(col)
+                order = sd.get("order", True)
+                sorted_list = sorted(sorted_list, key=_get_entity_property(col, DataNode), reverse=not order)
+        else:
+            sorted_list = entities
+        return [self.data_node_adapter(e, sorts, adapt_dn) for e in sorted_list]
+
     def __do_datanodes_tree(self):
         if self.data_nodes_by_owner is None:
             self.data_nodes_by_owner = defaultdict(list)
             for dn in get_data_nodes():
                 self.data_nodes_by_owner[dn.owner_id].append(dn)
 
-    def get_datanodes_tree(self, scenarios: t.Optional[t.Union[Scenario, t.List[Scenario]]]):
+    def get_datanodes_tree(
+        self,
+        scenarios: t.Optional[t.Union[Scenario, t.List[Scenario]]],
+        datanodes: t.Optional[t.List[DataNode]],
+        filters: t.Optional[t.List[t.Dict[str, t.Any]]],
+        sorts: t.Optional[t.List[t.Dict[str, t.Any]]],
+    ):
+        base_list = []
         with self.lock:
             self.__do_datanodes_tree()
-        if scenarios is None:
-            return (self.data_nodes_by_owner.get(None, []) if self.data_nodes_by_owner else []) + (
-                self.get_scenarios(None, None, None) or []
-            )
-        if not self.data_nodes_by_owner:
-            return []
-        if isinstance(scenarios, (list, tuple)) and len(scenarios) > 1:
-            return scenarios
-        owners = scenarios if isinstance(scenarios, (list, tuple)) else [scenarios]
-        return [d for owner in owners for d in self.data_nodes_by_owner.get(owner.id, [])]
-
-    def data_node_adapter(self, data):
-        if isinstance(data, (tuple, list)):
+        if datanodes is None:
+            if scenarios is None:
+                base_list = (self.data_nodes_by_owner or {}).get(None, []) + (
+                    self.get_scenarios(None, None, None) or []
+                )
+            else:
+                if isinstance(scenarios, (list, tuple)) and len(scenarios) > 1:
+                    base_list = scenarios
+                else:
+                    if self.data_nodes_by_owner:
+                        owners = scenarios if isinstance(scenarios, (list, tuple)) else [scenarios]
+                        base_list = [d for owner in owners for d in (self.data_nodes_by_owner).get(owner.id, [])]
+                    else:
+                        base_list = []
+        else:
+            base_list = datanodes
+        adapted_list = self.get_sorted_datanode_list(base_list, sorts)
+        return self.get_filtered_datanode_list(adapted_list, filters)
+
+    def data_node_adapter(
+        self,
+        data: t.Union[Cycle, Scenario, Sequence, DataNode],
+        sorts: t.Optional[t.List[t.Dict[str, t.Any]]] = None,
+        adapt_dn=True,
+    ):
+        if isinstance(data, tuple):
+            raise NotImplementedError
+        if isinstance(data, list):
+            if data[2] and isinstance(data[2][0], (Cycle, Scenario, Sequence, DataNode)):
+                data[2] = self.get_sorted_datanode_list(data[2], sorts, adapt_dn)
             return data
         try:
             if hasattr(data, "id") and is_readable(data.id) and core_get(data.id) is not None:
                 if isinstance(data, DataNode):
-                    return (data.id, data.get_simple_label(), None, _EntityType.DATANODE.value, False)
+                    return (
+                        [data.id, data.get_simple_label(), None, _EntityType.DATANODE.value, False]
+                        if adapt_dn
+                        else data
+                    )
 
                 with self.lock:
                     self.__do_datanodes_tree()
-                    if self.data_nodes_by_owner:
-                        if isinstance(data, Cycle):
-                            return (
-                                data.id,
-                                data.get_simple_label(),
-                                self.data_nodes_by_owner[data.id] + self.scenario_by_cycle.get(data, []),
-                                _EntityType.CYCLE.value,
-                                False,
-                            )
-                        elif isinstance(data, Scenario):
-                            return (
+                if self.data_nodes_by_owner:
+                    if isinstance(data, Cycle):
+                        return [
+                            data.id,
+                            data.get_simple_label(),
+                            self.get_sorted_datanode_list(
+                                self.data_nodes_by_owner.get(data.id, [])
+                                + (self.scenario_by_cycle or {}).get(data, []),
+                                sorts,
+                                adapt_dn,
+                            ),
+                            _EntityType.CYCLE.value,
+                            False,
+                        ]
+                    elif isinstance(data, Scenario):
+                        return [
+                            data.id,
+                            data.get_simple_label(),
+                            self.get_sorted_datanode_list(
+                                self.data_nodes_by_owner.get(data.id, []) + list(data.sequences.values()),
+                                sorts,
+                                adapt_dn,
+                            ),
+                            _EntityType.SCENARIO.value,
+                            data.is_primary,
+                        ]
+                    elif isinstance(data, Sequence):
+                        if datanodes := self.data_nodes_by_owner.get(data.id):
+                            return [
                                 data.id,
                                 data.get_simple_label(),
-                                self.data_nodes_by_owner[data.id] + list(data.sequences.values()),
-                                _EntityType.SCENARIO.value,
-                                data.is_primary,
-                            )
-                        elif isinstance(data, Sequence):
-                            if datanodes := self.data_nodes_by_owner.get(data.id):
-                                return (
-                                    data.id,
-                                    data.get_simple_label(),
-                                    datanodes,
-                                    _EntityType.SEQUENCE.value,
-                                    False,
-                                )
+                                self.get_sorted_datanode_list(datanodes, sorts, adapt_dn),
+                                _EntityType.SEQUENCE.value,
+                            ]
         except Exception as e:
             _warn(
                 f"Access to {type(data)} ({data.id if hasattr(data, 'id') else 'No_id'}) failed",
@@ -745,7 +846,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
             if isinstance(ent, Scenario):
                 tags = data.get(_GuiCoreContext.__PROP_SCENARIO_TAGS)
                 if isinstance(tags, (list, tuple)):
-                    ent.tags = dict(tags)
+                    ent.tags = set(tags)
             name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
             if isinstance(name, str):
                 if hasattr(ent, _GuiCoreContext.__PROP_ENTITY_NAME):
@@ -765,20 +866,6 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     if key and key not in _GuiCoreContext.__ENTITY_PROPS:
                         ent.properties.pop(key, None)
 
-    @staticmethod
-    def get_entity_property(col: str):
-        def sort_key(entity: t.Union[Scenario, Cycle]):
-            # we compare only strings
-            try:
-                val = attrgetter(col)(entity)
-            except AttributeError as e:
-                if _is_debugging():
-                    _warn("Attribute", e)
-                val = ""
-            return val.isoformat() if isinstance(val, (datetime, date)) else str(val)
-
-        return sort_key
-
     def get_scenarios_for_owner(self, owner_id: str):
         cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
         with self.lock:
@@ -797,7 +884,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         cycles_scenarios.extend(scenarios_cycle)
                     elif isinstance(entity, Scenario):
                         cycles_scenarios.append(entity)
-        return sorted(cycles_scenarios, key=_GuiCoreContext.get_entity_property("creation_date"))
+        return sorted(cycles_scenarios, key=_get_entity_property("creation_date", Scenario))
 
     def get_data_node_history(self, id: str):
         if id and (dn := core_get(id)) and isinstance(dn, DataNode):

+ 26 - 3
taipy/gui_core/viselements.json

@@ -90,7 +90,7 @@
                     {
                         "name": "scenarios",
                         "type": "dynamic(list[Scenario|Cycle])",
-                        "doc": "TODO: The list of Scenario/Cycle to show. Shows all Cycle/Scenario if value is None."
+                        "doc": "TODO: The list of <code>Scenario^</code>/<code>Cycle^</code> to show. Shows all Cycle/Scenario if value is None."
                     },
                     {
                         "name": "multiple",
@@ -102,7 +102,7 @@
                         "name": "filter",
                         "type": "bool|str|list[str]",
                         "default_value": "\"Config id;Label;Creation date;Cycle label;Cycle start;Cycle end;Primary;Tags\"",
-                        "doc": "TODO: a list of scenario attributes to filter on. If False, do not allow filter."
+                        "doc": "TODO: a list of <code>Scenario^</code> attributes to filter on. If False, do not allow filter."
                     },
                     {
                         "name": "show_search",
@@ -114,7 +114,7 @@
                         "name": "sort",
                         "type": "bool|str|list[str]",
                         "default_value": "\"Config id;Label;Creation date\"",
-                        "doc": "TODO: a list of scenario attributes to sort on. If False, do not allow sort."
+                        "doc": "TODO: a list of <code>Scenario^</code> attributes to sort on. If False, do not allow sort."
                     }
                 ]
             }
@@ -342,11 +342,34 @@
                         "type": "dynamic(Scenario|list[Scenario])",
                         "doc": "TODO: If the <code>Scenario^</code> is set, the selector will only show datanodes owned by this scenario."
                     },
+                    {
+                        "name": "datanodes",
+                        "type": "dynamic(list[DataNode|Scenario|Cycle])",
+                        "doc": "TODO: The list of <code>DataNode^</code>/<code>Scenario^</code>/<code>Cycle^</code> to show. Shows all Cycle/Scenario/DataNode if value is None."
+                    },
                     {
                         "name": "multiple",
                         "type": "bool",
                         "default_value": "False",
                         "doc": "TODO: If True, the user can select multiple datanodes."
+                    },
+                    {
+                        "name": "filter",
+                        "type": "bool|str|list[str]",
+                        "default_value": "\"Config id;Label;Up to date;Last edit date;Input;Output;Intermediate;Expiration date;Expired\"",
+                        "doc": "TODO: a list of <code>DataNode^</code> attributes to filter on. If False, do not allow filter."
+                    },
+                    {
+                        "name": "show_search",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "TODO: If True, allows the user to search locally on label."
+                    },
+                    {
+                        "name": "sort",
+                        "type": "bool|str|list[str]",
+                        "default_value": "\"Config id;Label;Last edit date;Expiration date\"",
+                        "doc": "TODO: a list of <code>DataNode^</code> attributes to sort on. If False, do not allow sort."
                     }
                 ]
             }

+ 1 - 1
tests/gui_core/test_context_is_readable.py

@@ -201,7 +201,7 @@ class TestGuiCoreContext_is_readable:
         with patch("taipy.gui_core._context.core_get", side_effect=mock_core_get):
             gui_core_context = _GuiCoreContext(Mock())
             outcome = gui_core_context.data_node_adapter(a_datanode)
-            assert isinstance(outcome, tuple)
+            assert isinstance(outcome, list)
             assert outcome[0] == a_datanode.id
 
             with patch("taipy.gui_core._context.is_readable", side_effect=mock_is_readable_false):

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor