瀏覽代碼

datanode viewer WiP no chart (#251) (#266)

* #251 datanode viewer WiP

* fixes

* #232 on_create action

* #232 on_creation before|after

* Handle delayed loading of history & data
fix issue with None

* #232 one call is enough (Fab)

* make things easier for scenario creation

* history: Edits

* data WiP

* Updating datanode value when type is scalar

* reinitialize data value on cancel

* unique ids

* tabular data show in table and edit

* property dependent inner properties

* filterable table

* update dependencies

* split too long strings

* pycodestyle 120 limit

* pycodestyle

* pycodestyle

* flake8

* need zoneinfo

* zoneinfo

* support python < 3.9

* black

* mypy

* pycodestyle

* doc

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 1 年之前
父節點
當前提交
1a526a6396

+ 4 - 3
Pipfile

@@ -4,10 +4,11 @@ verify_ssl = true
 name = "pypi"
 
 [packages]
+"backports.zoneinfo" = {version="==0.2.1", markers="python_version < '3.9'", extras=["tzdata"]}
 cookiecutter = "==2.1.1"
-taipy-gui = {ref = "develop", git = "https://github.com/avaiga/taipy-gui.git"}
-taipy-rest = {ref = "develop", git = "https://github.com/avaiga/taipy-rest.git"}
-taipy-templates = {ref = "develop", git = "https://github.com/avaiga/taipy-templates.git"}
+taipy-gui = {git="https://git@github.com/Avaiga/taipy-gui.git@develop"}
+taipy-rest = {git="https://git@github.com/Avaiga/taipy-rest.git@develop"}
+taipy-templates = {git="https://git@github.com/Avaiga/taipy-templates.git@develop"}
 
 [dev-packages]
 autopep8 = "*"

File diff suppressed because it is too large
+ 345 - 241
gui/package-lock.json


+ 22 - 32
gui/src/CoreSelector.tsx

@@ -39,7 +39,7 @@ import {
     Pipeline as PipelineIcon,
     Scenario as ScenarioIcon,
 } from "./icons";
-import { BadgePos, BadgeSx, BaseTreeViewSx, FlagSx, ParentItemSx, tinyIconButtonSx } from "./utils";
+import { BadgePos, BadgeSx, BaseTreeViewSx, FlagSx, ParentItemSx, iconLabelSx, tinyIconButtonSx, tinySelPinIconButtonSx } from "./utils";
 
 export interface EditProps {
     id: string;
@@ -71,14 +71,9 @@ interface CoreSelectorProps {
     leafType: NodeType;
     editComponent?: ComponentType<EditProps>;
     showPins?: boolean;
+    onSelect?: (id: string) => void;
 }
 
-const treeItemLabelSx = {
-    display: "flex",
-    alignItems: "center",
-    gap: 1,
-};
-
 const tinyPinIconButtonSx = (theme: Theme) => ({
     ...tinyIconButtonSx,
     backgroundColor: alpha(theme.palette.text.secondary, 0.15),
@@ -90,17 +85,6 @@ const tinyPinIconButtonSx = (theme: Theme) => ({
     },
 });
 
-const tinySelPinIconButtonSx = (theme: Theme) => ({
-    ...tinyIconButtonSx,
-    backgroundColor: "secondary.main",
-    color: "secondary.contrastText",
-
-    "&:hover": {
-        backgroundColor: alpha(theme.palette.secondary.main, 0.75),
-        color: "secondary.contrastText",
-    },
-});
-
 const switchBoxSx = {ml: 2};
 
 const CoreItem = (props: {
@@ -139,7 +123,7 @@ const CoreItem = (props: {
             data-selectable={nodeType === props.leafType}
             label={
                 <Grid container alignItems="center" direction="row" flexWrap="nowrap" spacing={1}>
-                    <Grid item xs sx={treeItemLabelSx}>
+                    <Grid item xs sx={iconLabelSx}>
                         {nodeType === NodeType.CYCLE ? (
                             <CycleIcon fontSize="small" color="primary" />
                         ) : nodeType === NodeType.SCENARIO ? (
@@ -240,6 +224,11 @@ const CoreSelector = (props: CoreSelectorProps) => {
         value,
         defaultValue,
         showPins = true,
+        updateVarName,
+        updateVars,
+        onChange,
+        onSelect,
+        coreChanged
     } = props;
 
     const [selected, setSelected] = useState("");
@@ -249,39 +238,40 @@ const CoreSelector = (props: CoreSelectorProps) => {
     const dispatch = useDispatch();
     const module = useModule();
 
-    useDispatchRequestUpdateOnFirstRender(dispatch, id, module, props.updateVars);
+    useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars);
 
-    const onSelect = useCallback(
+    const onNodeSelect = useCallback(
         (e: SyntheticEvent, nodeId: string) => {
             const { selectable = "false" } = e.currentTarget.parentElement?.dataset || {};
-            const scenariosVar = getUpdateVar(props.updateVars, lovPropertyName);
+            const scenariosVar = getUpdateVar(updateVars, lovPropertyName);
             dispatch(
                 createSendUpdateAction(
-                    props.updateVarName,
+                    updateVarName,
                     selectable === "true" ? nodeId : undefined,
                     module,
-                    props.onChange,
+                    onChange,
                     propagate,
                     scenariosVar
                 )
             );
             setSelected(nodeId);
+            onSelect && selectable && onSelect(nodeId);
         },
-        [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module, lovPropertyName]
+        [updateVarName, updateVars, onChange, onSelect, propagate, dispatch, module, lovPropertyName]
     );
 
     const unselect = useCallback(() => {
         setSelected((sel) => {
             if (sel) {
-                const lovVar = getUpdateVar(props.updateVars, lovPropertyName);
+                const lovVar = getUpdateVar(updateVars, lovPropertyName);
                 dispatch(
-                    createSendUpdateAction(props.updateVarName, undefined, module, props.onChange, propagate, lovVar)
+                    createSendUpdateAction(updateVarName, undefined, module, onChange, propagate, lovVar)
                 );
                 return "";
             }
             return sel;
         });
-    }, [props.updateVarName, props.updateVars, props.onChange, propagate, dispatch, module, lovPropertyName]);
+    }, [updateVarName, updateVars, onChange, propagate, dispatch, module, lovPropertyName]);
 
     useEffect(() => {
         if (value !== undefined && value !== null) {
@@ -310,11 +300,11 @@ const CoreSelector = (props: CoreSelectorProps) => {
 
     // Refresh on broadcast
     useEffect(() => {
-        if (props.coreChanged?.scenario) {
-            const updateVar = getUpdateVar(props.updateVars, lovPropertyName);
+        if (coreChanged?.scenario) {
+            const updateVar = getUpdateVar(updateVars, lovPropertyName);
             updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
         }
-    }, [props.coreChanged, props.updateVars, module, dispatch, id, lovPropertyName]);
+    }, [coreChanged, updateVars, module, dispatch, id, lovPropertyName]);
 
     const treeViewSx = useMemo(() => ({ ...BaseTreeViewSx, maxHeight: props.height || "50vh" }), [props.height]);
 
@@ -388,7 +378,7 @@ const CoreSelector = (props: CoreSelectorProps) => {
                 defaultCollapseIcon={<ExpandMore />}
                 defaultExpandIcon={<ChevronRight />}
                 sx={treeViewSx}
-                onNodeSelect={onSelect}
+                onNodeSelect={onNodeSelect}
                 selected={selected}
                 multiSelect={multiple && !multiple}
             >

+ 704 - 0
gui/src/DataNodeViewer.tsx

@@ -0,0 +1,704 @@
+/*
+ * 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, { useState, useCallback, useEffect, useMemo, ChangeEvent, SyntheticEvent, MouseEvent } from "react";
+import Accordion from "@mui/material/Accordion";
+import AccordionDetails from "@mui/material/AccordionDetails";
+import AccordionSummary from "@mui/material/AccordionSummary";
+import Box from "@mui/material/Box";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import IconButton from "@mui/material/IconButton";
+import InputAdornment from "@mui/material/InputAdornment";
+import Popover from "@mui/material/Popover";
+import Switch from "@mui/material/Switch";
+import Tab from "@mui/material/Tab";
+import Tabs from "@mui/material/Tabs";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import { CheckCircle, Cancel, ArrowForwardIosSharp, Launch } from "@mui/icons-material";
+import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
+import { BaseDateTimePickerSlotsComponentsProps } from "@mui/x-date-pickers/DateTimePicker/shared";
+import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
+import { format } from "date-fns";
+
+import {
+    RowValue,
+    Table,
+    TableValueType,
+    createRequestUpdateAction,
+    createSendActionNameAction,
+    getUpdateVar,
+    useDispatch,
+    useDynamicProperty,
+    useModule,
+} from "taipy-gui";
+
+import { Cycle as CycleIcon, Scenario as ScenarioIcon } from "./icons";
+import {
+    AccordionIconSx,
+    AccordionSummarySx,
+    FieldNoMaxWidth,
+    IconPaddingSx,
+    MainBoxSx,
+    hoverSx,
+    iconLabelSx,
+    popoverOrigin,
+    tinySelPinIconButtonSx,
+    useClassNames,
+} from "./utils";
+import PropertiesEditor from "./PropertiesEditor";
+import { NodeType, Scenarios } from "./utils/types";
+import CoreSelector from "./CoreSelector";
+import { useUniqueId } from "./utils/hooks";
+
+const editTimestampFormat = "YYY/MM/dd HH:mm";
+
+const tabBoxSx = { borderBottom: 1, borderColor: "divider" };
+const noDisplay = { display: "none" };
+const gridSx = { mt: 0 };
+const editSx = {
+    borderRight: 1,
+    color: "secondary.main",
+    fontSize: "smaller",
+    "& > div": { writingMode: "vertical-rl", transform: "rotate(180deg)", paddingBottom: "1em" },
+};
+const textFieldProps = { textField: { margin: "dense" } } as BaseDateTimePickerSlotsComponentsProps<Date>;
+
+type DataNodeFull = [string, string, string, string, string, string, string, string, number, Array<[string, string]>];
+
+enum DataNodeFullProps {
+    id,
+    type,
+    config_id,
+    last_edit_date,
+    expiration_date,
+    label,
+    ownerId,
+    ownerLabel,
+    ownerType,
+    properties,
+}
+const DataNodeFullLength = Object.keys(DataNodeFullProps).length / 2;
+
+type DatanodeData = [RowValue, string | null, boolean | null, string | null];
+enum DatanodeDataProps {
+    value,
+    type,
+    tabular,
+    error,
+}
+
+interface DataNodeViewerProps {
+    id?: string;
+    expandable?: boolean;
+    expanded?: boolean;
+    updateVarName?: string;
+    updateVars: string;
+    defaultDataNode?: string;
+    dataNode?: DataNodeFull | Array<DataNodeFull>;
+    onEdit?: string;
+    onIdSelect?: string;
+    error?: string;
+    coreChanged?: Record<string, unknown>;
+    defaultActive: boolean;
+    active: boolean;
+    showConfig?: boolean;
+    showOwner?: boolean;
+    showEditDate?: boolean;
+    showExpirationDate?: boolean;
+    showProperties?: boolean;
+    showHistory?: boolean;
+    showData?: boolean;
+    chartConfig?: string;
+    libClassName?: string;
+    className?: string;
+    dynamicClassName?: string;
+    scenarios?: Scenarios;
+    history?: Array<[string, string, string]>;
+    data?: DatanodeData;
+    tabularData?: TableValueType;
+    tabularColumns?: string;
+    onDataValue?: string;
+    onTabularDataEdit?: string;
+}
+
+const getDataValue = (value?: RowValue, dType?: string | null) => (dType == "date" ? new Date(value as string) : value);
+
+const getValidDataNode = (datanode: DataNodeFull | DataNodeFull[]) =>
+    datanode.length == DataNodeFullLength && typeof datanode[DataNodeFullProps.id] === "string"
+        ? (datanode as DataNodeFull)
+        : datanode.length == 1
+        ? (datanode[0] as DataNodeFull)
+        : undefined;
+
+const DataNodeViewer = (props: DataNodeViewerProps) => {
+    const {
+        id = "",
+        expandable = true,
+        expanded = true,
+        showConfig = false,
+        showOwner = true,
+        showEditDate = false,
+        showExpirationDate = false,
+        showProperties = true,
+        showHistory = true,
+        showData = true,
+    } = props;
+
+    const dispatch = useDispatch();
+    const module = useModule();
+    const uniqid = useUniqueId(id);
+
+    const [
+        dnId,
+        dnType,
+        dnConfig,
+        dnEditDate,
+        dnExpirationDate,
+        dnLabel,
+        dnOwnerId,
+        dnOwnerLabel,
+        dnOwnerType,
+        dnProperties,
+        isDataNode,
+    ] = useMemo(() => {
+        let dn: DataNodeFull | undefined = undefined;
+        if (Array.isArray(props.dataNode)) {
+            dn = getValidDataNode(props.dataNode);
+        } else if (props.defaultDataNode) {
+            try {
+                dn = getValidDataNode(JSON.parse(props.defaultDataNode));
+            } catch {
+                // DO nothing
+            }
+        }
+        return dn ? [...dn, true] : ["", "", "", "", "", "", "", "", -1, [], false];
+    }, [props.dataNode, props.defaultDataNode]);
+
+    const active = useDynamicProperty(props.active, props.defaultActive, true);
+    const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
+
+    // history & data
+    const [historyRequested, setHistoryRequested] = useState(false);
+    const [dataRequested, setDataRequested] = useState(false);
+
+    // userExpanded
+    const [userExpanded, setUserExpanded] = useState(isDataNode && expanded);
+    const onExpand = useCallback(
+        (e: SyntheticEvent, expand: boolean) => expandable && setUserExpanded(expand),
+        [expandable]
+    );
+
+    // focus
+    const [focusName, setFocusName] = useState("");
+    const onFocus = useCallback((e: MouseEvent<HTMLElement>) => {
+        e.stopPropagation();
+        setFocusName(e.currentTarget.dataset.focus || "");
+    }, []);
+
+    // Label
+    const [label, setLabel] = useState<string>();
+    const editLabel = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            if (isDataNode) {
+                dispatch(createSendActionNameAction(id, module, props.onEdit, { id: dnId, name: label }));
+                setFocusName("");
+            }
+        },
+        [isDataNode, props.onEdit, dnId, label, id, dispatch, module]
+    );
+    const cancelLabel = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            setLabel(dnLabel);
+            setFocusName("");
+        },
+        [dnLabel, setLabel, setFocusName]
+    );
+    const onLabelChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setLabel(e.target.value), []);
+
+    // scenarios
+    const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
+    const showScenarios = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            if (isDataNode) {
+                dispatch(createSendActionNameAction(id, module, props.onIdSelect, { owner_id: dnOwnerId }));
+                setAnchorEl(e.currentTarget);
+            }
+        },
+        [dnOwnerId, dispatch, id, isDataNode, module, props.onIdSelect]
+    );
+    const handleClose = useCallback(() => setAnchorEl(null), []);
+    const scenarioUpdateVars = useMemo(
+        () => [getUpdateVar(props.updateVars, "scenario"), getUpdateVar(props.updateVars, "scenarios")],
+        [props.updateVars]
+    );
+
+    // on datanode change
+    useEffect(() => {
+        setLabel(dnLabel);
+        setUserExpanded(expanded && isDataNode);
+        setTabValue(0);
+        setHistoryRequested(false);
+        setDataRequested(false);
+    }, [dnLabel, isDataNode, expanded]);
+
+    // Datanode data
+    const dtValue = (props.data && props.data[DatanodeDataProps.value]) ?? undefined;
+    const dtType = props.data && props.data[DatanodeDataProps.type];
+    const dtTabular = (props.data && props.data[DatanodeDataProps.tabular]) ?? false;
+    const dtError = props.data && props.data[DatanodeDataProps.error];
+    const [dataValue, setDataValue] = useState<RowValue | Date>();
+    const editDataValue = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            if (isDataNode) {
+                dispatch(
+                    createSendActionNameAction(id, module, props.onDataValue, {
+                        id: dnId,
+                        value: dataValue,
+                        type: dtType,
+                    })
+                );
+                setFocusName("");
+            }
+        },
+        [isDataNode, props.onDataValue, dnId, dataValue, dtType, id, dispatch, module]
+    );
+    const cancelDataValue = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            setDataValue(getDataValue(dtValue, dtType));
+            setFocusName("");
+        },
+        [dtValue, dtType, setDataValue, setFocusName]
+    );
+    const onDataValueChange = useCallback((e: ChangeEvent<HTMLInputElement>) => setDataValue(e.target.value), []);
+    const onDataValueDateChange = useCallback((d: Date | null) => d && setDataValue(d), []);
+    useEffect(() => {
+        if (dtValue !== undefined) {
+            setDataValue(getDataValue(dtValue, dtType));
+        }
+    }, [dtValue, dtType]);
+
+    // Refresh on broadcast
+    useEffect(() => {
+        const ids = props.coreChanged?.datanode;
+        if (typeof ids === "string" ? ids === dnId : Array.isArray(ids) ? ids.includes(dnId) : ids) {
+            props.updateVarName && dispatch(createRequestUpdateAction(id, module, [props.updateVarName], true));
+        }
+    }, [props.coreChanged, props.updateVarName, id, module, dispatch, dnId]);
+
+    // Tabs
+    const [tabValue, setTabValue] = useState(0);
+    const handleTabChange = useCallback(
+        (_: SyntheticEvent, newValue: number) => {
+            if (isDataNode) {
+                newValue == 1 &&
+                    setHistoryRequested(
+                        (req) =>
+                            req ||
+                            dispatch(createSendActionNameAction(id, module, props.onIdSelect, { history_id: dnId })) ||
+                            true
+                    );
+                newValue == 2 &&
+                    setDataRequested(
+                        (req) =>
+                            req ||
+                            dispatch(createSendActionNameAction(id, module, props.onIdSelect, { data_id: dnId })) ||
+                            true
+                    );
+                setTabValue(newValue);
+            }
+        },
+        [dnId, dispatch, id, isDataNode, module, props.onIdSelect]
+    );
+
+    return (
+        <>
+            <Box sx={MainBoxSx} id={id} onClick={onFocus} className={className}>
+                <Accordion
+                    defaultExpanded={expanded}
+                    expanded={userExpanded}
+                    onChange={onExpand}
+                    disabled={!isDataNode}
+                >
+                    <AccordionSummary
+                        expandIcon={expandable ? <ArrowForwardIosSharp sx={AccordionIconSx} /> : null}
+                        sx={AccordionSummarySx}
+                    >
+                        <Grid container alignItems="baseline" direction="row" spacing={1}>
+                            <Grid item>{dnLabel}</Grid>
+                            <Grid item>
+                                <Typography fontSize="smaller">{dnType}</Typography>
+                            </Grid>
+                        </Grid>
+                    </AccordionSummary>
+                    <AccordionDetails>
+                        <Box sx={tabBoxSx}>
+                            <Tabs value={tabValue} onChange={handleTabChange} aria-label="basic tabs example">
+                                <Tab
+                                    label="Properties"
+                                    id={`${uniqid}-properties`}
+                                    aria-controls={`${uniqid}-dn-tabpanel-properties`}
+                                />
+                                <Tab
+                                    label="History"
+                                    id={`${uniqid}-history`}
+                                    aria-controls={`${uniqid}-dn-tabpanel-history`}
+                                    style={showHistory ? undefined : noDisplay}
+                                />
+                                <Tab
+                                    label="Data"
+                                    id={`${uniqid}-data`}
+                                    aria-controls={`${uniqid}-dn-tabpanel-data`}
+                                    style={showData ? undefined : noDisplay}
+                                />
+                            </Tabs>
+                        </Box>
+                        <div
+                            role="tabpanel"
+                            hidden={tabValue !== 0}
+                            id={`${uniqid}-dn-tabpanel-properties`}
+                            aria-labelledby={`${uniqid}-properties`}
+                        >
+                            <Grid container rowSpacing={2} sx={gridSx}>
+                                <Grid item xs={12} container justifyContent="space-between" spacing={1}>
+                                    <Grid
+                                        item
+                                        xs={12}
+                                        container
+                                        justifyContent="space-between"
+                                        data-focus="label"
+                                        onClick={onFocus}
+                                        sx={hoverSx}
+                                    >
+                                        {active && focusName === "label" ? (
+                                            <TextField
+                                                label="Label"
+                                                variant="outlined"
+                                                fullWidth
+                                                sx={FieldNoMaxWidth}
+                                                value={label || ""}
+                                                onChange={onLabelChange}
+                                                InputProps={{
+                                                    endAdornment: (
+                                                        <InputAdornment position="end">
+                                                            <IconButton sx={IconPaddingSx} onClick={editLabel}>
+                                                                <CheckCircle color="primary" />
+                                                            </IconButton>
+                                                            <IconButton sx={IconPaddingSx} onClick={cancelLabel}>
+                                                                <Cancel color="inherit" />
+                                                            </IconButton>
+                                                        </InputAdornment>
+                                                    ),
+                                                }}
+                                                disabled={!isDataNode}
+                                            />
+                                        ) : (
+                                            <>
+                                                <Grid item xs={4}>
+                                                    <Typography variant="subtitle2">Label</Typography>
+                                                </Grid>
+                                                <Grid item xs={8}>
+                                                    <Typography variant="subtitle2">{dnLabel}</Typography>
+                                                </Grid>
+                                            </>
+                                        )}
+                                    </Grid>
+                                </Grid>
+                                {showEditDate ? (
+                                    <Grid item xs={12} container justifyContent="space-between">
+                                        <Grid item xs={4}>
+                                            <Typography variant="subtitle2">Last edit date</Typography>
+                                        </Grid>
+                                        <Grid item xs={8}>
+                                            <Typography variant="subtitle2">{dnEditDate}</Typography>
+                                        </Grid>
+                                    </Grid>
+                                ) : null}
+                                {showExpirationDate ? (
+                                    <Grid item xs={12} container justifyContent="space-between">
+                                        <Grid item xs={4}>
+                                            <Typography variant="subtitle2">Expiration date</Typography>
+                                        </Grid>
+                                        <Grid item xs={8}>
+                                            <Typography variant="subtitle2">{dnExpirationDate}</Typography>
+                                        </Grid>
+                                    </Grid>
+                                ) : null}
+                                {showConfig ? (
+                                    <Grid item xs={12} container justifyContent="space-between">
+                                        <Grid item xs={4} pb={2}>
+                                            <Typography variant="subtitle2">Config ID</Typography>
+                                        </Grid>
+                                        <Grid item xs={8}>
+                                            <Typography variant="subtitle2">{dnConfig}</Typography>
+                                        </Grid>
+                                    </Grid>
+                                ) : null}
+                                {showOwner ? (
+                                    <Grid item xs={12} container justifyContent="space-between">
+                                        <Grid item xs={4}>
+                                            <Typography variant="subtitle2">Owner</Typography>
+                                        </Grid>
+                                        <Grid item xs={7} sx={iconLabelSx}>
+                                            {dnOwnerType === NodeType.CYCLE ? (
+                                                <CycleIcon fontSize="small" color="primary" />
+                                            ) : dnOwnerType === NodeType.SCENARIO ? (
+                                                <ScenarioIcon fontSize="small" color="primary" />
+                                            ) : null}
+                                            <Typography variant="subtitle2">{dnOwnerLabel}</Typography>
+                                        </Grid>
+                                        <Grid item xs={1}>
+                                            <IconButton
+                                                sx={tinySelPinIconButtonSx}
+                                                onClick={showScenarios}
+                                                disabled={!isDataNode}
+                                            >
+                                                <Launch />
+                                            </IconButton>
+                                            <Popover
+                                                open={Boolean(anchorEl)}
+                                                anchorEl={anchorEl}
+                                                onClose={handleClose}
+                                                anchorOrigin={popoverOrigin}
+                                            >
+                                                <CoreSelector
+                                                    entities={props.scenarios}
+                                                    leafType={NodeType.SCENARIO}
+                                                    lovPropertyName="scenarios"
+                                                    height="50vh"
+                                                    showPins={false}
+                                                    updateVarName={scenarioUpdateVars[0]}
+                                                    updateVars={`scenarios=${scenarioUpdateVars[1]}`}
+                                                    onSelect={handleClose}
+                                                />
+                                            </Popover>
+                                        </Grid>
+                                    </Grid>
+                                ) : null}
+                                <Grid item xs={12}>
+                                    <Divider />
+                                </Grid>
+                                <PropertiesEditor
+                                    entityId={dnId}
+                                    active={active}
+                                    isDefined={isDataNode}
+                                    entProperties={dnProperties}
+                                    show={showProperties}
+                                    focusName={focusName}
+                                    setFocusName={setFocusName}
+                                    onFocus={onFocus}
+                                    onEdit={props.onEdit}
+                                />
+                            </Grid>
+                        </div>
+                        <div
+                            role="tabpanel"
+                            hidden={tabValue !== 1}
+                            id={`${uniqid}-dn-tabpanel-history`}
+                            aria-labelledby={`${uniqid}-history`}
+                        >
+                            {historyRequested && Array.isArray(props.history) ? (
+                                <Grid container spacing={1}>
+                                    {props.history.map((edit, idx) => (
+                                        <>
+                                            {idx != 0 ? (
+                                                <Grid item xs={12}>
+                                                    <Divider />
+                                                </Grid>
+                                            ) : null}
+                                            <Grid item container key={`edit-${idx}`}>
+                                                <Grid item xs={0.4} sx={editSx}>
+                                                    <Box>{(props.history || []).length - idx}</Box>
+                                                </Grid>
+                                                <Grid item xs={0.1}></Grid>
+                                                <Grid item container xs={11.5}>
+                                                    <Grid item xs={12}>
+                                                        <Typography variant="subtitle1">
+                                                            {edit[0]
+                                                                ? format(new Date(edit[0]), editTimestampFormat)
+                                                                : "no date"}
+                                                        </Typography>
+                                                    </Grid>
+                                                    <Grid item xs={12}>
+                                                        {edit[2]}
+                                                    </Grid>
+                                                    <Grid item xs={12}>
+                                                        <Typography fontSize="smaller">{edit[1]}</Typography>
+                                                    </Grid>
+                                                </Grid>
+                                            </Grid>
+                                        </>
+                                    ))}
+                                </Grid>
+                            ) : (
+                                "History will come here"
+                            )}
+                        </div>
+                        <div
+                            role="tabpanel"
+                            hidden={tabValue !== 2}
+                            id={`${uniqid}-dn-tabpanel-data`}
+                            aria-labelledby={`${uniqid}-data`}
+                        >
+                            {dataRequested ? (
+                                dtValue !== undefined ? (
+                                    <Grid container justifyContent="space-between" spacing={1}>
+                                        <Grid
+                                            item
+                                            container
+                                            xs={12}
+                                            justifyContent="space-between"
+                                            data-focus="data-value"
+                                            onClick={onFocus}
+                                            sx={hoverSx}
+                                        >
+                                            {active && focusName === "data-value" ? (
+                                                typeof dtValue == "boolean" ? (
+                                                    <>
+                                                        <Grid item xs={10}>
+                                                            <Switch
+                                                                value={dataValue as boolean}
+                                                                onChange={onDataValueChange}
+                                                            />
+                                                        </Grid>
+                                                        <Grid item xs={2}>
+                                                            <IconButton
+                                                                onClick={editDataValue}
+                                                                size="small"
+                                                                sx={IconPaddingSx}
+                                                            >
+                                                                <CheckCircle color="primary" />
+                                                            </IconButton>
+                                                            <IconButton
+                                                                onClick={cancelDataValue}
+                                                                size="small"
+                                                                sx={IconPaddingSx}
+                                                            >
+                                                                <Cancel color="inherit" />
+                                                            </IconButton>
+                                                        </Grid>
+                                                    </>
+                                                ) : dtType == "date" ? (
+                                                    <LocalizationProvider dateAdapter={AdapterDateFns}>
+                                                        <Grid item xs={10}>
+                                                            <DateTimePicker
+                                                                value={dataValue as Date}
+                                                                onChange={onDataValueDateChange}
+                                                                slotProps={textFieldProps}
+                                                            />
+                                                        </Grid>
+                                                        <Grid item xs={2}>
+                                                            <IconButton
+                                                                onClick={editDataValue}
+                                                                size="small"
+                                                                sx={IconPaddingSx}
+                                                            >
+                                                                <CheckCircle color="primary" />
+                                                            </IconButton>
+                                                            <IconButton
+                                                                onClick={cancelDataValue}
+                                                                size="small"
+                                                                sx={IconPaddingSx}
+                                                            >
+                                                                <Cancel color="inherit" />
+                                                            </IconButton>
+                                                        </Grid>
+                                                    </LocalizationProvider>
+                                                ) : (
+                                                    <TextField
+                                                        label="Value"
+                                                        variant="outlined"
+                                                        fullWidth
+                                                        sx={FieldNoMaxWidth}
+                                                        value={dataValue || ""}
+                                                        onChange={onDataValueChange}
+                                                        type={typeof dtValue == "number" ? "number" : undefined}
+                                                        InputProps={{
+                                                            endAdornment: (
+                                                                <InputAdornment position="end">
+                                                                    <IconButton
+                                                                        sx={IconPaddingSx}
+                                                                        onClick={editDataValue}
+                                                                    >
+                                                                        <CheckCircle color="primary" />
+                                                                    </IconButton>
+                                                                    <IconButton
+                                                                        sx={IconPaddingSx}
+                                                                        onClick={cancelDataValue}
+                                                                    >
+                                                                        <Cancel color="inherit" />
+                                                                    </IconButton>
+                                                                </InputAdornment>
+                                                            ),
+                                                        }}
+                                                        disabled={!isDataNode}
+                                                    />
+                                                )
+                                            ) : (
+                                                <>
+                                                    <Grid item xs={4}>
+                                                        <Typography variant="subtitle2">Value</Typography>
+                                                    </Grid>
+                                                    <Grid item xs={8}>
+                                                        {typeof dtValue == "boolean" ? (
+                                                            <Switch
+                                                                defaultChecked={dtValue}
+                                                                disabled={true}
+                                                                title={`${dtValue}`}
+                                                            />
+                                                        ) : (
+                                                            <Typography variant="subtitle2">
+                                                                {dtType == "date"
+                                                                    ? dataValue &&
+                                                                      format(dataValue as Date, "yyyy/MM/dd HH:mm:ss")
+                                                                    : dtValue}
+                                                            </Typography>
+                                                        )}
+                                                    </Grid>
+                                                </>
+                                            )}
+                                        </Grid>
+                                    </Grid>
+                                ) : dtError ? (
+                                    <Typography>{dtError}</Typography>
+                                ) : dtTabular ? (
+                                    <Table
+                                        defaultColumns={props.tabularColumns || ""}
+                                        updateVarName={getUpdateVar(props.updateVars, "tabularData")}
+                                        data={props.tabularData}
+                                        userData={dnId}
+                                        onEdit={props.onTabularDataEdit}
+                                        filter={true}
+                                    ></Table>
+                                ) : (
+                                    "type: unknown"
+                                )
+                            ) : (
+                                "Data shall be shown here"
+                            )}
+                        </div>
+                    </AccordionDetails>
+                </Accordion>
+                <Box>{props.error}</Box>
+            </Box>
+        </>
+    );
+};
+export default DataNodeViewer;

+ 3 - 7
gui/src/JobSelector.tsx

@@ -24,7 +24,7 @@ import InputLabel from "@mui/material/InputLabel";
 import ListItemText from "@mui/material/ListItemText";
 import MenuItem from "@mui/material/MenuItem";
 import Paper from "@mui/material/Paper";
-import Popover, { PopoverOrigin } from "@mui/material/Popover";
+import Popover from "@mui/material/Popover";
 import Select from "@mui/material/Select";
 import Table from "@mui/material/Table";
 import TableBody from "@mui/material/TableBody";
@@ -48,7 +48,7 @@ import {
     useModule,
 } from "taipy-gui";
 
-import { disableColor, useClassNames } from "./utils";
+import { disableColor, popoverOrigin, useClassNames } from "./utils";
 
 interface JobSelectorProps {
     updateVarName?: string;
@@ -142,10 +142,6 @@ type FilterData = {
     value: string;
 };
 
-const origin: PopoverOrigin = {
-    vertical: "bottom",
-    horizontal: "left",
-};
 interface FilterProps {
     open: boolean;
     anchorEl: HTMLButtonElement | null;
@@ -210,7 +206,7 @@ const Filter = ({ open, anchorEl, handleFilterClose, handleApplyFilter, columns
             open={open}
             anchorEl={anchorEl}
             onClose={handleFilterClose}
-            anchorOrigin={origin}
+            anchorOrigin={popoverOrigin}
         >
             <form onSubmit={form.handleSubmit}>
                 <Grid container p={3} sx={containerPopupSx}>

+ 2 - 2
gui/src/NodeSelector.tsx

@@ -14,7 +14,7 @@
 import React from "react";
 import Box from "@mui/material/Box";
 
-import { MainBoxSx, useClassNames } from "./utils";
+import { MainTreeBoxSx, useClassNames } from "./utils";
 import { Cycles, DataNodes, NodeType, Scenarios } from "./utils/types";
 import CoreSelector from "./CoreSelector";
 
@@ -42,7 +42,7 @@ const NodeSelector = (props: NodeSelectorProps) => {
     const { showPins = true } = props;
     const className = useClassNames(props.libClassName, props.dynamicClassName, props.className);
     return (
-        <Box sx={MainBoxSx} id={props.id} className={className}>
+        <Box sx={MainTreeBoxSx} id={props.id} className={className}>
             <CoreSelector
                 {...props}
                 entities={props.datanodes}

+ 317 - 0
gui/src/PropertiesEditor.tsx

@@ -0,0 +1,317 @@
+/*
+ * 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, { useState, useCallback, useEffect, ChangeEvent, MouseEvent } from "react";
+import Divider from "@mui/material/Divider";
+import Grid from "@mui/material/Grid";
+import IconButton from "@mui/material/IconButton";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import { DeleteOutline, CheckCircle, Cancel } from "@mui/icons-material";
+
+import { createSendActionNameAction, useDispatch, useModule } from "taipy-gui";
+
+import { FieldNoMaxWidth, IconPaddingSx, disableColor, hoverSx } from "./utils";
+
+type Property = {
+    id: string;
+    key: string;
+    value: string;
+};
+
+type PropertiesEditPayload = {
+    id: string;
+    properties?: Property[];
+    deleted_properties?: Array<Partial<Property>>;
+};
+
+const DeleteIconSx = { height: 50, width: 50, p: 0 };
+
+interface PropertiesEditorProps {
+    id?: string;
+    entityId: string;
+    active: boolean;
+    show: boolean;
+    entProperties: Array<[string, string]>;
+    onFocus: (e: MouseEvent<HTMLElement>) => void;
+    focusName: string;
+    setFocusName: (name: string) => void;
+    isDefined: boolean;
+    onEdit?: string;
+}
+
+const PropertiesEditor = (props: PropertiesEditorProps) => {
+    const { id, entityId, isDefined, show, active, onFocus, focusName, setFocusName, entProperties } = props;
+
+    const dispatch = useDispatch();
+    const module = useModule();
+
+    const [properties, setProperties] = useState<Property[]>([]);
+    const [newProp, setNewProp] = useState<Property>({
+        id: "",
+        key: "",
+        value: "",
+    });
+
+    // Properties
+    const updatePropertyField = useCallback((e: ChangeEvent<HTMLInputElement>) => {
+        const { id = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
+        if (name) {
+            if (id) {
+                setProperties((ps) => ps.map((p) => (id === p.id ? { ...p, [name]: e.target.value } : p)));
+            } else {
+                setNewProp((np) => ({ ...np, [name]: e.target.value }));
+            }
+        }
+    }, []);
+
+    const editProperty = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            if (isDefined) {
+                const { id: propId = "" } = e.currentTarget.dataset || {};
+                const property = propId ? properties.find((p) => p.id === propId) : newProp;
+                if (property) {
+                    const oldId = property.id;
+                    const payload: PropertiesEditPayload = { id: entityId, properties: [property] };
+                    if (oldId && oldId != property.key) {
+                        payload.deleted_properties = [{ key: oldId }];
+                    }
+                    dispatch(createSendActionNameAction(id, module, props.onEdit, payload));
+                }
+                setNewProp((np) => ({ ...np, key: "", value: "" }));
+                setFocusName("");
+            }
+        },
+        [isDefined, props.onEdit, entityId, properties, newProp, id, dispatch, module, setFocusName]
+    );
+    const cancelProperty = useCallback(
+        (e: MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            if (isDefined) {
+                const { id: propId = "" } = e.currentTarget.dataset || {};
+                const property = entProperties.find(([key]) => key === propId);
+                property &&
+                    setProperties((ps) =>
+                        ps.map((p) => (p.id === property[0] ? { ...p, key: property[0], value: property[1] } : p))
+                    );
+                setFocusName("");
+            }
+        },
+        [isDefined, entProperties, setFocusName]
+    );
+
+    const deleteProperty = useCallback(
+        (e: React.MouseEvent<HTMLElement>) => {
+            e.stopPropagation();
+            const { id: propId = "" } = 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: entityId,
+                        deleted_properties: [property],
+                    })
+                );
+            setFocusName("");
+        },
+        [props.onEdit, entityId, id, dispatch, module, properties, setFocusName]
+    );
+
+    useEffect(() => {
+        show &&
+            setProperties(
+                entProperties.map(([k, v]) => ({
+                    id: k,
+                    key: k,
+                    value: v,
+                }))
+            );
+    }, [show, entProperties]);
+
+    return show ? (
+        <>
+            <Grid item xs={12} container rowSpacing={2}>
+                {properties
+                    ? properties.map((property) => {
+                          const propName = `property-${property.id}`;
+                          return (
+                              <Grid
+                                  item
+                                  xs={12}
+                                  spacing={1}
+                                  container
+                                  justifyContent="space-between"
+                                  key={property.id}
+                                  data-focus={propName}
+                                  onClick={onFocus}
+                                  sx={hoverSx}
+                              >
+                                  {active && focusName === propName ? (
+                                      <>
+                                          <Grid item xs={4}>
+                                              <TextField
+                                                  label="Key"
+                                                  variant="outlined"
+                                                  value={property.key}
+                                                  sx={FieldNoMaxWidth}
+                                                  disabled={!isDefined}
+                                                  data-name="key"
+                                                  data-id={property.id}
+                                                  onChange={updatePropertyField}
+                                              />
+                                          </Grid>
+                                          <Grid item xs={5}>
+                                              <TextField
+                                                  label="Value"
+                                                  variant="outlined"
+                                                  value={property.value}
+                                                  sx={FieldNoMaxWidth}
+                                                  disabled={!isDefined}
+                                                  data-name="value"
+                                                  data-id={property.id}
+                                                  onChange={updatePropertyField}
+                                              />
+                                          </Grid>
+                                          <Grid
+                                              item
+                                              xs={2}
+                                              container
+                                              alignContent="center"
+                                              alignItems="center"
+                                              justifyContent="center"
+                                          >
+                                              <IconButton
+                                                  sx={IconPaddingSx}
+                                                  data-id={property.id}
+                                                  onClick={editProperty}
+                                              >
+                                                  <CheckCircle color="primary" />
+                                              </IconButton>
+                                              <IconButton
+                                                  sx={IconPaddingSx}
+                                                  data-id={property.id}
+                                                  onClick={cancelProperty}
+                                              >
+                                                  <Cancel color="inherit" />
+                                              </IconButton>
+                                          </Grid>
+                                          <Grid
+                                              item
+                                              xs={1}
+                                              container
+                                              alignContent="center"
+                                              alignItems="center"
+                                              justifyContent="center"
+                                          >
+                                              <IconButton
+                                                  sx={DeleteIconSx}
+                                                  data-id={property.id}
+                                                  onClick={deleteProperty}
+                                                  disabled={!isDefined}
+                                              >
+                                                  <DeleteOutline
+                                                      fontSize="small"
+                                                      color={disableColor("primary", !isDefined)}
+                                                  />
+                                              </IconButton>
+                                          </Grid>
+                                      </>
+                                  ) : (
+                                      <>
+                                          <Grid item xs={4}>
+                                              <Typography variant="subtitle2">{property.key}</Typography>
+                                          </Grid>
+                                          <Grid item xs={5}>
+                                              <Typography variant="subtitle2">{property.value}</Typography>
+                                          </Grid>{" "}
+                                          <Grid item xs={3} />
+                                      </>
+                                  )}
+                              </Grid>
+                          );
+                      })
+                    : null}
+                <Grid
+                    item
+                    xs={12}
+                    spacing={1}
+                    container
+                    justifyContent="space-between"
+                    data-focus="new-property"
+                    onClick={onFocus}
+                    sx={hoverSx}
+                >
+                    {active && focusName == "new-property" ? (
+                        <>
+                            <Grid item xs={4}>
+                                <TextField
+                                    value={newProp.key}
+                                    data-name="key"
+                                    onChange={updatePropertyField}
+                                    label="Key"
+                                    variant="outlined"
+                                    sx={FieldNoMaxWidth}
+                                    disabled={!isDefined}
+                                />
+                            </Grid>
+                            <Grid item xs={5}>
+                                <TextField
+                                    value={newProp.value}
+                                    data-name="value"
+                                    onChange={updatePropertyField}
+                                    label="Value"
+                                    variant="outlined"
+                                    sx={FieldNoMaxWidth}
+                                    disabled={!isDefined}
+                                />
+                            </Grid>
+                            <Grid
+                                item
+                                xs={2}
+                                container
+                                alignContent="center"
+                                alignItems="center"
+                                justifyContent="center"
+                            >
+                                <IconButton sx={IconPaddingSx} onClick={editProperty}>
+                                    <CheckCircle color="primary" />
+                                </IconButton>
+                                <IconButton sx={IconPaddingSx} onClick={cancelProperty}>
+                                    <Cancel color="inherit" />
+                                </IconButton>
+                            </Grid>
+                            <Grid item xs={1} />
+                        </>
+                    ) : (
+                        <>
+                            <Grid item xs={4}>
+                                <Typography variant="subtitle2">New Property Key</Typography>
+                            </Grid>
+                            <Grid item xs={5}>
+                                <Typography variant="subtitle2">Value</Typography>
+                            </Grid>
+                            <Grid item xs={3} />
+                        </>
+                    )}
+                </Grid>
+            </Grid>
+            <Grid item xs={12}>
+                <Divider />
+            </Grid>
+        </>
+    ) : null;
+};
+
+export default PropertiesEditor;

+ 13 - 10
gui/src/ScenarioSelector.tsx

@@ -34,12 +34,13 @@ import { Close, DeleteOutline, Add, EditOutlined } from "@mui/icons-material";
 import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
 import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 import { useFormik } from "formik";
+
 import { useDispatch, useModule, createSendActionNameAction, getUpdateVar, createSendUpdateAction } from "taipy-gui";
 
 import ConfirmDialog from "./utils/ConfirmDialog";
-import { MainBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx } from "./utils";
+import { MainTreeBoxSx, ScFProps, ScenarioFull, useClassNames, tinyIconButtonSx } from "./utils";
 import CoreSelector, { EditProps } from "./CoreSelector";
-import { NodeType } from "./utils/types";
+import { Cycles, NodeType, Scenarios } from "./utils/types";
 
 type Property = {
     id: string;
@@ -47,9 +48,9 @@ type Property = {
     value: string;
 };
 
-type Scenario = [string, string, undefined, number, boolean];
-type Scenarios = Array<Scenario>;
-type Cycles = Array<[string, string, Scenarios, number, boolean]>;
+// type Scenario = [string, string, undefined, number, boolean];
+// type Scenarios = Array<Scenario>;
+// type Cycles = Array<[string, string, Scenarios, number, boolean]>;
 
 interface ScenarioDict {
     id?: string;
@@ -65,11 +66,12 @@ interface ScenarioSelectorProps {
     displayCycles?: boolean;
     showPrimaryFlag?: boolean;
     updateVarName?: string;
+    updateVars: string;
     scenarios?: Cycles | Scenarios;
     onScenarioCrud: string;
     onChange?: string;
+    onCreation?: string;
     coreChanged?: Record<string, unknown>;
-    updateVars: string;
     configs?: Array<[string, string]>;
     error?: string;
     propagate?: boolean;
@@ -181,7 +183,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
             values.properties = [...properties];
             setProperties([]);
             submit(actionEdit, false, values);
-            form.resetForm({values: {...emptyScenario, config: configs?.length === 1 ? configs[0][0] : ""}});
+            form.resetForm({ values: { ...emptyScenario, config: configs?.length === 1 ? configs[0][0] : "" } });
             close();
         },
     });
@@ -196,7 +198,7 @@ const ScenarioEditDialog = ({ scenario, submit, open, actionEdit, configs, close
                       date: scenario[ScFProps.creation_date],
                       properties: [],
                   }
-                : {...emptyScenario, config: configs?.length === 1 ? configs[0][0] : ""}
+                : { ...emptyScenario, config: configs?.length === 1 ? configs[0][0] : "" }
         );
         setProperties(
             scenario ? scenario[ScFProps.properties].map(([k, v], i) => ({ id: i + "", key: k, value: v })) : []
@@ -434,7 +436,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
 
     const onSubmit = useCallback(
         (...values: unknown[]) => {
-            dispatch(createSendActionNameAction(props.id, module, props.onScenarioCrud, ...values));
+            dispatch(createSendActionNameAction(props.id, module, props.onScenarioCrud, ...values, props.onCreation));
             if (values.length > 1 && values[1]) {
                 // delete requested => unselect current node
                 const lovVar = getUpdateVar(props.updateVars, "scenarios");
@@ -452,6 +454,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
             props.onChange,
             props.updateVarName,
             props.updateVars,
+            props.onCreation,
         ]
     );
 
@@ -466,7 +469,7 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
 
     return (
         <>
-            <Box sx={MainBoxSx} id={props.id} className={className}>
+            <Box sx={MainTreeBoxSx} id={props.id} className={className}>
                 <CoreSelector
                     {...props}
                     entities={props.scenarios}

+ 29 - 305
gui/src/ScenarioViewer.tsx

@@ -25,7 +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 { FlagOutlined, DeleteOutline, Send, CheckCircle, Cancel, ArrowForwardIosSharp } from "@mui/icons-material";
+import { FlagOutlined, Send, CheckCircle, Cancel, ArrowForwardIosSharp } from "@mui/icons-material";
 
 import {
     createRequestUpdateAction,
@@ -35,19 +35,22 @@ import {
     useModule,
 } from "taipy-gui";
 
-import { FlagSx, ScFProps, ScenarioFull, ScenarioFullLength, disableColor, useClassNames } from "./utils";
+import {
+    AccordionIconSx,
+    AccordionSummarySx,
+    FieldNoMaxWidth,
+    FlagSx,
+    IconPaddingSx,
+    MainBoxSx,
+    ScFProps,
+    ScenarioFull,
+    ScenarioFullLength,
+    disableColor,
+    hoverSx,
+    useClassNames,
+} from "./utils";
 import ConfirmDialog from "./utils/ConfirmDialog";
-
-type Property = {
-    id: string;
-    key: string;
-    value: string;
-};
-type ScenarioEditPayload = {
-    id: string;
-    properties?: Property[];
-    deleted_properties?: Array<Partial<Property>>;
-};
+import PropertiesEditor from "./PropertiesEditor";
 
 interface ScenarioViewerProps {
     id?: string;
@@ -91,18 +94,7 @@ interface PipelinesRowProps {
     submittable: boolean;
 }
 
-const MainBoxSx = {
-    overflowY: "auto",
-};
-
-const FieldNoMaxWidth = {
-    maxWidth: "none",
-};
-
-const AccordionIconSx = { fontSize: "0.9rem" };
 const ChipSx = { ml: 1 };
-const IconPaddingSx = { padding: 0 };
-const DeleteIconSx = { height: 50, width: 50, p: 0 };
 
 const tagsAutocompleteSx = {
     "& .MuiOutlinedInput-root": {
@@ -111,25 +103,6 @@ const tagsAutocompleteSx = {
     maxWidth: "none",
 };
 
-const hoverSx = {
-    "&:hover": {
-        bgcolor: "action.hover",
-        cursor: "text",
-    },
-    mt: 0,
-};
-
-const AccordionSummarySx = {
-    flexDirection: "row-reverse",
-    "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": {
-        transform: "rotate(90deg)",
-        mr: 1,
-    },
-    "& .MuiAccordionSummary-content": {
-        mr: 1,
-    },
-};
-
 const PipelineRow = ({
     active,
     number,
@@ -295,13 +268,6 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
         }
     }, [isScenario, props.onEdit, scId, id, dispatch, module]);
 
-    const [properties, setProperties] = useState<Property[]>([]);
-    const [newProp, setNewProp] = useState<Property>({
-        id: "",
-        key: "",
-        value: "",
-    });
-
     // userExpanded
     const [userExpanded, setUserExpanded] = useState(isScenario && expanded);
     const onExpand = useCallback(
@@ -378,72 +344,6 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     );
     const onChangeTags = useCallback((_: SyntheticEvent, tags: string[]) => setTags(tags), []);
 
-    // Properties
-    const updatePropertyField = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
-        const { id = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
-        if (name) {
-            if (id) {
-                setProperties((ps) => ps.map((p) => (id === p.id ? { ...p, [name]: e.target.value } : p)));
-            } else {
-                setNewProp((np) => ({ ...np, [name]: e.target.value }));
-            }
-        }
-    }, []);
-
-    const editProperty = useCallback(
-        (e: MouseEvent<HTMLElement>) => {
-            e.stopPropagation();
-            if (isScenario) {
-                const { id: propId = "" } = e.currentTarget.dataset || {};
-                const property = propId ? properties.find((p) => p.id === propId) : newProp;
-                if (property) {
-                    const oldId = property.id;
-                    const payload: ScenarioEditPayload = { id: scId, properties: [property] };
-                    if (oldId && oldId != property.key) {
-                        payload.deleted_properties = [{ key: oldId }];
-                    }
-                    dispatch(createSendActionNameAction(id, module, props.onEdit, payload));
-                }
-                setNewProp((np) => ({ ...np, key: "", value: "" }));
-                setFocusName("");
-            }
-        },
-        [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 property = scProperties.find(([key]) => key === propId);
-                property &&
-                    setProperties((ps) =>
-                        ps.map((p) => (p.id === property[0] ? { ...p, key: property[0], value: property[1] } : p))
-                    );
-                setFocusName("");
-            }
-        },
-        [isScenario, scProperties]
-    );
-
-    const deleteProperty = useCallback(
-        (e: React.MouseEvent<HTMLElement>) => {
-            e.stopPropagation();
-            const { id: propId = "" } = 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(
         (id: string, label: string) => {
@@ -458,17 +358,9 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
     // on scenario change
     useEffect(() => {
         showTags && setTags(scTags);
-        showProperties &&
-            setProperties(
-                scProperties.map(([k, v]) => ({
-                    id: k,
-                    key: k,
-                    value: v,
-                }))
-            );
         setLabel(scLabel);
         setUserExpanded(expanded && isScenario);
-    }, [scTags, scProperties, scLabel, isScenario, showTags, showProperties, expanded]);
+    }, [scTags, scLabel, isScenario, showTags, expanded]);
 
     // Refresh on broadcast
     useEffect(() => {
@@ -589,7 +481,7 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                                                 <Typography variant="subtitle2">{scLabel}</Typography>
                                             </Grid>
                                         </>
-                                    )}{" "}
+                                    )}
                                 </Grid>
                                 {showTags ? (
                                     <Grid
@@ -665,185 +557,17 @@ const ScenarioViewer = (props: ScenarioViewerProps) => {
                             <Grid item xs={12}>
                                 <Divider />
                             </Grid>
-                            {showProperties ? (
-                                <>
-                                    <Grid item xs={12} container rowSpacing={2}>
-                                        {properties
-                                            ? properties.map((property) => {
-                                                  const propName = `property-${property.id}`;
-                                                  return (
-                                                      <Grid
-                                                          item
-                                                          xs={12}
-                                                          spacing={1}
-                                                          container
-                                                          justifyContent="space-between"
-                                                          key={property.id}
-                                                          data-focus={propName}
-                                                          onClick={onFocus}
-                                                          sx={hoverSx}
-                                                      >
-                                                          {active && focusName === propName ? (
-                                                              <>
-                                                                  <Grid item xs={4}>
-                                                                      <TextField
-                                                                          label="Key"
-                                                                          variant="outlined"
-                                                                          value={property.key}
-                                                                          sx={FieldNoMaxWidth}
-                                                                          disabled={!isScenario}
-                                                                          data-name="key"
-                                                                          data-id={property.id}
-                                                                          onChange={updatePropertyField}
-                                                                      />
-                                                                  </Grid>
-                                                                  <Grid item xs={5}>
-                                                                      <TextField
-                                                                          label="Value"
-                                                                          variant="outlined"
-                                                                          value={property.value}
-                                                                          sx={FieldNoMaxWidth}
-                                                                          disabled={!isScenario}
-                                                                          data-name="value"
-                                                                          data-id={property.id}
-                                                                          onChange={updatePropertyField}
-                                                                      />
-                                                                  </Grid>
-                                                                  <Grid
-                                                                      item
-                                                                      xs={2}
-                                                                      container
-                                                                      alignContent="center"
-                                                                      alignItems="center"
-                                                                      justifyContent="center"
-                                                                  >
-                                                                      <IconButton
-                                                                          sx={IconPaddingSx}
-                                                                          data-id={property.id}
-                                                                          onClick={editProperty}
-                                                                      >
-                                                                          <CheckCircle color="primary" />
-                                                                      </IconButton>
-                                                                      <IconButton
-                                                                          sx={IconPaddingSx}
-                                                                          data-id={property.id}
-                                                                          onClick={cancelProperty}
-                                                                      >
-                                                                          <Cancel color="inherit" />
-                                                                      </IconButton>
-                                                                  </Grid>
-                                                                  <Grid
-                                                                      item
-                                                                      xs={1}
-                                                                      container
-                                                                      alignContent="center"
-                                                                      alignItems="center"
-                                                                      justifyContent="center"
-                                                                  >
-                                                                      <IconButton
-                                                                          sx={DeleteIconSx}
-                                                                          data-id={property.id}
-                                                                          onClick={deleteProperty}
-                                                                          disabled={!isScenario}
-                                                                      >
-                                                                          <DeleteOutline
-                                                                              fontSize="small"
-                                                                              color={disableColor(
-                                                                                  "primary",
-                                                                                  !isScenario
-                                                                              )}
-                                                                          />
-                                                                      </IconButton>
-                                                                  </Grid>
-                                                              </>
-                                                          ) : (
-                                                              <>
-                                                                  <Grid item xs={4}>
-                                                                      <Typography variant="subtitle2">
-                                                                          {property.key}
-                                                                      </Typography>
-                                                                  </Grid>
-                                                                  <Grid item xs={5}>
-                                                                      <Typography variant="subtitle2">
-                                                                          {property.value}
-                                                                      </Typography>
-                                                                  </Grid>{" "}
-                                                                  <Grid item xs={3} />
-                                                              </>
-                                                          )}
-                                                      </Grid>
-                                                  );
-                                              })
-                                            : null}
-                                        <Grid
-                                            item
-                                            xs={12}
-                                            spacing={1}
-                                            container
-                                            justifyContent="space-between"
-                                            data-focus="new-property"
-                                            onClick={onFocus}
-                                            sx={hoverSx}
-                                        >
-                                            {active && focusName == "new-property" ? (
-                                                <>
-                                                    <Grid item xs={4}>
-                                                        <TextField
-                                                            value={newProp.key}
-                                                            data-name="key"
-                                                            onChange={updatePropertyField}
-                                                            label="Key"
-                                                            variant="outlined"
-                                                            sx={FieldNoMaxWidth}
-                                                            disabled={!isScenario}
-                                                        />
-                                                    </Grid>
-                                                    <Grid item xs={5}>
-                                                        <TextField
-                                                            value={newProp.value}
-                                                            data-name="value"
-                                                            onChange={updatePropertyField}
-                                                            label="Value"
-                                                            variant="outlined"
-                                                            sx={FieldNoMaxWidth}
-                                                            disabled={!isScenario}
-                                                        />
-                                                    </Grid>
-                                                    <Grid
-                                                        item
-                                                        xs={2}
-                                                        container
-                                                        alignContent="center"
-                                                        alignItems="center"
-                                                        justifyContent="center"
-                                                    >
-                                                        <IconButton sx={IconPaddingSx} onClick={editProperty}>
-                                                            <CheckCircle color="primary" />
-                                                        </IconButton>
-                                                        <IconButton sx={IconPaddingSx} onClick={cancelProperty}>
-                                                            <Cancel color="inherit" />
-                                                        </IconButton>
-                                                    </Grid>
-                                                    <Grid item xs={1} />
-                                                </>
-                                            ) : (
-                                                <>
-                                                    <Grid item xs={4}>
-                                                        <Typography variant="subtitle2">New Property Key</Typography>
-                                                    </Grid>
-                                                    <Grid item xs={5}>
-                                                        <Typography variant="subtitle2">Value</Typography>
-                                                    </Grid>
-                                                    <Grid item xs={3} />
-                                                </>
-                                            )}
-                                        </Grid>
-                                    </Grid>
-                                    <Grid item xs={12}>
-                                        <Divider />
-                                    </Grid>
-                                </>
-                            ) : null}
+                            <PropertiesEditor
+                                entityId={scId}
+                                active={active}
+                                isDefined={isScenario}
+                                entProperties={scProperties}
+                                show={showProperties}
+                                focusName={focusName}
+                                setFocusName={setFocusName}
+                                onFocus={onFocus}
+                                onEdit={props.onEdit}
+                            />
                             {showPipelines ? (
                                 <>
                                     <Grid item xs={12} container justifyContent="space-between">

+ 2 - 1
gui/src/index.ts

@@ -3,5 +3,6 @@ import ScenarioViewer from "./ScenarioViewer";
 import ScenarioDag from "./ScenarioDag";
 import NodeSelector from "./NodeSelector";
 import JobSelector from "./JobSelector";
+import DataNodeViewer from "./DataNodeViewer";
 
-export { ScenarioSelector, ScenarioDag, ScenarioViewer as Scenario, NodeSelector as DataNodeSelector, JobSelector };
+export { ScenarioSelector, ScenarioDag, ScenarioViewer as Scenario, NodeSelector as DataNodeSelector, JobSelector, DataNodeViewer as DataNode };

+ 56 - 1
gui/src/utils.ts

@@ -10,6 +10,8 @@
  * 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 { Theme, alpha } from "@mui/material";
+import { PopoverOrigin } from "@mui/material/Popover";
 
 import { useDynamicProperty } from "taipy-gui";
 
@@ -74,7 +76,7 @@ export const BadgeSx = {
     },
 };
 
-export const MainBoxSx = {
+export const MainTreeBoxSx = {
     maxWidth: 300,
     overflowY: "auto",
 };
@@ -131,3 +133,56 @@ export const useClassNames = (libClassName?: string, dynamicClassName?: string,
     ((libClassName || "") + " " + (useDynamicProperty(dynamicClassName, className, undefined) || "")).trim();
 
 export const disableColor = <T>(color: T, disabled: boolean) => (disabled ? ("disabled" as T) : color);
+
+export const hoverSx = {
+    "&:hover": {
+        bgcolor: "action.hover",
+        cursor: "text",
+    },
+    mt: 0,
+};
+
+export const FieldNoMaxWidth = {
+    maxWidth: "none",
+};
+
+export const IconPaddingSx = { padding: 0 };
+
+export const MainBoxSx = {
+    overflowY: "auto",
+};
+
+export const AccordionIconSx = { fontSize: "0.9rem" };
+
+export const AccordionSummarySx = {
+    flexDirection: "row-reverse",
+    "& .MuiAccordionSummary-expandIconWrapper.Mui-expanded": {
+        transform: "rotate(90deg)",
+        mr: 1,
+    },
+    "& .MuiAccordionSummary-content": {
+        mr: 1,
+    },
+};
+
+export const tinySelPinIconButtonSx = (theme: Theme) => ({
+    ...tinyIconButtonSx,
+    backgroundColor: "secondary.main",
+    color: "secondary.contrastText",
+
+    "&:hover": {
+        backgroundColor: alpha(theme.palette.secondary.main, 0.75),
+        color: "secondary.contrastText",
+    },
+});
+
+export const popoverOrigin: PopoverOrigin = {
+    vertical: "bottom",
+    horizontal: "left",
+};
+
+export const iconLabelSx = {
+    display: "flex",
+    alignItems: "center",
+    gap: 1,
+};

+ 15 - 0
gui/src/utils/hooks.ts

@@ -0,0 +1,15 @@
+/*
+ * 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 { useMemo } from "react";
+
+export const useUniqueId = (id?: string) => useMemo(() => id ? id : new Date().toISOString() + Math.random(), [id]);

+ 1 - 0
setup.py

@@ -28,6 +28,7 @@ with open(f"src{os.sep}taipy{os.sep}version.json") as version_file:
         version_string = f"{version_string}.{vext}"
 
 requirements = [
+    "backports.zoneinfo>=0.2.1,<0.3;python_version<'3.9'",
     "cookiecutter>=2.1.1,<2.2",
     "taipy-gui@git+https://git@github.com/Avaiga/taipy-gui.git@develop",
     "taipy-rest@git+https://git@github.com/Avaiga/taipy-rest.git@develop",

+ 392 - 53
src/taipy/gui_core/GuiCoreLib.py

@@ -9,12 +9,19 @@
 # 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 json
 import typing as t
 from collections import defaultdict
 from datetime import datetime
 from enum import Enum
+from numbers import Number
 from threading import Lock
 
+try:
+    import zoneinfo
+except ImportError:
+    from backports import zoneinfo  # type: ignore[no-redef]
+
 from dateutil import parser
 
 from taipy.config import Config
@@ -32,10 +39,12 @@ from taipy.core import (
     set_primary,
 )
 from taipy.core import submit as core_submit
+from taipy.core.data._abstract_tabular import _AbstractTabularDataNode
 from taipy.core.notification import CoreEventConsumerBase, EventEntityType
 from taipy.core.notification.event import Event
 from taipy.core.notification.notifier import Notifier
 from taipy.gui import Gui, State
+from taipy.gui._warnings import _warn
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
 from taipy.gui.gui import _DoNotUpdate
 from taipy.gui.utils import _TaipyBase
@@ -137,20 +146,63 @@ class _GuiCoreScenarioDagAdapter(_TaipyBase):
         return _TaipyBase._HOLDER_PREFIX + "ScG"
 
 
+class _GuiCoreDatanodeAdapter(_TaipyBase):
+    __INNER_PROPS = ["name"]
+
+    def get(self):
+        data = super().get()
+        if isinstance(data, DataNode):
+            datanode = core_get(data.id)
+            if datanode:
+                owner = core_get(datanode.owner_id) if datanode.owner_id else None
+                return [
+                    datanode.id,
+                    datanode.storage_type() if hasattr(datanode, "storage_type") else "",
+                    datanode.config_id,
+                    f"{datanode.last_edit_date}" if datanode.last_edit_date else "",
+                    f"{datanode.expiration_date}" if datanode.last_edit_date else "",
+                    datanode.get_simple_label(),
+                    datanode.owner_id or "",
+                    owner.get_simple_label() if owner else "GLOBAL",
+                    _EntityType.CYCLE.value
+                    if isinstance(owner, Cycle)
+                    else _EntityType.SCENARIO.value
+                    if isinstance(owner, Scenario)
+                    else -1,
+                    [
+                        (k, f"{v}")
+                        for k, v in datanode.properties.items()
+                        if k not in _GuiCoreDatanodeAdapter.__INNER_PROPS
+                    ]
+                    if datanode.properties
+                    else [],
+                ]
+        return None
+
+    @staticmethod
+    def get_hash():
+        return _TaipyBase._HOLDER_PREFIX + "Dn"
+
+
 class _GuiCoreContext(CoreEventConsumerBase):
     __PROP_ENTITY_ID = "id"
-    __PROP_SCENARIO_CONFIG_ID = "config"
-    __PROP_SCENARIO_DATE = "date"
+    __PROP_CONFIG_ID = "config"
+    __PROP_DATE = "date"
     __PROP_ENTITY_NAME = "name"
     __PROP_SCENARIO_PRIMARY = "primary"
     __PROP_SCENARIO_TAGS = "tags"
-    __SCENARIO_PROPS = (__PROP_SCENARIO_CONFIG_ID, __PROP_SCENARIO_DATE, __PROP_ENTITY_NAME)
+    __ENTITY_PROPS = (__PROP_CONFIG_ID, __PROP_DATE, __PROP_ENTITY_NAME)
     __ACTION = "action"
     _CORE_CHANGED_NAME = "core_changed"
     _SCENARIO_SELECTOR_ERROR_VAR = "gui_core_sc_error"
     _SCENARIO_SELECTOR_ID_VAR = "gui_core_sc_id"
     _SCENARIO_VIZ_ERROR_VAR = "gui_core_sv_error"
     _JOB_SELECTOR_ERROR_VAR = "gui_core_js_error"
+    _DATANODE_VIZ_ERROR_VAR = "gui_core_dv_error"
+    _DATANODE_VIZ_OWNER_ID_VAR = "gui_core_dv_owner_id"
+    _DATANODE_VIZ_HISTORY_ID_VAR = "gui_core_dv_history_id"
+    _DATANODE_VIZ_DATA_ID_VAR = "gui_core_dv_data_id"
+    _DATANODE_VIZ_DATA_NODE_PROP = "data_node"
 
     def __init__(self, gui: Gui) -> None:
         self.gui = gui
@@ -195,7 +247,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
 
     def scenario_adapter(self, data):
         if hasattr(data, "id") and core_get(data.id) is not None:
-            if isinstance(data, Cycle):
+            if self.scenario_by_cycle and isinstance(data, Cycle):
                 return (
                     data.id,
                     data.get_simple_label(),
@@ -256,6 +308,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
         delete = args[1]
         data = args[2]
         scenario = None
+
         name = data.get(_GuiCoreContext.__PROP_ENTITY_NAME)
         if update:
             scenario_id = data.get(_GuiCoreContext.__PROP_ENTITY_ID)
@@ -267,18 +320,51 @@ class _GuiCoreContext(CoreEventConsumerBase):
             else:
                 scenario = core_get(scenario_id)
         else:
-            config_id = data.get(_GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
+            config_id = data.get(_GuiCoreContext.__PROP_CONFIG_ID)
             scenario_config = Config.scenarios.get(config_id)
             if scenario_config is None:
                 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_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}")
                 return
             try:
+                gui: Gui = state._gui
+                on_creation = args[3] if len(args) > 3 and isinstance(args[3], str) else None
+                on_creation_function = gui._get_user_function(on_creation) if on_creation else None
+                if callable(on_creation_function):
+                    try:
+                        res = gui._call_function_with_state(
+                            on_creation_function,
+                            [
+                                id,
+                                on_creation,
+                                {
+                                    "config": scenario_config,
+                                    "date": date,
+                                    "label": name,
+                                    "properties": {
+                                        v.get("key"): v.get("value") for v in data.get("properties", dict())
+                                    },
+                                },
+                            ],
+                        )
+                        if isinstance(res, Scenario):
+                            # everything's fine
+                            state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
+                            return
+                        if res:
+                            # do not create
+                            state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"{res}")
+                            return
+                    except Exception as e:  # pragma: no cover
+                        if not gui._call_on_exception(on_creation, e):
+                            _warn(f"on_creation(): Exception raised in '{on_creation}()':\n{e}")
+                else:
+                    _warn(f"on_creation(): '{on_creation}' is not a function.")
                 scenario = create_scenario(scenario_config, date, name)
             except Exception as e:
                 state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, f"Error creating Scenario. {e}")
@@ -289,11 +375,11 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     try:
                         new_keys = [prop.get("key") for prop in props]
                         for key in t.cast(dict, sc.properties).keys():
-                            if key and key not in _GuiCoreContext.__SCENARIO_PROPS and key not in new_keys:
+                            if key and key not in _GuiCoreContext.__ENTITY_PROPS and key not in new_keys:
                                 t.cast(dict, sc.properties).pop(key, None)
                         for prop in props:
                             key = prop.get("key")
-                            if key and key not in _GuiCoreContext.__SCENARIO_PROPS:
+                            if key and key not in _GuiCoreContext.__ENTITY_PROPS:
                                 sc._properties[key] = prop.get("value")
                         state.assign(_GuiCoreContext._SCENARIO_SELECTOR_ERROR_VAR, "")
                     except Exception as e:
@@ -312,27 +398,8 @@ class _GuiCoreContext(CoreEventConsumerBase):
                     primary = data.get(_GuiCoreContext.__PROP_SCENARIO_PRIMARY)
                     if primary is True:
                         set_primary(entity)
-                with entity as ent:
-                    if isinstance(ent, Scenario):
-                        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)
-                    if isinstance(name, str):
-                        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:
-                                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:
-                                ent.properties.pop(key, None)
-                    state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
+                self.__edit_properties(entity, data)
+                state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, "")
             except Exception as e:
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error updating Scenario. {e}")
 
@@ -350,39 +417,44 @@ class _GuiCoreContext(CoreEventConsumerBase):
             except Exception as e:
                 state.assign(_GuiCoreContext._SCENARIO_VIZ_ERROR_VAR, f"Error submitting entity. {e}")
 
+    def __do_datanodes_tree(self):
+        if self.data_nodes_by_owner is None:
+            self.data_nodes_by_owner = defaultdict(list)
+            for dn in get_data_nodes():
+                self.data_nodes_by_owner[dn.owner_id].append(dn)
+
     def get_datanodes_tree(self):
         with self.lock:
-            if self.data_nodes_by_owner is None:
-                self.data_nodes_by_owner = defaultdict(list)
-                for dn in get_data_nodes():
-                    self.data_nodes_by_owner[dn.owner_id].append(dn)
+            self.__do_datanodes_tree()
         return self.get_scenarios()
 
     def data_node_adapter(self, data):
-        if hasattr(data, "id") and core_get(data.id) is not None:
+        if data and hasattr(data, "id") and core_get(data.id) is not None:
             if isinstance(data, DataNode):
                 return (data.id, data.get_simple_label(), None, _EntityType.DATANODE.value, False)
             else:
                 with self.lock:
-                    if isinstance(data, Cycle):
-                        return (
-                            data.id,
-                            data.get_simple_label(),
-                            self.data_nodes_by_owner[data.id] + self.scenario_by_cycle.get(data, []),
-                            _EntityType.CYCLE.value,
-                            False,
-                        )
-                    elif isinstance(data, Scenario):
-                        return (
-                            data.id,
-                            data.get_simple_label(),
-                            self.data_nodes_by_owner[data.id] + list(data.pipelines.values()),
-                            _EntityType.SCENARIO.value,
-                            data.is_primary,
-                        )
-                    elif isinstance(data, Pipeline):
-                        if datanodes := self.data_nodes_by_owner.get(data.id):
-                            return (data.id, data.get_simple_label(), datanodes, _EntityType.PIPELINE.value, False)
+                    self.__do_datanodes_tree()
+                    if self.data_nodes_by_owner:
+                        if isinstance(data, Cycle):
+                            return (
+                                data.id,
+                                data.get_simple_label(),
+                                self.data_nodes_by_owner[data.id] + self.scenario_by_cycle.get(data, []),
+                                _EntityType.CYCLE.value,
+                                False,
+                            )
+                        elif isinstance(data, Scenario):
+                            return (
+                                data.id,
+                                data.get_simple_label(),
+                                self.data_nodes_by_owner[data.id] + list(data.pipelines.values()),
+                                _EntityType.SCENARIO.value,
+                                data.is_primary,
+                            )
+                        elif isinstance(data, Pipeline):
+                            if datanodes := self.data_nodes_by_owner.get(data.id):
+                                return (data.id, data.get_simple_label(), datanodes, _EntityType.PIPELINE.value, False)
         return None
 
     def get_jobs_list(self):
@@ -429,6 +501,206 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         errs.append(f"Error canceling job. {e}")
             state.assign(_GuiCoreContext._JOB_SELECTOR_ERROR_VAR, "<br/>".join(errs) if errs else "")
 
+    def edit_data_node(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: DataNode = core_get(entity_id)
+        if isinstance(entity, DataNode):
+            try:
+                self.__edit_properties(entity, data)
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+            except Exception as e:
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode. {e}")
+
+    def __edit_properties(self, entity: t.Union[Scenario, Pipeline, DataNode], data: t.Dict[str, str]):
+        with entity as ent:
+            if isinstance(ent, Scenario):
+                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)
+            if isinstance(name, str):
+                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.__ENTITY_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.__ENTITY_PROPS:
+                        ent.properties.pop(key, None)
+
+    def get_scenarios_for_owner(self, owner_id: str):
+        cycles_scenarios: t.List[t.Union[Scenario, Cycle]] = []
+        with self.lock:
+            if self.scenario_by_cycle is None:
+                self.scenario_by_cycle = get_cycles_scenarios()
+            if owner_id:
+                if owner_id == "GLOBAL":
+                    for cycle, scenarios in self.scenario_by_cycle.items():
+                        if cycle is None:
+                            cycles_scenarios.extend(scenarios)
+                        else:
+                            cycles_scenarios.append(cycle)
+                else:
+                    entity = core_get(owner_id)
+                    if entity and (scenarios := self.scenario_by_cycle.get(entity)):
+                        cycles_scenarios.extend(scenarios)
+                    elif isinstance(entity, Scenario):
+                        cycles_scenarios.append(entity)
+        return cycles_scenarios
+
+    def get_data_node_history(self, datanode: DataNode, id: str):
+        if (
+            id
+            and isinstance(datanode, DataNode)
+            and id == datanode.id
+            and (dn := core_get(id))
+            and isinstance(dn, DataNode)
+        ):
+            res = []
+            for e in dn.edits:
+                job: Job = core_get(e.get("job_id")) if "job_id" in e else None
+                res.append(
+                    (
+                        e.get("timestamp"),
+                        job.id if job else e.get("writer_identifier", "Unknown"),
+                        f"Execution of task {job.task.get_simple_label()}." if job and job.task else e.get("comments"),
+                    )
+                )
+            return list(reversed(sorted(res, key=lambda r: r[0])))
+        return _DoNotUpdate()
+
+    def get_data_node_data(self, datanode: DataNode, id: str):
+        if (
+            id
+            and isinstance(datanode, DataNode)
+            and id == datanode.id
+            and (dn := core_get(id))
+            and isinstance(dn, DataNode)
+        ):
+            if dn.is_ready_for_reading:
+                if isinstance(dn, _AbstractTabularDataNode):
+                    return (None, None, True, None)
+                try:
+                    value = dn.read()
+                    return (
+                        value,
+                        "date"
+                        if "date" in type(value).__name__
+                        else type(value).__name__
+                        if isinstance(value, Number)
+                        else None,
+                        None,
+                        None,
+                    )
+                except Exception as e:
+                    return (None, None, None, f"read data_node: {e}")
+            return (None, None, None, f"Data unavailable for {dn.get_simple_label()}")
+        return _DoNotUpdate()
+
+    def update_data(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: DataNode = core_get(entity_id)
+        if isinstance(entity, DataNode):
+            try:
+                entity.write(
+                    parser.parse(data.get("value"))
+                    if data.get("type") == "date"
+                    else int(data.get("value"))
+                    if data.get("type") == "int"
+                    else float(data.get("value"))
+                    if data.get("type") == "float"
+                    else data.get("value"),
+                    comment="Written by CoreGui DataNode viewer Element",
+                )
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+            except Exception as e:
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode value. {e}")
+            state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, entity_id)  # this will update the data value
+
+    def tabular_data_edit(self, state: State, var_name: str, action: str, payload: dict):
+        dn_id = payload.get("user_data")
+        datanode = core_get(dn_id)
+        if isinstance(datanode, DataNode):
+            try:
+                idx = payload.get("index")
+                col = payload.get("col")
+                tz = payload.get("tz")
+                val = (
+                    parser.parse(str(payload.get("value"))).astimezone(zoneinfo.ZoneInfo(tz)).replace(tzinfo=None)
+                    if tz is not None
+                    else payload.get("value")
+                )
+                # user_value = payload.get("user_value")
+                data = self.__read_tabular_data(datanode)
+                data.at[idx, col] = val
+                datanode.write(data)
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, "")
+            except Exception as e:
+                state.assign(_GuiCoreContext._DATANODE_VIZ_ERROR_VAR, f"Error updating Datanode tabular value. {e}")
+        setattr(state, _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, dn_id)
+
+    def __read_tabular_data(self, datanode: DataNode):
+        return datanode.read()
+
+    def get_data_node_tabular_data(self, datanode: DataNode, id: str):
+        if (
+            id
+            and isinstance(datanode, DataNode)
+            and id == datanode.id
+            and (dn := core_get(id))
+            and isinstance(dn, DataNode)
+            and dn.is_ready_for_reading
+            and isinstance(dn, _AbstractTabularDataNode)
+        ):
+            try:
+                return self.__read_tabular_data(dn)
+            except Exception:
+                return None
+        return None
+
+    def get_data_node_tabular_columns(self, datanode: DataNode, id: str):
+        if (
+            id
+            and isinstance(datanode, DataNode)
+            and id == datanode.id
+            and (dn := core_get(id))
+            and isinstance(dn, DataNode)
+            and dn.is_ready_for_reading
+            and isinstance(dn, _AbstractTabularDataNode)
+        ):
+            try:
+                return self.gui._tbl_cols(
+                    True, True, "{}", json.dumps({"data": "tabular_data"}), tabular_data=self.__read_tabular_data(dn)
+                )
+            except Exception:
+                return None
+        return _DoNotUpdate()
+
+    def select_id(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 and isinstance(args[0], dict):
+            return
+        data = args[0]
+        if owner_id := data.get("owner_id"):
+            state.assign(_GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR, owner_id)
+        elif history_id := data.get("history_id"):
+            state.assign(_GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR, history_id)
+        elif data_id := data.get("data_id"):
+            state.assign(_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR, data_id)
+
 
 class _GuiCore(ElementLibrary):
     __LIB_NAME = "taipy_gui_core"
@@ -450,6 +722,7 @@ class _GuiCore(ElementLibrary):
                 "height": ElementProperty(PropertyType.string, "50vh"),
                 "class_name": ElementProperty(PropertyType.dynamic_string),
                 "show_pins": ElementProperty(PropertyType.boolean, False),
+                "on_creation": ElementProperty(PropertyType.function),
             },
             inner_properties={
                 "scenarios": ElementProperty(PropertyType.lov, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
@@ -524,6 +797,64 @@ class _GuiCore(ElementLibrary):
                 "type": ElementProperty(PropertyType.inner, __DATANODE_ADAPTER),
             },
         ),
+        "data_node": Element(
+            _GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP,
+            {
+                "id": ElementProperty(PropertyType.string),
+                _GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP: ElementProperty(_GuiCoreDatanodeAdapter),
+                "active": ElementProperty(PropertyType.dynamic_boolean, True),
+                "expandable": ElementProperty(PropertyType.boolean, True),
+                "expanded": ElementProperty(PropertyType.boolean, True),
+                "show_config": ElementProperty(PropertyType.boolean, False),
+                "show_owner": ElementProperty(PropertyType.boolean, True),
+                "show_edit_date": ElementProperty(PropertyType.boolean, False),
+                "show_expiration_date": ElementProperty(PropertyType.boolean, False),
+                "show_properties": ElementProperty(PropertyType.boolean, True),
+                "show_history": ElementProperty(PropertyType.boolean, True),
+                "show_data": ElementProperty(PropertyType.boolean, True),
+                "chart_config": ElementProperty(PropertyType.dict),
+                "class_name": ElementProperty(PropertyType.dynamic_string),
+                "scenario": ElementProperty(PropertyType.lov_value),
+            },
+            inner_properties={
+                "on_edit": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.edit_data_node}}"),
+                "core_changed": ElementProperty(PropertyType.broadcast, _GuiCoreContext._CORE_CHANGED_NAME),
+                "error": ElementProperty(PropertyType.react, f"{{{_GuiCoreContext._DATANODE_VIZ_ERROR_VAR}}}"),
+                "scenarios": ElementProperty(
+                    PropertyType.lov,
+                    f"{{{__CTX_VAR_NAME}.get_scenarios_for_owner({_GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR})}}",
+                ),
+                "type": ElementProperty(PropertyType.inner, __SCENARIO_ADAPTER),
+                "on_id_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_id}}"),
+                "history": ElementProperty(
+                    PropertyType.react,
+                    f"{{{__CTX_VAR_NAME}.get_data_node_history("
+                    + f"<tp:prop:{_GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP}>, "
+                    + f"{_GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR})}}",
+                ),
+                "data": ElementProperty(
+                    PropertyType.react,
+                    f"{{{__CTX_VAR_NAME}.get_data_node_data(<tp:prop:{_GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP}>,"
+                    + f" {_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR})}}",
+                ),
+                "tabular_data": ElementProperty(
+                    PropertyType.data,
+                    f"{{{__CTX_VAR_NAME}.get_data_node_tabular_data("
+                    + f"<tp:prop:{_GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP}>, "
+                    + f"{_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR})}}",
+                ),
+                "tabular_columns": ElementProperty(
+                    PropertyType.string,
+                    f"{{{__CTX_VAR_NAME}.get_data_node_tabular_columns("
+                    + f"<tp:prop:{_GuiCoreContext._DATANODE_VIZ_DATA_NODE_PROP}>, "
+                    + f"{_GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR})}}",
+                ),
+                "on_data_value": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.update_data}}"),
+                "on_tabular_data_edit": ElementProperty(
+                    PropertyType.function, f"{{{__CTX_VAR_NAME}.tabular_data_edit}}"
+                ),
+            },
+        ),
         "job_selector": Element(
             "value",
             {
@@ -566,6 +897,10 @@ class _GuiCore(ElementLibrary):
                 _GuiCoreContext._SCENARIO_SELECTOR_ID_VAR: "",
                 _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR: "",
                 _GuiCoreContext._JOB_SELECTOR_ERROR_VAR: "",
+                _GuiCoreContext._DATANODE_VIZ_ERROR_VAR: "",
+                _GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR: "",
+                _GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR: "",
+                _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR: "",
             }
         )
         ctx = _GuiCoreContext(gui)
@@ -580,6 +915,10 @@ class _GuiCore(ElementLibrary):
             _GuiCoreContext._SCENARIO_SELECTOR_ID_VAR,
             _GuiCoreContext._SCENARIO_VIZ_ERROR_VAR,
             _GuiCoreContext._JOB_SELECTOR_ERROR_VAR,
+            _GuiCoreContext._DATANODE_VIZ_ERROR_VAR,
+            _GuiCoreContext._DATANODE_VIZ_OWNER_ID_VAR,
+            _GuiCoreContext._DATANODE_VIZ_HISTORY_ID_VAR,
+            _GuiCoreContext._DATANODE_VIZ_DATA_ID_VAR,
         ]:
             state._add_attribute(var, "")
 

+ 92 - 0
src/taipy/gui_core/viselements.json

@@ -61,6 +61,12 @@
                         "type": "bool",
                         "default_value": "False",
                         "doc": "If True, a pin is shown on each item of the selector and allows to restrict the numnber of displayed items."
+                    },
+                    {
+                      "name": "on_creation",
+                      "type": "Callback",
+                      "doc": "The name of the function that is triggered when a scenario is about to be created.<br/><br/>All the parameters of that function are optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the scenario selector.</li>\n<li>action (str): the name of the action that provoked the invocation of the callback.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>\nThis dictionary has the following keys:\n<ul>\n<li>config: the name of the selected scenario configuration.</li>\n<li>date: the creation date for the new scenario.</li>\n<li>name: the user entered name.</li>\n<li>properties: a dictionnary.</li>\n</ul>\n</li>\n<li>return: the callback can return a scenario, a string containing an error message (a scenario will not be created) or None (then a new scenario is created with the user parameters).</li>\n</ul>",
+                      "signature": [["state", "State"], ["id", "str"], ["action", "str"], ["payload", "dict"]]
                     }
                 ]
             }
@@ -327,6 +333,92 @@
                     }
                 ]
             }
+        ],
+        [
+            "data_node",
+            {
+                "inherits": [
+                    "core_gui_shared"
+                ],
+                "properties": [
+                    {
+                        "name": "data_node",
+                        "default_property": true,
+                        "type": "dynamic(DataNode|list[DataNode])",
+                        "doc": "The datanode to display and edit.<br/>If the value is a list, it must have a single element otherwise nothing is shown."
+                    },
+                    {
+                        "name": "active",
+                        "type": "dynamic(bool)",
+                        "default_value": "True",
+                        "doc": "Indicates if this component is active.<br/>An inactive component allows no user interaction."
+                    },
+                    {
+                        "name": "expandable",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If True, the scenario visualizer can be expanded.<br/>If False, the scenario visualizer is not expandable and it is shown depending on expanded value."
+                    },
+                    {
+                        "name": "expanded",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If True, when a valid scenario is selected, the scenario visualizer is expanded and its content is displayed.<br/>If False, the scenario visualizer is collapsed and only its name and <i>submit</i> button are visible."
+                    },
+                    {
+                        "name": "show_config",
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If False, the datanode configuration label is not visible."
+                    },
+                    {
+                        "name": "show_owner",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If False, the datanode owner label is not visible."
+                    },
+                    {
+                        "name": "show_edit_date",
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If False, the datanode edition date is not visible."
+                    },
+                    {
+                        "name": "show_expiration_date",
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If False, the datanode expiration date is not visible."
+                    },
+                    {
+                        "name": "show_properties",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If False, the datanode properties are not visible."
+                    },
+                    {
+                        "name": "show_history",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If False, the datanode history is not visible."
+                    },
+                    {
+                        "name": "show_data",
+                        "type": "bool",
+                        "default_value": "True",
+                        "doc": "If False, the datanode value is not visible."
+                    },
+                    {
+                        "name": "chart_config",
+                        "type": "dict",
+                        "doc": "Chart configs by datanode configuration id."
+                    },
+                    {
+                        "name": "scenario",
+                        "type": "dynamic(Scenario)",
+                        "doc": "Bound to the selected <code>Scenario^</code> from the owner list of Scenarios, or None if there is none."
+                    }
+                ]
+            }
         ]
     ],
     "undocumented": [

Some files were not shown because too many files changed in this diff