Pārlūkot izejas kodu

#115 connect Gui to Core (#123)

* #115 connect Gui to Core

* flake8

* black

* Fab's comment

* format

* #115 on_select called with selected scenario instances
Library registration is automatic
remove custom properties remove all button

* JR's comment

* isort

* pycodestyle

* use on_action with default

---------

Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
Fred Lefévère-Laoide 2 gadi atpakaļ
vecāks
revīzija
8de76f561b
6 mainītis faili ar 368 papildinājumiem un 535 dzēšanām
  1. 1 0
      Pipfile
  2. 0 22
      demo-scenario_selector.py
  3. 239 372
      gui/src/ScenarioSelector.tsx
  4. 0 127
      gui/src/data.tsx
  5. 4 0
      src/taipy/__init__.py
  6. 124 14
      src/taipy/gui_core/GuiCoreLib.py

+ 1 - 0
Pipfile

@@ -17,6 +17,7 @@ mypy = "*"
 pre-commit = "*"
 pytest = "*"
 tox = "*"
+types-python-dateutil = "*"
 
 [requires]
 python_version = "3"

+ 0 - 22
demo-scenario_selector.py

@@ -1,22 +0,0 @@
-# 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.
-
-from src.taipy.gui_core.GuiCoreLib import GuiCore
-from taipy.gui import Gui
-
-page = """
-# Getting started with example
-
-<|taipy_gui_core.scenario_selector|show_add_button|display_cycles|>
-
-"""
-Gui.add_library(GuiCore())
-Gui(page).run(port=8000, use_reloader=True)

+ 239 - 372
gui/src/ScenarioSelector.tsx

@@ -11,357 +11,294 @@
  * specific language governing permissions and limitations under the License.
  */
 
-import React from "react";
-import { useEffect, useState } from "react";
-import {
-  Badge,
-  Box,
-  Button,
-  DialogActions,
-  DialogContent,
-  DialogTitle,
-  FormControl,
-  FormGroup,
-  FormHelperText,
-  Grid,
-  IconButton,
-  InputLabel,
-  MenuItem,
-  Dialog as MuiDialog,
-  Select,
-  TextField,
-} from "@mui/material";
-import {
-  ChevronRight,
-  ExpandMore,
-  FlagOutlined,
-  Close,
-  DeleteOutline,
-  Add,
-} from "@mui/icons-material";
+import React, { useEffect, useState, useCallback } from "react";
+import Badge, { BadgeOrigin } from "@mui/material/Badge";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import FormControl from "@mui/material/FormControl";
+import FormGroup from "@mui/material/FormGroup";
+import FormHelperText from "@mui/material/FormHelperText";
+import Grid from "@mui/material/Grid";
+import IconButton from "@mui/material/IconButton";
+import InputLabel from "@mui/material/InputLabel";
+import MenuItem from "@mui/material/MenuItem";
+import Dialog from "@mui/material/Dialog";
+import Select from "@mui/material/Select";
+import TextField from "@mui/material/TextField";
+import Typography from "@mui/material/Typography";
+import { ChevronRight, ExpandMore, FlagOutlined, Close, DeleteOutline, Add } from "@mui/icons-material";
 import TreeItem from "@mui/lab/TreeItem";
-import { Typography } from "@mui/material";
+import TreeView from "@mui/lab/TreeView";
+import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
+import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
 import { useFormik } from "formik";
 
 import {
-  LoV,
   useDynamicProperty,
   useDispatch,
   useModule,
   createRequestUpdateAction,
   getUpdateVar,
   createSendActionNameAction,
+  useDispatchRequestUpdateOnFirstRender,
 } from "taipy-gui";
-import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers";
-import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
-import MuiTreeView from "@mui/lab/TreeView";
-import { cycles } from "./data";
-import { format } from "date-fns";
-
-export type Scenario = {
-  date: string;
-  name: string;
-  config: string;
-  id: string;
-};
-
-export type Property = {
-  id: string;
-  key: string;
-  value: string;
-};
-
-export type TreeNode = {
-  id: string;
-  label: string;
-  type: NodeType;
-  primary?: boolean;
-  children?: TreeNode[];
-};
 
-export enum NodeType {
+enum NodeType {
   CYCLE = 0,
   SCENARIO = 1,
 }
 
+type Scenario = [string, string, number, boolean];
+type Scenarios = Array<Scenario>;
+type Cycles = Array<[string, string, number, boolean, Scenarios]>;
+
 interface ScenarioSelectorProps {
+  id?: string;
   defaultShowAddButton: boolean;
   showAddButton?: boolean;
   defaultDisplayCycles: boolean;
   displayCycles?: boolean;
   defaultShowPrimaryFlag: boolean;
   showPrimaryFlag?: boolean;
-  scenarios?: LoV;
-  defaultScenarios?: LoV;
-  defaultScenarioId?: string;
-  scenarioId?: string;
-  onScenarioCreate?: string;
+  scenarios?: Cycles | Scenarios;
+  onScenarioCreate: string;
+  onCtxSelection: string;
+  onAction?: string;
   coreChanged?: Record<string, unknown>;
-  updateVarNames: string;
+  updateVars: string;
+  configs?: Array<[string, string]>;
+  error?: string;
 }
 
-// COMMENTED THIS OUT SINCE WE DONT NEED TO VALIDATE FOR NOW
-//
-// const scenarioSchema = Yup.object().shape({
-//   config: Yup.string()
-//     .trim("Cannot include leading and trailing spaces")
-//     .required("Config is required."),
-//   name: Yup.string()
-//     .trim("Cannot include leading and trailing spaces")
-//     .required("Name is required."),
-//   date: Yup.string().required("Date is required."),
-// });
+interface ScenarioNodesProps {
+  scenarios?: Scenarios | Scenario;
+  showPrimary?: boolean;
+}
+
+const BadgePos = {
+  vertical: "top",
+  horizontal: "left",
+} as BadgeOrigin;
+
+const BadgeSx = {
+  "& .MuiBadge-badge": {
+    marginLeft: "-12px",
+    height: "19px",
+    width: "12px",
+  },
+};
+
+const FlagSx = {
+  color: "#FFFFFF",
+  fontSize: "11px",
+};
+
+const ScenarioNodes = ({ scenarios = [], showPrimary = true }: ScenarioNodesProps) => {
+  const sc = Array.isArray(scenarios) && scenarios.length && Array.isArray(scenarios[0]) ? (scenarios as Scenarios) : scenarios ? [scenarios as Scenario] : [];
+  return (
+    <>
+      {sc.map(([id, label, _, primary]) => (
+        <TreeItem
+          key={id}
+          nodeId={id}
+          label={
+            showPrimary && primary ? (
+              <Badge badgeContent={<FlagOutlined sx={FlagSx} />} color="primary" anchorOrigin={BadgePos} sx={BadgeSx}>
+                <Grid container alignItems="center" direction="row" flexWrap="nowrap" justifyContent="flex-start" spacing={1}>
+                  <Grid item>{label}</Grid>
+                </Grid>
+              </Badge>
+            ) : (
+              <Grid container alignItems="center" direction="row" flexWrap="nowrap" justifyContent="flex-start" spacing={1}>
+                <Grid item>{label}</Grid>
+              </Grid>
+            )
+          }
+        />
+      ))}
+    </>
+  );
+};
+
+type Property = {
+  id: string;
+  key: string;
+  value: string;
+};
+
+const MainBoxSx = {
+  maxWidth: 300,
+  overflowY: "auto",
+};
+
+const TreeViewSx = {
+  mb: 2,
+};
+
+const CycleSx = {
+  ".MuiTreeItem-content": {
+    padding: "4px 8px",
+    gap: "4px",
+    borderRadius: "4px",
+    mb: "5px",
+  },
+  ".MuiTreeItem-label": {
+    fontWeight: "700",
+    fontSize: "16px",
+  },
+};
+
+const DialogContentSx = {
+  width: "500px",
+};
 
 const ScenarioSelector = (props: ScenarioSelectorProps) => {
+  const { id = "", scenarios = [] } = props;
   const [open, setOpen] = useState(false);
-  const [nodes, setNodes] = useState<TreeNode[]>([]);
   const [properties, setProperties] = useState<Property[]>([]);
-  const [key, setKey] = useState<string>("");
-  const [value, setValue] = useState<string>("");
+  const [newProp, setNewProp] = useState<Property>({ id: "", key: "", value: "" });
 
   const dispatch = useDispatch();
   const module = useModule();
 
-  const showAddButton = useDynamicProperty(
-    props.showAddButton,
-    props.defaultShowAddButton,
-    true
-  );
-  const displayCycles = useDynamicProperty(
-    props.displayCycles,
-    props.defaultDisplayCycles,
-    true
-  );
-  const showPrimaryFlag = useDynamicProperty(
-    props.showPrimaryFlag,
-    props.defaultShowPrimaryFlag,
-    true
-  );
-  const scenarioId = useDynamicProperty(
-    props.scenarioId,
-    props.defaultScenarioId,
-    ""
-  );
-
-  const onAdd = (node: Scenario) => {
-    dispatch(
-      createSendActionNameAction("", module, props.onScenarioCreate, node)
-    );
-  };
+  useDispatchRequestUpdateOnFirstRender(dispatch, "", module, props.updateVars);
 
-  const propertyAdd = (key: string, value: string) => {
-    let newProp: Property = {
-      id: properties.length + 1 + "",
-      key: key,
-      value: value,
-    };
-    setProperties([...properties, newProp]);
-    setKey("");
-    setValue("");
-  };
-  const propertyDelete = (id: string) => {
-    const filtered = properties.filter((itm) => itm.id !== id);
-    setProperties(filtered);
-  };
+  const showAddButton = useDynamicProperty(props.showAddButton, props.defaultShowAddButton, true);
+  const displayCycles = useDynamicProperty(props.displayCycles, props.defaultDisplayCycles, true);
+  const showPrimaryFlag = useDynamicProperty(props.showPrimaryFlag, props.defaultShowPrimaryFlag, true);
 
   const onSubmit = (values: any) => {
-    onAdd(values);
+    values.properties = [...properties];
+    dispatch(createSendActionNameAction(id, module, props.onScenarioCreate, values));
     form.resetForm();
     setOpen(false);
+    setProperties([]);
   };
 
+  const propertyAdd = () => {
+    setProperties((props) => [...props, { ...newProp, id: props.length + 1 + "" }]);
+    setNewProp({ id: "", key: "", value: "" });
+  };
+
+  const propertyDelete = useCallback((e: React.MouseEvent<HTMLElement>) => {
+    const { id = "-1" } = e.currentTarget.dataset;
+    setProperties((props) => props.filter((item) => item.id !== id));
+  }, []);
+
+  const updatePropertyField = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+    const { idx = "", name = "" } = e.currentTarget.parentElement?.parentElement?.dataset || {};
+    if (name) {
+      if (idx) {
+        setProperties((props) =>
+          props.map((p, i) => {
+            if (idx == i + "") {
+              p[name as keyof Property] = e.target.value;
+            }
+            return p;
+          })
+        );
+      } else {
+        setNewProp((np) => ({ ...np, [name]: e.target.value }));
+      }
+    }
+  }, []);
+
   const form = useFormik({
     initialValues: {
       config: "",
       name: "",
-      date: new Date().toString(),
-      properties: properties,
+      date: new Date().toISOString(),
+      properties: [],
     },
     onSubmit,
   });
 
-  const scenarioNodes = (scenarios?: TreeNode[]) =>
-    scenarios &&
-    scenarios?.map((child) => (
-      <TreeItem
-        key={child.id}
-        nodeId={child.id}
-        label={
-          showPrimaryFlag && child.primary ? (
-            <Badge
-              badgeContent={
-                <FlagOutlined
-                  sx={{
-                    color: "#FFFFFF",
-                    fontSize: "11px",
-                  }}
-                />
-              }
-              color="primary"
-              anchorOrigin={{
-                vertical: "top",
-                horizontal: "left",
-              }}
-              sx={{
-                "& .MuiBadge-badge": {
-                  marginLeft: "-12px",
-                  height: "19px",
-                  width: "12px",
-                },
-              }}
-            >
-              <Grid
-                container
-                alignItems="center"
-                direction="row"
-                flexWrap="nowrap"
-                justifyContent="flex-start"
-                spacing={1}
-              >
-                <Grid item>{child.label}</Grid>
-              </Grid>
-            </Badge>
-          ) : (
-            <Grid
-              container
-              alignItems="center"
-              direction="row"
-              flexWrap="nowrap"
-              justifyContent="flex-start"
-              spacing={1}
-            >
-              <Grid item>{child.label}</Grid>
-            </Grid>
-          )
-        }
-      />
-    ));
-
+  // Refresh on broadcast
   useEffect(() => {
     if (props.coreChanged?.scenario) {
-      const updateVar = getUpdateVar(props.updateVarNames, "scenarios");
-      updateVar &&
-        dispatch(createRequestUpdateAction("", module, [updateVar], true));
+      const updateVar = getUpdateVar(props.updateVars, "scenarios");
+      updateVar && dispatch(createRequestUpdateAction(id, module, [updateVar], true));
     }
-  }, [props.coreChanged, props.updateVarNames, module, dispatch]);
+  }, [props.coreChanged, props.updateVars, module, dispatch]);
 
-  useEffect(() => {
-    const data = cycles;
-    if (data) {
-      setNodes(data);
-    }
-  }, []);
+  const switchDialog = useCallback(() => setOpen((op) => !op), []);
+
+  const onSelect = useCallback(
+    (e: React.SyntheticEvent, nodeIds: Array<string> | string) => {
+      dispatch(
+        createSendActionNameAction(id, module, props.onCtxSelection, { ids: Array.isArray(nodeIds) ? nodeIds : [nodeIds], user_action: props.onAction })
+      );
+    },
+    [props.onCtxSelection, module, props.onAction]
+  );
 
   return (
     <div>
-      <Box
-        sx={{
-          maxWidth: 300,
-          overflowY: "auto",
-        }}
-      >
-        <Typography variant="h5" gutterBottom>
-          Scenarios
-        </Typography>
-        <MuiTreeView
-          defaultCollapseIcon={<ExpandMore />}
-          defaultExpandIcon={<ChevronRight />}
-          sx={{
-            mb: 2,
-          }}
-        >
-          {nodes.map((item) => (
-            <>
-              {displayCycles &&
-                (item.type === NodeType.CYCLE ? (
-                  <TreeItem
-                    key={item.id}
-                    nodeId={item.id}
-                    label={item.label}
-                    sx={{
-                      ".MuiTreeItem-content": {
-                        padding: "4px 8px",
-                        gap: "4px",
-                        borderRadius: "4px",
-                        mb: "5px",
-                      },
-                      ".MuiTreeItem-label": {
-                        fontWeight: "700",
-                        fontSize: "16px",
-                      },
-                    }}
-                  >
-                    {scenarioNodes(item.children)}
-                  </TreeItem>
-                ) : (
-                  scenarioNodes([item])
-                ))}
-              {!displayCycles &&
-                (item.type === NodeType.SCENARIO
-                  ? scenarioNodes([item])
-                  : scenarioNodes(item.children))}
-            </>
-          ))}
-        </MuiTreeView>
-
-        {showAddButton && (
-          <Button
-            variant="outlined"
-            color="error"
-            onClick={() => setOpen(true)}
-            fullWidth
-            sx={{ minHeight: "36px" }}
-          >
+      <Box sx={MainBoxSx}>
+        <TreeView defaultCollapseIcon={<ExpandMore />} defaultExpandIcon={<ChevronRight />} sx={TreeViewSx} onNodeSelect={onSelect}>
+          {scenarios
+            ? scenarios.map((item) => {
+                const [id, label, nodeType, _, scenarios] = item;
+                return (
+                  <>
+                    {displayCycles ? (
+                      nodeType === NodeType.CYCLE ? (
+                        <TreeItem key={id} nodeId={id} label={label} sx={CycleSx}>
+                          <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} />
+                        </TreeItem>
+                      ) : (
+                        <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} />
+                      )
+                    ) : nodeType === NodeType.SCENARIO ? (
+                      <ScenarioNodes scenarios={item as Scenario} showPrimary={showPrimaryFlag} />
+                    ) : (
+                      <ScenarioNodes scenarios={scenarios} showPrimary={showPrimaryFlag} />
+                    )}
+                  </>
+                );
+              })
+            : null}
+        </TreeView>
+
+        {showAddButton ? (
+          <Button variant="outlined" onClick={switchDialog} fullWidth>
             ADD SCENARIO &nbsp;&nbsp;
             <Add />
           </Button>
-        )}
+        ) : null}
+
+        <Box>{props.error}</Box>
       </Box>
 
-      <MuiDialog onClose={() => setOpen(false)} open={open}>
+      <Dialog onClose={switchDialog} open={open}>
         <DialogTitle>
-          <Grid
-            container
-            direction="row"
-            justifyContent="space-between"
-            alignItems="center"
-          >
+          <Grid container direction="row" justifyContent="space-between" alignItems="center">
             <Typography variant="h5">Create new scenario</Typography>
-            <IconButton
-              aria-label="close"
-              onClick={() => setOpen(false)}
-              sx={{ p: 0 }}
-            >
+            <IconButton aria-label="close" onClick={switchDialog} sx={{ p: 0 }}>
               <Close />
             </IconButton>
           </Grid>
         </DialogTitle>
         <form onSubmit={form.handleSubmit}>
-          <DialogContent
-            sx={{
-              width: "500px",
-            }}
-            dividers
-          >
+          <DialogContent sx={DialogContentSx} dividers>
             <Grid container rowSpacing={2}>
               <Grid item xs={12}>
                 <FormGroup>
                   <FormControl fullWidth>
                     <InputLabel id="select-config">Configuration</InputLabel>
-                    <Select
-                      labelId="select-config"
-                      label="Configuration"
-                      {...form.getFieldProps("config")}
-                      error={!!form.errors.config && form.touched.config}
-                    >
-                      <MenuItem value={1}>config_test_1</MenuItem>
-                      <MenuItem value={2}>config_test_2</MenuItem>
-                      <MenuItem value={3}>config_test_3</MenuItem>
+                    <Select labelId="select-config" label="Configuration" {...form.getFieldProps("config")} error={!!form.errors.config && form.touched.config}>
+                      {props.configs
+                        ? props.configs.map(([id, label]) => (
+                            <MenuItem key={id} value={id}>
+                              {label}
+                            </MenuItem>
+                          ))
+                        : null}
                     </Select>
-                    <FormHelperText
-                      error={!!form.errors.config && form.touched.config}
-                      sx={{ pl: 12 }}
-                    >
+                    <FormHelperText error={!!form.errors.config && form.touched.config} sx={{ pl: 12 }}>
                       {form.errors.config}
                     </FormHelperText>
                   </FormControl>
@@ -370,7 +307,6 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
               <Grid item xs={12}>
                 <FormGroup>
                   <TextField
-                    id="name"
                     {...form.getFieldProps("name")}
                     error={!!form.errors.name && form.touched.name}
                     helperText={form.errors.name}
@@ -382,104 +318,39 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
               <Grid item xs={12}>
                 <FormGroup>
                   <LocalizationProvider dateAdapter={AdapterDateFns}>
-                    <DatePicker
-                      label="Date"
-                      value={new Date(form.values.date)}
-                      onChange={(date) =>
-                        form.setFieldValue("date", date?.toString())
-                      }
-                    />
+                    <DatePicker label="Date" value={new Date(form.values.date)} onChange={(date) => form.setFieldValue("date", date?.toISOString())} />
                   </LocalizationProvider>
                 </FormGroup>
               </Grid>
               <Grid item xs={12} container justifyContent="space-between">
-                <Grid item xs={8} container alignItems="center">
-                  <Typography variant="h6">Custom Properties</Typography>
-                </Grid>
-                <Grid item xs={4} container justifyContent="flex-end">
-                  <Button
-                    variant="outlined"
-                    color="inherit"
-                    onClick={() => {
-                      setProperties([]);
-                    }}
-                  >
-                    REMOVE ALL
-                  </Button>
-                </Grid>
+                <Typography variant="h6">Custom Properties</Typography>
               </Grid>
-              {properties?.map((item, index) => (
-                <Grid
-                  item
-                  xs={12}
-                  key={item.id}
-                  container
-                  justifyContent="space-between"
-                >
-                  <Grid item xs={4}>
-                    <TextField
-                      id="property-key"
-                      value={item.key}
-                      label="Key"
-                      variant="outlined"
-                      onChange={(e) => {
-                        const updated = [...properties];
-                        updated[index].key = e.target.value;
-                        setProperties(updated);
-                      }}
-                    />
-                  </Grid>
-                  <Grid item xs={5}>
-                    <TextField
-                      id="property-value"
-                      value={item.value}
-                      label="Value"
-                      variant="outlined"
-                      onChange={(e) => {
-                        const updated = [...properties];
-                        updated[index].value = e.target.value;
-                        setProperties(updated);
-                      }}
-                    />
-                  </Grid>
-                  <Grid item xs={2}>
-                    <Button
-                      color="inherit"
-                      variant="outlined"
-                      component="label"
-                      onClick={() => propertyDelete(item.id)}
-                    >
-                      <DeleteOutline />
-                    </Button>
-                  </Grid>
-                </Grid>
-              ))}
+              {properties
+                ? properties.map((item, index) => (
+                    <Grid item xs={12} key={item.id} container justifyContent="space-between">
+                      <Grid item xs={4}>
+                        <TextField value={item.key} label="Key" variant="outlined" data-name="key" data-idx={index} onChange={updatePropertyField} />
+                      </Grid>
+                      <Grid item xs={5}>
+                        <TextField value={item.value} label="Value" variant="outlined" data-name="value" data-idx={index} onChange={updatePropertyField} />
+                      </Grid>
+                      <Grid item xs={2}>
+                        <Button variant="outlined" component="label" data-id={item.id} onClick={propertyDelete}>
+                          <DeleteOutline />
+                        </Button>
+                      </Grid>
+                    </Grid>
+                  ))
+                : null}
               <Grid item xs={12} container justifyContent="space-between">
                 <Grid item xs={4}>
-                  <TextField
-                    id="add-key"
-                    value={key}
-                    onChange={(e) => setKey(e.target.value)}
-                    label="Key"
-                    variant="outlined"
-                  />
+                  <TextField value={newProp.key} data-name="key" onChange={updatePropertyField} label="Key" variant="outlined" />
                 </Grid>
                 <Grid item xs={5}>
-                  <TextField
-                    id="add-value"
-                    value={value}
-                    onChange={(e) => setValue(e.target.value)}
-                    label="Value"
-                    variant="outlined"
-                  />
+                  <TextField value={newProp.value} data-name="value" onChange={updatePropertyField} label="Value" variant="outlined" />
                 </Grid>
                 <Grid item xs={2}>
-                  <Button
-                    color="primary"
-                    variant="outlined"
-                    component="label"
-                    onClick={() => propertyAdd(key, value)}
-                  >
+                  <Button variant="outlined" component="label" onClick={propertyAdd} disabled={!newProp.key || !newProp.value}>
                     <Add />
                   </Button>
                 </Grid>
@@ -488,19 +359,15 @@ const ScenarioSelector = (props: ScenarioSelectorProps) => {
           </DialogContent>
 
           <DialogActions>
-            <Button
-              variant="outlined"
-              color="inherit"
-              onClick={() => setOpen(false)}
-            >
+            <Button variant="outlined" onClick={switchDialog}>
               Cancel
             </Button>
-            <Button variant="contained" color="primary" type="submit">
+            <Button variant="contained" type="submit" disabled={!form.values.config || !form.values.name}>
               CREATE
             </Button>
           </DialogActions>
         </form>
-      </MuiDialog>
+      </Dialog>
     </div>
   );
 };

+ 0 - 127
gui/src/data.tsx

@@ -1,127 +0,0 @@
-/*
- * 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 { TreeNode } from "./ScenarioSelector";
-
-export const cycles: TreeNode[] = [
-  {
-    id: "cycle_1",
-    label: "Cycle 2023-02-04",
-    type: 0,
-    primary: false,
-    children: [
-      {
-        id: "scenario_1.1",
-        label: "Scenario 1.1",
-        type: 1,
-        primary: true,
-      },
-      {
-        id: "scenario_1.2",
-        label: "Scenario 1.2",
-        type: 1,
-        primary: false,
-      },
-      {
-        id: "scenario_1.3",
-        label: "Scenario 1.3",
-        type: 1,
-        primary: false,
-      },
-    ],
-  },
-  {
-    id: "cycle_2",
-    label: "Cycle 2023-02-05",
-    type: 0,
-    primary: false,
-    children: [
-      {
-        id: "scenario_2.1",
-        label: "Scenario 2.1",
-        type: 1,
-        primary: true,
-      },
-      {
-        id: "scenario_2.2",
-        label: "Scenario 2.2",
-        type: 1,
-        primary: false,
-      },
-      {
-        id: "scenario_2.3",
-        label: "Scenario 2.3",
-        type: 1,
-        primary: false,
-      },
-    ],
-  },
-  {
-    id: "cycle_3",
-    label: "Cycle 2023-02-06",
-    type: 0,
-    primary: false,
-    children: [
-      {
-        id: "scenario_3.1",
-        label: "Scenario 3.1",
-        type: 1,
-        primary: true,
-      },
-      {
-        id: "scenario_3.2",
-        label: "Scenario 3.2",
-        type: 1,
-        primary: false,
-      },
-      {
-        id: "scenario_3.3",
-        label: "Scenario 3.3",
-        type: 1,
-        primary: false,
-      },
-    ],
-  },
-  {
-    id: "cycle_4",
-    label: "Cycle 2023-02-07",
-    type: 0,
-    primary: false,
-    children: [
-      {
-        id: "scenario_4.1",
-        label: "Scenario 4.1",
-        type: 1,
-        primary: true,
-      },
-      {
-        id: "scenario_4.2",
-        label: "Scenario 4.2",
-        type: 1,
-        primary: false,
-      },
-      {
-        id: "scenario_4.3",
-        label: "Scenario 4.3",
-        type: 1,
-        primary: false,
-      },
-    ],
-  },
-  {
-    id: "scenario_5",
-    label: "Scenario 5",
-    type: 1,
-    primary: false,
-  },
-];

+ 4 - 0
src/taipy/__init__.py

@@ -67,6 +67,10 @@ if find_spec("taipy"):
     if find_spec("taipy.gui"):
         from taipy.gui import Gui
 
+        from .gui_core.GuiCoreLib import GuiCore
+
+        Gui.add_library(GuiCore())
+
         if find_spec("taipy.enterprise") and find_spec("taipy.enterprise.gui"):
             from taipy.enterprise.gui import _init_gui_enterprise
 

+ 124 - 14
src/taipy/gui_core/GuiCoreLib.py

@@ -10,51 +10,157 @@
 # specific language governing permissions and limitations under the License.
 
 import typing as t
+import warnings
 
-from taipy.core import Core, Scenario
+from dateutil import parser
+
+import taipy as tp
+from taipy.core import Scenario
+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.extension import Element, ElementLibrary, ElementProperty, PropertyType
 
 
-class GuiCoreContext:
+class GuiCoreContext(CoreEventConsumerBase):
+    __PROP_SCENARIO_CONFIG_ID = "config"
+    __PROP_SCENARIO_DATE = "date"
+    __PROP_SCENARIO_NAME = "name"
+    __SCENARIO_PROPS = (__PROP_SCENARIO_CONFIG_ID, __PROP_SCENARIO_DATE, __PROP_SCENARIO_NAME)
     _CORE_CHANGED_NAME = "core_changed"
-    _VAR_NAME = "__CCCtx"
+    _ERROR_VAR = "gui_core_error"
 
-    def __init__(self, gui: Gui, core: Core) -> None:
+    def __init__(self, gui: Gui) -> None:
         self.gui = gui
-        self.core = core
-        self.scenarios: t.List[Scenario]
+        self.scenarios: t.Optional[
+            t.List[t.Tuple[str, str, int, bool, t.Optional[t.List[t.Tuple[str, str, int, bool, None]]]]]
+        ] = None
+        self.scenario_configs: t.Optional[t.List[t.Tuple[str, str]]] = None
+        # register to taipy core notification
+        reg_id, reg_queue = Notifier.register()
+        super().__init__(reg_id, reg_queue)
+        self.start()
+
+    def process_event(self, event: Event):
+        if event.entity_type == EventEntityType.SCENARIO or event.entity_type == EventEntityType.CYCLE:
+            self.scenarios = None
+            self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME, {"scenario": True})
 
     def get_scenarios(self):
-        return []
+        if self.scenarios is None:
+
+            def add_scenarios(res, scens):
+                for scenario in scens:
+                    res.append((scenario.id, scenario.name, 1, scenario.is_primary, None))
+                return res
+
+            self.scenarios = []
+            for cycle, scenarios in tp.get_cycles_scenarios().items():
+                if cycle is None:
+                    add_scenarios(self.scenarios, scenarios)
+                else:
+                    self.scenarios.append((cycle.id, cycle.name, 0, False, add_scenarios([], scenarios)))
+        return self.scenarios
+
+    def get_scenario_configs(self):
+        if self.scenario_configs is None:
+            configs = tp.Config.scenarios
+            if isinstance(configs, dict):
+                self.scenario_configs = [(id, f"{c.id}") for id, c in configs.items()]
+        return self.scenario_configs
 
     def create_new_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
-        pass
+        print(f"create_new_scenario(state, {id}, {action}, {payload}")
+        args = payload.get("args")
+        if args is None or not isinstance(args, list) or len(args) == 0 or not isinstance(args[0], dict):
+            return
+        data = args[0]
+        config_id = data.get(GuiCoreContext.__PROP_SCENARIO_CONFIG_ID)
+        scenario_config = tp.Config.scenarios.get(config_id)
+        if scenario_config is None:
+            state.assign(GuiCoreContext._ERROR_VAR, f"Invalid configuration id ({config_id})")
+            return
+        date_str = data.get(GuiCoreContext.__PROP_SCENARIO_DATE)
+        try:
+            date = parser.parse(date_str) if isinstance(date_str, str) else None
+        except Exception as e:
+            state.assign(GuiCoreContext._ERROR_VAR, f"Invalid date ({date_str}).{e}")
+            return
+        scenario = tp.create_scenario(scenario_config, date, data.get(GuiCoreContext.__PROP_SCENARIO_NAME))
+        if props := data.get("properties"):
+            with scenario as sc:
+                for prop in props:
+                    key = prop.get("key")
+                    if key and key not in GuiCoreContext.__SCENARIO_PROPS:
+                        sc._properties[key] = prop.get("value")
+        state.assign(GuiCoreContext._ERROR_VAR, "")
+
+    def select_scenario(self, state: State, id: str, action: str, payload: t.Dict[str, str]):
+        print(f"select_scenario(state, {id}, {action}, {payload}")
+        args = payload.get("args")
+        if args is None or not isinstance(args, list) or len(args) == 0 or not isinstance(args[0], dict):
+            return
+        data = args[0]
+        action = data.get("user_action")
+        action_function = None
+        if action:
+            action_function = self.gui._get_user_function(action)
+            if not callable(action_function):
+                action_function = None
+                warnings.warn(f"on_select ({action_function}) function is not callable.")
+                return
+        if action_function is None:
+            if not hasattr(self.gui, "on_action"):
+                return
+            action_function = self.gui.on_action
+        ids = data.get("ids")
+        if not isinstance(ids, list) or len(ids) == 0:
+            state.assign(GuiCoreContext._ERROR_VAR, "Invalid selection.")
+            return
+        scenarios: t.List[Scenario] = []
+        for id in ids:
+            try:
+                entity = tp.get(id)
+                if isinstance(entity, Scenario):
+                    scenarios.append(entity)
+            except Exception:
+                pass
+        if len(scenarios) == 0:
+            return
+        self.gui._call_function_with_state(action_function, [id, action_function.__name__, scenarios[0]])
 
     def broadcast_core_changed(self):
-        self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME)
+        self.gui.broadcast(GuiCoreContext._CORE_CHANGED_NAME, "")
 
 
 class GuiCore(ElementLibrary):
+    __LIB_NAME = "taipy_gui_core"
+    __CTX_VAR_NAME = f"__{__LIB_NAME}_Ctx"
+
     __elts = {
         "scenario_selector": Element(
-            "scenario",
+            "scenario_id",
             {
                 "show_add_button": ElementProperty(PropertyType.dynamic_boolean, True),
                 "display_cycles": ElementProperty(PropertyType.dynamic_boolean, True),
                 "show_primary_flag": ElementProperty(PropertyType.dynamic_boolean, True),
                 "scenario_id": ElementProperty(PropertyType.dynamic_string),
-                "scenarios": ElementProperty(PropertyType.react, f"{GuiCoreContext._VAR_NAME}.get_scenarios()"),
+                "scenarios": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenarios()}}"),
                 "on_scenario_create": ElementProperty(
-                    PropertyType.function, f"{GuiCoreContext._VAR_NAME}.create_new_scenario()"
+                    PropertyType.function, f"{{{__CTX_VAR_NAME}.create_new_scenario}}"
                 ),
+                "configs": ElementProperty(PropertyType.react, f"{{{__CTX_VAR_NAME}.get_scenario_configs()}}"),
                 "core_changed": ElementProperty(PropertyType.broadcast, GuiCoreContext._CORE_CHANGED_NAME),
+                "error": ElementProperty(PropertyType.react, GuiCoreContext._ERROR_VAR),
+                "on_ctx_selection": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.select_scenario}}"),
+                "on_action": ElementProperty(PropertyType.function),
             },
         )
     }
 
     def get_name(self) -> str:
-        return "taipy_gui_core"
+        return GuiCore.__LIB_NAME
 
     def get_elements(self) -> t.Dict[str, Element]:
         return GuiCore.__elts
@@ -63,4 +169,8 @@ class GuiCore(ElementLibrary):
         return ["lib/taipy-gui-core.js"]
 
     def on_init(self, gui: Gui) -> t.Optional[t.Tuple[str, t.Any]]:
-        return GuiCoreContext._VAR_NAME, GuiCoreContext(gui, Core())
+        return GuiCore.__CTX_VAR_NAME, GuiCoreContext(gui)
+
+    def on_user_init(self, state: State):
+        state._add_attribute(GuiCoreContext._ERROR_VAR)
+        state._gui._bind_var_val(GuiCoreContext._ERROR_VAR, "")