Forráskód Böngészése

Connecting DataNode Selector (#175) (#178)

* #175 use eslint
is_deletable
check instances are not deleted
simplify comps

* format

* fix import

* shouldn't have touch this file :-(

* dev release proposal

* Connecting DataSelector

* no need for publish env as this is not publishing

* fix typo

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 2 éve
szülő
commit
863d8a2ab2

+ 46 - 7
.github/workflows/release-dev.yml

@@ -1,12 +1,18 @@
 name: Publish pre-release to github
 
 on:
-  workflow_dispatch
+  workflow_dispatch:
+    inputs:
+      taipy-gui-version:
+        description: "The taipy-gui version to use (ex: 2.3.0.dev0)"
+        required: true
+      taipy-rest-version:
+        description: "The taipy-rest version to use (ex: 2.3.0.dev0)"
+        required: true
 
 jobs:
   publish:
     timeout-minutes: 20
-    environment: publish
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
@@ -17,11 +23,16 @@ jobs:
         with:
           node-version: '18'
 
+      - name: Check dependencies are available
+        run: |
+          curl https://pypi.org/simple/taipy-gui/ | grep -o ">taipy-gui-${{ github.event.inputs.taipy-gui-version }}\.tar\.gz<"
+          curl https://pypi.org/simple/taipy-rest/ | grep -o ">taipy-rest-${{ github.event.inputs.taipy-rest-version }}\.tar\.gz<"
+
       - name: Ensure package version is properly set
-        id: current_version
+        id: current-version
         run: |
               echo """
-              import json, sys, os
+              import json, os
               with open(f\"src{os.sep}taipy{os.sep}version.json\") as version_file:
                   version_o = json.load(version_file)
               version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
@@ -35,14 +46,42 @@ jobs:
         run: |
             python -m pip install --upgrade pip
             pip install build
-            pip install git+https://git@github.com/Avaiga/taipy-gui.git@develop
+            pip install "taipy-gui==${{ github.event.inputs.taipy-gui-version }}"
+
+      - name: Update setup.py locally
+        run: |
+              mv setup.py setup.taipy.py
+              echo """
+              import sys
+              with open('setup.taipy.py', mode='r') as setup_r, open('setup.py', mode='w') as setup_w:
+                  in_requirements = False
+                  looking = True
+                  for line in setup_r:
+                      if looking:
+                          if line.lstrip().startswith('requirements') and line.rstrip().endswith('['):
+                              in_requirements = True
+                          elif in_requirements:
+                              if line.strip() == ']':
+                                  looking = False
+                              else:
+                                  if line.lstrip().startswith('\"taipy-gui@git+https'):
+                                      start = line.find('\"taipy-gui')
+                                      end = line.rstrip().find(',')
+                                      line = f'{line[:start]}\"taipy-gui=={sys.argv[1]}\"{line[end:]}'
+                                  elif line.lstrip().startswith('\"taipy-rest@git+https'):
+                                      start = line.find('\"taipy-rest')
+                                      end = line.rstrip().find(',')
+                                      line = f'{line[:start]}\"taipy-rest=={sys.argv[2]}\"{line[end:]}'
+                      setup_w.write(line)
+              """ > /tmp/write_setup_taipy.py
+              python /tmp/write_setup_taipy.py "${{ github.event.inputs.taipy-gui-version }}" "${{ github.event.inputs.taipy-rest-version }}"
 
       - name: Build package
         run: python setup.py build_py && python -m build
 
       - name: Create/update release and tag
         run: |
-            gh release delete dev-${{ steps.current_version.outputs.VERSION }} -y || true
-            gh release create dev-${{ steps.current_version.outputs.VERSION }} ./dist/taipy-${{ steps.current_version.outputs.VERSION }}.tar.gz --draft --prerelease --notes "Release Draft ${{ steps.current_version.outputs.VERSION }}"
+            gh release delete dev-${{ steps.current-version.outputs.VERSION }} -y || true
+            gh release create dev-${{ steps.current-version.outputs.VERSION }} ./dist/taipy-${{ steps.current-version.outputs.VERSION }}.tar.gz --draft --prerelease --notes "Release Draft ${{ steps.current-version.outputs.VERSION }}"
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 301 - 259
gui/package-lock.json


+ 1 - 0
gui/package.json

@@ -11,6 +11,7 @@
     "eslint": "^8.20.0",
     "eslint-plugin-react": "^7.30.1",
     "eslint-plugin-react-hooks": "^4.6.0",
+    "eslint-plugin-tsdoc": "^0.2.17",
     "eslint-webpack-plugin": "^4.0.0",
     "ts-loader": "^9.3.1",
     "typescript": "^5.0.2",

+ 141 - 140
gui/src/NodeSelector.tsx

@@ -11,12 +11,23 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useEffect, useCallback } from "react";
+import React, { useCallback, SyntheticEvent, useState, useEffect, useMemo } from "react";
+import Badge, { BadgeOrigin } from "@mui/material/Badge";
 import Box from "@mui/material/Box";
-import { ChevronRight, ExpandMore } from "@mui/icons-material";
+import { ChevronRight, ExpandMore, FlagOutlined } from "@mui/icons-material";
 import TreeItem from "@mui/lab/TreeItem";
 import TreeView from "@mui/lab/TreeView";
-import { useDispatch, useModule, createSendActionNameAction } from "taipy-gui";
+
+import {
+    useDispatch,
+    useModule,
+    useDynamicProperty,
+    getUpdateVar,
+    createSendUpdateAction,
+    useDispatchRequestUpdateOnFirstRender,
+    createRequestUpdateAction,
+} from "taipy-gui";
+
 import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Pipeline } from "./utils/types";
 import {
     Cycle as CycleIcon,
@@ -24,180 +35,170 @@ import {
     Pipeline as PipelineIcon,
     Scenario as ScenarioIcon,
 } from "./icons";
+import { BadgePos, BadgeSx, BaseTreeViewSx, FlagSx, MainBoxSx, ParentItemSx,  } from "./utils";
 
 interface NodeSelectorProps {
     id?: string;
     updateVarName?: string;
     datanodes?: Cycles | Scenarios | DataNodes;
     coreChanged?: Record<string, unknown>;
-    updateVars?: string;
-    onDataNodeSelect?: string;
+    updateVars: string;
+    onChange?: string;
     error?: string;
+    defaultDisplayCycles: boolean;
+    displayCycles: boolean;
+    defaultShowPrimaryFlag: boolean;
+    showPrimaryFlag: boolean;
+    propagate?: boolean;
+    value?: string;
+    defaultValue?: string;
+    height: string;
 }
 
-const MainBoxSx = {
-    maxWidth: 300,
-    overflowY: "auto",
-};
-
-const TreeViewSx = {
-    mb: 2,
-    "& .MuiTreeItem-root .MuiTreeItem-content": {
-        mb: 0.5,
-        py: 1,
-        px: 2,
-        borderRadius: 0.5,
-        backgroundColor: "background.paper",
-    },
-    "& .MuiTreeItem-iconContainer:empty": {
-        display: "none",
-    },
-};
 const treeItemLabelSx = {
     display: "flex",
     alignItems: "center",
     gap: 1,
 };
-const ParentTreeItemSx = {
-    "& > .MuiTreeItem-content": {
-        ".MuiTreeItem-label": {
-            fontWeight: "fontWeightBold",
-        },
-    },
-};
-
-const NodeItem = (props: { item: DataNode }) => {
-    const [id, label, items = [], nodeType, _] = props.item;
-    return (
-        <TreeItem
-            key={id}
-            nodeId={id}
-            data-nodetype={NodeType.NODE.toString()}
-            label={
-                <Box sx={treeItemLabelSx}>
-                    <DatanodeIcon fontSize="small" color="primary" />
-                    {label}
-                </Box>
-            }
-        />
-    );
-};
-
-const PipelineItem = (props: { item: Pipeline }) => {
-    const [id, label, items = [], nodeType, _] = props.item;
-
-    return (
-        <TreeItem
-            key={id}
-            nodeId={id}
-            data-nodetype={NodeType.PIPELINE.toString()}
-            label={
-                <Box sx={treeItemLabelSx}>
-                    <PipelineIcon fontSize="small" color="primary" />
-                    {label}
-                </Box>
-            }
-            sx={ParentTreeItemSx}
-        >
-            {items
-                ? items.map((item) => {
-                      return <NodeItem item={item} />;
-                  })
-                : null}
-        </TreeItem>
-    );
-};
 
-const ScenarioItem = (props: { item: Scenario }) => {
-    const [id, label, items = [], nodeType, _] = props.item;
-    return (
-        <TreeItem
-            key={id}
-            nodeId={id}
-            data-nodetype={NodeType.SCENARIO.toString()}
-            label={
-                <Box sx={treeItemLabelSx}>
-                    <ScenarioIcon fontSize="small" color="primary" />
-                    {label}
-                </Box>
-            }
-            sx={ParentTreeItemSx}
-        >
-            {items
-                ? items.map((item: any) => {
-                      const [id, label, items = [], nodeType, _] = item;
-                      return nodeType === NodeType.PIPELINE ? <PipelineItem item={item} /> : <NodeItem item={item} />;
-                  })
-                : null}
-        </TreeItem>
-    );
-};
+const CoreItem = (props: {
+    item: Cycle | Scenario | Pipeline | DataNode;
+    displayCycles: boolean;
+    showPrimaryFlag: boolean;
+}) => {
+    const [id, label, items = [], nodeType, primary] = props.item;
 
-const CycleItem = (props: { item: Cycle }) => {
-    const [id, label, items = [], nodeType, _] = props.item;
-    return (
+    return !props.displayCycles && nodeType === NodeType.CYCLE ? (
+        <>
+            {items.map((item) => (
+                <CoreItem key={item[0]} item={item} displayCycles={false} showPrimaryFlag={props.showPrimaryFlag} />
+            ))}
+        </>
+    ) : (
         <TreeItem
             key={id}
             nodeId={id}
-            data-nodetype={NodeType.CYCLE.toString()}
+            data-selectable={nodeType === NodeType.NODE}
             label={
                 <Box sx={treeItemLabelSx}>
-                    <CycleIcon fontSize="small" color="primary" />
+                    {nodeType === NodeType.CYCLE ? (
+                        <CycleIcon fontSize="small" color="primary" />
+                    ) : nodeType === NodeType.SCENARIO ? (
+                        props.showPrimaryFlag && primary ? (
+                            <Badge
+                                badgeContent={<FlagOutlined sx={FlagSx} />}
+                                color="primary"
+                                anchorOrigin={BadgePos as BadgeOrigin}
+                                sx={BadgeSx}
+                            >
+                                <ScenarioIcon fontSize="small" color="primary" />
+                            </Badge>
+                        ) : (
+                            <ScenarioIcon fontSize="small" color="primary" />
+                        )
+                    ) : nodeType === NodeType.PIPELINE ? (
+                        <PipelineIcon fontSize="small" color="primary" />
+                    ) : (
+                        <DatanodeIcon fontSize="small" color="primary" />
+                    )}
                     {label}
                 </Box>
             }
-            sx={ParentTreeItemSx}
+            sx={nodeType === NodeType.NODE ? undefined : ParentItemSx}
         >
-            {items
-                ? items.map((item: any) => {
-                      const [id, label, items = [], nodeType, _] = item;
-                      return nodeType === NodeType.SCENARIO ? <ScenarioItem item={item} /> : <NodeItem item={item} />;
-                  })
-                : null}
+            {items.map((item) => (
+                <CoreItem key={item[0]} item={item} displayCycles={true} showPrimaryFlag={props.showPrimaryFlag} />
+            ))}
         </TreeItem>
     );
 };
 
 const NodeSelector = (props: NodeSelectorProps) => {
-    const { id = "", datanodes } = props;
+    const { id = "", datanodes = [], propagate = true } = props;
+
+    const [selected, setSelected] = useState("");
 
     const dispatch = useDispatch();
     const module = useModule();
 
-    const onSelect = useCallback((e: React.SyntheticEvent | undefined, nodeId: string) => {
-        const keyId = nodeId || "";
-        const { nodetype = "" } = e.currentTarget.dataset || {};
-        if (nodetype === NodeType.NODE.toString()) {
-            //TODO: handle on select node
-            dispatch(createSendActionNameAction(id, module, props.onDataNodeSelect, keyId));
+    useDispatchRequestUpdateOnFirstRender(dispatch, id, module, props.updateVars);
+
+    const displayCycles = useDynamicProperty(props.displayCycles, props.defaultDisplayCycles, true);
+    const showPrimaryFlag = useDynamicProperty(props.showPrimaryFlag, props.defaultShowPrimaryFlag, true);
+
+    const onSelect = useCallback(
+        (e: SyntheticEvent, nodeId: string) => {
+            const { selectable = "false" } = e.currentTarget.parentElement?.dataset || {};
+            const scenariosVar = getUpdateVar(props.updateVars, "datanodes");
+            dispatch(
+                createSendUpdateAction(
+                    props.updateVarName,
+                    selectable === "true" ? nodeId : undefined,
+                    module,
+                    props.onChange,
+                    propagate,
+                    scenariosVar
+                )
+            );
+            setSelected(nodeId);
+        },
+        [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module]
+    );
+
+    const unselect = useCallback(() => {
+        if (selected) {
+            const scenariosVar = getUpdateVar(props.updateVars, "datanodes");
+            dispatch(
+                createSendUpdateAction(props.updateVarName, undefined, module, props.onChange, propagate, scenariosVar)
+            );
+            setSelected("");
+        }
+    }, [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module, selected]);
+
+    useEffect(() => {
+        if (props.value !== undefined) {
+            setSelected(props.value);
+        } else if (props.defaultValue) {
+            setSelected(props.defaultValue);
+        }
+    }, [props.defaultValue, props.value]);
+
+    useEffect(() => {
+        if (!datanodes.length) {
+            unselect();
+        }
+    }, [datanodes, unselect]);
+
+    // Refresh on broadcast
+    useEffect(() => {
+        if (props.coreChanged?.scenario) {
+            const updateVar = getUpdateVar(props.updateVars, "datanodes");
+            updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
         }
-    }, []);
+    }, [props.coreChanged, props.updateVars, module, dispatch, id]);
+
+    const treeViewSx = useMemo(() => ({ ...BaseTreeViewSx, maxHeight: props.height || "50vh" }), [props.height]);
 
     return (
-        <>
-            <Box sx={MainBoxSx}>
-                <TreeView
-                    defaultCollapseIcon={<ExpandMore />}
-                    defaultExpandIcon={<ChevronRight />}
-                    sx={TreeViewSx}
-                    onNodeSelect={onSelect}
-                >
-                    {datanodes
-                        ? datanodes.map((item) => {
-                              const [id, label, items = [], nodeType, _] = item;
-                              return nodeType === NodeType.CYCLE ? (
-                                  <CycleItem item={item as Cycle} />
-                              ) : nodeType === NodeType.SCENARIO ? (
-                                  <ScenarioItem item={item as Scenario} />
-                              ) : (
-                                  <NodeItem item={item as DataNode} />
-                              );
-                          })
-                        : null}
-                </TreeView>
-                <Box>{props.error}</Box>
-            </Box>
-        </>
+        <Box sx={MainBoxSx}>
+            <TreeView
+                defaultCollapseIcon={<ExpandMore />}
+                defaultExpandIcon={<ChevronRight />}
+                sx={treeViewSx}
+                onNodeSelect={onSelect}
+                selected={selected}
+            >
+                {datanodes.map((item) => (
+                    <CoreItem
+                        key={item[0]}
+                        item={item}
+                        displayCycles={displayCycles}
+                        showPrimaryFlag={showPrimaryFlag}
+                    />
+                ))}
+            </TreeView>
+            <Box>{props.error}</Box>
+        </Box>
     );
 };
 

+ 16 - 9
gui/src/ScenarioDag.tsx

@@ -16,7 +16,14 @@ import { Close, ZoomIn } from "@mui/icons-material";
 import { DisplayModel } from "./utils/types";
 import { initDiagram, populateModel, relayoutDiagram } from "./utils/diagram";
 import { TaipyDiagramModel } from "./projectstorm/models";
-import { createRequestUpdateAction, createSendUpdateAction, getUpdateVar, useDispatch, useDynamicProperty, useModule } from "taipy-gui";
+import {
+    createRequestUpdateAction,
+    createSendUpdateAction,
+    getUpdateVar,
+    useDispatch,
+    useDynamicProperty,
+    useModule,
+} from "taipy-gui";
 
 interface ScenarioDagProps {
     id?: string;
@@ -66,9 +73,11 @@ const DagTitle = (props: DagTitleProps) => (
             <IconButton edge="end" color="inherit" onClick={props.zoomToFit} title="zoom to fit">
                 <ZoomIn />
             </IconButton>{" "}
-            {props.hideDialog ? <IconButton edge="end" color="inherit" onClick={props.hideDialog} title="close">
-                <Close />
-            </IconButton>: null }
+            {props.hideDialog ? (
+                <IconButton edge="end" color="inherit" onClick={props.hideDialog} title="close">
+                    <Close />
+                </IconButton>
+            ) : null}
         </Toolbar>
     </AppBar>
 );
@@ -101,7 +110,7 @@ const ScenarioDag = (props: ScenarioDagProps) => {
         if (props.coreChanged?.scenario) {
             props.updateVarName && dispatch(createRequestUpdateAction(props.id, module, [props.updateVarName], true));
         }
-    }, [props.coreChanged, props.updateVarName, module, dispatch]);
+    }, [props.coreChanged, props.updateVarName, module, dispatch, props.id]);
 
     useEffect(() => {
         const displayModel = Array.isArray(props.scenario)
@@ -139,10 +148,8 @@ const ScenarioDag = (props: ScenarioDagProps) => {
 
     useEffect(() => {
         const showVar = getUpdateVar(props.updateVars, "show");
-        showVar && dispatch(
-            createSendUpdateAction(showVar, show, module)
-        );
-    }, [show, props.updateVars]);
+        showVar && dispatch(createSendUpdateAction(showVar, show, module));
+    }, [show, props.updateVars, dispatch, module]);
 
     return withButton ? (
         <>

+ 149 - 175
gui/src/ScenarioSelector.tsx

@@ -11,8 +11,8 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React, { useEffect, useState, useCallback } from "react";
-import { alpha, useTheme } from "@mui/material";
+import React, { useEffect, useState, useCallback, useMemo } from "react";
+import { Theme, alpha } from "@mui/material";
 import Badge, { BadgeOrigin } from "@mui/material/Badge";
 import Box from "@mui/material/Box";
 import Button from "@mui/material/Button";
@@ -30,18 +30,7 @@ import Dialog from "@mui/material/Dialog";
 import Select from "@mui/material/Select";
 import TextField from "@mui/material/TextField";
 import Typography from "@mui/material/Typography";
-import {
-    ChevronRight,
-    ExpandMore,
-    FlagOutlined,
-    Close,
-    DeleteOutline,
-    Add,
-    EditOutlined,
-    Send,
-    CheckCircle,
-    Cancel,
-} from "@mui/icons-material";
+import { ChevronRight, ExpandMore, FlagOutlined, Close, DeleteOutline, Add, EditOutlined } from "@mui/icons-material";
 import TreeItem from "@mui/lab/TreeItem";
 import TreeView from "@mui/lab/TreeView";
 import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
@@ -60,20 +49,13 @@ import {
 
 import { Cycle, Scenario } from "./icons";
 import ConfirmDialog from "./utils/ConfirmDialog";
-import { ScFProps, ScenarioFull } from "./utils";
+import { BadgePos, BadgeSx, BaseTreeViewSx, FlagSx, MainBoxSx, ParentItemSx, ScFProps, ScenarioFull } from "./utils";
 
 enum NodeType {
     CYCLE = 0,
     SCENARIO = 1,
 }
 
-interface ScenarioDict {
-    config: string;
-    name: string;
-    date: string;
-    properties: Array<[string, string]>;
-}
-
 type Property = {
     id: string;
     key: string;
@@ -85,10 +67,11 @@ type Scenarios = Array<Scenario>;
 type Cycles = Array<[string, string, Scenarios, number, boolean]>;
 
 interface ScenarioDict {
+    id?: string;
     config: string;
     name: string;
     date: string;
-    properties: Array<[string, string]>;
+    properties: Array<Property>;
 }
 
 interface ScenarioSelectorProps {
@@ -99,7 +82,6 @@ interface ScenarioSelectorProps {
     displayCycles?: boolean;
     defaultShowPrimaryFlag: boolean;
     showPrimaryFlag?: boolean;
-    value?: Record<string, any>;
     updateVarName?: string;
     scenarios?: Cycles | Scenarios;
     onScenarioCrud: string;
@@ -111,6 +93,9 @@ interface ScenarioSelectorProps {
     propagate?: boolean;
     scenarioEdit?: ScenarioFull;
     onScenarioSelect: string;
+    value?: string;
+    defaultValue?: string;
+    height: string;
 }
 
 interface ScenarioNodesProps {
@@ -128,7 +113,7 @@ interface ScenarioItemProps {
 
 interface ScenarioEditDialogProps {
     scenario?: ScenarioFull;
-    submit: (...values: any[]) => void;
+    submit: (...values: unknown[]) => void;
     open: boolean;
     actionEdit: boolean;
     configs?: Array<[string, string]>;
@@ -142,28 +127,6 @@ const emptyScenario: ScenarioDict = {
     properties: [],
 };
 
-const BadgePos = {
-    vertical: "top",
-    horizontal: "left",
-} as BadgeOrigin;
-
-const BadgeSx = {
-    flex: "0 0 auto",
-
-    "& .MuiBadge-badge": {
-        fontSize: "1rem",
-        width: "1em",
-        height: "1em",
-        p: 0,
-        minWidth: "0",
-    },
-};
-
-const FlagSx = {
-    color: "common.white",
-    fontSize: "0.75em",
-};
-
 const tinyIconButtonSx = {
     position: "relative",
     display: "flex",
@@ -189,41 +152,12 @@ const tinyIconButtonSx = {
 
 const ActionContentSx = { mr: 2, ml: 2 };
 
-const MainBoxSx = {
-    maxWidth: 300,
-    overflowY: "auto",
-};
-
-const TreeViewSx = {
-    mb: 2,
-
-    "& .MuiTreeItem-root .MuiTreeItem-content": {
-        mb: 0.5,
-        py: 1,
-        px: 2,
-        borderRadius: 0.5,
-        backgroundColor: "background.paper",
-    },
-
-    "& .MuiTreeItem-iconContainer:empty": {
-        display: "none",
-    },
-};
-
 const treeItemLabelSx = {
     display: "flex",
     alignItems: "center",
     gap: 1,
 };
 
-const CycleSx = {
-    "& > .MuiTreeItem-content": {
-        ".MuiTreeItem-label": {
-            fontWeight: "fontWeightBold",
-        },
-    },
-};
-
 const DialogContentSx = {
     maxHeight: "calc(100vh - 256px)",
 
@@ -249,72 +183,68 @@ const IconButtonSx = {
     p: 0,
 };
 
-const ScenarioItem = ({ scenarioId, label, isPrimary, openEditDialog }: ScenarioItemProps) => {
-    const theme = useTheme();
-
-    const tinyEditIconButtonSx = React.useMemo(
-        () => ({
-            ...tinyIconButtonSx,
-            backgroundColor: alpha(theme.palette.text.primary, 0.15),
-            color: "text.primary",
-
-            "&:hover": {
-                backgroundColor: "primary.main",
-                color: "primary.contrastText",
-            },
-        }),
-        [theme]
-    );
+const tinyEditIconButtonSx = (theme: Theme) => ({
+    ...tinyIconButtonSx,
+    backgroundColor: alpha(theme.palette.text.primary, 0.15),
+    color: "text.primary",
 
-    return (
-        <Grid container alignItems="center" direction="row" flexWrap="nowrap" spacing={1}>
-            <Grid item xs sx={treeItemLabelSx} key="label">
-                {isPrimary ? (
-                    <Badge
-                        badgeContent={<FlagOutlined sx={FlagSx} />}
-                        color="primary"
-                        anchorOrigin={BadgePos}
-                        sx={BadgeSx}
-                    >
-                        <Scenario fontSize="small" color="primary" />
-                    </Badge>
-                ) : (
+    "&:hover": {
+        backgroundColor: "primary.main",
+        color: "primary.contrastText",
+    },
+});
+
+const ScenarioItem = ({ scenarioId, label, isPrimary, openEditDialog }: ScenarioItemProps) => (
+    <Grid container alignItems="center" direction="row" flexWrap="nowrap" spacing={1}>
+        <Grid item xs sx={treeItemLabelSx}>
+            {isPrimary ? (
+                <Badge
+                    badgeContent={<FlagOutlined sx={FlagSx} />}
+                    color="primary"
+                    anchorOrigin={BadgePos as BadgeOrigin}
+                    sx={BadgeSx}
+                >
                     <Scenario fontSize="small" color="primary" />
-                )}
-                {label}
-            </Grid>
-            <Grid item xs="auto" key="button">
-                <IconButton data-id={scenarioId} onClick={openEditDialog} sx={tinyEditIconButtonSx}>
-                    <EditOutlined />
-                </IconButton>
-            </Grid>
+                </Badge>
+            ) : (
+                <Scenario fontSize="small" color="primary" />
+            )}
+            {label}
         </Grid>
-    );
-};
+        <Grid item xs="auto">
+            <IconButton data-id={scenarioId} onClick={openEditDialog} sx={tinyEditIconButtonSx}>
+                <EditOutlined />
+            </IconButton>
+        </Grid>
+    </Grid>
+);
 
 const ScenarioNodes = ({ scenarios = [], showPrimary = true, openEditDialog }: ScenarioNodesProps) => {
     const sc =
-        Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0])
-            ? (scenarios as Scenarios)
-            : scenarios
-            ? [scenarios as Scenario]
+        Array.isArray(scenarios) && scenarios.length
+            ? Array.isArray(scenarios[0])
+                ? (scenarios as Scenarios)
+                : [scenarios as Scenario]
             : [];
     return (
         <>
-            {sc.map(([id, label, _, _nodeType, primary]) => (
-                <TreeItem
-                    key={id}
-                    nodeId={id}
-                    label={
-                        <ScenarioItem
-                            scenarioId={id}
-                            label={label}
-                            isPrimary={showPrimary && primary}
-                            openEditDialog={openEditDialog}
-                        />
-                    }
-                />
-            ))}
+            {
+                // eslint-disable-next-line @typescript-eslint/no-unused-vars
+                sc.map(([id, label, _, _nodeType, primary]) => (
+                    <TreeItem
+                        key={id}
+                        nodeId={id}
+                        label={
+                            <ScenarioItem
+                                scenarioId={id}
+                                label={label}
+                                isPrimary={showPrimary && primary}
+                                openEditDialog={openEditDialog}
+                            />
+                        }
+                    />
+                ))
+            }
         </>
     );
 };
@@ -328,22 +258,22 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
         value: "",
     });
 
-    const propertyAdd = () => {
-        setProperties((props) => [...props, { ...newProp, id: props.length + 1 + "" }]);
+    const propertyAdd = useCallback(() => {
+        setProperties((ps) => [...ps, { ...newProp, id: ps.length + 1 + "" }]);
         setNewProp({ id: "", key: "", value: "" });
-    };
+    }, [newProp]);
 
     const propertyDelete = useCallback((e: React.MouseEvent<HTMLElement>) => {
         const { id = "-1" } = e.currentTarget.dataset;
-        setProperties((props) => props.filter((item) => item.id !== id));
+        setProperties((ps) => ps.filter((item) => item.id !== id));
     }, []);
 
     const updatePropertyField = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
         const { idx = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
         if (name) {
             if (idx) {
-                setProperties((props) =>
-                    props.map((p, i) => {
+                setProperties((ps) =>
+                    ps.map((p, i) => {
                         if (idx == i + "") {
                             p[name as keyof Property] = e.target.value;
                         }
@@ -356,37 +286,40 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
         }
     }, []);
 
+    const form = useFormik({
+        initialValues: emptyScenario,
+        onSubmit: (values: ScenarioDict) => {
+            values.properties = [...properties];
+            setProperties([]);
+            submit(actionEdit, false, values);
+            form.resetForm();
+            close();
+        },
+    });
+
     useEffect(() => {
         form.setValues(
             scenario
                 ? {
+                      id: scenario[ScFProps.id],
                       config: scenario[ScFProps.config_id],
                       name: scenario[ScFProps.label],
                       date: scenario[ScFProps.creation_date],
-                      properties: scenario[ScFProps.properties],
+                      properties: [],
                   }
                 : emptyScenario
         );
-        setProperties(scenario ? scenario[ScFProps.properties].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []);
+        setProperties(
+            scenario ? scenario[ScFProps.properties].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []
+        );
+        // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [scenario]);
 
-    const form = useFormik({
-        initialValues: emptyScenario,
-        onSubmit: (values: any) => {
-            values.properties = [...properties];
-            actionEdit && scenario && (values.id = scenario[ScFProps.id]);
-            setProperties([]);
-            submit(actionEdit, false, values);
-            form.resetForm();
-            close();
-        },
-    });
-
     const onDeleteScenario = useCallback(() => {
         submit(actionEdit, true, { id: scenario && scenario[ScFProps.id] });
         setConfirmDialogOpen(false);
         close();
-    }, [close, actionEdit, scenario]);
+    }, [close, actionEdit, scenario, submit]);
 
     const onConfirmDialogOpen = useCallback(() => setConfirmDialogOpen(true), []);
 
@@ -534,7 +467,12 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                         <Grid container justifyContent="space-between" sx={ActionContentSx}>
                             {actionEdit && (
                                 <Grid item xs={6}>
-                                    <Button variant="outlined" color="error" onClick={onConfirmDialogOpen} disabled={!scenario || !scenario[ScFProps.deletable]}>
+                                    <Button
+                                        variant="outlined"
+                                        color="error"
+                                        onClick={onConfirmDialogOpen}
+                                        disabled={!scenario || !scenario[ScFProps.deletable]}
+                                    >
                                         Delete
                                     </Button>
                                 </Grid>
@@ -573,14 +511,15 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
 };
 
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
-    const { id = "", scenarios = [], propagate = true } = props;
+    const { id = "", scenarios = [], propagate = true, defaultValue = "", value } = props;
     const [open, setOpen] = useState(false);
     const [actionEdit, setActionEdit] = useState<boolean>(false);
+    const [selected, setSelected] = useState("");
 
     const dispatch = useDispatch();
     const module = useModule();
 
-    useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
+    useDispatchRequestUpdateOnFirstRender(dispatch, id, module, props.updateVars);
 
     const showAddButton = useDynamicProperty(props.showAddButton, props.defaultShowAddButton, true);
     const displayCycles = useDynamicProperty(props.displayCycles, props.defaultDisplayCycles, true);
@@ -606,18 +545,47 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             setOpen(true);
             setActionEdit(true);
         },
-        [props.onScenarioSelect]
+        [props.onScenarioSelect, id, dispatch, module]
     );
 
+    const onSelect = useCallback(
+        (e: React.SyntheticEvent, nodeId: string) => {
+            const { cycle = false } = (e?.currentTarget as HTMLElement)?.parentElement?.dataset || {};
+            const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
+            dispatch(
+                createSendUpdateAction(
+                    props.updateVarName,
+                    cycle ? undefined : nodeId,
+                    module,
+                    props.onChange,
+                    propagate,
+                    scenariosVar
+                )
+            );
+            setSelected(nodeId);
+        },
+        [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module]
+    );
+
+    const unselect = useCallback(() => {
+        if (selected) {
+            const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
+            dispatch(
+                createSendUpdateAction(props.updateVarName, undefined, module, props.onChange, propagate, scenariosVar)
+            );
+            setSelected("");
+        }
+    }, [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module, selected]);
+
     const onSubmit = useCallback(
-        (...values: any[]) => {
+        (...values: unknown[]) => {
             dispatch(createSendActionNameAction(id, module, props.onScenarioCrud, ...values));
             if (values.length > 1 && values[1]) {
                 // delete requested => unselect current node
-                onSelect(undefined, []);
+                unselect();
             }
         },
-        [id, module, props.onScenarioCrud]
+        [id, props.onScenarioCrud, dispatch, module, unselect]
     );
 
     // Refresh on broadcast
@@ -626,18 +594,23 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             const updateVar = getUpdateVar(props.updateVars, "scenarios");
             updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
         }
-    }, [props.coreChanged, props.updateVars, module, dispatch]);
+    }, [props.coreChanged, props.updateVars, module, dispatch, id]);
 
-    const onSelect = useCallback(
-        (e: React.SyntheticEvent | undefined, nodeIds: Array<string> | string) => {
-            const { cycle = false } = (e?.currentTarget as HTMLElement)?.parentElement?.dataset || {};
-            const scenariosVar = getUpdateVar(props.updateVars, "scenarios");
-            dispatch(
-                createSendUpdateAction(props.updateVarName, cycle ? undefined : nodeIds, module, props.onChange, propagate, scenariosVar)
-            );
-        },
-        [props.updateVarName, props.updateVars, props.onChange, propagate, module]
-    );
+    useEffect(() => {
+        if (value !== undefined) {
+            setSelected(value);
+        } else if (defaultValue) {
+            setSelected(defaultValue);
+        }
+    }, [defaultValue, value]);
+
+    useEffect(() => {
+        if (!scenarios.length) {
+            unselect();
+        }
+    }, [scenarios, unselect]);
+
+    const treeViewSx = useMemo(() => ({ ...BaseTreeViewSx, maxHeight: props.height || "50vh" }), [props.height]);
 
     return (
         <div>
@@ -645,12 +618,13 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                 <TreeView
                     defaultCollapseIcon={<ExpandMore />}
                     defaultExpandIcon={<ChevronRight />}
-                    sx={TreeViewSx}
+                    sx={treeViewSx}
                     onNodeSelect={onSelect}
+                    selected={selected}
                 >
                     {scenarios
                         ? scenarios.map((item) => {
-                              const [id, label, scenarios, nodeType, _] = item;
+                              const [id, label, scenarios, nodeType] = item;
                               return displayCycles ? (
                                   nodeType === NodeType.CYCLE ? (
                                       <TreeItem
@@ -662,7 +636,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
                                                   {label}
                                               </Box>
                                           }
-                                          sx={CycleSx}
+                                          sx={ParentItemSx}
                                           data-cycle
                                       >
                                           <ScenarioNodes

+ 115 - 98
gui/src/ScenarioViewer.tsx

@@ -14,7 +14,7 @@
 import React, { useState, useCallback, useEffect, useMemo, ChangeEvent, SyntheticEvent, MouseEvent } from "react";
 import Accordion from "@mui/material/Accordion";
 import AccordionDetails from "@mui/material/AccordionDetails";
-import AccordionSummary, { AccordionSummaryProps } from "@mui/material/AccordionSummary";
+import AccordionSummary from "@mui/material/AccordionSummary";
 import Autocomplete from "@mui/material/Autocomplete";
 import Chip from "@mui/material/Chip";
 import Box from "@mui/material/Box";
@@ -25,8 +25,7 @@ import IconButton from "@mui/material/IconButton";
 import InputAdornment from "@mui/material/InputAdornment";
 import TextField from "@mui/material/TextField";
 import Typography from "@mui/material/Typography";
-import { styled } from "@mui/material";
-import { FlagOutlined, DeleteOutline, Add, Send, CheckCircle, Cancel, ArrowForwardIosSharp } from "@mui/icons-material";
+import { FlagOutlined, DeleteOutline, Send, CheckCircle, Cancel, ArrowForwardIosSharp } from "@mui/icons-material";
 
 import {
     createRequestUpdateAction,
@@ -85,7 +84,7 @@ const FieldNoMaxWidth = {
     maxWidth: "none",
 };
 
-const AccordionSummarySx = { fontSize: "0.9rem" };
+const AccordionIconSx = { fontSize: "0.9rem" };
 const ChipSx = { ml: 1 };
 const IconPaddingSx = { padding: 0 };
 const DeleteIconSx = { height: 50, width: 50, p: 0 };
@@ -105,6 +104,17 @@ const hoverSx = {
     mt: 0,
 };
 
+const AccordionSummarySx = {
+    flexDirection: "row-reverse",
+    "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": {
+        transform: "rotate(90deg)",
+        mr: 1,
+    },
+    "& .MuiAccordionSummary-content": {
+        mr: 1,
+    },
+};
+
 const disableColor = <T,>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
 
 const PipelineRow = ({
@@ -128,7 +138,7 @@ const PipelineRow = ({
             e.stopPropagation();
             editLabel(id, pipeline);
         },
-        [id, pipeline]
+        [id, pipeline, editLabel]
     );
     const onCancelField = useCallback(
         (e: MouseEvent<HTMLElement>) => {
@@ -136,7 +146,7 @@ const PipelineRow = ({
             setPipeline(label);
             setFocusName("");
         },
-        [label]
+        [label, setFocusName]
     );
     const onSubmitPipeline = useCallback(() => submitEntity(id), [submitEntity, id]);
 
@@ -185,19 +195,6 @@ const PipelineRow = ({
     );
 };
 
-const MuiAccordionSummary = styled((props: AccordionSummaryProps) => (
-    <AccordionSummary expandIcon={<ArrowForwardIosSharp sx={AccordionSummarySx} />} {...props} />
-))(({ theme }) => ({
-    flexDirection: "row-reverse",
-    "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": {
-        transform: "rotate(90deg)",
-        marginRight: theme.spacing(1),
-    },
-    "& .MuiAccordionSummary-content": {
-        marginLeft: theme.spacing(1),
-    },
-}));
-
 const ScenarioViewer = (props: ScenarioViewerProps) => {
     const {
         id = "",
@@ -216,17 +213,17 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const module = useModule();
 
     const [
-        scenarioId = "",
-        primary = false,
-        scConfig = "",
-        date = "",
-        scLabel = "",
-        scenarioTags = [],
-        scenarioProperties = [],
-        scPipelines = [],
-        authorizedTags = [],
-        deletable = false,
-        isScenario = false,
+        scId,
+        scPrimary,
+        scConfig,
+        scDate,
+        scLabel,
+        scTags,
+        scProperties,
+        scPipelines,
+        scAuthorizedTags,
+        scDeletable,
+        isScenario,
     ] = useMemo(() => {
         const sc = Array.isArray(props.scenario)
             ? props.scenario.length == ScenarioFullLength && typeof props.scenario[ScFProps.id] === "string"
@@ -235,7 +232,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                 ? (props.scenario[0] as ScenarioFull)
                 : undefined
             : undefined;
-        return sc ? [...sc, true] : [];
+        return sc ? [...sc, true] : ["", false, "", "", "", [], [], [], [], false, false];
     }, [props.scenario]);
 
     const active = useDynamicProperty(props.active, props.defaultActive, true);
@@ -247,9 +244,9 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const onDeleteScenario = useCallback(() => {
         setDeleteDialogOpen(false);
         if (isScenario) {
-            dispatch(createSendActionNameAction(id, module, props.onDelete, true, true, { id: scenarioId }));
+            dispatch(createSendActionNameAction(id, module, props.onDelete, true, true, { id: scId }));
         }
-    }, [isScenario, props.onDelete, scenarioId]);
+    }, [isScenario, props.onDelete, scId, id, dispatch, module]);
 
     const [primaryDialog, setPrimaryDialog] = useState(false);
     const openPrimaryDialog = useCallback(() => setPrimaryDialog(true), []);
@@ -257,33 +254,37 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     const onPromote = useCallback(() => {
         setPrimaryDialog(false);
         if (isScenario) {
-            dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scenarioId, primary: true }));
+            dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, primary: true }));
         }
-    }, [isScenario, props.onEdit, scenarioId]);
+    }, [isScenario, props.onEdit, scId, id, dispatch, module]);
 
-    const [scProperties, setProperties] = useState<Property[]>([]);
+    const [properties, setProperties] = useState<Property[]>([]);
     const [newProp, setNewProp] = useState<Property>({
         id: "",
         key: "",
         value: "",
     });
 
+    // userExpanded
+    const [userExpanded, setUserExpanded] = useState(false);
+    const onExpand = useCallback((e: SyntheticEvent, exp: boolean) => setUserExpanded(exp), []);
+
     // submits
     const submitPipeline = useCallback(
         (pipelineId: string) => {
             dispatch(createSendActionNameAction(id, module, props.onSubmit, { id: pipelineId }));
         },
-        [props.onSubmit]
+        [props.onSubmit, id, dispatch, module]
     );
 
     const submitScenario = useCallback(
         (e: React.MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             if (isScenario) {
-                dispatch(createSendActionNameAction(id, module, props.onSubmit, { id: scenarioId }));
+                dispatch(createSendActionNameAction(id, module, props.onSubmit, { id: scId }));
             }
         },
-        [isScenario, props.onSubmit]
+        [isScenario, props.onSubmit, id, scId, dispatch, module]
     );
 
     // focus
@@ -299,11 +300,11 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             if (isScenario) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scenarioId, name: label }));
+                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, name: label }));
                 setFocusName("");
             }
         },
-        [isScenario, props.onEdit, scenarioId, label]
+        [isScenario, props.onEdit, scId, label, id, dispatch, module]
     );
     const cancelLabel = useCallback(
         (e: MouseEvent<HTMLElement>) => {
@@ -311,29 +312,29 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
             setLabel(scLabel);
             setFocusName("");
         },
-        [scLabel]
+        [scLabel, setLabel, setFocusName]
     );
     const onLabelChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setLabel(e.target.value), []);
 
     // tags
-    const [scTags, setTags] = useState<string[]>([]);
+    const [tags, setTags] = useState<string[]>([]);
     const editTags = useCallback(
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             if (isScenario) {
-                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scenarioId, tags: scTags }));
+                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: scId, tags: tags }));
                 setFocusName("");
             }
         },
-        [isScenario, props.onEdit, scenarioId, scTags]
+        [isScenario, props.onEdit, scId, tags, id, dispatch, module]
     );
     const cancelTags = useCallback(
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
-            setTags(scenarioTags);
+            setTags(scTags);
             setFocusName("");
         },
-        [scenarioTags]
+        [scTags]
     );
     const onChangeTags = useCallback((_: SyntheticEvent, tags: string[]) => setTags(tags), []);
 
@@ -342,8 +343,8 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         const { id = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
         if (name) {
             if (id) {
-                setProperties((props) =>
-                    props.map((p) => {
+                setProperties((ps) =>
+                    ps.map((p) => {
                         if (id == p.id) {
                             p[name as keyof Property] = e.target.value;
                         }
@@ -361,49 +362,53 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
             e.stopPropagation();
             if (isScenario) {
                 const { id: propId = "" } = e.currentTarget.dataset || {};
-                const property = propId ? scProperties.find((p) => p.id === propId) : newProp;
+                const property = propId ? properties.find((p) => p.id === propId) : newProp;
                 property &&
                     dispatch(
-                        createSendActionNameAction(id, module, props.onEdit, { id: scenarioId, properties: [property] })
+                        createSendActionNameAction(id, module, props.onEdit, { id: scId, properties: [property] })
                     );
                 setNewProp((np) => ({ ...np, key: "", value: "" }));
                 setFocusName("");
             }
         },
-        [isScenario, props.onEdit, scenarioId, scProperties, newProp]
+        [isScenario, props.onEdit, scId, properties, newProp, id, dispatch, module]
     );
     const cancelProperty = useCallback(
         (e: MouseEvent<HTMLElement>) => {
             e.stopPropagation();
             if (isScenario) {
                 const { id: propId = "" } = e.currentTarget.dataset || {};
-                const propertyIdx = scProperties.findIndex((p) => p.id === propId);
+                const propertyIdx = properties.findIndex((p) => p.id === propId);
                 propertyIdx > -1 &&
-                    propertyIdx < scenarioProperties.length &&
-                    setProperties((props) =>
-                        props.map((p, idx) =>
-                            idx == propertyIdx
-                                ? { ...p, key: scenarioProperties[idx][0], value: scenarioProperties[idx][1] }
-                                : p
+                    propertyIdx < scProperties.length &&
+                    setProperties((ps) =>
+                        ps.map((p, idx) =>
+                            idx == propertyIdx ? { ...p, key: scProperties[idx][0], value: scProperties[idx][1] } : p
                         )
                     );
                 setFocusName("");
             }
         },
-        [isScenario, props.onEdit, scenarioId, scProperties]
+        [isScenario, properties, scProperties]
     );
 
-    const deleteProperty = useCallback((e: React.MouseEvent<HTMLElement>) => {
-        e.stopPropagation();
-        const { id: propId = "-1" } = e.currentTarget.dataset;
-        setProperties((props) => props.filter((item) => item.id !== propId));
-        const property = scProperties.find((p) => p.id === propId);
-        property &&
-            dispatch(
-                createSendActionNameAction(id, module, props.onEdit, { id: scenarioId, deleted_properties: [property] })
-            );
-        setFocusName("");
-    }, []);
+    const deleteProperty = useCallback(
+        (e: React.MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            const { id: propId = "-1" } = e.currentTarget.dataset;
+            setProperties((ps) => ps.filter((item) => item.id !== propId));
+            const property = properties.find((p) => p.id === propId);
+            property &&
+                dispatch(
+                    createSendActionNameAction(id, module, props.onEdit, {
+                        id: scId,
+                        deleted_properties: [property],
+                    })
+                );
+            setFocusName("");
+        },
+        [props.onEdit, scId, id, dispatch, module, properties]
+    );
 
     // pipelines
     const editPipeline = useCallback(
@@ -413,36 +418,45 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                 setFocusName("");
             }
         },
-        [isScenario, props.onEdit]
+        [isScenario, props.onEdit, dispatch, module]
     );
 
     // on scenario change
     useEffect(() => {
-        showTags && setTags(scenarioTags);
+        showTags && setTags(scTags);
         showProperties &&
             setProperties(
-                scenarioProperties.map(([k, v], i) => ({
+                scProperties.map(([k, v], i) => ({
                     id: i + "",
                     key: k,
                     value: v,
                 }))
             );
         setLabel(scLabel);
-    }, [scenarioTags, scenarioProperties, scLabel]);
+        isScenario || setUserExpanded(false);
+    }, [scTags, scProperties, scLabel, isScenario, showTags, showProperties]);
 
     // Refresh on broadcast
     useEffect(() => {
         const ids = props.coreChanged?.scenario;
-        if (typeof ids === "string" ? ids === scenarioId : Array.isArray(ids) ? ids.includes(scenarioId) : ids) {
+        if (typeof ids === "string" ? ids === scId : Array.isArray(ids) ? ids.includes(scId) : ids) {
             props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
         }
-    }, [props.coreChanged, props.updateVarName, module, dispatch, scenarioId]);
+    }, [props.coreChanged, props.updateVarName, id, module, dispatch, scId]);
 
     return (
         <>
             <Box sx={MainBoxSx} id={id} onClick={onFocus}>
-                <Accordion defaultExpanded={expandable ? expanded : isScenario} disabled={!isScenario}>
-                    <MuiAccordionSummary>
+                <Accordion
+                    defaultExpanded={expandable && expanded}
+                    expanded={userExpanded}
+                    onChange={onExpand}
+                    disabled={!isScenario}
+                >
+                    <AccordionSummary
+                        expandIcon={<ArrowForwardIosSharp sx={AccordionIconSx} />}
+                        sx={AccordionSummarySx}
+                    >
                         <Grid
                             container
                             alignItems="center"
@@ -453,7 +467,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                         >
                             <Grid item>
                                 {scLabel}
-                                {primary && (
+                                {scPrimary && (
                                     <Chip
                                         color="primary"
                                         label={<FlagOutlined sx={FlagSx} />}
@@ -474,7 +488,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 ) : null}
                             </Grid>
                         </Grid>
-                    </MuiAccordionSummary>
+                    </AccordionSummary>
                     <AccordionDetails>
                         <Grid container rowSpacing={2}>
                             {showConfig ? (
@@ -493,7 +507,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         <Typography variant="subtitle2">Cycle / Frequency</Typography>
                                     </Grid>
                                     <Grid item xs={8}>
-                                        <Typography variant="subtitle2">{date}</Typography>
+                                        <Typography variant="subtitle2">{scDate}</Typography>
                                     </Grid>
                                 </Grid>
                             ) : null}
@@ -553,19 +567,22 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         {active && focusName === "tags" ? (
                                             <Autocomplete
                                                 multiple
-                                                options={authorizedTags}
-                                                freeSolo={!authorizedTags.length}
+                                                options={scAuthorizedTags}
+                                                freeSolo={!scAuthorizedTags.length}
                                                 renderTags={(value: readonly string[], getTagProps) =>
-                                                    value.map((option: string, index: number) => (
-                                                        <Chip
-                                                            variant="outlined"
-                                                            label={option}
-                                                            sx={IconPaddingSx}
-                                                            {...getTagProps({ index })}
-                                                        />
-                                                    ))
+                                                    value.map((option: string, index: number) => {
+                                                        return (
+                                                            // eslint-disable-next-line react/jsx-key
+                                                            <Chip
+                                                                variant="outlined"
+                                                                label={option}
+                                                                sx={IconPaddingSx}
+                                                                {...getTagProps({ index })}
+                                                            />
+                                                        );
+                                                    })
                                                 }
-                                                value={scTags}
+                                                value={tags}
                                                 onChange={onChangeTags}
                                                 fullWidth
                                                 renderInput={(params) => (
@@ -598,7 +615,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                                     <Typography variant="subtitle2">Tags</Typography>
                                                 </Grid>
                                                 <Grid item xs={8}>
-                                                    {scTags.map((tag, index) => (
+                                                    {tags.map((tag, index) => (
                                                         <Chip key={index} label={tag} variant="outlined" />
                                                     ))}
                                                 </Grid>
@@ -617,8 +634,8 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                         <Typography variant="h6">Custom Properties</Typography>
                                     </Grid>
                                     <Grid item xs={12} container rowSpacing={2}>
-                                        {scProperties
-                                            ? scProperties.map((property) => {
+                                        {properties
+                                            ? properties.map((property) => {
                                                   const propName = `property-${property.id}`;
                                                   return (
                                                       <Grid
@@ -830,7 +847,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                     <Button
                                         variant="outlined"
                                         color="primary"
-                                        disabled={!active || !isScenario || !deletable}
+                                        disabled={!active || !isScenario || !scDeletable}
                                         onClick={openDeleteDialog}
                                     >
                                         DELETE
@@ -839,7 +856,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                 <Button
                                     variant="outlined"
                                     color="primary"
-                                    disabled={!active || !isScenario || primary}
+                                    disabled={!active || !isScenario || scPrimary}
                                     onClick={openPrimaryDialog}
                                 >
                                     PROMOTE TO PRIMARY

+ 5 - 4
gui/src/icons/cycle.tsx

@@ -1,8 +1,9 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Cycle = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m11.16 13.2c.23-.15.3-.46.16-.69-.15-.23-.45-.3-.69-.16-1.95 1.23-4.46.95-6.1-.69-1-1-1.52-2.37-1.43-3.78.02-.28-.19-.51-.47-.53-.27 0-.51.19-.53.47-.1 1.69.52 3.35 1.72 4.55 1.15 1.15 2.66 1.74 4.18 1.74 1.08 0 2.18-.3 3.16-.92z" />
-    <path d="m8.01 2.27c-1.58 0-3.07.62-4.18 1.73-.01.01-.02.03-.03.04v-1.24c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2.81c0 .28.22.5.5.5h2.81c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.9c.11-.14.2-.28.33-.41 1.92-1.92 5.04-1.92 6.96 0 1.61 1.61 1.9 4.18.68 6.11-.15.23-.08.54.15.69.08.05.18.08.27.08.17 0 .33-.08.42-.23 1.47-2.32 1.13-5.41-.82-7.35-1.12-1.12-2.6-1.73-4.19-1.73z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m11.16 13.2c.23-.15.3-.46.16-.69-.15-.23-.45-.3-.69-.16-1.95 1.23-4.46.95-6.1-.69-1-1-1.52-2.37-1.43-3.78.02-.28-.19-.51-.47-.53-.27 0-.51.19-.53.47-.1 1.69.52 3.35 1.72 4.55 1.15 1.15 2.66 1.74 4.18 1.74 1.08 0 2.18-.3 3.16-.92z" />
+        <path d="m8.01 2.27c-1.58 0-3.07.62-4.18 1.73-.01.01-.02.03-.03.04v-1.24c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2.81c0 .28.22.5.5.5h2.81c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.9c.11-.14.2-.28.33-.41 1.92-1.92 5.04-1.92 6.96 0 1.61 1.61 1.9 4.18.68 6.11-.15.23-.08.54.15.69.08.05.18.08.27.08.17 0 .33-.08.42-.23 1.47-2.32 1.13-5.41-.82-7.35-1.12-1.12-2.6-1.73-4.19-1.73z" />
+    </SvgIcon>
 );

+ 6 - 5
gui/src/icons/datanode.tsx

@@ -1,9 +1,10 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Datanode = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m1.58 6.07 5.18 2.92c.38.22.81.33 1.25.33s.86-.11 1.25-.33l5.18-2.92c.31-.17.49-.49.49-.84s-.18-.67-.49-.84l-5.16-2.98c-.78-.45-1.76-.45-2.54 0l-5.17 2.97c-.31.18-.49.49-.49.84s.19.67.49.84zm5.65-3.8c.24-.14.5-.21.77-.21s.53.07.77.21l5.16 2.93-5.18 2.92c-.46.26-1.04.26-1.51 0l-5.18-2.87 5.16-2.98z" />
-    <path d="m14.85 8.26c-.14-.24-.44-.32-.68-.19l-5.25 2.97c-.56.32-1.27.32-1.83 0l-5.25-2.97c-.24-.13-.55-.05-.68.19-.14.24-.05.55.19.68l5.25 2.97c.43.25.92.37 1.41.37s.97-.12 1.41-.37l5.25-2.97c.24-.14.33-.44.19-.68z" />
-    <path d="m14.17 11.02-5.25 2.97c-.56.32-1.27.32-1.83 0l-5.25-2.97c-.24-.13-.55-.05-.68.19-.14.24-.05.55.19.68l5.25 2.97c.43.25.92.37 1.41.37s.97-.12 1.41-.37l5.25-2.97c.24-.14.33-.44.19-.68s-.44-.32-.68-.19z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m1.58 6.07 5.18 2.92c.38.22.81.33 1.25.33s.86-.11 1.25-.33l5.18-2.92c.31-.17.49-.49.49-.84s-.18-.67-.49-.84l-5.16-2.98c-.78-.45-1.76-.45-2.54 0l-5.17 2.97c-.31.18-.49.49-.49.84s.19.67.49.84zm5.65-3.8c.24-.14.5-.21.77-.21s.53.07.77.21l5.16 2.93-5.18 2.92c-.46.26-1.04.26-1.51 0l-5.18-2.87 5.16-2.98z" />
+        <path d="m14.85 8.26c-.14-.24-.44-.32-.68-.19l-5.25 2.97c-.56.32-1.27.32-1.83 0l-5.25-2.97c-.24-.13-.55-.05-.68.19-.14.24-.05.55.19.68l5.25 2.97c.43.25.92.37 1.41.37s.97-.12 1.41-.37l5.25-2.97c.24-.14.33-.44.19-.68z" />
+        <path d="m14.17 11.02-5.25 2.97c-.56.32-1.27.32-1.83 0l-5.25-2.97c-.24-.13-.55-.05-.68.19-.14.24-.05.55.19.68l5.25 2.97c.43.25.92.37 1.41.37s.97-.12 1.41-.37l5.25-2.97c.24-.14.33-.44.19-.68s-.44-.32-.68-.19z" />
+    </SvgIcon>
 );

+ 1 - 0
gui/src/icons/input.tsx

@@ -1,3 +1,4 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Input = (props: SvgIconProps) => (

+ 6 - 5
gui/src/icons/job.tsx

@@ -1,9 +1,10 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Job = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m3.76 3.69s-.04.04-.06.06v-1.3c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2.89c0 .28.22.5.5.5h2.89c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.97c.12-.15.22-.31.35-.44 1.97-1.97 5.19-1.97 7.16 0 .93.93 1.46 2.18 1.48 3.5 0 .27.23.49.5.49.28 0 .5-.23.49-.51-.03-1.58-.66-3.07-1.78-4.19-2.36-2.37-6.21-2.37-8.58 0z" />
-    <path d="m12.8 10.03h-2.89c-.28 0-.5.22-.5.5s.22.5.5.5h1.97c-.12.15-.22.31-.35.44-.96.96-2.23 1.48-3.58 1.48s-2.62-.53-3.58-1.48c-.88-.88-1.41-2.06-1.48-3.3-.02-.28-.25-.5-.53-.47-.28.01-.49.25-.47.53.08 1.49.71 2.9 1.77 3.96 1.18 1.18 2.74 1.77 4.29 1.77s3.11-.59 4.29-1.77l.06-.06v1.3c0 .28.22.5.5.5s.5-.22.5-.5v-2.89c0-.28-.22-.5-.5-.5z" />
-    <path d="m7.41 9.85c.13 0 .26-.05.35-.15l2.51-2.51c.2-.2.2-.51 0-.71s-.51-.2-.71 0l-2.16 2.16-.99-.99c-.2-.2-.51-.2-.71 0s-.2.51 0 .71l1.34 1.34c.1.1.23.15.35.15z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m3.76 3.69s-.04.04-.06.06v-1.3c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2.89c0 .28.22.5.5.5h2.89c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.97c.12-.15.22-.31.35-.44 1.97-1.97 5.19-1.97 7.16 0 .93.93 1.46 2.18 1.48 3.5 0 .27.23.49.5.49.28 0 .5-.23.49-.51-.03-1.58-.66-3.07-1.78-4.19-2.36-2.37-6.21-2.37-8.58 0z" />
+        <path d="m12.8 10.03h-2.89c-.28 0-.5.22-.5.5s.22.5.5.5h1.97c-.12.15-.22.31-.35.44-.96.96-2.23 1.48-3.58 1.48s-2.62-.53-3.58-1.48c-.88-.88-1.41-2.06-1.48-3.3-.02-.28-.25-.5-.53-.47-.28.01-.49.25-.47.53.08 1.49.71 2.9 1.77 3.96 1.18 1.18 2.74 1.77 4.29 1.77s3.11-.59 4.29-1.77l.06-.06v1.3c0 .28.22.5.5.5s.5-.22.5-.5v-2.89c0-.28-.22-.5-.5-.5z" />
+        <path d="m7.41 9.85c.13 0 .26-.05.35-.15l2.51-2.51c.2-.2.2-.51 0-.71s-.51-.2-.71 0l-2.16 2.16-.99-.99c-.2-.2-.51-.2-.71 0s-.2.51 0 .71l1.34 1.34c.1.1.23.15.35.15z" />
+    </SvgIcon>
 );

+ 1 - 0
gui/src/icons/output.tsx

@@ -1,3 +1,4 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Output = (props: SvgIconProps) => (

+ 4 - 3
gui/src/icons/pipeline.tsx

@@ -1,7 +1,8 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Pipeline = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m1.08 6.71c.27 0 .48-.21.49-.47h3.64c.26 0 .47.21.47.47v.84h-.09c-.28 0-.5.22-.5.5s.22.5.5.5h.09v3.77c0 1.11.9 2.01 2.01 2.01h6.5c.06.21.24.37.47.37.28 0 .5-.22.5-.5v-4.4c0-.28-.22-.5-.5-.5s-.5.22-.5.5v.05h-4v-1.33c.19-.07.32-.25.32-.46s-.13-.39-.32-.46v-1.33h3.84c.02.25.23.46.49.46.28 0 .5-.22.5-.5v-4.42c0-.28-.22-.5-.5-.5-.26 0-.47.2-.49.46l-12.44-.02c-.03-.25-.23-.44-.49-.44-.28 0-.5.22-.5.5v4.4c0 .28.22.5.5.5zm13.09 4.13v2.49h-6.48c-.56 0-1.01-.45-1.01-1.01v-3.77h2.49v1.74l.05.55zm-4.45-5.58-.55.05v2.24h-2.49v-.84c0-.81-.66-1.47-1.47-1.47h-3.63v-2.48l12.42.02v2.49h-4.28z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m1.08 6.71c.27 0 .48-.21.49-.47h3.64c.26 0 .47.21.47.47v.84h-.09c-.28 0-.5.22-.5.5s.22.5.5.5h.09v3.77c0 1.11.9 2.01 2.01 2.01h6.5c.06.21.24.37.47.37.28 0 .5-.22.5-.5v-4.4c0-.28-.22-.5-.5-.5s-.5.22-.5.5v.05h-4v-1.33c.19-.07.32-.25.32-.46s-.13-.39-.32-.46v-1.33h3.84c.02.25.23.46.49.46.28 0 .5-.22.5-.5v-4.42c0-.28-.22-.5-.5-.5-.26 0-.47.2-.49.46l-12.44-.02c-.03-.25-.23-.44-.49-.44-.28 0-.5.22-.5.5v4.4c0 .28.22.5.5.5zm13.09 4.13v2.49h-6.48c-.56 0-1.01-.45-1.01-1.01v-3.77h2.49v1.74l.05.55zm-4.45-5.58-.55.05v2.24h-2.49v-.84c0-.81-.66-1.47-1.47-1.47h-3.63v-2.48l12.42.02v2.49h-4.28z" />
+    </SvgIcon>
 );

+ 4 - 3
gui/src/icons/scenario.tsx

@@ -1,7 +1,8 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Scenario = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m15.06 6.21h-6.93l6.84-.97c.21-.03.4-.14.52-.31s.18-.38.15-.59l-.28-1.99c-.12-.86-.92-1.46-1.78-1.34l-12.05 1.7c-.42.06-.79.28-1.04.61-.25.34-.36.75-.3 1.17l.32 2.25v6.68c0 .87.71 1.58 1.58 1.58h12.17c.87 0 1.58-.71 1.58-1.58v-6.44c0-.43-.35-.77-.77-.77zm-8.41-.8-2.44.34 1.23-2.58 2.44-.34zm2.42-2.75 2.44-.34-1.23 2.58-2.44.34zm4.65-.66c.32-.05.61.18.65.49l.25 1.79-3.16.45 1.23-2.58 1.03-.14zm-12.44 1.93c.09-.12.23-.2.38-.22l2.59-.37-1.23 2.58-1.6.23-.25-1.79c-.02-.15.02-.31.11-.43zm13.55 9.5c0 .32-.26.58-.58.58h-12.17c-.32 0-.58-.26-.58-.58v-6.22h13.33z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m15.06 6.21h-6.93l6.84-.97c.21-.03.4-.14.52-.31s.18-.38.15-.59l-.28-1.99c-.12-.86-.92-1.46-1.78-1.34l-12.05 1.7c-.42.06-.79.28-1.04.61-.25.34-.36.75-.3 1.17l.32 2.25v6.68c0 .87.71 1.58 1.58 1.58h12.17c.87 0 1.58-.71 1.58-1.58v-6.44c0-.43-.35-.77-.77-.77zm-8.41-.8-2.44.34 1.23-2.58 2.44-.34zm2.42-2.75 2.44-.34-1.23 2.58-2.44.34zm4.65-.66c.32-.05.61.18.65.49l.25 1.79-3.16.45 1.23-2.58 1.03-.14zm-12.44 1.93c.09-.12.23-.2.38-.22l2.59-.37-1.23 2.58-1.6.23-.25-1.79c-.02-.15.02-.31.11-.43zm13.55 9.5c0 .32-.26.58-.58.58h-12.17c-.32 0-.58-.26-.58-.58v-6.22h13.33z" />
+    </SvgIcon>
 );

+ 4 - 3
gui/src/icons/task.tsx

@@ -1,7 +1,8 @@
+import React from "react";
 import { SvgIcon, SvgIconProps } from "@mui/material";
 
 export const Task = (props: SvgIconProps) => (
-  <SvgIcon {...props} viewBox="0 0 16 16">
-    <path d="m15.17 5.42c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-.4c0-1.11-.91-2.02-2.02-2.02h-.55v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-2.08v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-2.08v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-.25c-1.11 0-2.02.91-2.02 2.02v.4h-1.16c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v2.08h-1.14c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v2.08h-1.14c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v.4c0 1.11.91 2.02 2.02 2.02h.25v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h2.08v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h2.08v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h.55c1.11 0 2.02-.91 2.02-2.02v-.4h1.18c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-2.08h1.18c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-2.08h1.18zm-2.18 6.57c0 .56-.46 1.02-1.02 1.02h-7.98c-.56 0-1.02-.46-1.02-1.02v-7.97c0-.56.46-1.02 1.02-1.02h7.97c.56 0 1.02.46 1.02 1.02v7.97z" />
-  </SvgIcon>
+    <SvgIcon {...props} viewBox="0 0 16 16">
+        <path d="m15.17 5.42c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-.4c0-1.11-.91-2.02-2.02-2.02h-.55v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-2.08v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-2.08v-1.14c0-.28-.22-.5-.5-.5s-.5.22-.5.5v1.14h-.25c-1.11 0-2.02.91-2.02 2.02v.4h-1.16c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v2.08h-1.14c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v2.08h-1.14c-.28 0-.5.22-.5.5s.22.5.5.5h1.14v.4c0 1.11.91 2.02 2.02 2.02h.25v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h2.08v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h2.08v1.14c0 .28.22.5.5.5s.5-.22.5-.5v-1.14h.55c1.11 0 2.02-.91 2.02-2.02v-.4h1.18c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-2.08h1.18c.28 0 .5-.22.5-.5s-.22-.5-.5-.5h-1.18v-2.08h1.18zm-2.18 6.57c0 .56-.46 1.02-1.02 1.02h-7.98c-.56 0-1.02-.46-1.02-1.02v-7.97c0-.56.46-1.02 1.02-1.02h7.97c.56 0 1.02.46 1.02 1.02v7.97z" />
+    </SvgIcon>
 );

+ 2 - 1
gui/src/projectstorm/NodeWidget.tsx

@@ -11,7 +11,7 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import { useCallback } from "react";
+import React, { useCallback } from "react";
 import styled from "@emotion/styled";
 import { DefaultPortModel, PortWidget } from "@projectstorm/react-diagrams";
 import { DiagramEngine } from "@projectstorm/react-diagrams-core";
@@ -22,6 +22,7 @@ import { TaipyNodeModel } from "./models";
 import { IN_PORT_NAME } from "../utils/diagram";
 import { Input, Output } from "../icons";
 
+// eslint-disable-next-line @typescript-eslint/no-namespace
 namespace S {
     export const Node = styled.div<{ background?: string; selected?: boolean }>`
         background-color: ${(p) => p.background};

+ 18 - 16
gui/src/projectstorm/factories.tsx

@@ -11,31 +11,33 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import { AbstractReactFactory, GenerateModelEvent, GenerateWidgetEvent, AbstractModelFactory } from "@projectstorm/react-canvas-core";
+import React from "react";
+import { AbstractReactFactory, GenerateWidgetEvent, AbstractModelFactory } from "@projectstorm/react-canvas-core";
 import { DiagramEngine } from "@projectstorm/react-diagrams-core";
 import { TaipyNodeModel, TaipyPortModel } from "./models";
 import NodeWidget from "./NodeWidget";
 
 export class TaipyNodeFactory extends AbstractReactFactory<TaipyNodeModel, DiagramEngine> {
-  constructor(nodeType: string) {
-    super(nodeType);
-  }
+    constructor(nodeType: string) {
+        super(nodeType);
+    }
 
-  generateReactWidget(event: GenerateWidgetEvent<TaipyNodeModel>): JSX.Element {
-    return <NodeWidget engine={this.engine} node={event.model} />;
-  }
+    generateReactWidget(event: GenerateWidgetEvent<TaipyNodeModel>): JSX.Element {
+        return <NodeWidget engine={this.engine} node={event.model} />;
+    }
 
-  generateModel(_: GenerateModelEvent): TaipyNodeModel {
-    return new TaipyNodeModel();
-  }
+    generateModel(): TaipyNodeModel {
+        return new TaipyNodeModel();
+    }
 }
 
 export class TaipyPortFactory extends AbstractModelFactory<TaipyPortModel, DiagramEngine> {
-  constructor() {
-    super("taipy-port");
-  }
+    constructor() {
+        super("taipy-port");
+    }
 
-  generateModel(_: GenerateModelEvent): TaipyPortModel {
-    return new TaipyPortModel({ type: "taipy-port", name: "fred" });
-  }
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    generateModel(): TaipyPortModel {
+        return new TaipyPortModel({ type: "taipy-port", name: "fred" });
+    }
 }

+ 76 - 20
gui/src/utils.ts

@@ -13,37 +13,93 @@
 
 // id, is_primary, config_id, creation_date, label, tags, properties(key, value), pipelines(id, label), authorized_tags, deletable
 export type ScenarioFull = [
-  string,
-  boolean,
-  string,
-  string,
-  string,
-  string[],
-  Array<[string, string]>,
-  Array<[string, string]>,
-  string[],
-  boolean
+    string,
+    boolean,
+    string,
+    string,
+    string,
+    string[],
+    Array<[string, string]>,
+    Array<[string, string]>,
+    string[],
+    boolean
 ];
 
 export enum ScFProps {
-    id, is_primary, config_id, creation_date, label, tags, properties, pipelines, authorized_tags, deletable
+    id,
+    is_primary,
+    config_id,
+    creation_date,
+    label,
+    tags,
+    properties,
+    pipelines,
+    authorized_tags,
+    deletable,
 }
 export const ScenarioFullLength = Object.keys(ScFProps).length / 2;
 
 export interface ScenarioDict {
-  config: string;
-  name: string;
-  date: string;
-  properties: Array<[string, string]>;
+    config: string;
+    name: string;
+    date: string;
+    properties: Array<[string, string]>;
 }
 
 export type Property = {
-  id: string;
-  key: string;
-  value: string;
+    id: string;
+    key: string;
+    value: string;
 };
 
 export const FlagSx = {
-  color: "common.white",
-  fontSize: "0.75em",
+    color: "common.white",
+    fontSize: "0.75em",
+};
+
+export const BadgePos = {
+    vertical: "top",
+    horizontal: "left",
+};
+
+export const BadgeSx = {
+    flex: "0 0 auto",
+
+    "& .MuiBadge-badge": {
+        fontSize: "1rem",
+        width: "1em",
+        height: "1em",
+        p: 0,
+        minWidth: "0",
+    },
+};
+
+export const MainBoxSx = {
+    maxWidth: 300,
+    overflowY: "auto",
+};
+
+export const BaseTreeViewSx = {
+    mb: 2,
+
+    "& .MuiTreeItem-root .MuiTreeItem-content": {
+        mb: 0.5,
+        py: 1,
+        px: 2,
+        borderRadius: 0.5,
+        backgroundColor: "background.paper",
+    },
+
+    "& .MuiTreeItem-iconContainer:empty": {
+        display: "none",
+    },
+    maxHeight: "50vh"
+};
+
+export const ParentItemSx = {
+    "& > .MuiTreeItem-content": {
+        ".MuiTreeItem-label": {
+            fontWeight: "fontWeightBold",
+        },
+    },
 };

+ 2 - 4
gui/src/utils/diagram.ts

@@ -20,12 +20,10 @@ import createEngine, {
     DiagramEngine,
     DagreEngine,
     PointModel,
-    DeleteItemsAction,
 } from "@projectstorm/react-diagrams";
 
-import { DataNode, Pipeline, Scenario, Task } from "./names";
 import { getNodeColor } from "./config";
-import { TaipyDiagramModel, TaipyNodeModel, TaipyPortModel } from "../projectstorm/models";
+import { TaipyDiagramModel, TaipyNodeModel } from "../projectstorm/models";
 import { TaipyNodeFactory, TaipyPortFactory } from "../projectstorm/factories";
 import { nodeTypes } from "./config";
 import { DisplayModel } from "./types";
@@ -63,7 +61,7 @@ export const getLinkId = (link: LinkModel) =>
     )}`;
 export const getNodeId = (node: DefaultNodeModel) => `${node.getType()}.${node.getID()}`;
 
-export const createNode = (nodeType: string, id: string, name: string, subtype: string, createPorts = true) =>
+export const createNode = (nodeType: string, id: string, name: string, subtype: string) =>
     new TaipyNodeModel({
         id: id,
         type: nodeType,

+ 48 - 42
gui/webpack.config.js

@@ -13,53 +13,59 @@
 
 const webpack = require("webpack");
 const path = require("path");
+const ESLintPlugin = require("eslint-webpack-plugin");
 require("dotenv").config();
 
 module.exports = (_env, options) => {
-  return {
-    mode: options.mode, // "development" | "production"
-    entry: ["./src/index.ts"],
-    output: {
-      filename: "taipy-gui-core.js",
-      path: path.resolve(__dirname, "../src/taipy/gui_core/lib"),
-      library: {
-        // Camel case transformation of the library name "example"
-        name: "TaipyGuiCore",
-        type: "umd",
-      },
-      publicPath: "/",
-    },
-    // The Taipy GUI library is indicated as external so that it is
-    // excluded from bundling.
-    externals: { "taipy-gui": "TaipyGui" },
+    return {
+        mode: options.mode, // "development" | "production"
+        entry: ["./src/index.ts"],
+        output: {
+            filename: "taipy-gui-core.js",
+            path: path.resolve(__dirname, "../src/taipy/gui_core/lib"),
+            library: {
+                // Camel case transformation of the library name "example"
+                name: "TaipyGuiCore",
+                type: "umd",
+            },
+            publicPath: "/",
+        },
+        // The Taipy GUI library is indicated as external so that it is
+        // excluded from bundling.
+        externals: { "taipy-gui": "TaipyGui" },
 
-    // Enable sourcemaps for debugging webpack's output.
-    devtool: options.mode === "development" && "inline-source-map",
-    resolve: {
-      // All the code is TypeScript
-      extensions: [".ts", ".tsx", ".js"],
-    },
+        // Enable sourcemaps for debugging webpack's output.
+        devtool: options.mode === "development" && "inline-source-map",
+        resolve: {
+            // All the code is TypeScript
+            extensions: [".ts", ".tsx", ".js"],
+        },
 
-    module: {
-      rules: [
-        {
-          test: /\.tsx?$/,
-          use: "ts-loader",
-          exclude: /node_modules/,
+        module: {
+            rules: [
+                {
+                    test: /\.tsx?$/,
+                    use: "ts-loader",
+                    exclude: /node_modules/,
+                },
+            ],
         },
-      ],
-    },
 
-    plugins: [
-      new webpack.DllReferencePlugin({
-        // We assume the current directory is orignal directory in the taipy-gui repository.
-        // If this file is moved, this path must be updated
-        manifest: path.resolve(
-          __dirname,
-          `${process.env.TAIPY_GUI_DIR}/taipy/gui/webapp/taipy-gui-deps-manifest.json`
-        ),
-        name: "TaipyGuiDependencies",
-      }),
-    ],
-  };
+        plugins: [
+            new webpack.DllReferencePlugin({
+                // We assume the current directory is orignal directory in the taipy-gui repository.
+                // If this file is moved, this path must be updated
+                manifest: path.resolve(
+                    __dirname,
+                    `${process.env.TAIPY_GUI_DIR}/taipy/gui/webapp/taipy-gui-deps-manifest.json`
+                ),
+                name: "TaipyGuiDependencies",
+            }),
+            new ESLintPlugin({
+                extensions: [`ts`, `tsx`],
+                exclude: [`/node_modules/`],
+                eslintPath: require.resolve("eslint"),
+            }),
+        ],
+    };
 };

+ 179 - 123
src/taipy/gui_core/GuiCoreLib.py

@@ -11,6 +11,7 @@
 
 import typing as t
 from datetime import datetime
+from threading import Lock
 
 from dateutil import parser
 
@@ -26,44 +27,34 @@ from taipy.gui.utils import _TaipyBase
 from ..version import _get_version
 
 
-class GuiCoreScenarioAdapter(_TaipyBase):
+class _GuiCoreScenarioAdapter(_TaipyBase):
     __INNER_PROPS = ["name"]
 
     def get(self):
         data = super().get()
         if isinstance(data, Scenario):
-            return [
-                data.id,
-                data.is_primary,
-                data.config_id,
-                data.creation_date,
-                data.get_simple_label(),
-                list(data.tags),
-                [(k, v) for k, v in data.properties.items() if k not in GuiCoreScenarioAdapter.__INNER_PROPS],
-                [(p.id, p.get_simple_label()) for p in data.pipelines.values()],
-                list(data.properties.get("authorized_tags", set())),
-                not data.is_primary,  # deletable
-            ]
-        return data
+            scenario = tp.get(data.id)
+            if scenario:
+                return [
+                    scenario.id,
+                    scenario.is_primary,
+                    scenario.config_id,
+                    scenario.creation_date,
+                    scenario.get_simple_label(),
+                    list(scenario.tags),
+                    [(k, v) for k, v in scenario.properties.items() if k not in _GuiCoreScenarioAdapter.__INNER_PROPS],
+                    [(p.id, p.get_simple_label()) for p in scenario.pipelines.values()],
+                    list(scenario.properties.get("authorized_tags", set())),
+                    tp.is_deletable(scenario),  # deletable
+                ]
+        return None
 
     @staticmethod
     def get_hash():
         return _TaipyBase._HOLDER_PREFIX + "Sc"
 
 
-class GuiCoreScenarioIdAdapter(_TaipyBase):
-    def get(self):
-        data = super().get()
-        if isinstance(data, Scenario):
-            return data.id
-        return data
-
-    @staticmethod
-    def get_hash():
-        return _TaipyBase._HOLDER_PREFIX + "ScI"
-
-
-class GuiCoreScenarioDagAdapter(_TaipyBase):
+class _GuiCoreScenarioDagAdapter(_TaipyBase):
     @staticmethod
     def get_entity_type(node: t.Any):
         return DataNode.__name__ if isinstance(node.entity, DataNode) else node.type
@@ -71,31 +62,33 @@ class GuiCoreScenarioDagAdapter(_TaipyBase):
     def get(self):
         data = super().get()
         if isinstance(data, Scenario):
-            dag = data._get_dag()
-            nodes = dict()
-            for id, node in dag.nodes.items():
-                entityType = GuiCoreScenarioDagAdapter.get_entity_type(node)
-                cat = nodes.get(entityType)
-                if cat is None:
-                    cat = dict()
-                    nodes[entityType] = cat
-                cat[id] = {
-                    "name": node.entity.get_simple_label(),
-                    "type": node.entity.storage_type() if hasattr(node.entity, "storage_type") else None,
-                }
-            return [
-                data.get_label(),
-                nodes,
-                [
-                    (
-                        GuiCoreScenarioDagAdapter.get_entity_type(e.src),
-                        e.src.entity.id,
-                        GuiCoreScenarioDagAdapter.get_entity_type(e.dest),
-                        e.dest.entity.id,
-                    )
-                    for e in dag.edges
-                ],
-            ]
+            scenario = tp.get(data.id)
+            if scenario:
+                dag = data._get_dag()
+                nodes = dict()
+                for id, node in dag.nodes.items():
+                    entityType = _GuiCoreScenarioDagAdapter.get_entity_type(node)
+                    cat = nodes.get(entityType)
+                    if cat is None:
+                        cat = dict()
+                        nodes[entityType] = cat
+                    cat[id] = {
+                        "name": node.entity.get_simple_label(),
+                        "type": node.entity.storage_type() if hasattr(node.entity, "storage_type") else None,
+                    }
+                return [
+                    data.get_label(),
+                    nodes,
+                    [
+                        (
+                            _GuiCoreScenarioDagAdapter.get_entity_type(e.src),
+                            e.src.entity.id,
+                            _GuiCoreScenarioDagAdapter.get_entity_type(e.dest),
+                            e.dest.entity.id,
+                        )
+                        for e in dag.edges
+                    ],
+                ]
         return None
 
     @staticmethod
@@ -103,7 +96,7 @@ class GuiCoreScenarioDagAdapter(_TaipyBase):
         return _TaipyBase._HOLDER_PREFIX + "ScG"
 
 
-class GuiCoreContext(CoreEventConsumerBase):
+class _GuiCoreContext(CoreEventConsumerBase):
     __PROP_ENTITY_ID = "id"
     __PROP_SCENARIO_CONFIG_ID = "config"
     __PROP_SCENARIO_DATE = "date"
@@ -118,47 +111,57 @@ class GuiCoreContext(CoreEventConsumerBase):
 
     def __init__(self, gui: Gui) -> None:
         self.gui = gui
-        self.cycles_scenarios: t.Optional[t.List[t.Union[Cycle, Scenario]]] = None
+        self.scenarios_base_level: t.Optional[t.List[t.Union[Cycle, Scenario]]] = None
+        self.data_nodes_base_level: t.Optional[t.List[t.Union[Cycle, Scenario, Pipeline, DataNode]]] = None
         self.scenario_configs: t.Optional[t.List[t.Tuple[str, str]]] = None
         # register to taipy core notification
         reg_id, reg_queue = Notifier.register()
+        self.lock = Lock()
         super().__init__(reg_id, reg_queue)
         self.start()
 
     def process_event(self, event: Event):
-        if event.entity_type == EventEntityType.SCENARIO or event.entity_type == EventEntityType.CYCLE:
-            self.cycles_scenarios = None
+        if event.entity_type == EventEntityType.SCENARIO:
+            self.scenarios_base_level = None
+            scenario = tp.get(event.entity_id) if event.operation.value != 3 else None
             self.gui.broadcast(
-                GuiCoreContext._CORE_CHANGED_NAME,
-                {"scenario": (event.entity_id if event.entity_type == EventEntityType.SCENARIO else True) or True},
+                _GuiCoreContext._CORE_CHANGED_NAME,
+                {"scenario": event.entity_id if scenario else True},
             )
-        elif event.entity_type == EventEntityType.PIPELINE and event.entity_id:
-            pipeline = tp.get(event.entity_id)
-            self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME, {"scenario": [x for x in pipeline.parent_ids]})
+            self.data_nodes_base_level = None
+        elif event.entity_type == EventEntityType.PIPELINE and event.entity_id:  # TODO import EventOperation
+            pipeline = tp.get(event.entity_id) if event.operation.value != 3 else None
+            if pipeline:
+                if hasattr(pipeline, "parent_ids") and pipeline.parent_ids:
+                    self.gui.broadcast(
+                        _GuiCoreContext._CORE_CHANGED_NAME, {"scenario": [x for x in pipeline.parent_ids]}
+                    )
 
     @staticmethod
     def scenario_adapter(data):
-        if isinstance(data, Cycle):
-            return (data.id, data.name, tp.get_scenarios(data), 0, False)
-        elif isinstance(data, Scenario):
-            return (data.id, data.name, None, 1, data.is_primary)
-        return data
+        if hasattr(data, "id") and tp.get(data.id) is not None:
+            if isinstance(data, Cycle):
+                return (data.id, data.name, tp.get_scenarios(data), 0, False)
+            elif isinstance(data, Scenario):
+                return (data.id, data.name, None, 1, data.is_primary)
+        return None
 
     def get_scenarios(self):
-        if self.cycles_scenarios is None:
-            self.cycles_scenarios = []
-            for cycle, scenarios in tp.get_cycles_scenarios().items():
-                if cycle is None:
-                    self.cycles_scenarios.extend(scenarios)
-                else:
-                    self.cycles_scenarios.append(cycle)
-        return self.cycles_scenarios
+        with self.lock:
+            if self.scenarios_base_level is None:
+                self.scenarios_base_level = []
+                for cycle, scenarios in tp.get_cycles_scenarios().items():
+                    if cycle is None:
+                        self.scenarios_base_level.extend(scenarios)
+                    else:
+                        self.scenarios_base_level.append(cycle)
+            return self.scenarios_base_level
 
     def select_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) == 0:
             return
-        state.assign(GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, args[0])
+        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR, args[0])
 
     def get_scenario_by_id(self, id: str) -> t.Optional[Scenario]:
         if not id:
@@ -169,11 +172,12 @@ class GuiCoreContext(CoreEventConsumerBase):
             return None
 
     def get_scenario_configs(self):
-        if self.scenario_configs is None:
-            configs = tp.Config.scenarios
-            if isinstance(configs, dict):
-                self.scenario_configs = [(id, f"{c.id}") for id, c in configs.items()]
-        return self.scenario_configs
+        with self.lock:
+            if self.scenario_configs is None:
+                configs = tp.Config.scenarios
+                if isinstance(configs, dict):
+                    self.scenario_configs = [(id, f"{c.id}") for id, c in configs.items()]
+            return self.scenario_configs
 
     def crud_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
         args = payload.get("args")
@@ -190,97 +194,142 @@ class GuiCoreContext(CoreEventConsumerBase):
         delete = args[1]
         data = args[2]
         scenario = None
-        name = data.get(GuiCoreContext.__PROP_ENTITY_NAME)
+        name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
         if update:
-            scenario_id = data.get(GuiCoreContext.__PROP_ENTITY_ID)
+            scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
             if delete:
                 try:
                     tp.delete(scenario_id)
                 except Exception as e:
-                    state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error deleting Scenario. {e}")
+                    state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error deleting Scenario. {e}")
             else:
                 scenario = tp.get(scenario_id)
         else:
-            config_id = data.get(GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
+            config_id = data.get(_GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
             scenario_config = tp.Config.scenarios.get(config_id)
             if scenario_config is None:
-                state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})")
+                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid configuration id ({config_id})")
                 return
-            date_str = data.get(GuiCoreContext.__PROP_SCENARIO_DATE)
+            date_str = data.get(_GuiCoreContext.__PROP_SCENARIO_DATE)
             try:
                 date = parser.parse(date_str) if isinstance(date_str, str) else None
             except Exception as e:
-                state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
+                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Invalid date ({date_str}).{e}")
                 return
             try:
                 scenario = tp.create_scenario(scenario_config, date, name)
             except Exception as e:
-                state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
+                state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
         if scenario:
             with scenario as sc:
-                sc._properties[GuiCoreContext.__PROP_ENTITY_NAME] = name
+                sc._properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
                 if props := data.get("properties"):
                     try:
                         for prop in props:
                             key = prop.get("key")
-                            if key and key not in GuiCoreContext.__SCENARIO_PROPS:
+                            if key and key not in _GuiCoreContext.__SCENARIO_PROPS:
                                 sc._properties[key] = prop.get("value")
-                        state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
+                        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
                     except Exception as e:
-                        state.assign(GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
+                        state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
 
     def edit_entity(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
         data = args[0]
-        entity_id = data.get(GuiCoreContext.__PROP_ENTITY_ID)
+        entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity: t.Union[Scenario, Pipeline] = tp.get(entity_id)
         if entity:
             with entity as ent:
                 try:
                     if isinstance(entity, Scenario):
-                        primary = data.get(GuiCoreContext.__PROP_SCENARIO_PRIMARY)
+                        primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                         if primary is True:
                             tp.set_primary(ent)
-                        tags = data.get(GuiCoreContext.__PROP_SCENARIO_TAGS)
+                        tags = data.get(_GuiCoreContext.__PROP_SCENARIO_TAGS)
                         if isinstance(tags, (list, tuple)):
                             ent.tags = {t for t in tags}
-                    name = data.get(GuiCoreContext.__PROP_ENTITY_NAME)
+                    name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
                     if isinstance(name, str):
-                        ent.properties[GuiCoreContext.__PROP_ENTITY_NAME] = name
+                        ent.properties[_GuiCoreContext.__PROP_ENTITY_NAME] = name
                     props = data.get("properties")
                     if isinstance(props, (list, tuple)):
                         for prop in props:
                             key = prop.get("key")
-                            if key and key not in GuiCoreContext.__SCENARIO_PROPS:
+                            if key and key not in _GuiCoreContext.__SCENARIO_PROPS:
                                 ent.properties[key] = prop.get("value")
                     deleted_props = data.get("deleted_properties")
                     if isinstance(deleted_props, (list, tuple)):
                         for prop in deleted_props:
                             key = prop.get("key")
-                            if key and key not in GuiCoreContext.__SCENARIO_PROPS:
+                            if key and key not in _GuiCoreContext.__SCENARIO_PROPS:
                                 ent.properties.pop(key, None)
-                    state.assign(GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
+                    state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
                 except Exception as e:
-                    state.assign(GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating Scenario. {e}")
+                    state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating Scenario. {e}")
 
     def submit_entity(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
         args = payload.get("args")
         if args is None or not isinstance(args, list) or len(args) < 1 or not isinstance(args[0], dict):
             return
         data = args[0]
-        entity_id = data.get(GuiCoreContext.__PROP_ENTITY_ID)
+        entity_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
         entity = tp.get(entity_id)
         if entity:
             try:
                 tp.submit(entity)
-                state.assign(GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
+                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
             except Exception as e:
-                state.assign(GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
+                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
+
+    def get_datanodes_tree(self):
+        with self.lock:
+            if self.data_nodes_base_level is None:
+                self.data_nodes_base_level = _GuiCoreContext.__get_data_nodes()
+                for cycle, scenarios in tp.get_cycles_scenarios().items():
+                    if cycle is None:
+                        self.data_nodes_base_level.extend(scenarios)
+                    else:
+                        self.data_nodes_base_level.append(cycle)
+            return self.data_nodes_base_level
+
+    @staticmethod
+    def __get_data_nodes(id: t.Optional[str] = None):
+        def from_parent(dn: DataNode):
+            if id is None and dn.owner_id is None:
+                return True
+            return False if id is None or dn.owner_id is None else dn.owner_id == id
+
+        return [x for x in tp.get_data_nodes() if from_parent(x)]
+
+    @staticmethod
+    def data_node_adapter(data):
+        if hasattr(data, "id") and tp.get(data.id) is not None:
+            if isinstance(data, Cycle):
+                return (
+                    data.id,
+                    data.get_simple_label(),
+                    _GuiCoreContext.__get_data_nodes(data.id) + tp.get_scenarios(data),
+                    0,
+                    False,
+                )
+            elif isinstance(data, Scenario):
+                return (
+                    data.id,
+                    data.get_simple_label(),
+                    _GuiCoreContext.__get_data_nodes(data.id) + [tp.get(p) for p in data._pipelines],
+                    1,
+                    data.is_primary,
+                )
+            elif isinstance(data, Pipeline):
+                return (data.id, data.get_simple_label(), _GuiCoreContext.__get_data_nodes(data.id), 2, False)
+            elif isinstance(data, DataNode):
+                return (data.id, data.get_simple_label(), None, 3, False)
+        return None
 
     def broadcast_core_changed(self):
-        self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME, "")
+        self.gui.broadcast(_GuiCoreContext._CORE_CHANGED_NAME, "")
 
 
 class GuiCore(ElementLibrary):
@@ -296,18 +345,19 @@ class GuiCore(ElementLibrary):
                 "show_primary_flag": ElementProperty(PropertyType.dynamic_boolean, True),
                 "value": ElementProperty(PropertyType.lov_value),
                 "on_change": ElementProperty(PropertyType.function),
+                "height": ElementProperty(PropertyType.string, "50vh"),
             },
             inner_properties={
                 "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
                 "on_scenario_crud": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
                 "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
-                "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(PropertyType.react, f"{{{GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR}}}"),
+                "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
+                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR}}}"),
                 "type": ElementProperty(PropertyType.inner, Scenario),
-                "adapter": ElementProperty(PropertyType.inner, GuiCoreContext.scenario_adapter),
+                "adapter": ElementProperty(PropertyType.inner, _GuiCoreContext.scenario_adapter),
                 "scenario_edit": ElementProperty(
-                    GuiCoreScenarioAdapter,
-                    f"{{{__CTX_VAR_NAME}.get_scenario_by_id({GuiCoreContext._SCENARIO_SELECTOR_ID_VAR})}}",
+                    _GuiCoreScenarioAdapter,
+                    f"{{{__CTX_VAR_NAME}.get_scenario_by_id({_GuiCoreContext._SCENARIO_SELECTOR_ID_VAR})}}",
                 ),
                 "on_scenario_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_scenario}}"),
             },
@@ -316,7 +366,7 @@ class GuiCore(ElementLibrary):
             "scenario",
             {
                 "id": ElementProperty(PropertyType.string),
-                "scenario": ElementProperty(GuiCoreScenarioAdapter),
+                "scenario": ElementProperty(_GuiCoreScenarioAdapter),
                 "active": ElementProperty(PropertyType.dynamic_boolean, True),
                 "expandable": ElementProperty(PropertyType.boolean, True),
                 "expanded": ElementProperty(PropertyType.dynamic_boolean, False),
@@ -333,15 +383,15 @@ class GuiCore(ElementLibrary):
                 "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_entity}}"),
                 "on_submit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.submit_entity}}"),
                 "on_delete": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.crud_scenario}}"),
-                "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
-                "error": ElementProperty(PropertyType.react, f"{{{GuiCoreContext._SCENARIO_VIZ_ERROR_VAR}}}"),
+                "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
+                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR}}}"),
             },
         ),
         "dag": Element(
             "scenario",
             {
                 "id": ElementProperty(PropertyType.string),
-                "scenario": ElementProperty(GuiCoreScenarioDagAdapter),
+                "scenario": ElementProperty(_GuiCoreScenarioDagAdapter),
                 "button_label": ElementProperty(PropertyType.dynamic_string),
                 "show": ElementProperty(PropertyType.dynamic_boolean, True),
                 "with_button": ElementProperty(PropertyType.boolean, True),
@@ -349,17 +399,23 @@ class GuiCore(ElementLibrary):
                 "height": ElementProperty(PropertyType.string),
             },
             inner_properties={
-                "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
+                "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
             },
         ),
         "data_node_selector": Element(
-            "val",
+            "value",
             {
-                "val": ElementProperty(GuiCoreScenarioDagAdapter),
+                "display_cycles": ElementProperty(PropertyType.dynamic_boolean, True),
+                "show_primary_flag": ElementProperty(PropertyType.dynamic_boolean, True),
+                "value": ElementProperty(PropertyType.lov_value),
+                "on_change": ElementProperty(PropertyType.function),
+                "height": ElementProperty(PropertyType.string, "50vh"),
             },
             inner_properties={
-                "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
-                "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
+                "datanodes": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_datanodes_tree()}}"),
+                "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
+                "type": ElementProperty(PropertyType.inner, DataNode),
+                "adapter": ElementProperty(PropertyType.inner, _GuiCoreContext.data_node_adapter),
             },
         ),
     }
@@ -374,13 +430,13 @@ class GuiCore(ElementLibrary):
         return ["lib/taipy-gui-core.js"]
 
     def on_init(self, gui: Gui) -> t.Optional[t.Tuple[str, t.Any]]:
-        return GuiCore.__CTX_VAR_NAME, GuiCoreContext(gui)
+        return GuiCore.__CTX_VAR_NAME, _GuiCoreContext(gui)
 
     def on_user_init(self, state: State):
         for var in [
-            GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
-            GuiCoreContext._SCENARIO_SELECTOR_ID_VAR,
-            GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
+            _GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR,
+            _GuiCoreContext._SCENARIO_SELECTOR_ID_VAR,
+            _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
         ]:
             state._add_attribute(var, "")
 

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott