Prechádzať zdrojové kódy

#157 DataNode selector (#176)

* feat: Node explorer UI implementation - Initial commit

* fix: Removed unused import

* feat: Data Node Selector initial API integration

* fix: Fix datanodes structure and change data_node_explorer to data_node_selector

* fix: Code cleanup and PR comment fixes

* fix: Additional code cleanups
pavle-avaiga 2 rokov pred
rodič
commit
25fa42e836

+ 204 - 0
gui/src/NodeSelector.tsx

@@ -0,0 +1,204 @@
+/*
+ * Copyright 2023 Avaiga Private Limited
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+import React, { useEffect, useCallback } from "react";
+import Box from "@mui/material/Box";
+import { ChevronRight, ExpandMore } from "@mui/icons-material";
+import TreeItem from "@mui/lab/TreeItem";
+import TreeView from "@mui/lab/TreeView";
+import { useDispatch, useModule, createSendActionNameAction } from "taipy-gui";
+import { Cycles, Cycle, DataNodes, NodeType, Scenarios, Scenario, DataNode, Pipeline } from "./utils/types";
+import {
+    Cycle as CycleIcon,
+    Datanode as DatanodeIcon,
+    Pipeline as PipelineIcon,
+    Scenario as ScenarioIcon,
+} from "./icons";
+
+interface NodeSelectorProps {
+    id?: string;
+    updateVarName?: string;
+    datanodes?: Cycles | Scenarios | DataNodes;
+    coreChanged?: Record<string, unknown>;
+    updateVars?: string;
+    onDataNodeSelect?: string;
+    error?: 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 CycleItem = (props: { item: Cycle }) => {
+    const [id, label, items = [], nodeType, _] = props.item;
+    return (
+        <TreeItem
+            key={id}
+            nodeId={id}
+            data-nodetype={NodeType.CYCLE.toString()}
+            label={
+                <Box sx={treeItemLabelSx}>
+                    <CycleIcon fontSize="small" color="primary" />
+                    {label}
+                </Box>
+            }
+            sx={ParentTreeItemSx}
+        >
+            {items
+                ? items.map((item: any) => {
+                      const [id, label, items = [], nodeType, _] = item;
+                      return nodeType === NodeType.SCENARIO ? <ScenarioItem item={item} /> : <NodeItem item={item} />;
+                  })
+                : null}
+        </TreeItem>
+    );
+};
+
+const NodeSelector = (props: NodeSelectorProps) => {
+    const { id = "", datanodes } = props;
+
+    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));
+        }
+    }, []);
+
+    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>
+        </>
+    );
+};
+
+export default NodeSelector;

+ 2 - 1
gui/src/index.ts

@@ -1,5 +1,6 @@
 import ScenarioSelector from "./ScenarioSelector";
 import ScenarioViewer from "./ScenarioViewer";
 import ScenarioDag from "./ScenarioDag";
+import NodeSelector from "./NodeSelector";
 
-export { ScenarioSelector, ScenarioDag as Dag, ScenarioViewer as Scenario };
+export { ScenarioSelector, ScenarioDag as Dag, ScenarioViewer as Scenario, NodeSelector as DataNodeSelector };

+ 21 - 1
gui/src/utils/types.ts

@@ -1 +1,21 @@
-export type DisplayModel = [string, Record<string, Record<string, {name: string, type: string}>>, Array<[string, string, string, string]>];
+export type DisplayModel = [
+    string,
+    Record<string, Record<string, { name: string; type: string }>>,
+    Array<[string, string, string, string]>
+];
+
+export type DataNode = [string, string, undefined, number, boolean];
+export type DataNodes = Array<DataNode>;
+export type Pipeline = [string, string, DataNodes, number, boolean];
+export type Pipelines = Array<Pipeline>;
+export type Scenario = [string, string, DataNodes | Pipelines, number, boolean];
+export type Scenarios = Array<Scenario>;
+export type Cycle = [string, string, Scenarios | DataNodes, number, boolean];
+export type Cycles = Array<Cycle>;
+
+export enum NodeType {
+    CYCLE = 0,
+    SCENARIO = 1,
+    PIPELINE = 2,
+    NODE = 3,
+}

+ 10 - 0
src/taipy/gui_core/GuiCoreLib.py

@@ -352,6 +352,16 @@ class GuiCore(ElementLibrary):
                 "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
             },
         ),
+        "data_node_selector": Element(
+            "val",
+            {
+                "val": ElementProperty(GuiCoreScenarioDagAdapter),
+            },
+            inner_properties={
+                "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
+                "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
+            },
+        ),
     }
 
     def get_name(self) -> str: